Compare commits

...

54 Commits

Author SHA1 Message Date
YHH
73a882f75e 2.1.21 2025-06-19 15:01:20 +08:00
YHH
310f5f2349 支持先加入实体后加入系统以让matcher进行实体匹配/优化行为树节点效果及逻辑 2025-06-19 15:00:14 +08:00
YHH
8c86d6b696 更新文档及优化行为树编辑器 2025-06-19 10:38:31 +08:00
YHH
82cd163adc 清理冗余代码及频繁日志输出 2025-06-18 23:37:47 +08:00
YHH
802ee25621 新增Blackboard 2025-06-18 23:31:53 +08:00
YHH
f48ebb65ba 修复插件生成代码报错问题 2025-06-18 20:22:17 +08:00
YHH
aaa2a8ed2c 新增装饰节点选中功能 2025-06-18 18:31:29 +08:00
YHH
5a06f5420b 条件装饰节点和条件装饰器结合 2025-06-18 18:21:55 +08:00
YHH
343f5a44f2 新增根节点 2025-06-18 15:53:48 +08:00
YHH
92125aee3a 删除连线操作 2025-06-18 15:43:17 +08:00
YHH
96f651b7ca 新增cocos右键打开和保存行为树功能 2025-06-18 15:20:07 +08:00
YHH
06ea01e928 拖拽逻辑更新 2025-06-17 23:59:30 +08:00
YHH
577f1e429a 新增行为树编辑器 2025-06-17 18:28:57 +08:00
YHH
7808f64fe5 更新生成代码工具 2025-06-17 10:46:47 +08:00
YHH
e6789e49e4 2.1.20 2025-06-17 07:35:46 +08:00
YHH
797619aece 更新池利用率 2025-06-17 07:35:23 +08:00
YHH
1b5363611d 新增cocos-debug-profiler 2025-06-17 00:32:16 +08:00
YHH
103f773286 添加Cocos Creator ECS编辑器插件:完整的框架管理和模板生成功能 2025-06-16 18:32:44 +08:00
YHH
d9ef0b587e 2.1.19 2025-06-16 09:36:59 +08:00
YHH
d5b98256f0 优化bits性能及移除组件上限 2025-06-16 09:36:36 +08:00
YHH
efcceaa898 2.1.18 2025-06-12 09:47:44 +08:00
YHH
e4aad11965 update readme 2025-06-12 09:47:25 +08:00
YHH
47207fad52 2.1.17 2025-06-12 09:44:05 +08:00
YHH
202bf82896 更新场景切换状态 2025-06-12 09:43:57 +08:00
YHH
0e3274a743 移除coreevents事件派发机制 2025-06-12 09:42:35 +08:00
YHH
b06174926d 更新快速入门指南 2025-06-10 13:22:28 +08:00
YHH
abb23a3c02 2.1.16 2025-06-10 13:12:22 +08:00
YHH
0c8f232282 文档及教程更新 2025-06-10 13:12:14 +08:00
YHH
ef023d27bf 2.1.15 2025-06-10 09:50:05 +08:00
YHH
7a591825eb 先移除wasm后续再通过其他方式接入 2025-06-10 09:49:55 +08:00
YHH
e71c49d596 移除额外的打包流程 2025-06-09 18:27:20 +08:00
YHH
e6ce8995ba 2.1.14 2025-06-09 18:01:34 +08:00
YHH
f6250b6d5b 规范为cococs wasm 2025-06-09 18:00:50 +08:00
YHH
757eff2937 2.1.13 2025-06-09 15:55:26 +08:00
YHH
996a7f3ddf 重构WASM架构:移除npm包中的WASM文件,改为独立发布 - 移除自动WASM加载逻辑 - 添加手动initializeWasm API - 创建专门的WASM发布包构建脚本 - 更新Cocos Creator使用指南 2025-06-09 15:54:34 +08:00
YHH
94c050bacb 2.1.12 2025-06-09 15:47:10 +08:00
YHH
3f4aa59a29 完善Cocos Creator支持:添加手动WASM初始化API和使用指南 2025-06-09 15:46:14 +08:00
YHH
bee7cf4278 2.1.11 2025-06-09 15:32:11 +08:00
YHH
b9db6f0b40 2.1.10 2025-06-09 15:16:31 +08:00
YHH
8967cba3c7 修复WASM路径解析问题,支持Cocos Creator环境 - 改进多路径WASM加载策略 - 添加Core.disableWasm()方法 - 简化WASM初始化逻辑以避免环境兼容性问题 2025-06-09 15:15:41 +08:00
YHH
d04ad2eea9 Update build-rollup.js 2025-06-09 14:53:15 +08:00
YHH
f2d3880a06 重构项目结构:整理gitignore,移动source目录到根目录,统一依赖管理 2025-06-09 14:51:26 +08:00
YHH
ec5f70ecfc fix: 修复npm包构建和发布问题 - 将esbuild target从es2017升级到es2020以支持BigInt - 修复浏览器环境下Node.js模块依赖问题 - 添加define配置处理require等Node.js全局变量 - 成功发布@esengine/ecs-framework@2.1.7到npm 2025-06-09 13:34:46 +08:00
YHH
40b3fe7165 docs: 更新项目文档 - 添加EntityManager、事件系统、性能优化使用示例和说明 2025-06-09 13:25:10 +08:00
YHH
4095f1e946 refactor: 规范化代码注释和更新核心模块 - 移除冗余JSDoc注释,统一代码风格 2025-06-09 13:24:54 +08:00
YHH
e219fc47ba feat: 添加ECS核心功能模块 - EntityManager实体管理器、EventBus事件总线、性能优化系统 2025-06-09 13:24:24 +08:00
YHH
6e2e7a4af5 refactor: 拆分WasmCore大文件为模块化结构 - 将23KB的WasmCore.ts拆分为types/loader/fallback/core/instance五个模块 2025-06-09 13:23:46 +08:00
YHH
2e7f764d6c refactor: 细化ECS/Core目录结构 - 按功能拆分为Events/Query/Performance/Storage四个子模块 2025-06-09 13:23:29 +08:00
YHH
ce64de5b3d 更改为es2020模块适应Cocos/laya引擎 2025-06-09 10:42:19 +08:00
YHH
35ca1dd7ea 将dist目录添加到.gitignore中 - dist目录是npm包构建的输出目录,不应该提交到版本控制 2025-06-09 10:39:37 +08:00
YHH
8d0ad6b871 新增wasm以优化实体update速度 2025-06-08 21:50:50 +08:00
YHH
0aa4791cf7 更新文档并预留wasm接口 2025-06-08 10:20:51 +08:00
YHH
082c2b46d0 移除数学库 2025-06-07 21:45:11 +08:00
YHH
50420f9052 增加性能测试 2025-06-07 21:28:31 +08:00
219 changed files with 39952 additions and 15409 deletions

80
.gitignore vendored
View File

@@ -1,11 +1,69 @@
/source/node_modules
/source/bin
/demo/bin-debug
/demo/bin-release
/.idea
/.vscode
/demo_wxgame
/demo/.wing
/demo/.idea
/demo/.vscode
/source/docs
# 依赖目录
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# 构建输出
bin/
dist/
*.tgz
# TypeScript
*.tsbuildinfo
# 临时文件
*.tmp
*.temp
.cache/
# IDE 配置
.idea/
.vscode/
*.swp
*.swo
*~
# 操作系统文件
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# 日志文件
logs/
*.log
# 环境配置
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# 测试覆盖率
coverage/
*.lcov
# 包管理器锁文件保留npm的忽略其他的
yarn.lock
pnpm-lock.yaml
# 文档生成
docs/api/
docs/build/
# 备份文件
*.bak
*.backup
# 演示项目构建产物
/demo/bin-debug/
/demo/bin-release/
/demo/.wing/
/demo/.idea/
/demo/.vscode/
/demo_wxgame/

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "thirdparty/BehaviourTree-ai"]
path = thirdparty/BehaviourTree-ai
url = https://github.com/esengine/BehaviourTree-ai.git

40
.npmignore Normal file
View File

@@ -0,0 +1,40 @@
# 源代码文件
src/
tsconfig*.json
*.ts
!bin/**/*.d.ts
# 开发文件
dev-bin/
scripts/
.vscode/
.git/
.gitignore
# 测试文件
**/*.test.*
**/*.spec.*
**/test/
**/tests/
# 构建缓存
node_modules/
*.log
*.tmp
*.temp
# 文档草稿
docs/draft/
*.draft.md
# 编辑器文件
.DS_Store
Thumbs.db
*.swp
*.swo
*~
# 环境文件
.env
.env.local
.env.*.local

570
README.md
View File

@@ -3,293 +3,435 @@
[![npm version](https://badge.fury.io/js/%40esengine%2Fecs-framework.svg)](https://badge.fury.io/js/%40esengine%2Fecs-framework)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
一个轻量级的 TypeScript ECSEntity-Component-System框架,专为游戏开发设计,适用于 Laya、Cocos 等游戏引擎
TypeScript ECS (Entity-Component-System) 框架,专为游戏开发设计。
## ✨ 特性
> 🤔 **什么是 ECS** 不熟悉 ECS 架构?建议先阅读 [ECS 架构基础](docs/concepts-explained.md#ecs-架构基础) 了解核心概念
- 🚀 **轻量级 ECS 架构** - 基于实体组件系统,提供清晰的代码结构
- 📡 **事件系统** - 内置 Emitter 事件发射器,支持类型安全的事件管理
-**定时器系统** - 完整的定时器管理,支持延迟和重复任务
- 🔍 **查询系统** - 基于位掩码的高性能实体查询
- 🛠️ **性能监控** - 内置性能监控工具,帮助优化游戏性能
- 🎯 **对象池** - 内存管理优化,减少垃圾回收压力
- 📊 **数学库** - 完整的 2D 数学运算支持
## 特性
## 📦 安装
- 🔧 **完整的 TypeScript 支持** - 强类型检查和代码提示
- 📡 **[类型安全事件系统](docs/concepts-explained.md#事件系统)** - 事件装饰器和异步事件处理
- 🔍 **[查询系统](docs/concepts-explained.md#实体管理)** - 流式 API 和智能缓存
-**[性能优化](docs/concepts-explained.md#性能优化技术)** - 组件索引、Archetype 系统、脏标记
- 🎯 **[实体管理器](docs/concepts-explained.md#实体管理)** - 统一的实体生命周期管理
- 🧰 **调试工具** - 内置性能监控和调试信息
> 📖 **不熟悉这些概念?** 查看我们的 [技术概念详解](docs/concepts-explained.md) 了解它们的作用和应用场景
## 安装
```bash
npm install @esengine/ecs-framework
```
## 🚀 快速开始
## 快速开始
### 1. 初始化框架
### 基础设置
```typescript
import { Core, CoreEvents } from '@esengine/ecs-framework';
import { Core, Scene, Entity, Component, EntitySystem } from '@esengine/ecs-framework';
// 创建 Core 实例
const core = Core.create(true); // true 表示开启调试模式
// 在游戏循环中更新框架
function gameLoop() {
// 发送帧更新事件
Core.emitter.emit(CoreEvents.frameUpdated);
}
```
### 2. 创建场景
```typescript
import { Scene, Vector2, EntitySystem } from '@esengine/ecs-framework';
class GameScene extends Scene {
public initialize() {
// 创建玩家实体
const player = this.createEntity("Player");
// 设置位置
player.position = new Vector2(100, 100);
// 添加自定义组件
const movement = player.addComponent(new MovementComponent());
// 添加系统
this.addEntityProcessor(new MovementSystem());
}
public onStart() {
console.log("游戏场景已启动");
}
}
// 设置当前场景
Core.scene = new GameScene();
```
### 3. 创建组件
```typescript
import { Component, Vector2, Time } from '@esengine/ecs-framework';
class MovementComponent extends Component {
public speed: number = 100;
public direction: Vector2 = Vector2.zero;
public update() {
if (this.direction.length > 0) {
const movement = this.direction.multiply(this.speed * Time.deltaTime);
this.entity.position = this.entity.position.add(movement);
// 创建核心实例 - 使用配置对象(推荐)
const core = Core.create({
debug: true, // 启用调试模式
enableEntitySystems: true, // 启用实体系统
debugConfig: { // 可选:调试配置
enabled: true,
websocketUrl: 'ws://localhost:8080',
autoReconnect: true,
updateInterval: 1000,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
}
}
});
// 简化创建 - 向后兼容(仍然支持)
const core2 = Core.create(true); // 等同于 { debug: true, enableEntitySystems: true }
// 创建场景
const scene = new Scene();
Core.scene = scene;
```
### 4. 创建系统
### 定义组件
```typescript
import { EntitySystem, Entity } from '@esengine/ecs-framework';
class MovementSystem extends EntitySystem {
protected process(entities: Entity[]) {
for (const entity of entities) {
const movement = entity.getComponent(MovementComponent);
if (movement) {
movement.update();
}
}
class PositionComponent extends Component {
constructor(public x: number = 0, public y: number = 0) {
super();
}
}
```
## 📚 核心概念
### Entity实体
实体是游戏世界中的基本对象,包含位置、旋转、缩放等基本属性,可以添加组件来扩展功能。
```typescript
import { Vector2 } from '@esengine/ecs-framework';
const entity = scene.createEntity("MyEntity");
entity.position = new Vector2(100, 200);
entity.rotation = Math.PI / 4;
entity.scale = new Vector2(2, 2);
```
### Component组件
组件包含数据和行为,定义了实体的特性。
```typescript
import { Component } from '@esengine/ecs-framework';
class VelocityComponent extends Component {
constructor(public dx: number = 0, public dy: number = 0) {
super();
}
}
class HealthComponent extends Component {
public maxHealth: number = 100;
public currentHealth: number = 100;
public takeDamage(damage: number) {
this.currentHealth = Math.max(0, this.currentHealth - damage);
if (this.currentHealth <= 0) {
this.entity.destroy();
}
constructor(
public maxHealth: number = 100,
public currentHealth: number = 100
) {
super();
}
}
```
### System系统
系统处理实体集合,实现游戏逻辑。
### 创建实体
```typescript
import { EntitySystem, Entity } from '@esengine/ecs-framework';
// 基础实体创建
const player = scene.createEntity("Player");
player.addComponent(new PositionComponent(100, 100));
player.addComponent(new VelocityComponent(5, 0));
player.addComponent(new HealthComponent(100, 100));
class HealthSystem extends EntitySystem {
protected process(entities: Entity[]) {
// 批量创建实体
const enemies = scene.createEntities(50, "Enemy");
```
### 创建系统
```typescript
class MovementSystem extends EntitySystem {
constructor() {
super();
}
public process(entities: Entity[]) {
for (const entity of entities) {
const health = entity.getComponent(HealthComponent);
if (health && health.currentHealth <= 0) {
entity.destroy();
const position = entity.getComponent(PositionComponent);
const velocity = entity.getComponent(VelocityComponent);
if (position && velocity) {
position.x += velocity.dx;
position.y += velocity.dy;
}
}
}
}
// 添加系统到场景
scene.addEntityProcessor(new MovementSystem());
```
## 🎮 高级功能
### 游戏循环
### 事件系统
ECS框架需要在游戏引擎的更新循环中调用
```typescript
import { Core, CoreEvents } from '@esengine/ecs-framework';
// 监听事件
Core.emitter.addObserver(CoreEvents.frameUpdated, this.onFrameUpdate, this);
// 发射自定义事件
Core.emitter.emit("playerDied", { player: entity, score: 1000 });
// 移除监听
Core.emitter.removeObserver(CoreEvents.frameUpdated, this.onFrameUpdate);
// 统一的API传入deltaTime
Core.update(deltaTime);
```
### 定时器系统
**不同平台的集成示例:**
```typescript
import { Core } from '@esengine/ecs-framework';
// 延迟执行
Core.schedule(2.0, false, this, (timer) => {
console.log("2秒后执行");
// Laya引擎
Laya.timer.frameLoop(1, this, () => {
const deltaTime = Laya.timer.delta / 1000; // 转换为秒
Core.update(deltaTime);
});
// 重复执行
Core.schedule(1.0, true, this, (timer) => {
console.log("每秒执行一次");
});
```
### 实体查询
```typescript
// 按名称查找
const player = scene.findEntity("Player");
// 按标签查找
const enemies = scene.findEntitiesByTag(1);
// 按ID查找
const entity = scene.findEntityById(123);
```
### 性能监控
```typescript
import { PerformanceMonitor } from '@esengine/ecs-framework';
// 获取性能数据
const monitor = PerformanceMonitor.instance;
console.log("平均FPS:", monitor.averageFPS);
console.log("内存使用:", monitor.memoryUsage);
```
## 🛠️ 开发工具
### 对象池
```typescript
// 创建对象池
class BulletPool extends es.Pool<Bullet> {
protected createObject(): Bullet {
return new Bullet();
}
// Cocos Creator
update(deltaTime: number) {
Core.update(deltaTime);
}
const bulletPool = new BulletPool();
// 获取对象
const bullet = bulletPool.obtain();
// 释放对象
bulletPool.free(bullet);
// 原生浏览器环境
let lastTime = 0;
function gameLoop(currentTime: number) {
const deltaTime = lastTime > 0 ? (currentTime - lastTime) / 1000 : 0.016;
lastTime = currentTime;
Core.update(deltaTime);
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
```
### 实体调试
## 实体管理器
EntityManager 提供了统一的实体管理接口:
```typescript
// 获取实体调试信息
const debugInfo = entity.getDebugInfo();
console.log("实体信息:", debugInfo);
import { EntityManager } from '@esengine/ecs-framework';
// 获取场景统计
const stats = scene.getStats();
console.log("场景统计:", stats);
const entityManager = new EntityManager();
// 流式查询 API
const results = entityManager
.query()
.withAll(PositionComponent, VelocityComponent)
.withNone(HealthComponent)
.withTag(1)
.execute();
// 批量操作使用Scene的方法
const bullets = scene.createEntities(100, "bullet");
// 按标签查询
const enemies = entityManager.getEntitiesByTag(2);
```
## 📖 文档
## 事件系统
- [快速入门](docs/getting-started.md) - 从零开始学习框架使用
- [核心概念](docs/core-concepts.md) - 深入了解 ECS 架构和设计原理
- [查询系统使用指南](docs/query-system-usage.md) - 学习高性能查询系统的详细用法
### [基础事件](docs/concepts-explained.md#类型安全事件)
## 🔗 扩展库
类型安全的事件系统,编译时检查事件名和数据类型。
- [路径寻找库](https://github.com/esengine/ecs-astar) - A*、广度优先、Dijkstra、GOAP 算法
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI 系统
```typescript
import { EventBus, ECSEventType } from '@esengine/ecs-framework';
## 🤝 贡献
const eventBus = entityManager.eventBus;
欢迎提交 Issue 和 Pull Request
// 监听预定义事件
eventBus.onEntityCreated((data) => {
console.log(`实体创建: ${data.entityName}`);
});
### 开发环境设置
eventBus.onComponentAdded((data) => {
console.log(`组件添加: ${data.componentType}`);
});
// 自定义事件
eventBus.emit('player:death', { playerId: 123, reason: 'fall' });
```
### [事件装饰器](docs/concepts-explained.md#事件装饰器)
使用装饰器语法自动注册事件监听器,减少样板代码。
```typescript
import { EventHandler, ECSEventType } from '@esengine/ecs-framework';
class GameSystem {
@EventHandler(ECSEventType.ENTITY_DESTROYED)
onEntityDestroyed(data: EntityDestroyedEventData) {
console.log('实体销毁:', data.entityName);
}
@EventHandler('player:levelup')
onPlayerLevelUp(data: { playerId: number; newLevel: number }) {
console.log(`玩家 ${data.playerId} 升级到 ${data.newLevel}`);
}
}
```
## 性能优化
### [组件索引](docs/concepts-explained.md#组件索引系统)
通过建立索引避免线性搜索,将查询复杂度从 O(n) 降低到 O(1)。
```typescript
// 使用Scene的查询系统进行组件索引
const querySystem = scene.querySystem;
// 查询具有特定组件的实体
const entitiesWithPosition = querySystem.queryAll(PositionComponent).entities;
const entitiesWithVelocity = querySystem.queryAll(VelocityComponent).entities;
// 性能统计
const stats = querySystem.getStats();
console.log('查询效率:', stats.hitRate);
```
**索引类型选择:**
- **哈希索引** - 适合稳定的、大量的组件(如位置、生命值)
- **位图索引** - 适合频繁变化的组件如Buff、状态
> 📋 详细选择指南参见 [索引类型选择指南](docs/concepts-explained.md#索引类型选择指南)
### [Archetype 系统](docs/concepts-explained.md#archetype-系统)
将具有相同组件组合的实体分组,减少查询时的组件检查开销。
```typescript
// 使用查询系统的Archetype功能
const querySystem = scene.querySystem;
// 查询统计
const stats = querySystem.getStats();
console.log('缓存命中率:', stats.hitRate);
```
### [脏标记系统](docs/concepts-explained.md#脏标记系统)
追踪数据变化,只处理发生改变的实体,避免不必要的计算。
```typescript
// 脏标记通过组件系统自动管理
// 组件变化时会自动标记为脏数据
// 查询系统会自动处理脏标记优化
const movingEntities = scene.querySystem.queryAll(PositionComponent, VelocityComponent);
```
> 💡 **不确定何时使用这些优化?** 查看 [性能优化建议](docs/concepts-explained.md#性能建议) 了解适用场景
## API 参考
### 核心类
| 类 | 描述 |
|---|---|
| `Core` | 框架核心管理类 |
| `Scene` | 场景容器,管理实体和系统 |
| `Entity` | 实体对象,包含组件集合 |
| `Component` | 组件基类 |
| `EntitySystem` | 系统基类 |
| `EntityManager` | 实体管理器 |
### 查询 API
```typescript
entityManager
.query()
.withAll(...components) // 包含所有指定组件
.withAny(...components) // 包含任意指定组件
.withNone(...components) // 不包含指定组件
.withTag(tag) // 包含指定标签
.withoutTag(tag) // 不包含指定标签
.execute() // 执行查询
```
### 事件类型
```typescript
enum ECSEventType {
ENTITY_CREATED = 'entity:created',
ENTITY_DESTROYED = 'entity:destroyed',
COMPONENT_ADDED = 'component:added',
COMPONENT_REMOVED = 'component:removed',
SYSTEM_ADDED = 'system:added',
SYSTEM_REMOVED = 'system:removed'
}
```
## 与其他框架对比
| 特性 | @esengine/ecs-framework | bitECS | Miniplex |
|------|-------------------------|--------|----------|
| TypeScript 支持 | ✅ 原生支持 | ✅ 完整支持 | ✅ 原生支持 |
| 事件系统 | ✅ 内置+装饰器 | ❌ 需自己实现 | ✅ 响应式 |
| 查询系统 | ✅ 流式 API | ✅ 函数式 | ✅ 响应式 |
| 实体管理器 | ✅ 统一接口 | ❌ 低级 API | ✅ 高级接口 |
| 性能优化 | ✅ 多重优化 | ✅ 极致性能 | ✅ React 优化 |
| JavaScript引擎集成 | ✅ 专为JS引擎设计 | ✅ 通用设计 | ⚠️ 主要 React |
| 可视化调试工具 | ✅ [Cocos插件](https://store.cocos.com/app/detail/7823) | ❌ 无官方工具 | ✅ React DevTools |
**选择指南:**
- 选择本框架:需要完整的游戏开发工具链和中文社区支持
- 选择 bitECS需要极致性能和最小化设计
- 选择 Miniplex主要用于 React 应用开发
## 项目结构
```
ecs-framework/
├── src/
│ ├── ECS/ # ECS 核心系统
│ │ ├── Core/ # 核心管理器
│ │ ├── Systems/ # 系统类型
│ │ └── Utils/ # ECS 工具
│ ├── Types/ # TypeScript接口定义
│ └── Utils/ # 通用工具
├── docs/ # 文档
└── scripts/ # 构建脚本
```
## 文档
### 🎯 新手入门
- **[📖 新手教程完整指南](docs/beginner-tutorials.md)** - 完整学习路径,从零开始 ⭐ **强烈推荐**
- **[🚀 快速入门](docs/getting-started.md)** - 详细的入门教程包含Laya/Cocos/Node.js集成指南 ⭐ **平台集成必读**
- 💡 **Cocos Creator用户特别提示**:我们提供[专用调试插件](https://store.cocos.com/app/detail/7823)支持可视化ECS调试
- [🧠 技术概念详解](docs/concepts-explained.md) - 通俗易懂的技术概念解释 ⭐ **推荐新手阅读**
- [🎯 位掩码使用指南](docs/bitmask-guide.md) - 位掩码概念、原理和高级使用技巧
- [💡 使用场景示例](docs/use-cases.md) - 不同类型游戏的具体应用案例
- [🔧 框架类型系统](docs/concepts-explained.md#框架类型系统) - TypeScript接口设计和使用指南
### 📚 核心功能
- [🎭 实体管理指南](docs/entity-guide.md) - 实体的创建和使用方法
- [🧩 组件设计指南](docs/component-design-guide.md) - 如何设计高质量组件 ⭐ **设计必读**
- [⚙️ 系统详解指南](docs/system-guide.md) - 四种系统类型的详细使用
- [🎬 场景管理指南](docs/scene-management-guide.md) - 场景切换和数据管理
- [⏰ 定时器系统指南](docs/timer-guide.md) - 定时器的完整使用方法
### API 参考
- [核心 API 参考](docs/core-concepts.md) - 完整的 API 使用说明
- [实体基础指南](docs/entity-guide.md) - 实体的基本概念和操作
- [EntityManager 指南](docs/entity-manager-example.md) - 高性能查询和批量操作
- [事件系统指南](docs/event-system-example.md) - 事件系统完整用法
- [查询系统指南](docs/query-system-usage.md) - 查询系统使用方法
### 性能相关
- [性能优化指南](docs/performance-optimization.md) - 性能优化技术和策略
## 构建
```bash
# 克隆项目
git clone https://github.com/esengine/ecs-framework.git
# 进入源码目录
cd ecs-framework/source
# 安装依赖
npm install
# 构建项目
npm run build
# 运行测试
npm test
# 监听模式
npm run build:watch
# 清理构建文件
npm run clean
# 重新构建
npm run rebuild
```
### 构建要求
## 性能监控
框架提供内置性能统计:
```typescript
// 场景统计
const sceneStats = scene.getStats();
console.log('性能统计:', {
实体数量: sceneStats.entityCount,
系统数量: sceneStats.processorCount
});
// 查询系统统计
const queryStats = scene.querySystem.getStats();
console.log('查询统计:', {
缓存命中率: queryStats.hitRate + '%',
查询次数: queryStats.queryCount
});
```
## 扩展库
- [路径寻找库](https://github.com/esengine/ecs-astar) - A*、BFS、Dijkstra 算法
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI
## 社区
- QQ 群:[ecs游戏框架交流](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
- GitHub[提交 Issue](https://github.com/esengine/ecs-framework/issues)
## 贡献
欢迎提交 Pull Request 和 Issue
### 开发要求
- Node.js >= 14.0.0
- TypeScript >= 4.0.0
## 📄 许可证
## 许可证
本项目采用 [MIT](LICENSE) 许可证。
## 💬 交流群
加入 QQ 群讨论:[ecs游戏框架交流](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
---
**ECS Framework** - 让游戏开发更简单、更高效!
[MIT](LICENSE)

View File

@@ -1,21 +1,53 @@
# Security Policy
# 安全政策
## Supported Versions
## 支持的版本
Use this section to tell people about which versions of your project are
currently being supported with security updates.
我们为以下版本提供安全更新:
| Version | Supported |
| 版本 | 支持状态 |
| ------- | ------------------ |
| 5.1.x | :white_check_mark: |
| 5.0.x | :x: |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |
| 2.0.x | :white_check_mark: |
| 1.0.x | :x: |
## Reporting a Vulnerability
## 报告漏洞
Use this section to tell people how to report a vulnerability.
如果您发现了安全漏洞,请通过以下方式报告:
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.
### 报告渠道
- **邮箱**: [安全邮箱将在实际部署时提供]
- **GitHub**: 创建私有安全报告(推荐)
### 报告流程
1. **不要**在公开的 issue 中报告安全漏洞
2. 提供详细的漏洞描述,包括:
- 受影响的版本
- 复现步骤
- 潜在的影响范围
- 如果可能,提供修复建议
### 响应时间
- **确认收到**: 72小时内
- **初步评估**: 1周内
- **修复发布**: 根据严重程度通常在2-4周内
### 处理流程
1. 我们会确认漏洞的存在和严重程度
2. 开发修复方案并进行测试
3. 发布安全更新
4. 在修复发布后,会在相关渠道公布漏洞详情
### 安全最佳实践
使用 ECS Framework 时,请遵循以下安全建议:
- 始终使用最新的稳定版本
- 定期更新依赖项
- 在生产环境中禁用调试模式
- 验证所有外部输入数据
- 不要在客户端存储敏感信息
感谢您帮助保持 ECS Framework 的安全性!

BIN
docs/beginner-tutorials.md Normal file

Binary file not shown.

431
docs/bitmask-guide.md Normal file
View File

@@ -0,0 +1,431 @@
# 位掩码使用指南
本文档详细解释ECS框架中位掩码的概念、原理和使用方法。
## 目录
1. [什么是位掩码](#什么是位掩码)
2. [位掩码的优势](#位掩码的优势)
3. [基础使用方法](#基础使用方法)
4. [高级位掩码操作](#高级位掩码操作)
5. [实际应用场景](#实际应用场景)
6. [性能优化技巧](#性能优化技巧)
## 什么是位掩码
### 基本概念
位掩码BitMask是一种使用二进制位来表示状态或属性的技术。在ECS框架中每个组件类型对应一个二进制位实体的组件组合可以用一个数字来表示。
### 简单例子
假设我们有以下组件:
- PositionComponent → 位置 0 (二进制: 001)
- VelocityComponent → 位置 1 (二进制: 010)
- HealthComponent → 位置 2 (二进制: 100)
那么一个同时拥有Position和Health组件的实体其位掩码就是
```
001 (Position) + 100 (Health) = 101 (二进制) = 5 (十进制)
```
### 可视化理解
```typescript
// 组件类型对应的位位置
PositionComponent 0 2^0 = 1 二进制: 001
VelocityComponent 1 2^1 = 2 二进制: 010
HealthComponent 2 2^2 = 4 二进制: 100
RenderComponent 3 2^3 = 8 二进制: 1000
// 实体的组件组合示例
实体A: Position + Velocity 001 + 010 = 011 () = 3 ()
实体B: Position + Health 001 + 100 = 101 () = 5 ()
实体C: Position + Velocity + Health 001 + 010 + 100 = 111 () = 7 ()
```
## 位掩码的优势
### 1. 极快的查询速度
```typescript
// 传统方式:需要遍历组件列表
function hasComponents(entity, componentTypes) {
for (const type of componentTypes) {
if (!entity.hasComponent(type)) {
return false;
}
}
return true;
}
// 位掩码方式:一次位运算即可
function hasComponentsMask(entityMask, requiredMask) {
return (entityMask & requiredMask) === requiredMask;
}
```
### 2. 内存效率
```typescript
// 一个bigint可以表示64个组件的组合状态
// 相比存储组件列表,内存使用量大大减少
const entity = scene.createEntity("Player");
entity.addComponent(new PositionComponent());
entity.addComponent(new HealthComponent());
// 获取位掩码(只是一个数字)
const mask = entity.componentMask; // bigint类型
console.log(`位掩码: ${mask}`); // 输出: 5 (二进制: 101)
```
### 3. 批量操作优化
```typescript
// 可以快速筛选大量实体
const entities = scene.getAllEntities();
const requiredMask = BigInt(0b101); // Position + Health
const filteredEntities = entities.filter(entity =>
(entity.componentMask & requiredMask) === requiredMask
);
```
## 基础使用方法
### 获取实体的位掩码
```typescript
import { Scene, Entity, Component } from '@esengine/ecs-framework';
// 创建组件
class PositionComponent extends Component {
constructor(public x: number = 0, public y: number = 0) {
super();
}
}
class HealthComponent extends Component {
constructor(public maxHealth: number = 100) {
super();
}
}
// 创建实体并添加组件
const scene = new Scene();
const entity = scene.createEntity("Player");
console.log(`初始位掩码: ${entity.componentMask}`); // 0
entity.addComponent(new PositionComponent(100, 200));
console.log(`添加Position后: ${entity.componentMask}`); // 可能是 1
entity.addComponent(new HealthComponent(100));
console.log(`添加Health后: ${entity.componentMask}`); // 可能是 5
// 查看二进制表示
console.log(`二进制表示: ${entity.componentMask.toString(2)}`);
```
### 手动检查位掩码
```typescript
// 检查实体是否拥有特定组件组合
function checkEntityComponents(entity: Entity) {
const mask = entity.componentMask;
// 将位掩码转换为二进制字符串查看
const binaryString = mask.toString(2).padStart(8, '0');
console.log(`实体组件状态: ${binaryString}`);
// 分析每一位
console.log(`位0 (Position): ${(mask & 1n) !== 0n ? '有' : '无'}`);
console.log(`位1 (Velocity): ${(mask & 2n) !== 0n ? '有' : '无'}`);
console.log(`位2 (Health): ${(mask & 4n) !== 0n ? '有' : '无'}`);
console.log(`位3 (Render): ${(mask & 8n) !== 0n ? '有' : '无'}`);
}
```
## 高级位掩码操作
### 使用BitMaskOptimizer
框架提供了BitMaskOptimizer类来简化位掩码操作
```typescript
import { BitMaskOptimizer } from '@esengine/ecs-framework';
// 获取优化器实例
const optimizer = BitMaskOptimizer.getInstance();
// 注册组件类型(建议在游戏初始化时进行)
optimizer.registerComponentType('PositionComponent');
optimizer.registerComponentType('VelocityComponent');
optimizer.registerComponentType('HealthComponent');
optimizer.registerComponentType('RenderComponent');
// 创建单个组件的掩码
const positionMask = optimizer.createSingleComponentMask('PositionComponent');
console.log(`Position掩码: ${positionMask} (二进制: ${positionMask.toString(2)})`);
// 创建组合掩码
const movementMask = optimizer.createCombinedMask(['PositionComponent', 'VelocityComponent']);
console.log(`Movement掩码: ${movementMask} (二进制: ${movementMask.toString(2)})`);
// 检查实体是否匹配掩码
const entity = scene.createEntity("TestEntity");
entity.addComponent(new PositionComponent());
entity.addComponent(new VelocityComponent());
const hasMovementComponents = optimizer.maskContainsAllComponents(
entity.componentMask,
['PositionComponent', 'VelocityComponent']
);
console.log(`实体拥有移动组件: ${hasMovementComponents}`);
```
### 位掩码分析工具
```typescript
// 分析位掩码的实用函数
class MaskAnalyzer {
private optimizer = BitMaskOptimizer.getInstance();
// 分析实体的组件组合
analyzeEntity(entity: Entity): void {
const mask = entity.componentMask;
const componentNames = this.optimizer.maskToComponentNames(mask);
const componentCount = this.optimizer.getComponentCount(mask);
console.log(`实体 ${entity.name} 分析:`);
console.log(`- 位掩码: ${mask} (二进制: ${mask.toString(2)})`);
console.log(`- 组件数量: ${componentCount}`);
console.log(`- 组件列表: ${componentNames.join(', ')}`);
}
// 比较两个实体的组件差异
compareEntities(entityA: Entity, entityB: Entity): void {
const maskA = entityA.componentMask;
const maskB = entityB.componentMask;
const commonMask = maskA & maskB;
const onlyInA = maskA & ~maskB;
const onlyInB = maskB & ~maskA;
console.log(`实体比较:`);
console.log(`- 共同组件: ${this.optimizer.maskToComponentNames(commonMask).join(', ')}`);
console.log(`- 仅在A中: ${this.optimizer.maskToComponentNames(onlyInA).join(', ')}`);
console.log(`- 仅在B中: ${this.optimizer.maskToComponentNames(onlyInB).join(', ')}`);
}
// 查找具有特定组件组合的实体
findEntitiesWithMask(entities: Entity[], requiredComponents: string[]): Entity[] {
const requiredMask = this.optimizer.createCombinedMask(requiredComponents);
return entities.filter(entity =>
(entity.componentMask & requiredMask) === requiredMask
);
}
}
// 使用示例
const analyzer = new MaskAnalyzer();
analyzer.analyzeEntity(entity);
```
## 实际应用场景
### 1. 高性能实体查询
```typescript
class GameSystem {
private optimizer = BitMaskOptimizer.getInstance();
private movementMask: bigint;
private combatMask: bigint;
constructor() {
// 预计算常用掩码
this.movementMask = this.optimizer.createCombinedMask([
'PositionComponent', 'VelocityComponent'
]);
this.combatMask = this.optimizer.createCombinedMask([
'PositionComponent', 'HealthComponent', 'WeaponComponent'
]);
}
// 快速查找移动实体
findMovingEntities(entities: Entity[]): Entity[] {
return entities.filter(entity =>
(entity.componentMask & this.movementMask) === this.movementMask
);
}
// 快速查找战斗单位
findCombatUnits(entities: Entity[]): Entity[] {
return entities.filter(entity =>
(entity.componentMask & this.combatMask) === this.combatMask
);
}
}
```
### 2. 实体分类和管理
```typescript
class EntityClassifier {
private optimizer = BitMaskOptimizer.getInstance();
// 定义实体类型掩码
private readonly ENTITY_TYPES = {
PLAYER: this.optimizer.createCombinedMask([
'PositionComponent', 'HealthComponent', 'InputComponent'
]),
ENEMY: this.optimizer.createCombinedMask([
'PositionComponent', 'HealthComponent', 'AIComponent'
]),
PROJECTILE: this.optimizer.createCombinedMask([
'PositionComponent', 'VelocityComponent', 'DamageComponent'
]),
PICKUP: this.optimizer.createCombinedMask([
'PositionComponent', 'PickupComponent'
])
};
// 根据组件组合判断实体类型
classifyEntity(entity: Entity): string {
const mask = entity.componentMask;
for (const [type, typeMask] of Object.entries(this.ENTITY_TYPES)) {
if ((mask & typeMask) === typeMask) {
return type;
}
}
return 'UNKNOWN';
}
// 批量分类实体
classifyEntities(entities: Entity[]): Map<string, Entity[]> {
const classified = new Map<string, Entity[]>();
for (const entity of entities) {
const type = this.classifyEntity(entity);
if (!classified.has(type)) {
classified.set(type, []);
}
classified.get(type)!.push(entity);
}
return classified;
}
}
```
## 性能优化技巧
### 1. 预计算常用掩码
```typescript
class MaskCache {
private optimizer = BitMaskOptimizer.getInstance();
// 预计算游戏中常用的组件组合
public readonly COMMON_MASKS = {
RENDERABLE: this.optimizer.createCombinedMask([
'PositionComponent', 'RenderComponent'
]),
MOVABLE: this.optimizer.createCombinedMask([
'PositionComponent', 'VelocityComponent'
]),
LIVING: this.optimizer.createCombinedMask([
'HealthComponent'
]),
INTERACTIVE: this.optimizer.createCombinedMask([
'PositionComponent', 'ColliderComponent'
])
};
constructor() {
// 预计算常用组合以提升性能
this.optimizer.precomputeCommonMasks([
['PositionComponent', 'RenderComponent'],
['PositionComponent', 'VelocityComponent'],
['PositionComponent', 'HealthComponent', 'WeaponComponent']
]);
}
}
```
### 2. 位掩码调试工具
```typescript
// 位掩码调试工具
class MaskDebugger {
private optimizer = BitMaskOptimizer.getInstance();
// 可视化位掩码
visualizeMask(mask: bigint, maxBits: number = 16): string {
const binary = mask.toString(2).padStart(maxBits, '0');
const components = this.optimizer.maskToComponentNames(mask);
let visualization = `位掩码: ${mask} (二进制: ${binary})\n`;
visualization += `组件: ${components.join(', ')}\n`;
visualization += `可视化: `;
for (let i = maxBits - 1; i >= 0; i--) {
const bit = (mask & (1n << BigInt(i))) !== 0n ? '1' : '0';
visualization += bit;
if (i % 4 === 0 && i > 0) visualization += ' ';
}
return visualization;
}
}
```
## 最佳实践
### 1. 组件注册
```typescript
// 在游戏初始化时注册所有组件类型
function initializeComponentTypes() {
const optimizer = BitMaskOptimizer.getInstance();
// 按使用频率注册(常用的组件分配较小的位位置)
optimizer.registerComponentType('PositionComponent'); // 位置0
optimizer.registerComponentType('VelocityComponent'); // 位置1
optimizer.registerComponentType('HealthComponent'); // 位置2
optimizer.registerComponentType('RenderComponent'); // 位置3
// ... 其他组件
}
```
### 2. 掩码缓存管理
```typescript
// 定期清理掩码缓存以避免内存泄漏
setInterval(() => {
const optimizer = BitMaskOptimizer.getInstance();
const stats = optimizer.getCacheStats();
// 如果缓存过大,清理一部分
if (stats.size > 1000) {
optimizer.clearCache();
console.log('位掩码缓存已清理');
}
}, 60000); // 每分钟检查一次
```
## 总结
位掩码是ECS框架中的核心优化技术它提供了
1. **极快的查询速度** - 位运算比遍历快数百倍
2. **内存效率** - 用一个数字表示复杂的组件组合
3. **批量操作优化** - 可以快速处理大量实体
4. **灵活的查询构建** - 支持复杂的组件组合查询
通过理解和正确使用位掩码,可以显著提升游戏的性能表现。记住要在游戏初始化时注册组件类型,预计算常用掩码,并合理管理缓存。

View File

@@ -0,0 +1,692 @@
# 组件设计最佳实践指南
组件是ECS架构的核心设计良好的组件是构建高质量游戏的基础。本指南将教你如何设计出清晰、高效、可维护的组件。
## 组件设计原则
### 1. 数据为主,逻辑为辅
**核心理念:** 组件主要存储数据,复杂逻辑放在系统中处理。
```typescript
// ✅ 好的设计:主要是数据
class HealthComponent extends Component {
public maxHealth: number;
public currentHealth: number;
public regenRate: number = 0;
public lastDamageTime: number = 0;
constructor(maxHealth: number = 100) {
super();
this.maxHealth = maxHealth;
this.currentHealth = maxHealth;
}
// 简单的辅助方法是可以的
isDead(): boolean {
return this.currentHealth <= 0;
}
getHealthPercentage(): number {
return this.currentHealth / this.maxHealth;
}
}
// ❌ 不好的设计:包含太多逻辑
class BadHealthComponent extends Component {
public maxHealth: number;
public currentHealth: number;
takeDamage(damage: number) {
this.currentHealth -= damage;
// 这些逻辑应该在系统中处理
if (this.currentHealth <= 0) {
this.entity.destroy(); // 销毁逻辑
this.playDeathSound(); // 音效逻辑
this.createDeathEffect(); // 特效逻辑
this.updatePlayerScore(100); // 分数逻辑
}
}
}
```
### 2. 单一职责原则
每个组件只负责一个方面的数据。
```typescript
// ✅ 好的设计:单一职责
class PositionComponent extends Component {
public x: number = 0;
public y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
class VelocityComponent extends Component {
public x: number = 0;
public y: number = 0;
public maxSpeed: number = 100;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
class RotationComponent extends Component {
public angle: number = 0;
public angularVelocity: number = 0;
constructor(angle: number = 0) {
super();
this.angle = angle;
}
}
// ❌ 不好的设计:职责混乱
class TransformComponent extends Component {
public x: number = 0;
public y: number = 0;
public velocityX: number = 0;
public velocityY: number = 0;
public angle: number = 0;
public scale: number = 1;
public health: number = 100; // 和变换无关
public ammo: number = 30; // 和变换无关
}
```
### 3. 组合优于继承
使用多个小组件组合,而不是大而全的组件继承。
```typescript
// ✅ 好的设计:组合方式
class Player {
constructor(scene: Scene) {
const player = scene.createEntity("Player");
// 通过组合不同组件实现功能
player.addComponent(new PositionComponent(100, 100));
player.addComponent(new VelocityComponent());
player.addComponent(new HealthComponent(100));
player.addComponent(new PlayerInputComponent());
player.addComponent(new WeaponComponent());
player.addComponent(new InventoryComponent());
return player;
}
}
// 创建不同类型的实体很容易
class Enemy {
constructor(scene: Scene) {
const enemy = scene.createEntity("Enemy");
// 复用相同的组件,但组合不同
enemy.addComponent(new PositionComponent(200, 200));
enemy.addComponent(new VelocityComponent());
enemy.addComponent(new HealthComponent(50));
enemy.addComponent(new AIComponent()); // 不同AI而不是玩家输入
enemy.addComponent(new WeaponComponent()); // 相同:都有武器
// 没有库存组件
return enemy;
}
}
// ❌ 不好的设计:继承方式
class GameObject {
public x: number;
public y: number;
public health: number;
// ... 很多属性
}
class PlayerGameObject extends GameObject {
public input: InputData;
public inventory: Item[];
// 强制继承了不需要的属性
}
class EnemyGameObject extends GameObject {
public ai: AIData;
// 继承了不需要的库存等属性
}
```
## 常见组件类型和设计
### 1. 数据组件Data Components
纯数据存储,没有或很少有方法。
```typescript
// 位置信息
class PositionComponent extends Component {
public x: number;
public y: number;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
// 简单的辅助方法
distanceTo(other: PositionComponent): number {
const dx = this.x - other.x;
const dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
set(x: number, y: number) {
this.x = x;
this.y = y;
}
}
// 统计信息
class StatsComponent extends Component {
public strength: number = 10;
public agility: number = 10;
public intelligence: number = 10;
public vitality: number = 10;
// 计算派生属性
getMaxHealth(): number {
return this.vitality * 10;
}
getDamage(): number {
return this.strength * 2;
}
getMoveSpeed(): number {
return this.agility * 5;
}
}
// 渲染信息
class SpriteComponent extends Component {
public textureName: string;
public width: number;
public height: number;
public tint: number = 0xFFFFFF;
public alpha: number = 1.0;
public visible: boolean = true;
constructor(textureName: string, width: number = 0, height: number = 0) {
super();
this.textureName = textureName;
this.width = width;
this.height = height;
}
}
```
### 2. 标记组件Tag Components
用于标识实体状态或类型的空组件。
```typescript
// 标记组件通常不包含数据
class PlayerComponent extends Component {
// 空组件,仅用于标记这是玩家实体
}
class EnemyComponent extends Component {
// 空组件,仅用于标记这是敌人实体
}
class DeadComponent extends Component {
// 标记实体已死亡
public deathTime: number;
constructor() {
super();
this.deathTime = Time.totalTime;
}
}
class InvincibleComponent extends Component {
// 标记实体无敌状态
public duration: number;
constructor(duration: number = 2.0) {
super();
this.duration = duration;
}
}
// 使用标记组件进行查询
class GameSystem {
updatePlayers() {
// 只处理玩家实体
const players = this.scene.findEntitiesWithComponent(PlayerComponent);
// ...
}
updateEnemies() {
// 只处理敌人实体
const enemies = this.scene.findEntitiesWithComponent(EnemyComponent);
// ...
}
}
```
### 3. 行为组件Behavior Components
包含简单行为逻辑的组件。
```typescript
class WeaponComponent extends Component {
public damage: number;
public fireRate: number;
public ammo: number;
public maxAmmo: number;
public lastFireTime: number = 0;
constructor(damage: number = 10, fireRate: number = 0.5) {
super();
this.damage = damage;
this.fireRate = fireRate;
this.maxAmmo = 30;
this.ammo = this.maxAmmo;
}
canFire(): boolean {
return this.ammo > 0 &&
Time.totalTime - this.lastFireTime >= this.fireRate;
}
fire(): boolean {
if (this.canFire()) {
this.ammo--;
this.lastFireTime = Time.totalTime;
return true;
}
return false;
}
reload() {
this.ammo = this.maxAmmo;
}
getAmmoPercentage(): number {
return this.ammo / this.maxAmmo;
}
}
class InventoryComponent extends Component {
private items: Map<string, number> = new Map();
public maxCapacity: number = 20;
addItem(itemType: string, quantity: number = 1): boolean {
if (this.getTotalItems() + quantity > this.maxCapacity) {
return false;
}
const current = this.items.get(itemType) || 0;
this.items.set(itemType, current + quantity);
return true;
}
removeItem(itemType: string, quantity: number = 1): boolean {
const current = this.items.get(itemType) || 0;
if (current < quantity) {
return false;
}
const newAmount = current - quantity;
if (newAmount === 0) {
this.items.delete(itemType);
} else {
this.items.set(itemType, newAmount);
}
return true;
}
hasItem(itemType: string, quantity: number = 1): boolean {
const current = this.items.get(itemType) || 0;
return current >= quantity;
}
getTotalItems(): number {
let total = 0;
this.items.forEach(quantity => total += quantity);
return total;
}
getItems(): Map<string, number> {
return new Map(this.items); // 返回副本
}
}
```
## 组件通信和依赖
### 1. 组件间通信
组件间不应直接通信,通过系统或事件系统进行通信。
```typescript
// ✅ 好的设计:通过事件通信
class HealthComponent extends Component {
public currentHealth: number;
public maxHealth: number;
takeDamage(damage: number) {
this.currentHealth -= damage;
// 发送事件,让其他系统响应
// 注意需要在实际使用中获取EntityManager实例
// 示例entityManager.eventBus.emit('health:damaged', {...});
if (this.currentHealth <= 0) {
// 示例entityManager.eventBus.emit('health:died', {...});
console.log('实体死亡');
}
}
}
// 其他组件响应事件
class AnimationComponent extends Component {
onAddedToEntity() {
super.onAddedToEntity();
// 监听受伤事件需要在实际使用中获取EntityManager实例
// 示例entityManager.eventBus.on('health:damaged', this.onDamaged, { context: this });
}
onRemovedFromEntity() {
// 事件监听会在组件移除时自动清理
// 如需手动清理保存listenerId并调用eventBus.off()
super.onRemovedFromEntity();
}
private onDamaged(data: any) {
if (data.entity === this.entity) {
this.playHurtAnimation();
}
}
}
// ❌ 不好的设计:直接依赖其他组件
class BadHealthComponent extends Component {
takeDamage(damage: number) {
this.currentHealth -= damage;
// 直接操作其他组件
const animation = this.entity.getComponent(AnimationComponent);
if (animation) {
animation.playHurtAnimation(); // 紧耦合
}
const sound = this.entity.getComponent(SoundComponent);
if (sound) {
sound.playHurtSound(); // 紧耦合
}
}
}
```
### 2. 可选依赖
有时组件需要其他组件配合工作,但应该优雅处理缺失的情况。
```typescript
class MovementComponent extends Component {
public speed: number = 100;
update() {
// 可选依赖:输入组件
const input = this.entity.getComponent(InputComponent);
const velocity = this.entity.getComponent(VelocityComponent);
if (input && velocity) {
// 根据输入设置速度
velocity.x = input.horizontal * this.speed;
velocity.y = input.vertical * this.speed;
}
// 可选依赖AI组件
const ai = this.entity.getComponent(AIComponent);
if (ai && velocity && !input) {
// AI控制移动如果没有输入
velocity.x = ai.moveDirection.x * this.speed;
velocity.y = ai.moveDirection.y * this.speed;
}
}
}
```
## 组件性能优化
### 1. 对象池优化
对于频繁创建/销毁的组件,使用对象池。
```typescript
class PooledBulletComponent extends Component {
public damage: number = 10;
public speed: number = 200;
public direction: { x: number; y: number } = { x: 0, y: 0 };
public lifetime: number = 5.0;
private currentLifetime: number = 0;
// 重置组件状态,用于对象池
reset() {
this.damage = 10;
this.speed = 200;
this.direction.set(0, 0);
this.lifetime = 5.0;
this.currentLifetime = 0;
}
// 配置子弹
configure(damage: number, speed: number, direction: { x: number; y: number }) {
this.damage = damage;
this.speed = speed;
this.direction = direction.copy();
}
update() {
this.currentLifetime += Time.deltaTime;
if (this.currentLifetime >= this.lifetime) {
// 生命周期结束,回收到对象池
BulletPool.release(this.entity);
}
}
}
// 对象池管理
class BulletPool {
private static pool: Entity[] = [];
static get(): Entity {
if (this.pool.length > 0) {
const bullet = this.pool.pop()!;
bullet.enabled = true;
return bullet;
} else {
return this.createBullet();
}
}
static release(bullet: Entity) {
bullet.enabled = false;
bullet.getComponent(PooledBulletComponent)?.reset();
this.pool.push(bullet);
}
private static createBullet(): Entity {
const bullet = Core.scene.createEntity("Bullet");
bullet.addComponent(new PooledBulletComponent());
bullet.addComponent(new PositionComponent());
bullet.addComponent(new VelocityComponent());
return bullet;
}
}
```
### 2. 数据紧凑性
保持组件数据紧凑,避免不必要的对象分配。
```typescript
// ✅ 好的设计:紧凑的数据结构
class ParticleComponent extends Component {
// 使用基本类型,避免对象分配
public x: number = 0;
public y: number = 0;
public velocityX: number = 0;
public velocityY: number = 0;
public life: number = 1.0;
public maxLife: number = 1.0;
public size: number = 1.0;
public color: number = 0xFFFFFF;
// 计算属性,避免存储冗余数据
get alpha(): number {
return this.life / this.maxLife;
}
}
// ❌ 不好的设计:过多对象分配
class BadParticleComponent extends Component {
public position: { x: number; y: number } = { x: 0, y: 0 }; // 对象分配
public velocity: { x: number; y: number } = { x: 0, y: 0 }; // 对象分配
public color: Color = new Color(); // 对象分配
public transform: Transform = new Transform(); // 对象分配
// 冗余数据
public alpha: number = 1.0;
public life: number = 1.0;
public maxLife: number = 1.0;
}
```
## 组件调试和测试
### 1. 调试友好的组件
```typescript
class DebugFriendlyComponent extends Component {
public someValue: number = 0;
private debugName: string;
constructor(debugName: string = "Unknown") {
super();
this.debugName = debugName;
}
// 提供有用的调试信息
toString(): string {
return `${this.constructor.name}(${this.debugName}): value=${this.someValue}`;
}
// 验证组件状态
validate(): boolean {
if (this.someValue < 0) {
console.warn(`${this} has invalid value: ${this.someValue}`);
return false;
}
return true;
}
// 获取调试信息
getDebugInfo(): any {
return {
name: this.debugName,
value: this.someValue,
entityId: this.entity?.id,
isValid: this.validate()
};
}
}
```
### 2. 单元测试
```typescript
// 组件测试示例
describe('HealthComponent', () => {
let healthComponent: HealthComponent;
beforeEach(() => {
healthComponent = new HealthComponent(100);
});
test('初始状态正确', () => {
expect(healthComponent.currentHealth).toBe(100);
expect(healthComponent.maxHealth).toBe(100);
expect(healthComponent.isDead()).toBe(false);
});
test('受伤功能正确', () => {
healthComponent.takeDamage(30);
expect(healthComponent.currentHealth).toBe(70);
expect(healthComponent.getHealthPercentage()).toBe(0.7);
});
test('死亡检测正确', () => {
healthComponent.takeDamage(100);
expect(healthComponent.isDead()).toBe(true);
});
});
```
## 常见问题和最佳实践
### Q: 组件应该有多大?
A: 组件应该尽可能小和专注。如果一个组件有超过10个字段考虑拆分。
### Q: 组件可以包含方法吗?
A: 可以,但应该是简单的辅助方法。复杂逻辑应该在系统中处理。
### Q: 如何处理组件之间的依赖?
A:
1. 优先使用组合而不是依赖
2. 通过事件系统通信
3. 在系统中处理组件间的协调
### Q: 什么时候使用继承?
A: 很少使用。只在有明确的"是一个"关系时使用,如:
```typescript
abstract class ColliderComponent extends Component {
abstract checkCollision(other: ColliderComponent): boolean;
}
class CircleColliderComponent extends ColliderComponent {
public radius: number;
checkCollision(other: ColliderComponent): boolean {
// 圆形碰撞检测
}
}
class BoxColliderComponent extends ColliderComponent {
public width: number;
public height: number;
checkCollision(other: ColliderComponent): boolean {
// 方形碰撞检测
}
}
```
遵循这些原则,你就能设计出高质量、易维护的组件系统!

665
docs/concepts-explained.md Normal file
View File

@@ -0,0 +1,665 @@
# 技术概念详解
本文档用通俗易懂的语言解释ECS框架中的关键技术概念帮助开发者理解这些技术的作用和应用场景。
## 目录
- [ECS 架构基础](#ecs-架构基础)
- [性能优化技术](#性能优化技术)
- [事件系统](#事件系统)
- [实体管理](#实体管理)
## ECS 架构基础
### 什么是 ECS
ECS (Entity-Component-System) 是一种编程架构模式,将游戏对象分解为三个独立的部分:
**传统面向对象方式:**
```typescript
// 传统继承方式 - 问题很多
class GameObject {
x: number; y: number;
render() { ... }
update() { ... }
}
class Player extends GameObject {
health: number;
shoot() { ... }
}
class Enemy extends Player { // 敌人需要射击但不需要玩家控制?
ai() { ... }
}
```
**ECS 方式:**
```typescript
// 数据和逻辑分离,灵活组合
const player = createEntity()
.add(PositionComponent) // 位置数据
.add(HealthComponent) // 生命值数据
.add(PlayerInputComponent) // 玩家输入标记
const enemy = createEntity()
.add(PositionComponent) // 复用位置数据
.add(HealthComponent) // 复用生命值数据
.add(AIComponent) // AI标记
// 系统处理具有特定组件的实体
MovementSystem.process([PositionComponent, VelocityComponent]);
```
### ECS 的优势
1. **灵活组合** - 像搭积木一样组装功能
2. **代码复用** - 组件可以在不同实体间复用
3. **性能优化** - 数据连续存储,缓存友好
4. **并行处理** - 系统间相互独立,可以并行执行
5. **易于测试** - 组件和系统可以独立测试
### 实际应用场景
**游戏开发中的例子:**
- **RPG游戏**玩家、NPC、怪物都有位置和生命值但只有玩家有输入组件
- **射击游戏**:子弹、玩家、敌人都有位置和碰撞体,但行为完全不同
- **策略游戏**:建筑、单位、资源都是实体,通过不同组件组合实现功能
## 性能优化技术
### 组件索引系统
**问题:** 没有索引时,查找组件需要遍历所有实体
```typescript
// 慢的方式:线性搜索 O(n)
function findEntitiesWithHealth() {
const result = [];
for (const entity of allEntities) { // 遍历10万个实体
if (entity.hasComponent(HealthComponent)) {
result.push(entity);
}
}
return result;
}
```
**解决方案:** 建立索引,直接访问
```typescript
// 快的方式:索引查找 O(1)
const healthIndex = componentIndex.get(HealthComponent);
const entitiesWithHealth = healthIndex.getEntities(); // 直接获取
```
**应用场景:**
- 频繁查询特定组件的实体
- 大规模实体场景(数千到数万个实体)
- 实时游戏中的系统更新
### 索引类型选择指南
框架提供两种索引类型,选择合适的类型对性能至关重要:
#### 🔸 哈希索引 (Hash Index)
**适用场景:**
- 实体数量较多(> 1000个
- 组件数据变化不频繁
- 需要快速查找特定实体
**优势:**
- 查询速度极快 O(1)
- 内存使用相对较少
- 适合大量实体
**缺点:**
- 添加/删除组件时有额外开销
- 不适合频繁变化的组件
```typescript
// 适合哈希索引的组件
componentIndex.setIndexType(PositionComponent, 'hash'); // 位置变化不频繁
componentIndex.setIndexType(HealthComponent, 'hash'); // 生命值组件稳定
componentIndex.setIndexType(PlayerComponent, 'hash'); // 玩家标记组件
```
#### 🔹 位图索引 (Bitmap Index)
**适用场景:**
- 组件频繁添加/删除
- 实体数量适中(< 10000个
- 需要批量操作
**优势:**
- 添加/删除组件极快
- 批量查询效率高
- 内存访问模式好
**缺点:**
- 随实体数量增长,内存占用增加
- 稀疏数据时效率降低
```typescript
// 适合位图索引的组件
componentIndex.setIndexType(BuffComponent, 'bitmap'); // Buff经常添加删除
componentIndex.setIndexType(TemporaryComponent, 'bitmap'); // 临时组件
componentIndex.setIndexType(StateComponent, 'bitmap'); // 状态组件变化频繁
```
#### 📊 选择决策表
| 考虑因素 | 哈希索引 (Hash) | 位图索引 (Bitmap) |
|---------|----------------|-------------------|
| **实体数量** | > 1,000 | < 10,000 |
| **组件变化频率** | 低频变化 | 高频变化 |
| **查询频率** | 高频查询 | 中等查询 |
| **内存使用** | 较少 | 随实体数增加 |
| **批量操作** | 一般 | 优秀 |
#### 🤔 快速决策流程
**第一步:判断组件变化频率**
- 组件经常添加/删除? → 选择 **位图索引**
- 组件相对稳定? → 继续第二步
**第二步:判断实体数量**
- 实体数量 > 1000 → 选择 **哈希索引**
- 实体数量 < 1000 → 选择 **位图索引**
**第三步:特殊情况**
- 需要频繁批量操作? → 选择 **位图索引**
- 内存使用很重要? → 选择 **哈希索引**
#### 🎮 实际游戏中的选择示例
**射击游戏:**
```typescript
// 稳定组件用哈希索引
componentIndex.setIndexType(PositionComponent, 'hash'); // 实体位置稳定存在
componentIndex.setIndexType(HealthComponent, 'hash'); // 生命值组件持续存在
componentIndex.setIndexType(WeaponComponent, 'hash'); // 武器组件不常变化
// 变化组件用位图索引
componentIndex.setIndexType(BuffComponent, 'bitmap'); // Buff频繁添加删除
componentIndex.setIndexType(ReloadingComponent, 'bitmap'); // 装弹状态临时组件
```
**策略游戏:**
```typescript
// 大量单位,核心组件用哈希
componentIndex.setIndexType(UnitComponent, 'hash'); // 单位类型稳定
componentIndex.setIndexType(OwnerComponent, 'hash'); // 所属玩家稳定
// 状态组件用位图
componentIndex.setIndexType(SelectedComponent, 'bitmap'); // 选中状态常变化
componentIndex.setIndexType(MovingComponent, 'bitmap'); // 移动状态变化
componentIndex.setIndexType(AttackingComponent, 'bitmap'); // 攻击状态临时
```
**RPG游戏**
```typescript
// 角色核心属性用哈希
componentIndex.setIndexType(StatsComponent, 'hash'); // 属性组件稳定
componentIndex.setIndexType(InventoryComponent, 'hash'); // 背包组件稳定
componentIndex.setIndexType(LevelComponent, 'hash'); // 等级组件稳定
// 临时状态用位图
componentIndex.setIndexType(StatusEffectComponent, 'bitmap'); // 状态效果变化
componentIndex.setIndexType(QuestComponent, 'bitmap'); // 任务状态变化
componentIndex.setIndexType(CombatComponent, 'bitmap'); // 战斗状态临时
```
#### ❌ 常见选择错误
**错误示例1大量实体使用位图索引**
```typescript
// ❌ 错误10万个单位用位图索引内存爆炸
const entityCount = 100000;
componentIndex.setIndexType(UnitComponent, 'bitmap'); // 内存占用过大!
// ✅ 正确:大量实体用哈希索引
componentIndex.setIndexType(UnitComponent, 'hash');
```
**错误示例2频繁变化组件用哈希索引**
```typescript
// ❌ 错误Buff频繁添加删除哈希索引效率低
componentIndex.setIndexType(BuffComponent, 'hash'); // 添加删除慢!
// ✅ 正确:变化频繁的组件用位图索引
componentIndex.setIndexType(BuffComponent, 'bitmap');
```
**错误示例3不考虑实际使用场景**
```typescript
// ❌ 错误:所有组件都用同一种索引
componentIndex.setIndexType(PositionComponent, 'hash');
componentIndex.setIndexType(BuffComponent, 'hash'); // 应该用bitmap
componentIndex.setIndexType(TemporaryComponent, 'hash'); // 应该用bitmap
// ✅ 正确:根据组件特性选择
componentIndex.setIndexType(PositionComponent, 'hash'); // 稳定组件
componentIndex.setIndexType(BuffComponent, 'bitmap'); // 变化组件
componentIndex.setIndexType(TemporaryComponent, 'bitmap'); // 临时组件
```
### Archetype 系统
**什么是 Archetype**
Archetype原型是具有相同组件组合的实体分组。
**没有 Archetype 的问题:**
```typescript
// 每次都要检查每个实体的组件组合
for (const entity of allEntities) {
if (entity.has(Position) && entity.has(Velocity) && !entity.has(Frozen)) {
// 处理移动
}
}
```
**Archetype 的解决方案:**
```typescript
// 实体按组件组合自动分组
const movableArchetype = [Position, Velocity, !Frozen];
const movableEntities = archetypeSystem.getEntities(movableArchetype);
// 直接处理,无需逐个检查
```
**应用场景:**
- 大量实体的游戏RTS、MMO
- 频繁的实体查询操作
- 批量处理相同类型的实体
### 脏标记系统
**什么是脏标记?**
脏标记Dirty Tracking追踪哪些数据发生了变化避免处理未变化的数据。
**没有脏标记的问题:**
```typescript
// 每帧都重新计算所有实体,即使它们没有移动
function renderSystem() {
for (const entity of entities) {
updateRenderPosition(entity); // 浪费计算
updateRenderRotation(entity); // 浪费计算
updateRenderScale(entity); // 浪费计算
}
}
```
**脏标记的解决方案:**
```typescript
// 只处理发生变化的实体
function renderSystem() {
const dirtyEntities = dirtyTracking.getDirtyEntities();
for (const entity of dirtyEntities) {
if (dirtyTracking.isDirty(entity, PositionComponent)) {
updateRenderPosition(entity); // 只在需要时计算
}
if (dirtyTracking.isDirty(entity, RotationComponent)) {
updateRenderRotation(entity);
}
}
dirtyTracking.clearDirtyFlags();
}
```
**应用场景:**
- 渲染系统优化(只更新变化的物体)
- 物理系统优化(只计算移动的物体)
- UI更新优化只刷新变化的界面元素
- 网络同步优化(只发送变化的数据)
**实际例子:**
```typescript
// 游戏中的应用
class MovementSystem {
process() {
// 玩家移动时标记为脏
if (playerInput.moved) {
dirtyTracking.markDirty(player, PositionComponent);
}
// 静止的敌人不会被标记为脏,渲染系统会跳过它们
}
}
```
## 事件系统
### 类型安全事件
**传统事件的问题:**
```typescript
// 类型不安全,容易出错
eventEmitter.emit('player_died', playerData);
eventEmitter.on('player_dead', handler); // 事件名拼写错误!
```
**类型安全事件的解决方案:**
```typescript
// 编译时检查,避免错误
enum GameEvents {
PLAYER_DIED = 'player:died',
LEVEL_COMPLETED = 'level:completed'
}
eventBus.emit(GameEvents.PLAYER_DIED, { playerId: 123 });
eventBus.on(GameEvents.PLAYER_DIED, (data) => {
// data 类型自动推断
});
```
### 事件装饰器
**什么是装饰器?**
装饰器让你用简单的语法自动注册事件监听器。
**传统方式:**
```typescript
class GameManager {
constructor() {
// 手动注册事件
eventBus.on('entity:created', this.onEntityCreated.bind(this));
eventBus.on('entity:destroyed', this.onEntityDestroyed.bind(this));
eventBus.on('component:added', this.onComponentAdded.bind(this));
}
onEntityCreated(data) { ... }
onEntityDestroyed(data) { ... }
onComponentAdded(data) { ... }
}
```
**装饰器方式:**
```typescript
class GameManager {
@EventHandler('entity:created')
onEntityCreated(data) { ... } // 自动注册
@EventHandler('entity:destroyed')
onEntityDestroyed(data) { ... } // 自动注册
@EventHandler('component:added')
onComponentAdded(data) { ... } // 自动注册
}
```
**应用场景:**
- 游戏状态管理
- UI更新响应
- 音效播放触发
- 成就系统检查
## 实体管理
### 实体生命周期
**创建实体的不同方式:**
```typescript
// 单个创建 - 适用于重要实体
const player = scene.createEntity("Player");
// 批量创建 - 适用于大量相似实体
const bullets = scene.createEntities(100, "Bullet");
// 延迟创建 - 避免性能峰值
// 分批创建大量实体以避免单帧卡顿
for (let i = 0; i < 100; i++) {
setTimeout(() => {
const batch = scene.createEntities(10, "Enemy");
// 配置批次实体...
}, i * 16); // 每16ms创建一批
}
```
### 查询系统
**流式API的优势**
```typescript
// 传统方式:复杂的条件判断
const result = [];
for (const entity of entities) {
if (entity.has(Position) &&
entity.has(Velocity) &&
!entity.has(Frozen) &&
entity.tag === EntityTag.ENEMY) {
result.push(entity);
}
}
// 流式API清晰表达意图
const result = entityManager
.query()
.withAll(Position, Velocity)
.withNone(Frozen)
.withTag(EntityTag.ENEMY)
.execute();
```
### 批量操作
**为什么需要批量操作?**
```typescript
// 慢的方式:逐个处理
for (let i = 0; i < 1000; i++) {
const bullet = createEntity();
bullet.addComponent(new PositionComponent());
bullet.addComponent(new VelocityComponent());
}
// 快的方式:批量处理
const bullets = scene.createEntities(1000, "Bullet");
bullets.forEach(bullet => {
bullet.addComponent(new PositionComponent());
bullet.addComponent(new VelocityComponent());
});
```
**应用场景:**
- 生成大量子弹/粒子
- 加载关卡时创建大量实体
- 清理场景时删除大量实体
## 性能建议
### 什么时候使用这些优化?
| 实体数量 | 推荐配置 | 说明 |
|---------|---------|------|
| < 1,000 | 默认配置 | 简单场景,不需要特殊优化 |
| 1,000 - 10,000 | 启用组件索引 | 中等规模,索引提升查询速度 |
| 10,000 - 50,000 | 启用Archetype | 大规模场景,分组优化 |
| > 50,000 | 全部优化 | 超大规模,需要所有优化技术 |
### 常见使用误区
**错误:过度优化**
```typescript
// 不要在小项目中使用所有优化
const entityManager = new EntityManager();
entityManager.enableAllOptimizations(); // 小项目不需要
```
**正确:按需优化**
```typescript
// 根据实际需求选择优化
if (entityCount > 10000) {
entityManager.enableArchetypeSystem();
}
if (hasFrequentQueries) {
entityManager.enableComponentIndex();
}
```
## 总结
这些技术概念可能看起来复杂,但它们解决的都是实际开发中的具体问题:
1. **ECS架构** - 让代码更灵活、可维护
2. **组件索引** - 让查询更快速
3. **Archetype系统** - 让批量操作更高效
4. **脏标记系统** - 让更新更智能
5. **事件系统** - 让组件间通信更安全
6. **实体管理** - 让大规模场景成为可能
从简单的场景开始,随着项目复杂度增加,逐步引入这些优化技术。
## 框架类型系统
### TypeScript接口设计
ECS框架采用了精简的TypeScript接口设计提供类型安全保障的同时保持实现的灵活性。
#### 核心接口
**IComponent接口**
```typescript
interface IComponent {
readonly id: number;
enabled: boolean;
updateOrder: number;
onAddedToEntity(): void;
onRemovedFromEntity(): void;
onEnabled(): void;
onDisabled(): void;
update(): void;
}
```
- 定义所有组件的基本契约
- Component基类实现此接口
- 确保组件生命周期方法的一致性
**ISystemBase接口**
```typescript
interface ISystemBase {
readonly systemName: string;
readonly entities: readonly any[];
updateOrder: number;
enabled: boolean;
initialize(): void;
update(): void;
lateUpdate?(): void;
}
```
- 为EntitySystem类提供类型约束
- 定义系统的核心执行方法
- 支持可选的延迟更新
**IEventBus接口**
```typescript
interface IEventBus {
emit<T>(eventType: string, data: T): void;
emitAsync<T>(eventType: string, data: T): Promise<void>;
on<T>(eventType: string, handler: (data: T) => void, config?: IEventListenerConfig): string;
// ... 其他事件方法
}
```
- 提供类型安全的事件系统契约
- 支持同步和异步事件处理
- EventBus类完整实现此接口
#### 事件数据接口
**事件数据层次结构**
```typescript
// 基础事件数据
interface IEventData {
timestamp: number;
source?: string;
eventId?: string;
}
// 实体相关事件
interface IEntityEventData extends IEventData {
entityId: number;
entityName?: string;
entityTag?: string;
}
// 组件相关事件
interface IComponentEventData extends IEntityEventData {
componentType: string;
component?: IComponent;
}
```
- 清晰的继承层次
- 类型安全的事件数据传递
- 便于事件处理器的实现
#### 类型别名
**ComponentType<T>**
```typescript
type ComponentType<T extends IComponent = IComponent> = new (...args: any[]) => T;
```
- 用于类型安全的组件操作
- 支持泛型约束
- 广泛用于实体和查询系统
### 设计原则
#### 1. 接口简化原则
- 只保留实际使用的接口
- 移除了未使用的复杂接口如IEntityManager、IEntityQueryBuilder等
- 减少认知负担,提高开发效率
#### 2. 实现灵活性原则
- 接口作为类型约束而非强制实现
- 允许具体类有更丰富的实现
- 保持向后兼容性
#### 3. 类型安全原则
- 编译时类型检查
- 泛型支持提供精确的类型推断
- 事件系统的完整类型安全
### 使用指南
#### 在项目中使用接口
```typescript
// 作为类型约束
function processComponent<T extends IComponent>(component: T) {
if (component.enabled) {
component.update();
}
}
// 作为参数类型
function registerSystem(system: ISystemBase) {
scene.addEntityProcessor(system);
}
// 作为泛型约束
function getComponent<T extends IComponent>(type: ComponentType<T>): T | null {
return entity.getComponent(type);
}
```
#### 扩展框架接口
```typescript
// 如果需要扩展组件接口
interface IAdvancedComponent extends IComponent {
priority: number;
category: string;
}
class AdvancedComponent extends Component implements IAdvancedComponent {
public priority: number = 0;
public category: string = "default";
// 实现基础接口方法
}
```
### 接口维护
当前的接口设计已经过精心清理,包含:
- **12个核心接口** - 涵盖组件、系统、事件等核心概念
- **0个冗余接口** - 移除了所有未使用的接口定义
- **完整的类型覆盖** - 为所有主要功能提供类型支持
这种设计确保了框架的类型安全性,同时保持了代码的简洁性和可维护性。

View File

@@ -1,6 +1,8 @@
# 核心概念
# 核心 API 参考
ECS Framework 基于 Entity-Component-System 架构模式,这是一种高度模块化和可扩展的游戏开发架构。本文档将详细介绍框架的核心概念
本文档详细介绍 ECS Framework 的核心 API 和使用方法
> 🤔 **不熟悉ECS概念** 建议先阅读 [技术概念详解](concepts-explained.md) 了解ECS架构基础和性能优化原理
## ECS 架构概述
@@ -17,28 +19,64 @@ Core 是框架的核心管理类,负责游戏的生命周期管理。
### 创建和配置
```typescript
import { Core } from './Core';
import { Core, ICoreConfig } from '@esengine/ecs-framework';
// 创建核心实例(调试模式
const core = Core.create(true);
// 创建核心实例(使用配置对象 - 推荐
const config: ICoreConfig = {
debug: true, // 启用调试模式
enableEntitySystems: true, // 启用实体系统
debugConfig: { // 可选:远程调试配置
enabled: true,
websocketUrl: 'ws://localhost:8080',
autoReconnect: true,
updateInterval: 1000,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
}
};
const core = Core.create(config);
// 创建核心实例(发布模式
const core = Core.create(false);
// 简化创建(向后兼容
const core1 = Core.create(true); // 调试模式
const core2 = Core.create(false); // 发布模式
const core3 = Core.create(); // 默认调试模式
```
### 事件系统
```typescript
import { CoreEvents } from './ECS/CoreEvents';
import { EntityManager, ECSEventType } from '@esengine/ecs-framework';
// 监听核心事件
Core.emitter.addObserver(CoreEvents.frameUpdated, this.onUpdate, this);
// 获取EntityManager的事件系统
const entityManager = new EntityManager();
const eventBus = entityManager.eventBus;
// 发送帧更新事件
Core.emitter.emit(CoreEvents.frameUpdated);
// 监听实体事件
eventBus.onEntityCreated((data) => {
console.log(`实体创建: ${data.entityName}`);
});
eventBus.onComponentAdded((data) => {
console.log(`组件添加: ${data.componentType}`);
});
// 发送自定义事件
Core.emitter.emit("customEvent", { data: "value" });
eventBus.emit("customEvent", { data: "value" });
// 使用事件装饰器(推荐)
import { EventHandler } from '@esengine/ecs-framework';
class GameSystem {
@EventHandler('entity:died')
onEntityDied(data: any) {
console.log('实体死亡:', data);
}
}
```
### 定时器系统
@@ -62,7 +100,7 @@ Core.schedule(1.0, true, this, (timer) => {
### 创建和使用场景
```typescript
import { Scene } from './ECS/Scene';
import { Scene } from '@esengine/ecs-framework';
// 创建场景
const scene = new Scene();
@@ -77,6 +115,23 @@ scene.update(); // 更新场景
scene.end(); // 结束场景
```
### 批量实体管理
```typescript
// 批量创建实体 - 高性能
const entities = scene.createEntities(1000, "Enemy");
// 批量添加实体(延迟缓存清理)
entities.forEach(entity => {
scene.addEntity(entity, false); // 延迟清理
});
scene.querySystem.clearCache(); // 手动清理缓存
// 获取性能统计
const stats = scene.getStats();
console.log(`实体数量: ${stats.entityCount}`);
```
## Entity实体
实体是游戏世界中的基本对象,包含位置、旋转、缩放等基本属性。
@@ -84,20 +139,8 @@ scene.end(); // 结束场景
### 实体的基本属性
```typescript
import { Vector2 } from './Math/Vector2';
const entity = scene.createEntity("MyEntity");
// 位置
entity.position = new Vector2(100, 200);
entity.position = entity.position.add(new Vector2(10, 0));
// 旋转(弧度)
entity.rotation = Math.PI / 4;
// 缩放
entity.scale = new Vector2(2, 2);
// 标签(用于分类)
entity.tag = 1;
@@ -109,6 +152,22 @@ entity.active = true;
// 更新顺序
entity.updateOrder = 10;
// 注意框架专注于ECS架构不提供Transform相关功能
// 位置、旋转、缩放等Transform功能需要通过组件实现
class TransformComponent extends Component {
public x: number = 0;
public y: number = 0;
public rotation: number = 0;
public scaleX: number = 1;
public scaleY: number = 1;
}
// 使用Transform组件
const transform = entity.addComponent(new TransformComponent());
transform.x = 100;
transform.y = 200;
transform.rotation = Math.PI / 4;
```
### 实体层级关系
@@ -161,7 +220,7 @@ console.log(debugInfo);
### 创建组件
```typescript
import { Component } from './ECS/Component';
import { Component } from '@esengine/ecs-framework';
class HealthComponent extends Component {
public maxHealth: number = 100;
@@ -244,6 +303,47 @@ entity.removeComponentByType(HealthComponent);
entity.removeAllComponents();
```
### 组件对象池优化
```typescript
import { Component, ComponentPoolManager } from '@esengine/ecs-framework';
class BulletComponent extends Component {
public damage: number = 10;
public speed: number = 300;
// 对象池重置方法
public reset() {
this.damage = 10;
this.speed = 300;
}
}
// 注册组件池
ComponentPoolManager.getInstance().registerPool(
'BulletComponent',
() => new BulletComponent(),
(bullet) => bullet.reset(),
1000
);
// 使用对象池获取组件
const bullet = ComponentPoolManager.getInstance().acquireComponent('BulletComponent');
if (bullet) {
entity.addComponent(bullet);
}
// 释放组件回对象池
ComponentPoolManager.getInstance().releaseComponent('BulletComponent', bullet);
// 预热所有组件池
ComponentPoolManager.getInstance().prewarmAll(100);
// 获取池统计
const stats = ComponentPoolManager.getInstance().getPoolStats();
console.log('组件池统计:', stats);
```
## Scene场景
场景是实体和系统的容器,管理游戏世界的状态。
@@ -251,7 +351,9 @@ entity.removeAllComponents();
### 场景生命周期
```typescript
class GameScene extends es.Scene {
import { Scene } from '@esengine/ecs-framework';
class GameScene extends Scene {
public initialize() {
// 场景初始化,创建实体和系统
this.setupEntities();
@@ -311,8 +413,14 @@ console.log("系统数量:", stats.processorCount);
最常用的系统类型,处理实体集合:
```typescript
class MovementSystem extends es.EntitySystem {
protected process(entities: es.Entity[]) {
import { EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(MovementComponent));
}
protected process(entities: Entity[]) {
for (const entity of entities) {
const movement = entity.getComponent(MovementComponent);
if (movement) {
@@ -328,12 +436,26 @@ class MovementSystem extends es.EntitySystem {
定期处理的系统:
```typescript
class HealthRegenerationSystem extends es.ProcessingSystem {
protected process(entities: es.Entity[]) {
for (const entity of entities) {
const health = entity.getComponent(HealthComponent);
import { ProcessingSystem, Time, Matcher } from '@esengine/ecs-framework';
class HealthRegenerationSystem extends ProcessingSystem {
constructor() {
super(Matcher.empty().all(HealthComponent));
}
public processSystem() {
// ProcessingSystem不处理具体实体而是执行全局逻辑
// 如果需要处理实体应该使用EntitySystem
this.regenerateAllPlayerHealth();
}
private regenerateAllPlayerHealth() {
// 通过场景查找所有玩家实体并恢复生命值
const players = this.scene.findEntitiesByTag(PlayerTag);
for (const player of players) {
const health = player.getComponent(HealthComponent);
if (health && health.currentHealth < health.maxHealth) {
health.currentHealth += 10 * es.Time.deltaTime;
health.currentHealth += 10 * Time.deltaTime;
}
}
}
@@ -345,12 +467,15 @@ class HealthRegenerationSystem extends es.ProcessingSystem {
按时间间隔执行的系统:
```typescript
class SpawnSystem extends es.IntervalSystem {
import { IntervalSystem, Matcher } from '@esengine/ecs-framework';
class SpawnSystem extends IntervalSystem {
constructor() {
super(3.0); // 每3秒执行一次
// IntervalSystem需要Matcher和间隔时间
super(Matcher.empty(), 3.0); // 每3秒执行一次
}
protected processSystem() {
protected process(entities: Entity[]) {
// 生成敌人
const enemy = this.scene.createEntity("Enemy");
enemy.addComponent(new EnemyComponent());
@@ -363,7 +488,13 @@ class SpawnSystem extends es.IntervalSystem {
被动系统,不自动处理实体:
```typescript
class CollisionSystem extends es.PassiveSystem {
import { PassiveSystem, Matcher } from '@esengine/ecs-framework';
class CollisionSystem extends PassiveSystem {
constructor() {
super(Matcher.empty());
}
public checkCollisions() {
// 手动调用的碰撞检测逻辑
}
@@ -375,54 +506,32 @@ class CollisionSystem extends es.PassiveSystem {
时间管理工具类,提供游戏时间相关功能:
```typescript
import { Time } from '@esengine/ecs-framework';
// 获取时间信息
console.log("帧时间:", es.Time.deltaTime);
console.log("总时间:", es.Time.totalTime);
console.log("帧数:", es.Time.frameCount);
console.log("时间缩放:", es.Time.timeScale);
console.log("帧时间:", Time.deltaTime);
console.log("总时间:", Time.totalTime);
console.log("帧数:", Time.frameCount);
console.log("时间缩放:", Time.timeScale);
// 设置时间缩放(慢动作效果)
es.Time.timeScale = 0.5;
Time.timeScale = 0.5;
// 检查时间间隔
if (es.Time.checkEvery(1.0, lastCheckTime)) {
if (Time.checkEvery(1.0, lastCheckTime)) {
// 每秒执行一次
}
```
## Vector2二维向量
二维向量类,提供数学运算:
```typescript
// 创建向量
const vec1 = new es.Vector2(10, 20);
const vec2 = es.Vector2.zero;
const vec3 = es.Vector2.one;
// 向量运算
const sum = vec1.add(vec2);
const diff = vec1.subtract(vec2);
const scaled = vec1.multiply(2);
const normalized = vec1.normalize();
// 向量属性
console.log("长度:", vec1.length);
console.log("长度平方:", vec1.lengthSquared);
// 静态方法
const distance = es.Vector2.distance(vec1, vec2);
const lerped = es.Vector2.lerp(vec1, vec2, 0.5);
const fromAngle = es.Vector2.fromAngle(Math.PI / 4);
```
## 性能监控
框架内置性能监控工具:
```typescript
import { PerformanceMonitor } from '@esengine/ecs-framework';
// 获取性能监控实例
const monitor = es.PerformanceMonitor.instance;
const monitor = PerformanceMonitor.instance;
// 查看性能数据
console.log("平均FPS:", monitor.averageFPS);
@@ -439,22 +548,46 @@ monitor.reset();
内存管理优化工具:
```typescript
// 创建对象池
class BulletPool extends es.Pool<Bullet> {
protected createObject(): Bullet {
return new Bullet();
import { Pool, IPoolable } from '@esengine/ecs-framework';
// 定义可池化的对象需要实现IPoolable接口
class Bullet implements IPoolable {
public x: number = 0;
public y: number = 0;
public speed: number = 0;
// 重置对象状态,准备重用
public reset(): void {
this.x = 0;
this.y = 0;
this.speed = 0;
}
}
const bulletPool = new BulletPool();
// 创建对象池
const bulletPool = new Pool<Bullet>(() => new Bullet(), 100);
// 预热对象池
bulletPool.warmUp(20);
// 使用对象池
const bullet = bulletPool.obtain();
// 使用bullet...
bullet.x = 100;
bullet.y = 200;
bullet.speed = 500;
// 使用完后归还到池中
bulletPool.free(bullet);
// 查看池统计信息
console.log(bulletPool.getStats());
// 清空对象池
bulletPool.clear();
// 使用静态方法(自动管理池)
const bullet2 = Pool.obtain(Bullet);
Pool.free(Bullet, bullet2);
```
## 最佳实践
@@ -483,14 +616,41 @@ bulletPool.clear();
- 监控性能数据
- 合理使用时间缩放
## 高级性能优化功能
### 查询系统优化
框架内部已集成查询优化,无需手动配置。查询系统会自动使用最优的算法:
```typescript
// 查询系统会自动优化这些操作
const movingEntities = scene.querySystem.queryAll(PositionComponent, VelocityComponent);
const renderableEntities = scene.querySystem.queryAll(PositionComponent, RenderComponent);
// 获取查询统计信息
const queryStats = scene.querySystem.getStats();
console.log('查询统计:', queryStats);
```
### 批量操作API
```typescript
// 批量创建实体 - 最高性能
const entities = scene.createEntities(10000, "Bullets");
// 批量查询优化
const movingEntities = scene.querySystem.queryAll(PositionComponent, VelocityComponent).entities;
```
## 总结
ECS Framework 提供了完整的实体组件系统架构:
- **Core** 管理游戏生命周期和全局功能
- **Entity** 作为游戏对象的基础容器
- **Component** 实现具体的功能模块
- **Component** 实现具体的功能模块,支持对象池优化
- **System** 处理游戏逻辑
- **Scene** 管理游戏世界状态
- **Scene** 管理游戏世界状态,支持批量操作
- **高级优化** 位掩码优化器、组件对象池、批量操作等
通过合理使用这些核心概念,可以构建出结构清晰、易于维护的游戏代码。
通过合理使用这些核心概念和优化功能,可以构建出高性能、结构清晰、易于维护的游戏代码。

370
docs/entity-guide.md Normal file
View File

@@ -0,0 +1,370 @@
# 实体基础指南
本指南介绍实体Entity的基本概念和基础使用方法。
> 📖 **需要高级实体管理?** 请参考 [EntityManager 指南](entity-manager-example.md) 了解高性能查询和批量操作
## 实体概述
实体Entity是 ECS 架构中的核心概念之一,它作为组件的容器存在。实体本身不包含游戏逻辑,所有功能都通过添加不同的组件来实现。
### 实体的特点
- **轻量级容器**:实体只是组件的载体,不包含具体的游戏逻辑
- **唯一标识**每个实体都有唯一的ID和名称
- **层次结构**:支持父子关系,可以构建复杂的实体层次
- **高性能查询**:基于位掩码的组件查询系统
- **生命周期管理**:完整的创建、更新、销毁流程
## 创建实体
### 基本创建方式
```typescript
import { Scene } from '@esengine/ecs-framework';
// 通过场景创建实体
const scene = new Scene();
const entity = scene.createEntity("Player");
console.log(entity.name); // "Player"
console.log(entity.id); // 唯一的数字ID
```
### 批量创建实体(推荐)
```typescript
import { Scene } from '@esengine/ecs-framework';
const scene = new Scene();
// 批量创建1000个实体 - 高性能
const entities = scene.createEntities(1000, "Enemy");
// 批量配置
entities.forEach((entity, index) => {
entity.tag = 2; // 敌人标签
// 添加组件...
});
```
### 使用流式API创建
```typescript
import { Core } from '@esengine/ecs-framework';
// 使用ECS流式API
const entity = Core.ecsAPI
?.entity("Enemy")
.withComponent(new PositionComponent(100, 200))
.withComponent(new HealthComponent(50))
.withTag(2)
.build();
```
## 实体属性
### 基本属性
```typescript
// 实体名称 - 用于调试和标识
entity.name = "Player";
// 实体ID - 只读,场景内唯一
console.log(entity.id); // 例如: 1
// 标签 - 用于分类和快速查询
entity.tag = 1; // 玩家标签
entity.tag = 2; // 敌人标签
// 更新顺序 - 控制实体在系统中的处理优先级
entity.updateOrder = 0; // 数值越小优先级越高
```
### 状态控制
```typescript
// 启用状态 - 控制实体是否参与更新和处理
entity.enabled = true; // 启用实体
entity.enabled = false; // 禁用实体
// 激活状态 - 控制实体及其子实体的活跃状态
entity.active = true; // 激活实体
entity.active = false; // 停用实体
// 检查层次结构中的激活状态
if (entity.activeInHierarchy) {
// 实体在整个层次结构中都是激活的
}
// 检查销毁状态
if (entity.isDestroyed) {
// 实体已被销毁
}
```
### 更新间隔
```typescript
// 控制实体更新频率
entity.updateInterval = 1; // 每帧更新
entity.updateInterval = 2; // 每2帧更新一次
entity.updateInterval = 5; // 每5帧更新一次
```
## 组件管理
### 添加组件
```typescript
// 创建并添加组件
const healthComponent = entity.addComponent(new HealthComponent(100));
// 使用工厂方法创建组件
const positionComponent = entity.createComponent(PositionComponent, 100, 200);
// 批量添加组件
const components = entity.addComponents([
new PositionComponent(0, 0),
new VelocityComponent(50, 0),
new HealthComponent(100)
]);
```
### 获取组件
```typescript
// 获取单个组件
const health = entity.getComponent(HealthComponent);
if (health) {
console.log(`当前生命值: ${health.currentHealth}`);
}
// 获取或创建组件(如果不存在则创建)
const position = entity.getOrCreateComponent(PositionComponent, 0, 0);
// 获取多个同类型组件(如果组件可以重复添加)
const allHealthComponents = entity.getComponents(HealthComponent);
```
### 检查组件
```typescript
// 检查是否拥有指定组件
if (entity.hasComponent(HealthComponent)) {
// 实体拥有生命值组件
}
// 检查组件掩码(高性能)
const mask = entity.componentMask;
console.log(`组件掩码: ${mask.toString(2)}`); // 二进制表示
```
### 移除组件
```typescript
// 移除指定组件实例
const healthComponent = entity.getComponent(HealthComponent);
if (healthComponent) {
entity.removeComponent(healthComponent);
}
// 按类型移除组件
const removedHealth = entity.removeComponentByType(HealthComponent);
// 批量移除组件
const removedComponents = entity.removeComponentsByTypes([
HealthComponent,
VelocityComponent
]);
// 移除所有组件
entity.removeAllComponents();
```
## 层次结构管理
### 父子关系
```typescript
// 创建父子实体
const player = scene.createEntity("Player");
const weapon = scene.createEntity("Weapon");
const shield = scene.createEntity("Shield");
// 添加子实体
player.addChild(weapon);
player.addChild(shield);
// 获取父实体
console.log(weapon.parent === player); // true
// 获取所有子实体
const children = player.children;
console.log(children.length); // 2
// 获取子实体数量
console.log(player.childCount); // 2
```
### 查找子实体
```typescript
// 按名称查找子实体
const weapon = player.findChild("Weapon");
// 递归查找子实体
const deepChild = player.findChild("DeepChild", true);
// 按标签查找子实体
const enemies = player.findChildrenByTag(2); // 查找所有敌人标签的子实体
// 递归按标签查找
const allEnemies = player.findChildrenByTag(2, true);
```
### 层次结构操作
```typescript
// 移除子实体
const removed = player.removeChild(weapon);
// 移除所有子实体
player.removeAllChildren();
// 获取根实体
const root = weapon.getRoot();
// 检查祖先关系
if (player.isAncestorOf(weapon)) {
// player 是 weapon 的祖先
}
// 检查后代关系
if (weapon.isDescendantOf(player)) {
// weapon 是 player 的后代
}
// 获取实体在层次结构中的深度
const depth = weapon.getDepth(); // 从根实体开始计算的深度
```
### 遍历子实体
```typescript
// 遍历直接子实体
player.forEachChild((child, index) => {
console.log(`子实体 ${index}: ${child.name}`);
});
// 递归遍历所有子实体
player.forEachChild((child, index) => {
console.log(`子实体 ${index}: ${child.name} (深度: ${child.getDepth()})`);
}, true);
```
## 实体生命周期
### 更新循环
```typescript
// 手动更新实体(通常由场景自动调用)
entity.update();
// 实体会自动调用所有组件的update方法
class MyComponent extends Component {
public update(): void {
// 组件更新逻辑
}
}
```
### 销毁实体
```typescript
// 销毁实体
entity.destroy();
// 检查是否已销毁
if (entity.isDestroyed) {
console.log("实体已被销毁");
}
// 销毁实体时会自动:
// 1. 移除所有组件
// 2. 从父实体中移除
// 3. 销毁所有子实体
// 4. 从场景中移除
```
# 高级特性请参考其他指南
> 📚 **更多功能:**
> - **高性能查询和批量操作** → [EntityManager 指南](entity-manager-example.md)
> - **性能优化技术** → [性能优化指南](performance-optimization.md)
> - **组件索引和缓存** → [技术概念详解](concepts-explained.md)
## 基础最佳实践
### 1. 合理使用标签
```typescript
// 定义标签常量
const EntityTags = {
PLAYER: 1,
ENEMY: 2,
PROJECTILE: 3,
PICKUP: 4
} as const;
// 使用标签进行分类
player.tag = EntityTags.PLAYER;
enemy.tag = EntityTags.ENEMY;
```
### 2. 正确的销毁处理
```typescript
// 确保正确销毁实体
if (!entity.isDestroyed) {
entity.destroy(); // 自动移除组件和层次关系
}
// 检查实体状态
if (entity.isDestroyed) {
return; // 避免操作已销毁的实体
}
```
### 3. 组件生命周期
```typescript
// 正确添加组件
const health = entity.addComponent(new HealthComponent(100));
// 安全获取组件
const healthComp = entity.getComponent(HealthComponent);
if (healthComp && healthComp.currentHealth <= 0) {
entity.destroy();
}
```
## 常见问题
### Q: 实体如何实现位置、旋转等变换?
A: 通过添加相应的组件:
```typescript
class TransformComponent extends Component {
public position = { x: 0, y: 0 };
public rotation = 0;
public scale = { x: 1, y: 1 };
}
entity.addComponent(new TransformComponent());
```
### Q: 实体可以在场景间移动吗?
A: 不可以。实体与场景绑定,需要在新场景中重新创建。

View File

@@ -0,0 +1,370 @@
# EntityManager 使用指南
本文档详细介绍 EntityManager 的使用方法和最佳实践。
## 目录
1. [基础用法](#基础用法)
2. [查询系统](#查询系统)
3. [实体管理](#实体管理)
4. [性能监控](#性能监控)
5. [最佳实践](#最佳实践)
## 基础用法
### 创建 EntityManager
```typescript
import { EntityManager, Scene } from '@esengine/ecs-framework';
// 创建场景和实体管理器
const scene = new Scene();
const entityManager = new EntityManager();
// 批量创建实体使用Scene方法
const enemies = scene.createEntities(50, "Enemy");
// 为实体添加组件
enemies.forEach((enemy, index) => {
enemy.addComponent(new PositionComponent(
Math.random() * 800,
Math.random() * 600
));
enemy.addComponent(new HealthComponent(100));
enemy.addComponent(new VelocityComponent(
(Math.random() - 0.5) * 100,
(Math.random() - 0.5) * 100
));
enemy.tag = 2; // 敌人标签
});
```
## 查询系统
### 基础查询
```typescript
// 按组件类型查询
const healthEntities = entityManager.getEntitiesWithComponent(HealthComponent);
// 按标签查询
const enemies = entityManager.getEntitiesByTag(2);
const players = entityManager.getEntitiesByTag(1);
// 按名称查询
const boss = entityManager.getEntityByName("BossEnemy");
// 获取所有实体
const allEntities = entityManager.getAllEntities();
```
### 流式查询 API
```typescript
// 复杂查询条件
const movingEnemies = entityManager
.query()
.withAll(PositionComponent, VelocityComponent, HealthComponent)
.withTag(2) // 敌人标签
.execute();
// 查询活跃的玩家
const activePlayers = entityManager
.query()
.withAll(PositionComponent)
.withTag(1) // 玩家标签
.active() // 只查询活跃实体
.execute();
// 排除特定组件的实体
const nonCombatEntities = entityManager
.query()
.withAll(PositionComponent)
.without(WeaponComponent, HealthComponent)
.execute();
// 自定义条件查询
const nearbyEnemies = entityManager
.query()
.withAll(PositionComponent)
.withTag(2)
.where(entity => {
const pos = entity.getComponent(PositionComponent);
return pos && Math.abs(pos.x - playerX) < 100;
})
.execute();
```
## 实体管理
### 创建和销毁实体
```typescript
// 创建单个实体
const player = entityManager.createEntity("Player");
player.addComponent(new PositionComponent(400, 300));
player.addComponent(new HealthComponent(100));
player.tag = 1;
// 销毁实体
entityManager.destroyEntity(player);
// 按名称销毁
entityManager.destroyEntity("Enemy_1");
// 按ID销毁
entityManager.destroyEntity(123);
```
### 实体查找
```typescript
// 按ID查找
const entity = entityManager.getEntity(123);
// 按名称查找
const player = entityManager.getEntityByName("Player");
// 检查实体是否存在
if (entity && !entity.isDestroyed) {
// 实体有效
}
```
## 性能监控
### 基础统计
```typescript
// 获取实体数量
console.log('总实体数:', entityManager.entityCount);
console.log('活跃实体数:', entityManager.activeEntityCount);
// 获取场景统计
const sceneStats = scene.getStats();
console.log('场景统计:', {
实体数量: sceneStats.entityCount,
系统数量: sceneStats.processorCount
});
// 获取查询系统统计
const queryStats = scene.querySystem.getStats();
console.log('查询统计:', queryStats);
```
## 最佳实践
### 1. 高效查询
```typescript
// ✅ 好的做法:缓存查询结果
class CombatSystem extends EntitySystem {
private cachedEnemies: Entity[] = [];
private lastUpdateFrame = 0;
protected process(entities: Entity[]): void {
// 每5帧更新一次缓存
if (Time.frameCount - this.lastUpdateFrame > 5) {
this.cachedEnemies = this.entityManager
.query()
.withAll(PositionComponent, HealthComponent)
.withTag(2)
.execute();
this.lastUpdateFrame = Time.frameCount;
}
// 使用缓存的结果
this.cachedEnemies.forEach(enemy => {
// 处理敌人逻辑
});
}
}
```
### 2. 批量操作
```typescript
// ✅ 好的做法:批量创建和配置
function createBulletWave(count: number): Entity[] {
// 使用Scene的批量创建
const bullets = scene.createEntities(count, "Bullet");
// 批量配置组件
bullets.forEach((bullet, index) => {
const angle = (index / count) * Math.PI * 2;
bullet.addComponent(new PositionComponent(400, 300));
bullet.addComponent(new VelocityComponent(
Math.cos(angle) * 200,
Math.sin(angle) * 200
));
bullet.addComponent(new BulletComponent());
bullet.tag = 3; // 子弹标签
});
return bullets;
}
```
### 3. 内存管理
```typescript
// ✅ 好的做法:及时清理无用实体
class CleanupSystem extends EntitySystem {
protected process(entities: Entity[]): void {
// 清理超出边界的子弹
const bullets = this.entityManager.getEntitiesByTag(3);
bullets.forEach(bullet => {
const pos = bullet.getComponent(PositionComponent);
if (pos && (pos.x < -100 || pos.x > 900 || pos.y < -100 || pos.y > 700)) {
this.entityManager.destroyEntity(bullet);
}
});
// 清理死亡的敌人
const deadEnemies = this.entityManager
.query()
.withAll(HealthComponent)
.withTag(2)
.where(entity => {
const health = entity.getComponent(HealthComponent);
return health && health.currentHealth <= 0;
})
.execute();
deadEnemies.forEach(enemy => {
this.entityManager.destroyEntity(enemy);
});
}
}
```
### 4. 查询优化
```typescript
// ✅ 好的做法:使用合适的查询方法
class GameSystem extends EntitySystem {
findTargetsInRange(attacker: Entity, range: number): Entity[] {
const attackerPos = attacker.getComponent(PositionComponent);
if (!attackerPos) return [];
// 先按标签快速筛选,再按距离过滤
return this.entityManager
.getEntitiesByTag(2) // 敌人标签
.filter(enemy => {
const enemyPos = enemy.getComponent(PositionComponent);
if (!enemyPos) return false;
const distance = Math.sqrt(
Math.pow(attackerPos.x - enemyPos.x, 2) +
Math.pow(attackerPos.y - enemyPos.y, 2)
);
return distance <= range;
});
}
}
```
## 完整示例
```typescript
import {
EntityManager,
Scene,
Entity,
Component,
EntitySystem,
Matcher
} from '@esengine/ecs-framework';
// 组件定义
class PositionComponent extends Component {
constructor(public x: number = 0, public y: number = 0) {
super();
}
}
class HealthComponent extends Component {
constructor(
public maxHealth: number = 100,
public currentHealth: number = 100
) {
super();
}
}
// 游戏管理器
class GameManager {
private scene: Scene;
private entityManager: EntityManager;
constructor() {
this.scene = new Scene();
this.entityManager = new EntityManager();
this.setupGame();
}
private setupGame(): void {
// 创建玩家
const player = this.entityManager.createEntity("Player");
player.addComponent(new PositionComponent(400, 300));
player.addComponent(new HealthComponent(100));
player.tag = 1;
// 创建敌人
const enemies = this.scene.createEntities(10, "Enemy");
enemies.forEach(enemy => {
enemy.addComponent(new PositionComponent(
Math.random() * 800,
Math.random() * 600
));
enemy.addComponent(new HealthComponent(50));
enemy.tag = 2;
});
// 添加系统
this.scene.addEntityProcessor(new HealthSystem());
}
public update(): void {
this.scene.update();
// 输出统计信息
console.log('实体数量:', this.entityManager.entityCount);
console.log('活跃实体数:', this.entityManager.activeEntityCount);
}
}
// 生命值系统
class HealthSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(HealthComponent));
}
protected process(entities: Entity[]): void {
const healthEntities = this.scene.querySystem.queryAll(HealthComponent);
healthEntities.entities.forEach(entity => {
const health = entity.getComponent(HealthComponent);
if (health && health.currentHealth <= 0) {
console.log(`实体 ${entity.name} 死亡`);
entity.destroy();
}
});
}
}
// 启动游戏
const game = new GameManager();
setInterval(() => game.update(), 16); // 60 FPS
```
## 总结
EntityManager 提供了强大的实体管理功能:
- **创建管理**`createEntity()`, `destroyEntity()`
- **查询功能**`getEntitiesWithComponent()`, `getEntitiesByTag()`, `query()`
- **实体查找**`getEntity()`, `getEntityByName()`
- **统计信息**`entityCount`, `activeEntityCount`
通过合理使用这些API可以构建高性能的游戏系统。记住要及时清理无用实体缓存频繁查询的结果并使用合适的查询方法来优化性能。

View File

@@ -0,0 +1,496 @@
# ECS事件系统使用指南
本文档介绍如何使用ECS框架的增强事件系统包括类型安全的事件发布订阅、预定义的ECS事件类型和高级功能。
## 目录
1. [基础用法](#基础用法)
2. [预定义ECS事件](#预定义ecs事件)
3. [事件装饰器](#事件装饰器)
4. [高级功能](#高级功能)
5. [性能优化](#性能优化)
6. [最佳实践](#最佳实践)
## 基础用法
### 创建事件总线
```typescript
import { EventBus, GlobalEventBus } from './ECS';
// 方式1创建独立的事件总线
const eventBus = new EventBus(true); // true启用调试模式
// 方式2使用全局事件总线
const globalEventBus = GlobalEventBus.getInstance(true);
```
### 基本事件发布订阅
```typescript
// 定义事件数据类型
interface PlayerDiedEvent {
playerId: number;
cause: string;
position: { x: number; y: number };
}
// 监听事件
const listenerId = eventBus.on<PlayerDiedEvent>('player:died', (data) => {
console.log(`Player ${data.playerId} died at (${data.position.x}, ${data.position.y})`);
console.log(`Cause: ${data.cause}`);
});
// 发射事件
eventBus.emit('player:died', {
playerId: 123,
cause: 'enemy_attack',
position: { x: 100, y: 200 }
});
// 移除监听器
eventBus.off('player:died', listenerId);
```
### 一次性事件监听
```typescript
// 只监听一次
eventBus.once<PlayerDiedEvent>('player:died', (data) => {
console.log('This will only be called once');
});
```
### 异步事件处理
```typescript
// 异步事件监听
eventBus.onAsync<PlayerDiedEvent>('player:died', async (data) => {
await savePlayerDeathToDatabase(data);
await updateLeaderboard(data.playerId);
});
// 异步事件发射
await eventBus.emitAsync('player:died', playerData);
```
## 预定义ECS事件
框架提供了完整的ECS事件类型定义支持实体、组件、系统等核心概念的事件。
### 实体事件
```typescript
import { ECSEventType, IEntityEventData } from './ECS';
// 监听实体创建事件
eventBus.onEntityCreated((data: IEntityEventData) => {
console.log(`Entity created: ${data.entityName} (ID: ${data.entityId})`);
});
// 监听实体销毁事件
eventBus.on<IEntityEventData>(ECSEventType.ENTITY_DESTROYED, (data) => {
console.log(`Entity destroyed: ${data.entityName}`);
});
// 手动发射实体事件
eventBus.emitEntityCreated({
timestamp: Date.now(),
source: 'GameManager',
entityId: 123,
entityName: 'Player',
entityTag: 'player'
});
```
### 组件事件
```typescript
import { IComponentEventData } from './ECS';
// 监听组件添加事件
eventBus.onComponentAdded((data: IComponentEventData) => {
console.log(`Component ${data.componentType} added to entity ${data.entityId}`);
});
// 监听组件移除事件
eventBus.on<IComponentEventData>(ECSEventType.COMPONENT_REMOVED, (data) => {
console.log(`Component ${data.componentType} removed from entity ${data.entityId}`);
});
```
### 系统事件
```typescript
import { ISystemEventData } from './ECS';
// 监听系统错误
eventBus.onSystemError((data: ISystemEventData) => {
console.error(`System error in ${data.systemName}: ${data.systemType}`);
});
// 监听系统处理开始/结束
eventBus.on<ISystemEventData>(ECSEventType.SYSTEM_PROCESSING_START, (data) => {
console.log(`System ${data.systemName} started processing`);
});
```
### 性能事件
```typescript
import { IPerformanceEventData } from './ECS';
// 监听性能警告
eventBus.onPerformanceWarning((data: IPerformanceEventData) => {
console.warn(`Performance warning: ${data.operation} took ${data.executionTime}ms`);
});
// 监听内存使用过高
eventBus.on<IPerformanceEventData>(ECSEventType.MEMORY_USAGE_HIGH, (data) => {
console.warn(`High memory usage: ${data.memoryUsage}MB`);
});
```
## 事件装饰器
使用装饰器可以自动注册事件监听器,简化代码编写。
### 基础装饰器
```typescript
import { EventHandler, AsyncEventHandler, EventPriority } from './ECS';
class GameManager {
@EventHandler(ECSEventType.ENTITY_CREATED, { priority: EventPriority.HIGH })
onEntityCreated(data: IEntityEventData) {
console.log(`New entity: ${data.entityName}`);
}
@AsyncEventHandler(ECSEventType.ENTITY_DESTROYED)
async onEntityDestroyed(data: IEntityEventData) {
await this.cleanupEntityResources(data.entityId);
}
@EventHandler('custom:game:event', { once: true })
onGameStart(data: any) {
console.log('Game started!');
}
// 需要手动调用初始化方法
constructor() {
// 如果类有initEventListeners方法会自动注册装饰器定义的监听器
if (this.initEventListeners) {
this.initEventListeners();
}
}
private async cleanupEntityResources(entityId: number) {
// 清理实体相关资源
}
}
```
### 优先级和配置
```typescript
class SystemManager {
@EventHandler(ECSEventType.SYSTEM_ERROR, {
priority: EventPriority.CRITICAL,
context: this
})
handleSystemError(data: ISystemEventData) {
this.logError(data);
this.restartSystem(data.systemName);
}
@AsyncEventHandler(ECSEventType.PERFORMANCE_WARNING, {
priority: EventPriority.LOW,
async: true
})
async handlePerformanceWarning(data: IPerformanceEventData) {
await this.optimizePerformance(data);
}
private logError(data: ISystemEventData) {
// 错误日志记录
}
private restartSystem(systemName: string) {
// 重启系统
}
private async optimizePerformance(data: IPerformanceEventData) {
// 性能优化逻辑
}
}
```
## 高级功能
### 事件批处理
```typescript
// 设置批处理配置
eventBus.setBatchConfig('entity:update', 100, 16); // 批量100个延迟16ms
// 发射事件(会被批处理)
for (let i = 0; i < 1000; i++) {
eventBus.emit('entity:update', { entityId: i, data: 'update' });
}
// 手动刷新批处理队列
eventBus.flushBatch('entity:update');
```
### 事件统计和监控
```typescript
// 获取单个事件统计
const stats = eventBus.getStats('entity:created');
console.log(`Event triggered ${stats.triggerCount} times`);
console.log(`Average execution time: ${stats.averageExecutionTime}ms`);
// 获取所有事件统计
const allStats = eventBus.getStats();
if (allStats instanceof Map) {
allStats.forEach((stat, eventType) => {
console.log(`${eventType}: ${stat.triggerCount} triggers`);
});
}
// 重置统计
eventBus.resetStats('entity:created');
```
### 事件类型验证
```typescript
import { EventTypeValidator } from './ECS';
// 检查事件类型是否有效
if (EventTypeValidator.isValid('entity:created')) {
eventBus.emit('entity:created', data);
}
// 添加自定义事件类型
EventTypeValidator.addCustomType('game:custom:event');
// 获取所有有效事件类型
const validTypes = EventTypeValidator.getAllValidTypes();
console.log('Valid event types:', validTypes);
```
### 调试和日志
```typescript
// 启用调试模式
eventBus.setDebugMode(true);
// 设置最大监听器数量
eventBus.setMaxListeners(50);
// 检查是否有监听器
if (eventBus.hasListeners('entity:created')) {
console.log('Has listeners for entity:created');
}
// 获取监听器数量
const count = eventBus.getListenerCount('entity:created');
console.log(`${count} listeners for entity:created`);
```
## 性能优化
### 事件过滤和条件监听
```typescript
// 使用条件过滤减少不必要的事件处理
eventBus.on<IEntityEventData>(ECSEventType.ENTITY_CREATED, (data) => {
// 只处理玩家实体
if (data.entityTag === 'player') {
handlePlayerCreated(data);
}
});
// 更好的方式:使用具体的事件类型
eventBus.on<IEntityEventData>('entity:player:created', handlePlayerCreated);
```
### 内存管理
```typescript
class EventManager {
private listeners: string[] = [];
public setupListeners() {
// 保存监听器ID以便清理
this.listeners.push(
eventBus.on('event1', this.handler1.bind(this)),
eventBus.on('event2', this.handler2.bind(this))
);
}
public cleanup() {
// 清理所有监听器
this.listeners.forEach(id => {
eventBus.off('event1', id);
eventBus.off('event2', id);
});
this.listeners.length = 0;
}
private handler1(data: any) { /* ... */ }
private handler2(data: any) { /* ... */ }
}
```
### 异步事件优化
```typescript
// 使用Promise.all并行处理多个异步事件
const promises = [
eventBus.emitAsync('save:player', playerData),
eventBus.emitAsync('update:leaderboard', scoreData),
eventBus.emitAsync('notify:friends', notificationData)
];
await Promise.all(promises);
```
## 最佳实践
### 1. 事件命名规范
```typescript
// 推荐的事件命名格式:模块:对象:动作
const EVENT_NAMES = {
// 实体相关
ENTITY_PLAYER_CREATED: 'entity:player:created',
ENTITY_ENEMY_DESTROYED: 'entity:enemy:destroyed',
// 游戏逻辑相关
GAME_LEVEL_COMPLETED: 'game:level:completed',
GAME_SCORE_UPDATED: 'game:score:updated',
// UI相关
UI_MENU_OPENED: 'ui:menu:opened',
UI_BUTTON_CLICKED: 'ui:button:clicked'
};
```
### 2. 类型安全
```typescript
// 定义强类型的事件数据接口
interface GameEvents {
'player:levelup': { playerId: number; newLevel: number; experience: number };
'inventory:item:added': { itemId: string; quantity: number; playerId: number };
'combat:damage:dealt': { attackerId: number; targetId: number; damage: number };
}
// 创建类型安全的事件发射器
class TypedEventBus {
private eventBus = new EventBus();
emit<K extends keyof GameEvents>(eventType: K, data: GameEvents[K]) {
this.eventBus.emit(eventType, data);
}
on<K extends keyof GameEvents>(
eventType: K,
handler: (data: GameEvents[K]) => void
) {
return this.eventBus.on(eventType, handler);
}
}
```
### 3. 错误处理
```typescript
// 在事件处理器中添加错误处理
eventBus.on<IEntityEventData>(ECSEventType.ENTITY_CREATED, (data) => {
try {
processEntityCreation(data);
} catch (error) {
console.error('Error processing entity creation:', error);
// 发射错误事件
eventBus.emit(ECSEventType.ERROR_OCCURRED, {
timestamp: Date.now(),
source: 'EntityCreationHandler',
error: error.message,
context: data
});
}
});
```
### 4. 模块化事件管理
```typescript
// 为不同模块创建专门的事件管理器
class PlayerEventManager {
constructor(private eventBus: EventBus) {
this.setupListeners();
}
private setupListeners() {
this.eventBus.onEntityCreated(this.onPlayerCreated.bind(this));
this.eventBus.on('player:levelup', this.onPlayerLevelUp.bind(this));
this.eventBus.on('player:died', this.onPlayerDied.bind(this));
}
private onPlayerCreated(data: IEntityEventData) {
if (data.entityTag === 'player') {
// 处理玩家创建逻辑
}
}
private onPlayerLevelUp(data: any) {
// 处理玩家升级逻辑
}
private onPlayerDied(data: any) {
// 处理玩家死亡逻辑
}
}
```
### 5. 与EntityManager集成
```typescript
import { EntityManager } from './ECS';
// EntityManager会自动设置事件总线
const entityManager = new EntityManager();
// 获取事件总线实例
const eventBus = entityManager.eventBus;
// 监听自动发射的ECS事件
eventBus.onEntityCreated((data) => {
console.log('Entity created automatically:', data);
});
eventBus.onComponentAdded((data) => {
console.log('Component added automatically:', data);
});
// 创建实体时会自动发射事件
const entity = entityManager.createEntity('Player');
// 添加组件时会自动发射事件
entity.addComponent(new HealthComponent(100));
```
## 总结
ECS框架的事件系统提供了
- **类型安全**完整的TypeScript类型支持
- **高性能**:批处理、缓存和优化机制
- **易用性**:装饰器、预定义事件类型
- **可扩展**:自定义事件类型和验证
- **调试友好**:详细的统计信息和调试模式
通过合理使用事件系统,可以实现松耦合的模块化架构,提高代码的可维护性和扩展性。

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,425 @@
# 性能优化指南
本文档介绍ECS框架的性能优化技术和最佳实践。
## 目录
1. [查询系统优化](#查询系统优化)
2. [实体管理优化](#实体管理优化)
3. [组件设计优化](#组件设计优化)
4. [系统设计优化](#系统设计优化)
5. [内存管理](#内存管理)
6. [性能监控](#性能监控)
## 查询系统优化
### 使用高效的查询方法
```typescript
// ✅ 推荐:使用标签查询(快速)
const enemies = entityManager.getEntitiesByTag(2);
// ✅ 推荐:使用组件查询
const healthEntities = entityManager.getEntitiesWithComponent(HealthComponent);
// ✅ 推荐使用Scene的查询系统
const movingEntities = scene.querySystem.queryAll(PositionComponent, VelocityComponent);
// ⚠️ 谨慎:自定义条件查询(较慢)
const nearbyEnemies = entityManager
.query()
.withAll(PositionComponent)
.where(entity => {
const pos = entity.getComponent(PositionComponent);
return pos && Math.abs(pos.x - playerX) < 100;
})
.execute();
```
### 查询结果缓存
```typescript
class OptimizedCombatSystem extends EntitySystem {
private cachedEnemies: Entity[] = [];
private lastCacheUpdate = 0;
private cacheInterval = 5; // 每5帧更新一次
protected process(entities: Entity[]): void {
// 缓存查询结果
if (Time.frameCount - this.lastCacheUpdate >= this.cacheInterval) {
this.cachedEnemies = this.entityManager.getEntitiesByTag(2);
this.lastCacheUpdate = Time.frameCount;
}
// 使用缓存的结果
this.cachedEnemies.forEach(enemy => {
this.processEnemy(enemy);
});
}
private processEnemy(enemy: Entity): void {
// 处理敌人逻辑
}
}
```
## 实体管理优化
### 批量创建实体
```typescript
// ✅ 推荐使用Scene的批量创建
function createEnemyWave(count: number): Entity[] {
const enemies = scene.createEntities(count, "Enemy");
// 批量配置组件
enemies.forEach((enemy, index) => {
enemy.addComponent(new PositionComponent(
Math.random() * 800,
Math.random() * 600
));
enemy.addComponent(new HealthComponent(100));
enemy.addComponent(new AIComponent());
enemy.tag = 2; // 敌人标签
});
return enemies;
}
// ❌ 避免:循环单独创建
function createEnemyWaveSlow(count: number): Entity[] {
const enemies: Entity[] = [];
for (let i = 0; i < count; i++) {
const enemy = entityManager.createEntity(`Enemy_${i}`);
enemy.addComponent(new PositionComponent());
enemy.addComponent(new HealthComponent());
enemies.push(enemy);
}
return enemies;
}
```
### 实体复用策略
```typescript
// 使用简单的实体复用策略
class EntityReusableManager {
private inactiveEntities: Entity[] = [];
private scene: Scene;
constructor(scene: Scene) {
this.scene = scene;
}
// 预创建实体
preCreateEntities(count: number, entityName: string): void {
const entities = this.scene.createEntities(count, entityName);
entities.forEach(entity => {
entity.enabled = false; // 禁用但不销毁
this.inactiveEntities.push(entity);
});
}
// 获取可复用实体
getReusableEntity(): Entity | null {
if (this.inactiveEntities.length > 0) {
const entity = this.inactiveEntities.pop()!;
entity.enabled = true;
return entity;
}
return null;
}
// 回收实体
recycleEntity(entity: Entity): void {
entity.enabled = false;
entity.removeAllComponents();
this.inactiveEntities.push(entity);
}
}
```
## 组件设计优化
### 数据局部性优化
```typescript
// ✅ 推荐:紧凑的数据结构
class OptimizedPositionComponent extends Component {
public x: number = 0;
public y: number = 0;
public z: number = 0;
// 避免对象分配
public setPosition(x: number, y: number, z: number = 0): void {
this.x = x;
this.y = y;
this.z = z;
}
}
// ❌ 避免:过多对象分配
class SlowPositionComponent extends Component {
public position: { x: number; y: number; z: number } = { x: 0, y: 0, z: 0 };
public velocity: { x: number; y: number; z: number } = { x: 0, y: 0, z: 0 };
public acceleration: { x: number; y: number; z: number } = { x: 0, y: 0, z: 0 };
}
```
### 组件池化
```typescript
// 使用框架内置的组件池
ComponentPoolManager.getInstance().registerPool(
'BulletComponent',
() => new BulletComponent(),
(bullet) => bullet.reset(),
1000
);
// 获取组件
const bullet = ComponentPoolManager.getInstance().acquireComponent('BulletComponent');
if (bullet) {
entity.addComponent(bullet);
}
// 释放组件
ComponentPoolManager.getInstance().releaseComponent('BulletComponent', bullet);
```
## 系统设计优化
### 系统更新顺序优化
```typescript
class OptimizedGameManager {
private scene: Scene;
constructor() {
this.scene = new Scene();
this.setupSystems();
}
private setupSystems(): void {
// 按依赖关系排序系统
this.scene.addEntityProcessor(new InputSystem()).updateOrder = 10;
this.scene.addEntityProcessor(new MovementSystem()).updateOrder = 20;
this.scene.addEntityProcessor(new CollisionSystem()).updateOrder = 30;
this.scene.addEntityProcessor(new RenderSystem()).updateOrder = 40;
this.scene.addEntityProcessor(new CleanupSystem()).updateOrder = 50;
}
}
```
### 时间分片处理
```typescript
class TimeSlicedAISystem extends EntitySystem {
private aiEntities: Entity[] = [];
private currentIndex = 0;
private entitiesPerFrame = 10;
protected process(entities: Entity[]): void {
// 获取所有AI实体
if (this.aiEntities.length === 0) {
this.aiEntities = this.entityManager.getEntitiesByTag(3); // AI标签
}
// 每帧只处理部分实体
const endIndex = Math.min(
this.currentIndex + this.entitiesPerFrame,
this.aiEntities.length
);
for (let i = this.currentIndex; i < endIndex; i++) {
this.processAI(this.aiEntities[i]);
}
// 更新索引
this.currentIndex = endIndex;
if (this.currentIndex >= this.aiEntities.length) {
this.currentIndex = 0;
this.aiEntities = []; // 重新获取实体列表
}
}
private processAI(entity: Entity): void {
// AI处理逻辑
}
}
```
## 内存管理
### 及时清理无用实体
```typescript
class CleanupSystem extends EntitySystem {
protected process(entities: Entity[]): void {
// 清理超出边界的子弹
const bullets = this.entityManager.getEntitiesByTag(4); // 子弹标签
bullets.forEach(bullet => {
const pos = bullet.getComponent(PositionComponent);
if (pos && this.isOutOfBounds(pos)) {
this.entityManager.destroyEntity(bullet);
}
});
// 清理死亡的实体
const deadEntities = this.entityManager
.query()
.withAll(HealthComponent)
.where(entity => {
const health = entity.getComponent(HealthComponent);
return health && health.currentHealth <= 0;
})
.execute();
deadEntities.forEach(entity => {
this.entityManager.destroyEntity(entity);
});
}
private isOutOfBounds(pos: PositionComponent): boolean {
return pos.x < -100 || pos.x > 900 || pos.y < -100 || pos.y > 700;
}
}
```
### 实体复用管理
```typescript
class GameEntityManager {
private bulletManager: EntityReusableManager;
private effectManager: EntityReusableManager;
constructor(scene: Scene) {
this.bulletManager = new EntityReusableManager(scene);
this.effectManager = new EntityReusableManager(scene);
// 预创建实体
this.bulletManager.preCreateEntities(100, "Bullet");
this.effectManager.preCreateEntities(50, "Effect");
}
createBullet(): Entity | null {
const bullet = this.bulletManager.getReusableEntity();
if (bullet) {
bullet.addComponent(new BulletComponent());
bullet.addComponent(new PositionComponent());
bullet.addComponent(new VelocityComponent());
}
return bullet;
}
destroyBullet(bullet: Entity): void {
this.bulletManager.recycleEntity(bullet);
}
}
```
## 性能监控
### 基础性能统计
```typescript
class PerformanceMonitor {
private scene: Scene;
private entityManager: EntityManager;
constructor(scene: Scene, entityManager: EntityManager) {
this.scene = scene;
this.entityManager = entityManager;
}
public getPerformanceReport(): any {
return {
// 实体统计
entities: {
total: this.entityManager.entityCount,
active: this.entityManager.activeEntityCount
},
// 场景统计
scene: this.scene.getStats(),
// 查询系统统计
querySystem: this.scene.querySystem.getStats(),
// 内存使用
memory: {
used: (performance as any).memory?.usedJSHeapSize || 0,
total: (performance as any).memory?.totalJSHeapSize || 0
}
};
}
public logPerformance(): void {
const report = this.getPerformanceReport();
console.log('性能报告:', report);
}
}
```
### 帧率监控
```typescript
class FPSMonitor {
private frameCount = 0;
private lastTime = performance.now();
private fps = 0;
public update(): void {
this.frameCount++;
const currentTime = performance.now();
if (currentTime - this.lastTime >= 1000) {
this.fps = this.frameCount;
this.frameCount = 0;
this.lastTime = currentTime;
if (this.fps < 30) {
console.warn(`低帧率警告: ${this.fps} FPS`);
}
}
}
public getFPS(): number {
return this.fps;
}
}
```
## 最佳实践总结
### 查询优化
1. 优先使用标签查询和组件查询
2. 缓存频繁使用的查询结果
3. 避免过度使用自定义条件查询
4. 合理设置查询缓存更新频率
### 实体管理
1. 使用批量创建方法
2. 实现实体池化减少GC压力
3. 及时清理无用实体
4. 合理设置实体标签
### 组件设计
1. 保持组件数据紧凑
2. 避免在组件中分配大量对象
3. 使用组件池化
4. 分离数据和行为
### 系统设计
1. 合理安排系统更新顺序
2. 对重计算任务使用时间分片
3. 避免在系统中进行复杂查询
4. 缓存系统间的共享数据
### 内存管理
1. 定期清理无用实体和组件
2. 使用对象池减少GC
3. 监控内存使用情况
4. 避免内存泄漏
通过遵循这些最佳实践可以显著提升ECS框架的性能表现。

View File

@@ -7,15 +7,14 @@ QuerySystem 是 ECS Framework 中的高性能实体查询系统,支持多级
### 1. 获取查询系统
```typescript
import { Scene } from './ECS/Scene';
import { Entity } from './ECS/Entity';
import { Scene, Entity } from '@esengine/ecs-framework';
// 创建场景,查询系统会自动创建
const scene = new Scene();
const querySystem = scene.querySystem;
// 或者从Core获取当前场景的查询系统
import { Core } from './Core';
import { Core } from '@esengine/ecs-framework';
const currentQuerySystem = Core.scene?.querySystem;
```
@@ -37,7 +36,7 @@ const noneResult = querySystem.queryNone(DeadComponent);
```typescript
// 类型安全的查询,返回实体和对应的组件
const typedResult = querySystem.queryAllTyped(PositionComponent, VelocityComponent);
const typedResult = querySystem.queryAll(PositionComponent, VelocityComponent);
for (let i = 0; i < typedResult.entities.length; i++) {
const entity = typedResult.entities[i];
const [position, velocity] = typedResult.components[i];
@@ -159,9 +158,6 @@ querySystem.warmUpCache(commonQueries);
### 2. 索引优化
```typescript
// 自动优化索引配置
querySystem.optimizeIndexes();
// 获取性能统计
const stats = querySystem.getStats();
console.log(`缓存命中率: ${(stats.hitRate * 100).toFixed(1)}%`);
@@ -206,10 +202,14 @@ console.log(`新增: ${diff.added.length}, 移除: ${diff.removed.length}`);
### 移动系统示例
```typescript
import { EntitySystem } from './ECS/Systems/EntitySystem';
import { EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
class MovementSystem extends EntitySystem {
public update(): void {
constructor() {
super(Matcher.empty().all(PositionComponent, VelocityComponent));
}
protected process(entities: Entity[]): void {
// 查询所有可移动的实体
const movableEntities = this.scene.querySystem.queryTwoComponents(
PositionComponent,
@@ -237,7 +237,11 @@ class MovementSystem extends EntitySystem {
```typescript
class CollisionSystem extends EntitySystem {
public update(): void {
constructor() {
super(Matcher.empty().all(PositionComponent, ColliderComponent));
}
protected process(entities: Entity[]): void {
// 获取所有具有碰撞器的实体
const collidableEntities = this.scene.querySystem.queryTwoComponents(
PositionComponent,
@@ -277,7 +281,11 @@ class CollisionSystem extends EntitySystem {
```typescript
class HealthSystem extends EntitySystem {
public update(): void {
constructor() {
super(Matcher.empty().all(HealthComponent));
}
protected process(entities: Entity[]): void {
// 查询所有具有生命值的实体
const healthEntities = this.scene.querySystem.queryComponentTyped(HealthComponent);
const deadEntities: Entity[] = [];

View File

@@ -0,0 +1,734 @@
# 场景管理完整指南
场景Scene是ECS框架中管理游戏对象和系统的核心容器。本指南将详细介绍如何有效地使用场景来构建和管理你的游戏。
## 场景基础概念
### 什么是场景?
场景是一个完整的游戏世界容器,它包含:
- 🎮 **实体集合** - 所有游戏对象
- ⚙️ **系统集合** - 处理游戏逻辑的系统
- 📊 **事件系统** - 场景内的事件通信
- 🔍 **查询系统** - 高效的实体查询
- 📈 **性能监控** - 场景级别的性能统计
```typescript
import { Scene, Core } from '@esengine/ecs-framework';
// 创建场景
const gameScene = new Scene();
// 设置为当前活动场景
Core.scene = gameScene;
```
### 场景的生命周期
```typescript
class GameScene extends Scene {
// 场景开始时调用
onStart() {
console.log("场景开始");
this.initializeScene();
}
// 场景更新时调用(每帧)
update() {
super.update(); // 调用父类更新
// 自定义更新逻辑
this.updateGameLogic();
}
// 场景结束时调用
onDestroy() {
console.log("场景结束");
this.cleanup();
super.onDestroy();
}
}
```
## 基础场景操作
### 1. 创建和配置场景
```typescript
class MenuScene extends Scene {
private backgroundMusic: AudioClip;
onStart() {
this.setupUI();
this.setupSystems();
this.setupInput();
this.playBackgroundMusic();
}
private setupUI() {
// 创建菜单UI实体
const titleEntity = this.createEntity("Title");
titleEntity.addComponent(new TextComponent("我的游戏", 48));
titleEntity.addComponent(new PositionComponent(400, 100));
const startButton = this.createEntity("StartButton");
startButton.addComponent(new ButtonComponent("开始游戏"));
startButton.addComponent(new PositionComponent(400, 300));
const settingsButton = this.createEntity("SettingsButton");
settingsButton.addComponent(new ButtonComponent("设置"));
settingsButton.addComponent(new PositionComponent(400, 400));
const exitButton = this.createEntity("ExitButton");
exitButton.addComponent(new ButtonComponent("退出"));
exitButton.addComponent(new PositionComponent(400, 500));
}
private setupSystems() {
// 添加UI相关系统
this.addEntityProcessor(new UIRenderSystem());
this.addEntityProcessor(new ButtonClickSystem());
this.addEntityProcessor(new MenuTransitionSystem());
}
private setupInput() {
// 监听按钮点击事件
this.eventBus.on('button:clicked', this.onButtonClicked, this);
}
private onButtonClicked(data: { buttonName: string }) {
switch (data.buttonName) {
case "开始游戏":
this.transitionToGame();
break;
case "设置":
this.showSettings();
break;
case "退出":
this.exitGame();
break;
}
}
private transitionToGame() {
// 切换到游戏场景
const gameScene = new GameScene();
Core.scene = gameScene;
}
}
```
### 2. 游戏主场景
```typescript
class GameScene extends Scene {
private player: Entity;
private enemySpawner: Entity;
private ui: Entity;
onStart() {
this.setupWorld();
this.setupPlayer();
this.setupEnemies();
this.setupSystems();
this.setupUI();
}
private setupWorld() {
// 创建背景
const background = this.createEntity("Background");
background.addComponent(new SpriteComponent("background.png"));
background.addComponent(new PositionComponent(0, 0));
// 创建边界
this.createWorldBounds();
}
private setupPlayer() {
this.player = this.createEntity("Player");
this.player.addComponent(new PositionComponent(400, 300));
this.player.addComponent(new VelocityComponent());
this.player.addComponent(new HealthComponent(100));
this.player.addComponent(new SpriteComponent("player.png"));
this.player.addComponent(new PlayerInputComponent());
this.player.addComponent(new WeaponComponent());
this.player.tag = EntityTags.PLAYER;
}
private setupEnemies() {
this.enemySpawner = this.createEntity("EnemySpawner");
this.enemySpawner.addComponent(new SpawnerComponent());
this.enemySpawner.addComponent(new PositionComponent(0, 0));
}
private setupSystems() {
// 输入系统
this.addEntityProcessor(new PlayerInputSystem()).updateOrder = 0;
// 游戏逻辑系统
this.addEntityProcessor(new MovementSystem()).updateOrder = 10;
this.addEntityProcessor(new AISystem()).updateOrder = 15;
this.addEntityProcessor(new WeaponSystem()).updateOrder = 20;
this.addEntityProcessor(new CollisionSystem()).updateOrder = 30;
this.addEntityProcessor(new HealthSystem()).updateOrder = 40;
// 生成和清理系统
this.addEntityProcessor(new EnemySpawnSystem()).updateOrder = 50;
this.addEntityProcessor(new EntityCleanupSystem()).updateOrder = 60;
// 渲染系统
this.addEntityProcessor(new RenderSystem()).updateOrder = 100;
this.addEntityProcessor(new UIRenderSystem()).updateOrder = 110;
// 特效和音频系统
this.addEntityProcessor(new ParticleSystem()).updateOrder = 120;
this.addEntityProcessor(new AudioSystem()).updateOrder = 130;
}
private setupUI() {
this.ui = this.createEntity("GameUI");
this.ui.addComponent(new HealthBarComponent());
this.ui.addComponent(new ScoreDisplayComponent());
this.ui.addComponent(new AmmoDisplayComponent());
}
private createWorldBounds() {
// 创建世界边界,防止实体跑出屏幕
const bounds = [
{ x: 0, y: 0, width: 10, height: 600 }, // 左边界
{ x: 790, y: 0, width: 10, height: 600 }, // 右边界
{ x: 0, y: 0, width: 800, height: 10 }, // 上边界
{ x: 0, y: 590, width: 800, height: 10 } // 下边界
];
bounds.forEach((bound, index) => {
const wall = this.createEntity(`Wall_${index}`);
wall.addComponent(new PositionComponent(bound.x, bound.y));
wall.addComponent(new ColliderComponent(bound.width, bound.height));
wall.addComponent(new WallComponent());
wall.tag = EntityTags.WALL;
});
}
}
```
## 场景切换和管理
### 1. 场景管理器
> **注意:** 以下的 SceneManager、TransitionManager 等是自定义的场景管理类示例不是ECS框架提供的内置API。你可以基于这些示例实现自己的场景管理系统。
```typescript
enum SceneType {
MENU = "menu",
GAME = "game",
PAUSE = "pause",
GAME_OVER = "game_over",
SETTINGS = "settings"
}
// 自定义场景管理器(示例实现)
class SceneManager {
private static instance: SceneManager;
private currentScene: Scene | null = null;
private previousScene: Scene | null = null;
private sceneHistory: Scene[] = [];
static getInstance(): SceneManager {
if (!this.instance) {
this.instance = new SceneManager();
}
return this.instance;
}
switchToScene(sceneType: SceneType, data?: any) {
// 保存当前场景到历史
if (this.currentScene) {
this.previousScene = this.currentScene;
this.sceneHistory.push(this.currentScene);
this.currentScene.onDestroy();
}
// 创建新场景
this.currentScene = this.createScene(sceneType, data);
Core.scene = this.currentScene;
console.log(`切换到场景: ${sceneType}`);
}
goBack(): boolean {
if (this.sceneHistory.length > 0) {
const previousScene = this.sceneHistory.pop()!;
if (this.currentScene) {
this.currentScene.onDestroy();
}
this.currentScene = previousScene;
Core.scene = this.currentScene;
return true;
}
return false;
}
pushScene(sceneType: SceneType, data?: any) {
// 暂停当前场景,不销毁
if (this.currentScene) {
this.previousScene = this.currentScene;
this.sceneHistory.push(this.currentScene);
this.pauseScene(this.currentScene);
}
this.currentScene = this.createScene(sceneType, data);
Core.scene = this.currentScene;
}
popScene() {
if (this.sceneHistory.length > 0) {
if (this.currentScene) {
this.currentScene.onDestroy();
}
this.currentScene = this.sceneHistory.pop()!;
this.resumeScene(this.currentScene);
Core.scene = this.currentScene;
}
}
private createScene(sceneType: SceneType, data?: any): Scene {
switch (sceneType) {
case SceneType.MENU:
return new MenuScene();
case SceneType.GAME:
return new GameScene(data);
case SceneType.PAUSE:
return new PauseScene();
case SceneType.GAME_OVER:
return new GameOverScene(data);
case SceneType.SETTINGS:
return new SettingsScene();
default:
throw new Error(`Unknown scene type: ${sceneType}`);
}
}
private pauseScene(scene: Scene) {
// 暂停场景的所有系统
scene.systems.forEach(system => {
system.enabled = false;
});
}
private resumeScene(scene: Scene) {
// 恢复场景的所有系统
scene.systems.forEach(system => {
system.enabled = true;
});
}
}
// 使用场景管理器
const sceneManager = SceneManager.getInstance();
// 切换场景
sceneManager.switchToScene(SceneType.MENU);
// 推入场景(用于暂停菜单等)
sceneManager.pushScene(SceneType.PAUSE);
// 弹出场景(返回游戏)
sceneManager.popScene();
```
### 2. 场景转场效果
```typescript
class TransitionManager {
private isTransitioning: boolean = false;
async fadeTransition(fromScene: Scene, toScene: Scene, duration: number = 1.0) {
if (this.isTransitioning) return;
this.isTransitioning = true;
// 创建转场覆盖层
const overlay = this.createFadeOverlay();
// 淡出当前场景
await this.fadeOut(overlay, duration / 2);
// 切换场景
fromScene.onDestroy();
Core.scene = toScene;
// 淡入新场景
await this.fadeIn(overlay, duration / 2);
// 清理覆盖层
overlay.destroy();
this.isTransitioning = false;
}
async slideTransition(fromScene: Scene, toScene: Scene, direction: 'left' | 'right' | 'up' | 'down') {
if (this.isTransitioning) return;
this.isTransitioning = true;
// 实现滑动转场效果
const slideDistance = this.getSlideDistance(direction);
// 移动当前场景
await this.slideScene(fromScene, slideDistance);
// 切换场景
fromScene.onDestroy();
Core.scene = toScene;
// 从相反方向滑入新场景
await this.slideScene(toScene, -slideDistance);
this.isTransitioning = false;
}
private createFadeOverlay(): Entity {
const overlay = Core.scene.createEntity("TransitionOverlay");
overlay.addComponent(new SpriteComponent("black_pixel.png"));
overlay.addComponent(new PositionComponent(0, 0));
const sprite = overlay.getComponent(SpriteComponent);
sprite.width = 800;
sprite.height = 600;
sprite.alpha = 0;
return overlay;
}
}
```
## 场景数据管理
### 1. 场景间数据传递
```typescript
interface GameData {
score: number;
level: number;
playerName: string;
difficulty: string;
}
class GameScene extends Scene {
private gameData: GameData;
constructor(data?: GameData) {
super();
this.gameData = data || {
score: 0,
level: 1,
playerName: "Player",
difficulty: "normal"
};
}
onStart() {
super.onStart();
// 根据传入数据配置场景
this.setupPlayerWithData();
this.setupLevelWithDifficulty();
}
private setupPlayerWithData() {
const player = this.createEntity("Player");
player.addComponent(new NameComponent(this.gameData.playerName));
player.addComponent(new ScoreComponent(this.gameData.score));
// ... 其他组件
}
private setupLevelWithDifficulty() {
const difficultySettings = {
easy: { enemySpawnRate: 2.0, enemyHealth: 50 },
normal: { enemySpawnRate: 1.5, enemyHealth: 75 },
hard: { enemySpawnRate: 1.0, enemyHealth: 100 }
};
const settings = difficultySettings[this.gameData.difficulty];
const spawner = this.createEntity("EnemySpawner");
const spawnerComp = new SpawnerComponent();
spawnerComp.spawnInterval = settings.enemySpawnRate;
spawnerComp.enemyHealth = settings.enemyHealth;
spawner.addComponent(spawnerComp);
}
// 游戏结束时传递数据到下一个场景
gameOver() {
const finalScore = this.getPlayerScore();
const sceneManager = SceneManager.getInstance();
sceneManager.switchToScene(SceneType.GAME_OVER, {
score: finalScore,
level: this.gameData.level,
playerName: this.gameData.playerName
});
}
}
class GameOverScene extends Scene {
constructor(private gameData: GameData) {
super();
}
onStart() {
this.displayResults();
this.setupRestartButton();
}
private displayResults() {
const scoreText = this.createEntity("ScoreText");
scoreText.addComponent(new TextComponent(`最终分数: ${this.gameData.score}`));
scoreText.addComponent(new PositionComponent(400, 200));
const levelText = this.createEntity("LevelText");
levelText.addComponent(new TextComponent(`到达关卡: ${this.gameData.level}`));
levelText.addComponent(new PositionComponent(400, 250));
}
}
```
### 2. 持久化数据管理
```typescript
class SaveManager {
private static SAVE_KEY = "game_save_data";
static saveScene(scene: Scene): void {
const saveData = {
playerData: this.extractPlayerData(scene),
sceneState: this.extractSceneState(scene),
timestamp: Date.now()
};
localStorage.setItem(this.SAVE_KEY, JSON.stringify(saveData));
console.log("游戏已保存");
}
static loadScene(): Scene | null {
const saveDataStr = localStorage.getItem(this.SAVE_KEY);
if (!saveDataStr) return null;
try {
const saveData = JSON.parse(saveDataStr);
return this.recreateScene(saveData);
} catch (error) {
console.error("读取存档失败:", error);
return null;
}
}
private static extractPlayerData(scene: Scene): any {
const player = scene.findEntitiesWithTag(EntityTags.PLAYER)[0];
if (!player) return null;
return {
position: player.getComponent(PositionComponent),
health: player.getComponent(HealthComponent),
inventory: player.getComponent(InventoryComponent)?.getItems(),
score: player.getComponent(ScoreComponent)?.score
};
}
private static extractSceneState(scene: Scene): any {
return {
enemies: this.extractEnemiesData(scene),
items: this.extractItemsData(scene),
level: this.getCurrentLevel(scene)
};
}
private static recreateScene(saveData: any): Scene {
const scene = new GameScene();
// 重建玩家
this.recreatePlayer(scene, saveData.playerData);
// 重建场景状态
this.recreateSceneState(scene, saveData.sceneState);
return scene;
}
}
// 自动保存系统
class AutoSaveSystem extends IntervalSystem {
constructor() {
super(30.0); // 每30秒自动保存
}
processSystem() {
SaveManager.saveScene(this.scene);
}
}
```
## 场景性能优化
### 1. 实体管理优化
```typescript
class OptimizedScene extends Scene {
private activeEntities: Set<Entity> = new Set();
private inactiveEntities: Set<Entity> = new Set();
createEntity(name?: string): Entity {
const entity = super.createEntity(name);
this.activeEntities.add(entity);
return entity;
}
destroyEntity(entity: Entity) {
this.activeEntities.delete(entity);
super.destroyEntity(entity);
}
// 暂时禁用实体而不销毁
deactivateEntity(entity: Entity) {
if (this.activeEntities.has(entity)) {
this.activeEntities.delete(entity);
this.inactiveEntities.add(entity);
entity.enabled = false;
}
}
// 重新激活实体
activateEntity(entity: Entity) {
if (this.inactiveEntities.has(entity)) {
this.inactiveEntities.delete(entity);
this.activeEntities.add(entity);
entity.enabled = true;
}
}
// 只更新活跃实体
update() {
for (const entity of this.activeEntities) {
if (entity.enabled) {
entity.update();
}
}
this.updateEntitySystems();
}
// 批量操作
deactivateAllEnemies() {
const enemies = this.findEntitiesWithTag(EntityTags.ENEMY);
enemies.forEach(enemy => this.deactivateEntity(enemy));
}
activateAllEnemies() {
const enemies = Array.from(this.inactiveEntities)
.filter(entity => entity.hasTag(EntityTags.ENEMY));
enemies.forEach(enemy => this.activateEntity(enemy));
}
}
```
### 2. 系统性能监控
```typescript
class PerformanceMonitoredScene extends Scene {
private systemPerformance: Map<string, number[]> = new Map();
addEntityProcessor<T extends EntitySystem>(system: T): T {
const wrappedSystem = this.wrapSystemWithMonitoring(system);
return super.addEntityProcessor(wrappedSystem);
}
private wrapSystemWithMonitoring<T extends EntitySystem>(system: T): T {
const originalUpdate = system.update.bind(system);
const systemName = system.constructor.name;
system.update = () => {
const startTime = performance.now();
originalUpdate();
const endTime = performance.now();
this.recordSystemPerformance(systemName, endTime - startTime);
};
return system;
}
private recordSystemPerformance(systemName: string, duration: number) {
if (!this.systemPerformance.has(systemName)) {
this.systemPerformance.set(systemName, []);
}
const records = this.systemPerformance.get(systemName)!;
records.push(duration);
// 只保留最近100次记录
if (records.length > 100) {
records.shift();
}
}
getPerformanceReport(): any {
const report = {};
this.systemPerformance.forEach((durations, systemName) => {
const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length;
const maxDuration = Math.max(...durations);
const minDuration = Math.min(...durations);
report[systemName] = {
average: avgDuration.toFixed(2) + 'ms',
max: maxDuration.toFixed(2) + 'ms',
min: minDuration.toFixed(2) + 'ms',
samples: durations.length
};
});
return report;
}
// 定期输出性能报告
private performanceReportTimer() {
Core.schedule(5.0, true, this, () => {
console.table(this.getPerformanceReport());
});
}
}
```
## 常见问题和最佳实践
### Q: 何时创建新场景?
A:
- 游戏的不同阶段(菜单、游戏、设置)
- 不同的关卡
- 需要完全不同系统配置的情况
- 需要清理大量实体时
### Q: 场景切换时如何保持数据?
A:
1. 使用场景构造函数传递数据
2. 使用全局数据管理器
3. 使用本地存储进行持久化
### Q: 如何优化场景性能?
A:
1. 合理使用实体的启用/禁用
2. 监控系统性能
3. 批量操作实体
4. 使用对象池减少垃圾回收
### Q: 多个场景可以同时存在吗?
A: 框架同时只支持一个活跃场景,但可以通过场景栈实现多场景管理(如暂停菜单)。
通过合理使用场景系统,你可以构建出结构清晰、性能优良的游戏架构!

519
docs/system-guide.md Normal file
View File

@@ -0,0 +1,519 @@
# 系统System详解指南
系统是ECS架构中的"S",负责处理拥有特定组件的实体。本指南详细介绍框架中的各种系统类型及其使用方法。
## 系统基础概念
### 什么是系统?
系统是处理游戏逻辑的地方,它们:
- 🎯 **专注单一职责** - 每个系统只处理一种类型的逻辑
- 🔄 **自动执行** - 系统会在每帧自动被调用
- 📊 **基于组件过滤** - 只处理包含特定组件的实体
-**高性能** - 利用ECS的数据局部性优势
### 系统的工作原理
```typescript
// 系统的基本工作流程:
// 1. 查询符合条件的实体
// 2. 遍历这些实体
// 3. 读取/修改实体的组件数据
// 4. 执行游戏逻辑
class MovementSystem extends EntitySystem {
process(entities: Entity[]) {
for (const entity of entities) {
const position = entity.getComponent(PositionComponent);
const velocity = entity.getComponent(VelocityComponent);
// 更新位置 = 当前位置 + 速度 * 时间
position.x += velocity.x * Time.deltaTime;
position.y += velocity.y * Time.deltaTime;
}
}
}
```
## 系统类型详解
### 1. EntitySystem - 基础系统
最常用的系统类型,每帧处理所有符合条件的实体。
```typescript
import { EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
class HealthSystem extends EntitySystem {
constructor() {
// 使用Matcher指定需要的组件
super(Matcher.empty().all(HealthComponent));
}
// 主要处理逻辑
protected process(entities: Entity[]) {
for (const entity of entities) {
const health = entity.getComponent(HealthComponent);
// 处理生命值逻辑
if (health.currentHealth <= 0) {
this.handleDeath(entity);
} else if (health.currentHealth < health.maxHealth) {
this.handleRegeneration(health);
}
}
}
private handleDeath(entity: Entity) {
// 添加死亡标记
entity.addComponent(new DeadComponent());
// 触发死亡事件
const eventBus = this.scene.entityManager.eventBus;
eventBus.emit('entity:died', {
entityId: entity.id,
entityName: entity.name
});
}
private handleRegeneration(health: HealthComponent) {
// 缓慢恢复生命值
health.currentHealth += health.regenRate * Time.deltaTime;
health.currentHealth = Math.min(health.currentHealth, health.maxHealth);
}
}
```
**适用场景:**
- 移动系统
- 渲染系统
- 碰撞检测系统
- AI系统
### 2. ProcessingSystem - 简化处理系统
不需要处理具体实体,主要用于执行全局逻辑或不依赖特定实体的系统处理。
```typescript
import { ProcessingSystem, Matcher } from '@esengine/ecs-framework';
class GameLogicSystem extends ProcessingSystem {
constructor() {
// ProcessingSystem可以不指定Matcher或使用空Matcher
super(Matcher.empty());
}
// 处理系统逻辑(每帧执行)
public processSystem() {
// 执行全局游戏逻辑
this.updateGameState();
this.checkWinConditions();
this.updateUI();
}
private updateGameState() {
// 更新游戏状态逻辑
console.log("更新游戏状态");
}
private checkWinConditions() {
// 检查胜利条件
const players = this.scene.findEntitiesByTag(EntityTags.PLAYER);
const enemies = this.scene.findEntitiesByTag(EntityTags.ENEMY);
if (enemies.length === 0) {
this.triggerVictory();
} else if (players.length === 0) {
this.triggerGameOver();
}
}
private updateUI() {
// 更新UI显示
const gameTime = Time.totalTime;
console.log(`游戏时间: ${gameTime.toFixed(1)}`);
}
}
private processIdle(entity: Entity, ai: AIComponent) {
ai.idleTimer += Time.deltaTime;
if (ai.idleTimer >= ai.idleTime) {
ai.state = AIState.PATROL;
ai.idleTimer = 0;
}
// 检查附近是否有玩家
const nearbyPlayer = this.findNearbyPlayer(entity, ai.detectionRange);
if (nearbyPlayer) {
ai.state = AIState.CHASE;
ai.target = nearbyPlayer;
}
}
private processPatrol(entity: Entity, ai: AIComponent, position: PositionComponent) {
// 简单的来回巡逻
if (!ai.patrolTarget) {
ai.patrolTarget = this.getNextPatrolPoint(ai);
}
const direction = ai.patrolTarget.subtract(position);
const distance = direction.length();
if (distance < 10) {
ai.patrolTarget = this.getNextPatrolPoint(ai);
} else {
const normalized = direction.normalize();
position.x += normalized.x * ai.moveSpeed * Time.deltaTime;
position.y += normalized.y * ai.moveSpeed * Time.deltaTime;
}
}
}
```
**适用场景:**
- 全局游戏逻辑系统
- 胜负判断系统
- UI更新系统
- 不依赖特定实体的处理
### 3. IntervalSystem - 间隔执行系统
不是每帧都执行,而是按指定间隔执行的系统,适合不需要高频更新的逻辑。
```typescript
import { IntervalSystem, Matcher } from '@esengine/ecs-framework';
class SpawnSystem extends IntervalSystem {
private spawnPoints: { x: number; y: number }[] = [
{ x: 100, y: 100 },
{ x: 700, y: 100 },
{ x: 400, y: 500 }
];
// 每2秒执行一次
constructor() {
// IntervalSystem需要指定Matcher和间隔时间
super(Matcher.empty().all(SpawnerComponent), 2.0);
}
// 间隔执行的逻辑重写process方法
protected process(entities: Entity[]) {
// entities就是匹配的生成器实体
for (const spawner of entities) {
const spawnerComp = spawner.getComponent(SpawnerComponent);
if (this.shouldSpawn(spawnerComp)) {
this.spawnEnemy(spawner, spawnerComp);
}
}
}
private shouldSpawn(spawner: SpawnerComponent): boolean {
// 检查是否应该生成
const currentEnemyCount = this.getCurrentEnemyCount();
return currentEnemyCount < spawner.maxEnemies &&
Math.random() < spawner.spawnChance;
}
private spawnEnemy(spawnerEntity: Entity, spawner: SpawnerComponent) {
// 随机选择生成点
const spawnPoint = this.spawnPoints[
Math.floor(Math.random() * this.spawnPoints.length)
];
// 创建敌人实体
const enemy = this.scene.createEntity("Enemy");
enemy.addComponent(new PositionComponent(spawnPoint.x, spawnPoint.y));
enemy.addComponent(new HealthComponent(50));
enemy.addComponent(new AIComponent());
enemy.addComponent(new VelocityComponent(0, 0));
enemy.tag = EntityTags.ENEMY;
// 更新生成器统计
spawner.spawnedCount++;
spawner.lastSpawnTime = Time.totalTime;
// 发送生成事件
const eventBus = this.scene.entityManager.eventBus;
eventBus.emit('enemy:spawned', {
enemyId: enemy.id,
spawnPoint: spawnPoint,
spawnerEntity: spawnerEntity.id
});
}
}
```
**适用场景:**
- 敌人生成系统
- 自动保存系统
- 资源回收系统
- 定期数据同步
### 4. PassiveSystem - 被动系统
不主动遍历实体,而是响应事件的系统。
```typescript
import { PassiveSystem, Matcher, Core } from '@esengine/ecs-framework';
class ScoreSystem extends PassiveSystem {
private score: number = 0;
private multiplier: number = 1;
private combo: number = 0;
constructor() {
// PassiveSystem也需要Matcher即使不使用
super(Matcher.empty());
}
initialize() {
super.initialize();
// 监听游戏事件使用EntityManager的事件系统
const eventBus = this.scene.entityManager.eventBus;
eventBus.on('enemy:killed', this.onEnemyKilled, { context: this });
eventBus.on('item:collected', this.onItemCollected, { context: this });
eventBus.on('combo:broken', this.onComboBroken, { context: this });
}
// PassiveSystem被移除时清理
destroy() {
// 事件监听会在系统销毁时自动清理
// 如需手动清理可以保存listenerId并调用eventBus.off()
}
private onEnemyKilled(data: { enemyType: string; position: { x: number; y: number } }) {
// 根据敌人类型给分
let baseScore = this.getScoreForEnemyType(data.enemyType);
// 连击奖励
this.combo++;
if (this.combo > 3) {
this.multiplier = Math.min(this.combo * 0.1, 3.0); // 最多3倍
}
const finalScore = Math.floor(baseScore * this.multiplier);
this.addScore(finalScore);
// 显示分数奖励
this.showScorePopup(data.position, finalScore);
}
private addScore(points: number) {
this.score += points;
// 发送分数更新事件
const eventBus = this.scene.entityManager.eventBus;
eventBus.emit('score:updated', {
score: this.score,
points: points,
multiplier: this.multiplier,
combo: this.combo
});
}
}
```
**适用场景:**
- 分数统计系统
- 音效播放系统
- UI更新系统
- 成就系统
## 系统管理和注册
### 在场景中添加系统
```typescript
import { Scene, Core } from '@esengine/ecs-framework';
const scene = new Scene();
// 添加各种系统使用addEntityProcessor方法
scene.addEntityProcessor(new MovementSystem());
scene.addEntityProcessor(new GameLogicSystem());
scene.addEntityProcessor(new SpawnSystem());
scene.addEntityProcessor(new ScoreSystem());
// 设置系统的执行优先级
const movementSystem = scene.getEntityProcessor(MovementSystem);
if (movementSystem) {
movementSystem.updateOrder = 10; // 数值越小越先执行
}
const renderSystem = scene.getEntityProcessor(RenderSystem);
if (renderSystem) {
renderSystem.updateOrder = 100; // 渲染系统最后执行
}
// 设置为当前场景
Core.scene = scene;
```
### 系统的启用和禁用
```typescript
// 暂时禁用某个系统
const gameLogicSystem = scene.getEntityProcessor(GameLogicSystem);
if (gameLogicSystem) {
gameLogicSystem.enabled = false;
}
// 重新启用
if (gameLogicSystem) {
gameLogicSystem.enabled = true;
}
// 移除系统
scene.removeEntityProcessor(gameLogicSystem);
```
## 系统设计最佳实践
### 1. 单一职责原则
```typescript
// ✅ 好的设计:每个系统只负责一件事
class MovementSystem extends EntitySystem {
// 只负责移动
}
class CollisionSystem extends EntitySystem {
// 只负责碰撞检测
}
class RenderSystem extends EntitySystem {
// 只负责渲染
}
// ❌ 不好的设计:一个系统做太多事情
class GameplaySystem extends EntitySystem {
// 既处理移动,又处理碰撞,还处理渲染...
}
```
### 2. 合理的系统执行顺序
```typescript
// 设置合理的执行顺序
scene.addEntityProcessor(new InputSystem()).updateOrder = 0; // 输入最先
scene.addEntityProcessor(new GameLogicSystem()).updateOrder = 10; // 游戏逻辑
scene.addEntityProcessor(new MovementSystem()).updateOrder = 20; // 移动计算
scene.addEntityProcessor(new CollisionSystem()).updateOrder = 30; // 碰撞检测
scene.addEntityProcessor(new HealthSystem()).updateOrder = 40; // 生命值处理
scene.addEntityProcessor(new RenderSystem()).updateOrder = 100; // 渲染最后
```
### 3. 系统间通信
```typescript
// 使用事件进行系统间通信
class CollisionSystem extends EntitySystem {
process(entities: Entity[]) {
// ... 碰撞检测逻辑
if (collision) {
// 发送碰撞事件,让其他系统响应
const eventBus = this.scene.entityManager.eventBus;
eventBus.emit('collision:detected', {
entity1: collider1,
entity2: collider2,
collisionPoint: point
});
}
}
}
class HealthSystem extends PassiveSystem {
onAddedToScene() {
// 监听碰撞事件
const eventBus = this.scene.entityManager.eventBus;
eventBus.on('collision:detected', this.onCollision, { context: this });
}
private onCollision(data: CollisionEventData) {
// 处理碰撞伤害
if (data.entity1.hasComponent(HealthComponent)) {
const health = data.entity1.getComponent(HealthComponent);
health.takeDamage(10);
}
}
}
```
### 4. 性能优化
```typescript
class OptimizedMovementSystem extends EntitySystem {
private lastUpdateTime: number = 0;
private readonly UPDATE_INTERVAL = 16; // 60FPS
process(entities: Entity[]) {
const currentTime = Time.totalTime;
// 限制更新频率
if (currentTime - this.lastUpdateTime < this.UPDATE_INTERVAL) {
return;
}
// 批量处理
this.processBatch(entities);
this.lastUpdateTime = currentTime;
}
private processBatch(entities: Entity[]) {
// 使用for循环而不是forEach性能更好
for (let i = 0; i < entities.length; i++) {
const entity = entities[i];
// 处理逻辑...
}
}
}
```
## 常见问题
### Q: 系统的执行顺序重要吗?
A: 非常重要!合理的执行顺序可以避免逻辑错误:
```typescript
// 正确顺序:
// 1. 输入系统(收集玩家输入)
// 2. AI系统敌人决策
// 3. 移动系统(更新位置)
// 4. 碰撞系统(检测碰撞)
// 5. 渲染系统(显示画面)
```
### Q: 什么时候使用哪种系统类型?
A:
- **EntitySystem** - 大部分游戏逻辑移动、AI、碰撞等
- **ProcessingSystem** - 复杂的单实体处理复杂AI、粒子系统
- **IntervalSystem** - 不需要每帧执行的逻辑(生成器、自动保存)
- **PassiveSystem** - 事件响应系统分数、音效、UI更新
### Q: 系统可以访问其他系统吗?
A: 不建议直接访问。推荐使用事件系统进行系统间通信,保持松耦合。
### Q: 如何调试系统性能?
A: 使用框架内置的性能监控:
```typescript
const monitor = PerformanceMonitor.instance;
monitor.startFrame('MovementSystem');
// 系统逻辑...
monitor.endFrame('MovementSystem');
// 查看性能报告
console.log(monitor.getReport());
```
通过合理使用这些系统类型,你可以构建出高性能、易维护的游戏逻辑!

653
docs/timer-guide.md Normal file
View File

@@ -0,0 +1,653 @@
# 定时器系统使用指南
定时器系统是游戏开发中的重要工具用于处理延迟执行、重复任务、倒计时等功能。本指南详细介绍如何使用ECS框架的定时器系统。
## 定时器基础概念
### 什么是定时器?
定时器允许你:
-**延迟执行** - 在指定时间后执行某个操作
- 🔄 **重复执行** - 定期重复执行某个操作
- 🛑 **取消执行** - 在执行前取消定时器
- 🎯 **精确控制** - 精确控制执行时机
### 定时器的优势
相比直接在游戏循环中计时,定时器系统提供:
- 🧹 **自动管理** - 自动处理定时器的生命周期
- 🎮 **游戏时间控制** - 支持游戏暂停、时间缩放
- 💾 **内存优化** - 自动回收完成的定时器
- 🔧 **易于使用** - 简单的API调用
## 基础定时器使用
### 1. 简单延迟执行
```typescript
import { Core, Timer } from '@esengine/ecs-framework';
// 3秒后执行一次
Core.schedule(3.0, false, this, (timer) => {
console.log("3秒钟到了");
});
// 实际游戏例子:延迟显示提示
class GameTutorial {
startTutorial() {
// 2秒后显示第一个提示
Core.schedule(2.0, false, this, () => {
this.showTip("欢迎来到游戏世界!");
});
// 5秒后显示移动提示
Core.schedule(5.0, false, this, () => {
this.showTip("使用WASD键移动角色");
});
// 8秒后显示攻击提示
Core.schedule(8.0, false, this, () => {
this.showTip("按空格键攻击敌人");
});
}
private showTip(message: string) {
// 显示提示的逻辑
console.log(`提示: ${message}`);
}
}
```
### 2. 重复执行
```typescript
// 每1秒执行一次持续执行
const repeatTimer = Core.schedule(1.0, true, this, (timer) => {
console.log("每秒执行一次");
});
// 实际游戏例子:生命值恢复
class HealthRegeneration {
private regenTimer: ITimer;
startRegeneration(entity: Entity) {
const health = entity.getComponent(HealthComponent);
// 每2秒恢复5点生命值
this.regenTimer = Core.schedule(2.0, true, this, () => {
if (health.currentHealth < health.maxHealth) {
health.currentHealth += 5;
health.currentHealth = Math.min(health.currentHealth, health.maxHealth);
console.log(`生命值恢复:${health.currentHealth}/${health.maxHealth}`);
// 满血时停止恢复
if (health.currentHealth >= health.maxHealth) {
this.stopRegeneration();
}
}
});
}
stopRegeneration() {
if (this.regenTimer) {
this.regenTimer.stop();
this.regenTimer = null;
}
}
}
```
### 3. 获取定时器引用进行控制
```typescript
import { ITimer } from '@esengine/ecs-framework';
class BombTimer {
private bombTimer: ITimer;
private explosionTime: number = 5.0;
startBomb(position: { x: number; y: number }) {
console.log("炸弹已放置5秒后爆炸...");
// 创建定时器并保存引用
this.bombTimer = Core.schedule(this.explosionTime, false, this, () => {
this.explode(position);
});
}
defuseBomb() {
if (this.bombTimer && !this.bombTimer.isDone) {
// 拆除炸弹
this.bombTimer.stop();
console.log("炸弹已被拆除!");
}
}
getRemainingTime(): number {
if (this.bombTimer && !this.bombTimer.isDone) {
return this.explosionTime - this.bombTimer.elapsedTime;
}
return 0;
}
private explode(position: { x: number; y: number }) {
console.log("💥 炸弹爆炸了!");
// 爆炸效果逻辑...
}
}
```
## 高级定时器功能
### 1. 定时器链 - 顺序执行多个任务
```typescript
class CutsceneManager {
playCutscene() {
// 第一个镜头2秒
Core.schedule(2.0, false, this, () => {
this.showScene("开场镜头");
// 第二个镜头3秒后
Core.schedule(3.0, false, this, () => {
this.showScene("角色登场");
// 第三个镜头2秒后
Core.schedule(2.0, false, this, () => {
this.showScene("背景介绍");
// 结束1秒后
Core.schedule(1.0, false, this, () => {
this.endCutscene();
});
});
});
});
}
private showScene(sceneName: string) {
console.log(`播放场景: ${sceneName}`);
}
private endCutscene() {
console.log("过场动画结束,开始游戏!");
}
}
// 更优雅的链式写法
class ImprovedCutsceneManager {
playCutscene() {
this.scheduleSequence([
{ delay: 2.0, action: () => this.showScene("开场镜头") },
{ delay: 3.0, action: () => this.showScene("角色登场") },
{ delay: 2.0, action: () => this.showScene("背景介绍") },
{ delay: 1.0, action: () => this.endCutscene() }
]);
}
private scheduleSequence(sequence: Array<{delay: number, action: () => void}>) {
let currentDelay = 0;
sequence.forEach(step => {
currentDelay += step.delay;
Core.schedule(currentDelay, false, this, step.action);
});
}
}
```
### 2. 条件定时器 - 满足条件时执行
```typescript
class ConditionalTimer {
waitForCondition(
condition: () => boolean,
action: () => void,
checkInterval: number = 0.1,
timeout: number = 10.0
) {
let timeElapsed = 0;
const checkTimer = Core.schedule(checkInterval, true, this, () => {
timeElapsed += checkInterval;
if (condition()) {
// 条件满足,执行动作并停止检查
checkTimer.stop();
action();
} else if (timeElapsed >= timeout) {
// 超时,停止检查
checkTimer.stop();
console.log("等待条件超时");
}
});
}
}
// 使用例子
class WaitForPlayerExample {
waitForPlayerToReachGoal() {
const player = this.getPlayer();
const goalPosition = { x: 500, y: 300 };
this.waitForCondition(
// 条件:玩家到达目标位置
() => {
const playerPos = player.getComponent(PositionComponent);
return playerPos.distanceTo(goalPosition) < 50;
},
// 动作:触发下一关
() => {
console.log("玩家到达目标!开始下一关");
this.loadNextLevel();
},
0.1, // 每0.1秒检查一次
30.0 // 30秒后超时
);
}
}
```
### 3. 可暂停的定时器
```typescript
class PausableTimer {
private timers: ITimer[] = [];
private isPaused: boolean = false;
schedule(delay: number, repeat: boolean, callback: () => void): ITimer {
const timer = Core.schedule(delay, repeat, this, callback);
this.timers.push(timer);
return timer;
}
pauseAll() {
this.isPaused = true;
this.timers.forEach(timer => {
if (!timer.isDone) {
timer.stop();
}
});
}
resumeAll() {
if (!this.isPaused) return;
this.isPaused = false;
// 重新启动所有未完成的定时器
// 注意:这是简化实现,实际需要保存剩余时间
this.timers = this.timers.filter(timer => !timer.isDone);
}
clearAll() {
this.timers.forEach(timer => timer.stop());
this.timers = [];
}
}
// 游戏暂停系统
class GamePauseSystem {
private gameTimers: PausableTimer = new PausableTimer();
private isGamePaused: boolean = false;
pauseGame() {
if (this.isGamePaused) return;
this.isGamePaused = true;
this.gameTimers.pauseAll();
// 显示暂停菜单
this.showPauseMenu();
}
resumeGame() {
if (!this.isGamePaused) return;
this.isGamePaused = false;
this.gameTimers.resumeAll();
// 隐藏暂停菜单
this.hidePauseMenu();
}
scheduleGameTimer(delay: number, repeat: boolean, callback: () => void) {
return this.gameTimers.schedule(delay, repeat, callback);
}
}
```
## 实际游戏应用示例
### 1. Buff/Debuff 系统
```typescript
class BuffSystem {
applyBuff(entity: Entity, buffType: string, duration: number) {
const buff = new BuffComponent(buffType, duration);
entity.addComponent(buff);
// 应用Buff效果
this.applyBuffEffect(entity, buffType);
// 设置定时器移除Buff
Core.schedule(duration, false, this, () => {
if (!entity.isDestroyed && entity.hasComponent(BuffComponent)) {
this.removeBuff(entity, buffType);
}
});
console.log(`应用了 ${buffType} Buff持续时间 ${duration}`);
}
private applyBuffEffect(entity: Entity, buffType: string) {
const stats = entity.getComponent(StatsComponent);
switch (buffType) {
case 'speed_boost':
stats.moveSpeed *= 1.5;
break;
case 'damage_boost':
stats.damage *= 2.0;
break;
case 'invincible':
entity.addComponent(new InvincibleComponent());
break;
}
}
private removeBuff(entity: Entity, buffType: string) {
const buff = entity.getComponent(BuffComponent);
if (buff && buff.buffType === buffType) {
entity.removeComponent(buff);
this.removeBuffEffect(entity, buffType);
console.log(`${buffType} Buff 已过期`);
}
}
}
```
### 2. 技能冷却系统
```typescript
class SkillSystem {
private cooldowns: Map<string, number> = new Map();
useSkill(player: Entity, skillName: string): boolean {
// 检查冷却
if (this.isOnCooldown(skillName)) {
const remainingTime = this.getCooldownRemaining(skillName);
console.log(`技能冷却中,还需 ${remainingTime.toFixed(1)}`);
return false;
}
// 执行技能
this.executeSkill(player, skillName);
// 启动冷却
const cooldownTime = this.getSkillCooldown(skillName);
this.startCooldown(skillName, cooldownTime);
return true;
}
private startCooldown(skillName: string, duration: number) {
const endTime = Time.totalTime + duration;
this.cooldowns.set(skillName, endTime);
// 设置定时器清理冷却
Core.schedule(duration, false, this, () => {
this.cooldowns.delete(skillName);
console.log(`技能 ${skillName} 冷却完成!`);
});
}
private isOnCooldown(skillName: string): boolean {
const endTime = this.cooldowns.get(skillName);
return endTime !== undefined && Time.totalTime < endTime;
}
private getCooldownRemaining(skillName: string): number {
const endTime = this.cooldowns.get(skillName);
return endTime ? Math.max(0, endTime - Time.totalTime) : 0;
}
private executeSkill(player: Entity, skillName: string) {
switch (skillName) {
case 'fireball':
this.castFireball(player);
break;
case 'heal':
this.castHeal(player);
break;
case 'dash':
this.performDash(player);
break;
}
}
private getSkillCooldown(skillName: string): number {
const cooldowns = {
'fireball': 3.0,
'heal': 10.0,
'dash': 5.0
};
return cooldowns[skillName] || 1.0;
}
}
```
### 3. 关卡时间限制
```typescript
class LevelTimer {
private timeLimit: number;
private timeRemaining: number;
private timerActive: boolean = false;
private updateTimer: ITimer;
startLevel(timeLimitSeconds: number) {
this.timeLimit = timeLimitSeconds;
this.timeRemaining = timeLimitSeconds;
this.timerActive = true;
// 每秒更新倒计时
this.updateTimer = Core.schedule(1.0, true, this, () => {
this.updateCountdown();
});
console.log(`关卡开始!时间限制:${timeLimitSeconds}`);
}
private updateCountdown() {
if (!this.timerActive) return;
this.timeRemaining--;
// 更新UI显示
this.updateTimerUI(this.timeRemaining);
// 时间警告
if (this.timeRemaining === 30) {
console.log("⚠️ 警告还剩30秒");
this.playWarningSound();
} else if (this.timeRemaining === 10) {
console.log("🚨 紧急还剩10秒");
this.playUrgentSound();
}
// 时间到
if (this.timeRemaining <= 0) {
this.timeUp();
}
}
private timeUp() {
this.timerActive = false;
this.updateTimer.stop();
console.log("⏰ 时间到!游戏结束");
// 触发游戏结束需要在实际使用中获取EntityManager实例
// 示例entityManager.eventBus.emit('level:timeout');
console.log('触发关卡超时事件');
}
completeLevel() {
if (this.timerActive) {
this.timerActive = false;
this.updateTimer.stop();
const completionTime = this.timeLimit - this.timeRemaining;
console.log(`🎉 关卡完成!用时:${completionTime}`);
// 根据剩余时间给予奖励
this.calculateTimeBonus(this.timeRemaining);
}
}
private calculateTimeBonus(timeLeft: number) {
const bonus = Math.floor(timeLeft * 10); // 每秒剩余10分
if (bonus > 0) {
console.log(`时间奖励:${bonus}`);
// 触发时间奖励事件需要在实际使用中获取EntityManager实例
// 示例entityManager.eventBus.emit('score:time_bonus', { bonus });
}
}
getTimeRemaining(): number {
return this.timeRemaining;
}
getTimeRemainingFormatted(): string {
const minutes = Math.floor(this.timeRemaining / 60);
const seconds = this.timeRemaining % 60;
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
}
```
## 定时器性能优化
### 1. 定时器池化
```typescript
class TimerPool {
private static instance: TimerPool;
private timerPool: ITimer[] = [];
static getInstance(): TimerPool {
if (!this.instance) {
this.instance = new TimerPool();
}
return this.instance;
}
getTimer(): ITimer {
return this.timerPool.pop() || this.createTimer();
}
releaseTimer(timer: ITimer) {
timer.stop();
this.timerPool.push(timer);
}
private createTimer(): ITimer {
// 创建新定时器的逻辑
return new Timer();
}
}
```
### 2. 批量定时器管理
```typescript
class BatchTimerManager {
private timers: Set<ITimer> = new Set();
scheduleMany(configs: Array<{delay: number, repeat: boolean, callback: () => void}>) {
return configs.map(config => {
const timer = Core.schedule(config.delay, config.repeat, this, config.callback);
this.timers.add(timer);
return timer;
});
}
stopAll() {
this.timers.forEach(timer => timer.stop());
this.timers.clear();
}
cleanup() {
// 清理已完成的定时器
this.timers.forEach(timer => {
if (timer.isDone) {
this.timers.delete(timer);
}
});
}
}
```
## 常见问题和最佳实践
### Q: 定时器会自动清理吗?
A: 是的,完成的定时器会自动清理。但如果需要提前停止,记得调用 `timer.stop()`
### Q: 定时器会受到游戏暂停影响吗?
A: 定时器使用游戏时间,如果实现了时间缩放功能,定时器会相应调整。
### Q: 如何实现精确的帧同步定时器?
A: 使用帧计数而不是时间:
```typescript
class FrameTimer {
private frameCount: number = 0;
private targetFrame: number;
scheduleFrames(frames: number, callback: () => void) {
this.targetFrame = this.frameCount + frames;
const checkFrame = () => {
this.frameCount++;
if (this.frameCount >= this.targetFrame) {
callback();
} else {
requestAnimationFrame(checkFrame);
}
};
requestAnimationFrame(checkFrame);
}
}
```
### Q: 如何避免定时器内存泄漏?
A:
1. 及时停止不需要的定时器
2. 在对象销毁时清理所有定时器
3. 使用弱引用避免循环引用
```typescript
class SafeTimerUser {
private timers: ITimer[] = [];
scheduleTimer(delay: number, callback: () => void) {
const timer = Core.schedule(delay, false, this, callback);
this.timers.push(timer);
return timer;
}
destroy() {
// 清理所有定时器
this.timers.forEach(timer => timer.stop());
this.timers = [];
}
}
```
定时器是游戏开发中非常有用的工具,合理使用可以让你的游戏逻辑更加优雅和高效!

600
docs/use-cases.md Normal file
View File

@@ -0,0 +1,600 @@
# ECS框架使用场景示例
本文档展示ECS框架在不同类型游戏中的具体应用案例。
## 目录
1. [小型休闲游戏](#小型休闲游戏)
2. [中型动作游戏](#中型动作游戏)
3. [大型策略游戏](#大型策略游戏)
4. [MMO游戏](#mmo游戏)
## 小型休闲游戏
### 场景:简单的飞机大战游戏
```typescript
import {
Scene,
EntityManager,
Entity,
Component,
EntitySystem,
Matcher
} from '@esengine/ecs-framework';
// 组件定义
class PositionComponent extends Component {
constructor(public x: number = 0, public y: number = 0) {
super();
}
}
class VelocityComponent extends Component {
constructor(public x: number = 0, public y: number = 0) {
super();
}
}
class PlayerComponent extends Component {}
class EnemyComponent extends Component {}
class BulletComponent extends Component {}
// 游戏管理器
class PlaneWarGame {
private scene: Scene;
private entityManager: EntityManager;
constructor() {
this.scene = new Scene();
this.entityManager = new EntityManager();
this.setupGame();
}
private setupGame(): void {
// 创建玩家
const player = this.entityManager.createEntity("Player");
player.addComponent(new PositionComponent(400, 500));
player.addComponent(new VelocityComponent(0, 0));
player.addComponent(new PlayerComponent());
player.tag = 1; // 玩家标签
// 创建敌人
this.spawnEnemies(5);
// 添加系统
this.scene.addEntityProcessor(new MovementSystem());
this.scene.addEntityProcessor(new CollisionSystem());
this.scene.addEntityProcessor(new CleanupSystem());
}
private spawnEnemies(count: number): void {
const enemies = this.scene.createEntities(count, "Enemy");
enemies.forEach((enemy, index) => {
enemy.addComponent(new PositionComponent(
Math.random() * 800,
-50
));
enemy.addComponent(new VelocityComponent(0, 100));
enemy.addComponent(new EnemyComponent());
enemy.tag = 2; // 敌人标签
});
}
public update(): void {
this.scene.update();
}
}
// 移动系统
class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(PositionComponent, VelocityComponent));
}
protected process(entities: Entity[]): void {
const movingEntities = this.scene.querySystem.queryAll(
PositionComponent,
VelocityComponent
);
movingEntities.entities.forEach(entity => {
const pos = entity.getComponent(PositionComponent);
const vel = entity.getComponent(VelocityComponent);
pos.x += vel.x * Time.deltaTime;
pos.y += vel.y * Time.deltaTime;
});
}
}
## 中型动作游戏
### 场景2D平台跳跃游戏
```typescript
// 更复杂的组件
class HealthComponent extends Component {
constructor(
public maxHealth: number = 100,
public currentHealth: number = 100
) {
super();
}
}
class AnimationComponent extends Component {
constructor(
public currentAnimation: string = "idle",
public frameIndex: number = 0,
public frameTime: number = 0
) {
super();
}
}
class PhysicsComponent extends Component {
constructor(
public mass: number = 1,
public friction: number = 0.8,
public isGrounded: boolean = false
) {
super();
}
}
// 平台游戏管理器
class PlatformGame {
private scene: Scene;
private entityManager: EntityManager;
constructor() {
this.scene = new Scene();
this.entityManager = new EntityManager();
this.setupGame();
}
private setupGame(): void {
// 创建玩家
this.createPlayer();
// 创建敌人
this.createEnemies(10);
// 创建平台
this.createPlatforms();
// 添加系统(按更新顺序)
this.scene.addEntityProcessor(new InputSystem()).updateOrder = 10;
this.scene.addEntityProcessor(new PhysicsSystem()).updateOrder = 20;
this.scene.addEntityProcessor(new AnimationSystem()).updateOrder = 30;
this.scene.addEntityProcessor(new CombatSystem()).updateOrder = 40;
this.scene.addEntityProcessor(new RenderSystem()).updateOrder = 50;
}
private createPlayer(): void {
const player = this.entityManager.createEntity("Player");
player.addComponent(new PositionComponent(100, 300));
player.addComponent(new VelocityComponent(0, 0));
player.addComponent(new HealthComponent(100));
player.addComponent(new AnimationComponent("idle"));
player.addComponent(new PhysicsComponent(1, 0.8));
player.tag = 1;
}
private createEnemies(count: number): void {
const enemies = this.scene.createEntities(count, "Enemy");
enemies.forEach((enemy, index) => {
enemy.addComponent(new PositionComponent(
200 + index * 100,
300
));
enemy.addComponent(new VelocityComponent(0, 0));
enemy.addComponent(new HealthComponent(50));
enemy.addComponent(new AnimationComponent("patrol"));
enemy.addComponent(new PhysicsComponent(0.8, 0.9));
enemy.tag = 2;
});
}
private createPlatforms(): void {
const platforms = this.scene.createEntities(5, "Platform");
platforms.forEach((platform, index) => {
platform.addComponent(new PositionComponent(
index * 200,
400 + Math.random() * 100
));
platform.tag = 3; // 平台标签
});
}
}
## 大型策略游戏
### 场景:即时战略游戏
```typescript
// 策略游戏组件
class UnitComponent extends Component {
constructor(
public unitType: string,
public playerId: number,
public level: number = 1
) {
super();
}
}
class AIComponent extends Component {
constructor(
public state: string = "idle",
public target: Entity | null = null,
public lastDecisionTime: number = 0
) {
super();
}
}
class ResourceComponent extends Component {
constructor(
public gold: number = 0,
public wood: number = 0,
public food: number = 0
) {
super();
}
}
// 策略游戏管理器
class StrategyGame {
private scene: Scene;
private entityManager: EntityManager;
private players: Map<number, Entity> = new Map();
constructor() {
this.scene = new Scene();
this.entityManager = new EntityManager();
this.setupGame();
}
private setupGame(): void {
// 创建玩家
this.createPlayers(4);
// 为每个玩家创建初始单位
this.players.forEach((player, playerId) => {
this.createInitialUnits(playerId, 10);
});
// 添加系统
this.scene.addEntityProcessor(new AISystem()).updateOrder = 10;
this.scene.addEntityProcessor(new CombatSystem()).updateOrder = 20;
this.scene.addEntityProcessor(new ResourceSystem()).updateOrder = 30;
this.scene.addEntityProcessor(new UnitManagementSystem()).updateOrder = 40;
}
private createPlayers(count: number): void {
for (let i = 0; i < count; i++) {
const player = this.entityManager.createEntity(`Player_${i}`);
player.addComponent(new ResourceComponent(1000, 500, 100));
player.tag = 10 + i; // 玩家标签从10开始
this.players.set(i, player);
}
}
private createInitialUnits(playerId: number, count: number): void {
const units = this.scene.createEntities(count, `Unit_${playerId}`);
units.forEach((unit, index) => {
unit.addComponent(new PositionComponent(
playerId * 200 + Math.random() * 100,
playerId * 200 + Math.random() * 100
));
unit.addComponent(new UnitComponent("warrior", playerId));
unit.addComponent(new HealthComponent(100));
unit.addComponent(new AIComponent());
unit.tag = 20 + playerId; // 单位标签
});
}
// 批量单位操作
public createArmy(playerId: number, unitType: string, count: number): Entity[] {
const units = this.scene.createEntities(count, `${unitType}_${playerId}`);
// 批量配置组件
units.forEach(unit => {
unit.addComponent(new UnitComponent(unitType, playerId));
unit.addComponent(new HealthComponent(100));
unit.addComponent(new PositionComponent(
Math.random() * 1000,
Math.random() * 1000
));
unit.tag = 20 + playerId;
});
return units;
}
// 查询玩家的所有单位
public getPlayerUnits(playerId: number): Entity[] {
return this.entityManager
.query()
.withAll(UnitComponent)
.withTag(20 + playerId)
.execute();
}
// 查询特定类型的单位
public getUnitsByType(unitType: string): Entity[] {
return this.entityManager
.query()
.withAll(UnitComponent)
.where(entity => {
const unit = entity.getComponent(UnitComponent);
return unit && unit.unitType === unitType;
})
.execute();
}
}
// AI系统
class AISystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(AIComponent, UnitComponent));
}
protected process(entities: Entity[]): void {
const aiUnits = this.entityManager
.query()
.withAll(AIComponent, UnitComponent)
.execute();
aiUnits.forEach(unit => {
this.processAI(unit);
});
}
private processAI(unit: Entity): void {
const ai = unit.getComponent(AIComponent);
const unitComp = unit.getComponent(UnitComponent);
if (!ai || !unitComp) return;
// 简单AI逻辑
switch (ai.state) {
case "idle":
this.findTarget(unit);
break;
case "attack":
this.attackTarget(unit);
break;
case "move":
this.moveToTarget(unit);
break;
}
}
private findTarget(unit: Entity): void {
const unitComp = unit.getComponent(UnitComponent);
if (!unitComp) return;
// 查找敌方单位
const enemies = this.entityManager
.query()
.withAll(UnitComponent)
.where(entity => {
const enemyUnit = entity.getComponent(UnitComponent);
return enemyUnit && enemyUnit.playerId !== unitComp.playerId;
})
.execute();
if (enemies.length > 0) {
const ai = unit.getComponent(AIComponent);
if (ai) {
ai.target = enemies[0];
ai.state = "attack";
}
}
}
private attackTarget(unit: Entity): void {
// 攻击逻辑
}
private moveToTarget(unit: Entity): void {
// 移动逻辑
}
}
## MMO游戏
### 场景:大型多人在线游戏
```typescript
// MMO特有组件
class NetworkComponent extends Component {
constructor(
public playerId: string,
public isLocal: boolean = false,
public lastSyncTime: number = 0
) {
super();
}
}
class InventoryComponent extends Component {
public items: Map<string, number> = new Map();
addItem(itemId: string, count: number): void {
const current = this.items.get(itemId) || 0;
this.items.set(itemId, current + count);
}
}
class GuildComponent extends Component {
constructor(
public guildId: string,
public rank: string = "member"
) {
super();
}
}
// MMO游戏管理器
class MMOGame {
private scene: Scene;
private entityManager: EntityManager;
private localPlayerId: string;
constructor(localPlayerId: string) {
this.scene = new Scene();
this.entityManager = new EntityManager();
this.localPlayerId = localPlayerId;
this.setupGame();
}
private setupGame(): void {
// 添加MMO特有系统
this.scene.addEntityProcessor(new NetworkSyncSystem()).updateOrder = 5;
this.scene.addEntityProcessor(new PlayerSystem()).updateOrder = 10;
this.scene.addEntityProcessor(new NPCSystem()).updateOrder = 15;
this.scene.addEntityProcessor(new GuildSystem()).updateOrder = 20;
this.scene.addEntityProcessor(new InventorySystem()).updateOrder = 25;
}
// 创建玩家角色
public createPlayer(playerId: string, isLocal: boolean = false): Entity {
const player = this.entityManager.createEntity(`Player_${playerId}`);
player.addComponent(new PositionComponent(0, 0));
player.addComponent(new HealthComponent(1000));
player.addComponent(new NetworkComponent(playerId, isLocal));
player.addComponent(new InventoryComponent());
player.tag = isLocal ? 1 : 2; // 本地玩家标签1远程玩家标签2
return player;
}
// 批量创建NPC
public createNPCs(count: number): Entity[] {
const npcs = this.scene.createEntities(count, "NPC");
npcs.forEach((npc, index) => {
npc.addComponent(new PositionComponent(
Math.random() * 2000,
Math.random() * 2000
));
npc.addComponent(new HealthComponent(500));
npc.addComponent(new AIComponent("patrol"));
npc.tag = 3; // NPC标签
});
return npcs;
}
// 查询附近的玩家
public getNearbyPlayers(centerX: number, centerY: number, radius: number): Entity[] {
return this.entityManager
.query()
.withAll(PositionComponent, NetworkComponent)
.where(entity => {
const pos = entity.getComponent(PositionComponent);
if (!pos) return false;
const distance = Math.sqrt(
Math.pow(pos.x - centerX, 2) +
Math.pow(pos.y - centerY, 2)
);
return distance <= radius;
})
.execute();
}
// 查询公会成员
public getGuildMembers(guildId: string): Entity[] {
return this.entityManager
.query()
.withAll(GuildComponent, NetworkComponent)
.where(entity => {
const guild = entity.getComponent(GuildComponent);
return guild && guild.guildId === guildId;
})
.execute();
}
// 获取在线玩家统计
public getOnlinePlayerStats(): any {
const allPlayers = this.entityManager.getEntitiesWithComponent(NetworkComponent);
const localPlayers = this.entityManager.getEntitiesByTag(1);
const remotePlayers = this.entityManager.getEntitiesByTag(2);
return {
total: allPlayers.length,
local: localPlayers.length,
remote: remotePlayers.length
};
}
}
// 网络同步系统
class NetworkSyncSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(NetworkComponent));
}
protected process(entities: Entity[]): void {
const networkEntities = this.entityManager.getEntitiesWithComponent(NetworkComponent);
networkEntities.forEach(entity => {
const network = entity.getComponent(NetworkComponent);
if (!network || network.isLocal) return;
// 同步远程实体数据
this.syncRemoteEntity(entity);
});
}
private syncRemoteEntity(entity: Entity): void {
// 网络同步逻辑
const network = entity.getComponent(NetworkComponent);
if (!network) return;
const currentTime = Date.now();
if (currentTime - network.lastSyncTime > 100) { // 100ms同步一次
// 发送同步数据
network.lastSyncTime = currentTime;
}
}
}
## 性能优化建议
### 小型游戏(< 1000实体
- 使用简单的查询方法
- 不需要复杂的优化
- 重点关注代码可读性
### 中型游戏1000-10000实体
- 使用标签查询优化性能
- 实现基础的对象池
- 缓存频繁查询的结果
### 大型游戏10000-100000实体
- 使用时间分片处理大量实体
- 实现空间分区优化邻近查询
- 使用批量操作减少单次调用开销
### MMO游戏100000+实体)
- 实现分区管理,只处理相关区域的实体
- 使用异步处理避免阻塞主线程
- 实现智能缓存和预加载机制
## 总结
ECS框架的灵活性使其能够适应各种规模的游戏开发需求
1. **小型游戏**:简单直接,快速开发
2. **中型游戏**:平衡性能和复杂度
3. **大型游戏**:充分利用优化特性
4. **MMO游戏**:处理海量实体和复杂交互
选择合适的架构模式和优化策略可以让ECS框架在不同场景下都发挥最佳性能。

View File

@@ -0,0 +1,2 @@
[InternetShortcut]
URL=https://docs.cocos.com/creator/manual/en/scripting/setup.html#custom-script-template

View File

@@ -0,0 +1,54 @@
{
"codeGeneration": {
"template": "typescript",
"useStrictMode": true,
"generateComments": true,
"generateImports": true,
"componentSuffix": "Component",
"systemSuffix": "System",
"indentStyle": "spaces",
"indentSize": 4
},
"performance": {
"enableMonitoring": true,
"warningThreshold": 16.67,
"criticalThreshold": 33.33,
"memoryWarningMB": 100,
"memoryCriticalMB": 200,
"maxRecentSamples": 60,
"enableFpsMonitoring": true,
"targetFps": 60
},
"debugging": {
"enableDebugMode": true,
"showEntityCount": true,
"showSystemExecutionTime": true,
"enablePerformanceWarnings": true,
"logLevel": "info",
"enableDetailedLogs": false
},
"editor": {
"autoRefreshAssets": true,
"showWelcomePanelOnStartup": true,
"enableAutoUpdates": false,
"updateChannel": "stable",
"enableNotifications": true
},
"template": {
"defaultEntityName": "GameEntity",
"defaultComponentName": "CustomComponent",
"defaultSystemName": "CustomSystem",
"createExampleFiles": true,
"includeDocumentation": true,
"useFactoryPattern": true
},
"events": {
"enableEventSystem": true,
"defaultEventPriority": 0,
"enableAsyncEvents": true,
"enableEventBatching": false,
"batchSize": 10,
"batchDelay": 16,
"maxEventListeners": 100
}
}

24
extensions/cocos/cocos-ecs/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
#///////////////////////////
# Cocos Creator 3D Project
#///////////////////////////
library/
temp/
local/
build/
profiles/
native
#//////////////////////////
# NPM
#//////////////////////////
node_modules/
#//////////////////////////
# VSCode
#//////////////////////////
.vscode/
#//////////////////////////
# WebStorm
#//////////////////////////
.idea/

View File

@@ -0,0 +1,14 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "da4522ce-bedb-42d5-8cba-63dcb4641265",
"files": [],
"subMetas": {},
"userData": {
"isBundle": true,
"bundleConfigID": "default",
"bundleName": "resources",
"priority": 8
}
}

View File

@@ -0,0 +1,818 @@
{
"nodes": [
{
"id": "root_1",
"type": "root",
"name": "行为树指南根",
"icon": "🌳",
"x": 1270,
"y": 50,
"children": [
"selector_main"
],
"properties": {},
"canHaveChildren": true,
"canHaveParent": false,
"hasError": false
},
{
"id": "selector_main",
"type": "selector",
"name": "主选择器",
"icon": "?",
"x": 1280,
"y": 180,
"children": [
"repeater_patrol",
"selector_combat",
"sequence_idle"
],
"properties": {
"abortType": {
"name": "中止类型",
"type": "select",
"value": "LowerPriority",
"description": "决定节点在何种情况下会被中止",
"options": [
"None",
"LowerPriority",
"Self",
"Both"
],
"required": false
}
},
"canHaveChildren": true,
"canHaveParent": true,
"hasError": false
},
{
"id": "repeater_patrol",
"type": "repeater",
"name": "巡逻重复器",
"icon": "🔄",
"x": 510,
"y": 360,
"children": [
"sequence_patrol"
],
"properties": {
"count": {
"name": "重复次数",
"type": "number",
"value": -1,
"description": "重复执行次数,-1表示无限重复必须是正整数",
"required": true
},
"continueOnFailure": {
"name": "失败时继续",
"type": "boolean",
"value": true,
"description": "子节点失败时是否继续重复",
"required": false
},
"delayBetween": {
"name": "重复间隔",
"type": "boolean",
"value": false,
"description": "重复之间是否有延迟",
"required": false
}
},
"canHaveChildren": true,
"canHaveParent": true,
"hasError": false
},
{
"id": "sequence_patrol",
"type": "sequence",
"name": "巡逻序列",
"icon": "→",
"x": 510,
"y": 580,
"children": [
"decorator_patrol_check",
"action_patrol"
],
"properties": {
"abortType": {
"name": "中止类型",
"type": "select",
"value": "None",
"description": "决定节点在何种情况下会被中止",
"options": [
"None",
"LowerPriority",
"Self",
"Both"
],
"required": false
}
},
"canHaveChildren": true,
"canHaveParent": true,
"hasError": false
},
{
"id": "decorator_patrol_check",
"type": "conditional-decorator",
"name": "巡逻条件检查",
"icon": "🔀",
"x": 400,
"y": 760,
"children": [
"log_patrolling"
],
"properties": {
"conditionType": {
"name": "条件类型",
"type": "select",
"value": "custom",
"description": "装饰器使用的条件类型",
"options": [
"custom",
"random",
"hasComponent",
"hasTag",
"isActive",
"numericCompare",
"propertyExists"
],
"required": false
},
"executeWhenTrue": {
"name": "条件为真时执行",
"type": "boolean",
"value": true,
"description": "条件为真时是否执行子节点",
"required": false
},
"executeWhenFalse": {
"name": "条件为假时执行",
"type": "boolean",
"value": false,
"description": "条件为假时是否执行子节点",
"required": false
},
"checkInterval": {
"name": "检查间隔",
"type": "number",
"value": 1,
"description": "条件检查间隔时间0表示每帧检查",
"required": false
}
},
"attachedCondition": {
"type": "condition-custom",
"name": "巡逻状态检查",
"icon": "⚙️",
"properties": {
"conditionCode": {
"name": "条件代码",
"type": "code",
"value": "(context) => {\n // 检查是否处于巡逻状态\n return context.blackboard && context.blackboard.getValue('state') === 'patrol';\n}",
"description": "条件判断函数代码",
"required": true
},
"conditionName": {
"name": "条件名称",
"type": "string",
"value": "巡逻状态检查",
"description": "用于调试的条件名称",
"required": false
}
}
},
"canHaveChildren": true,
"canHaveParent": true,
"hasError": false
},
{
"id": "log_patrolling",
"type": "log-action",
"name": "记录巡逻",
"icon": "📝",
"x": 400,
"y": 1000,
"children": [],
"properties": {
"message": {
"name": "日志消息",
"type": "string",
"value": "正在执行巡逻任务,当前状态: {{state}}",
"description": "使用{{}}引用黑板变量显示当前状态",
"required": true
},
"logLevel": {
"name": "日志级别",
"type": "select",
"value": "info",
"description": "日志输出级别",
"options": [
"debug",
"info",
"warn",
"error"
],
"required": false
}
},
"canHaveChildren": false,
"canHaveParent": true,
"hasError": false
},
{
"id": "action_patrol",
"type": "set-blackboard-value",
"name": "执行巡逻",
"icon": "📝",
"x": 620,
"y": 760,
"children": [],
"properties": {
"variableName": {
"name": "变量名",
"type": "string",
"value": "lastAction",
"description": "黑板变量名",
"required": true
},
"value": {
"name": "设置值",
"type": "string",
"value": "{{state}}_执行中",
"description": "使用{{}}引用当前状态并添加后缀",
"required": false
},
"sourceVariable": {
"name": "源变量名",
"type": "string",
"value": "",
"description": "从另一个黑板变量复制值",
"required": false
},
"force": {
"name": "强制设置",
"type": "boolean",
"value": false,
"description": "是否忽略只读限制",
"required": false
}
},
"canHaveChildren": false,
"canHaveParent": true,
"hasError": false
},
{
"id": "selector_combat",
"type": "selector",
"name": "战斗选择器",
"icon": "?",
"x": 1170,
"y": 360,
"children": [
"sequence_attack",
"sequence_defend"
],
"properties": {
"abortType": {
"name": "中止类型",
"type": "select",
"value": "None",
"description": "决定节点在何种情况下会被中止",
"options": [
"None",
"LowerPriority",
"Self",
"Both"
],
"required": false
}
},
"canHaveChildren": true,
"canHaveParent": true,
"hasError": false
},
{
"id": "sequence_attack",
"type": "sequence",
"name": "攻击序列",
"icon": "→",
"x": 950,
"y": 540,
"children": [
"inverter_enemy",
"action_attack"
],
"properties": {
"abortType": {
"name": "中止类型",
"type": "select",
"value": "Self",
"description": "决定节点在何种情况下会被中止",
"options": [
"None",
"LowerPriority",
"Self",
"Both"
],
"required": false
}
},
"canHaveChildren": true,
"canHaveParent": true,
"hasError": false
},
{
"id": "inverter_enemy",
"type": "inverter",
"name": "敌人检查反转",
"icon": "!",
"x": 840,
"y": 720,
"children": [
"condition_enemy"
],
"properties": {},
"canHaveChildren": true,
"canHaveParent": true,
"hasError": false
},
{
"id": "condition_enemy",
"type": "condition-random",
"name": "随机敌人出现",
"icon": "🎲",
"x": 840,
"y": 880,
"children": [],
"properties": {
"successProbability": {
"name": "成功概率",
"type": "number",
"value": 0.3,
"description": "条件成功的概率 (0.0 - 1.0)",
"required": true
}
},
"canHaveChildren": false,
"canHaveParent": true,
"hasError": false
},
{
"id": "action_attack",
"type": "log-action",
"name": "攻击动作",
"icon": "📝",
"x": 1060,
"y": 720,
"children": [],
"properties": {
"message": {
"name": "日志消息",
"type": "string",
"value": "发动攻击!生命值: {{health}}, 能量: {{energy}}",
"description": "使用{{}}引用显示战斗时的状态信息",
"required": true
},
"logLevel": {
"name": "日志级别",
"type": "select",
"value": "warn",
"description": "日志输出级别",
"options": [
"debug",
"info",
"warn",
"error"
],
"required": false
}
},
"canHaveChildren": false,
"canHaveParent": true,
"hasError": false
},
{
"id": "sequence_defend",
"type": "sequence",
"name": "防御序列",
"icon": "→",
"x": 1390,
"y": 540,
"children": [
"wait_defend",
"action_defend"
],
"properties": {
"abortType": {
"name": "中止类型",
"type": "select",
"value": "None",
"description": "决定节点在何种情况下会被中止",
"options": [
"None",
"LowerPriority",
"Self",
"Both"
],
"required": false
}
},
"canHaveChildren": true,
"canHaveParent": true,
"hasError": false
},
{
"id": "wait_defend",
"type": "wait-action",
"name": "防御准备",
"icon": "⏰",
"x": 1280,
"y": 720,
"children": [],
"properties": {
"waitTime": {
"name": "等待时间",
"type": "number",
"value": 0.5,
"description": "等待时间必须大于0",
"required": true
},
"useExternalTime": {
"name": "使用外部时间",
"type": "boolean",
"value": false,
"description": "是否使用上下文提供的deltaTime否则使用内部时间计算",
"required": false
}
},
"canHaveChildren": false,
"canHaveParent": true,
"hasError": false
},
{
"id": "action_defend",
"type": "execute-action",
"name": "执行防御",
"icon": "⚙️",
"x": 1500,
"y": 720,
"children": [],
"properties": {
"actionCode": {
"name": "动作代码",
"type": "code",
"value": "(context) => {\n // 防御逻辑\n console.log('开始防御姿态');\n if(context.blackboard) {\n context.blackboard.setValue('defendActive', true);\n context.blackboard.setValue('lastAction', '防御中');\n }\n return 'success';\n}",
"description": "要执行的动作函数代码",
"required": true
},
"actionName": {
"name": "动作名称",
"type": "string",
"value": "防御动作_生命值{{health}}",
"description": "使用{{}}引用在动作名称中显示生命值",
"required": false
}
},
"canHaveChildren": false,
"canHaveParent": true,
"hasError": false
},
{
"id": "sequence_idle",
"type": "sequence",
"name": "闲置序列",
"icon": "→",
"x": 1940,
"y": 360,
"children": [
"action_idle",
"log_status",
"wait_idle"
],
"properties": {
"abortType": {
"name": "中止类型",
"type": "select",
"value": "None",
"description": "决定节点在何种情况下会被中止",
"options": [
"None",
"LowerPriority",
"Self",
"Both"
],
"required": false
}
},
"canHaveChildren": true,
"canHaveParent": true,
"hasError": false
},
{
"id": "action_idle",
"type": "set-blackboard-value",
"name": "设置闲置",
"icon": "📝",
"x": 1720,
"y": 540,
"children": [],
"properties": {
"variableName": {
"name": "变量名",
"type": "string",
"value": "state",
"description": "黑板变量名",
"required": true
},
"value": {
"name": "设置值",
"type": "string",
"value": "idle",
"description": "要设置的值(留空则使用源变量)",
"required": false
},
"sourceVariable": {
"name": "源变量名",
"type": "string",
"value": "",
"description": "从另一个黑板变量复制值",
"required": false
},
"force": {
"name": "强制设置",
"type": "boolean",
"value": false,
"description": "是否忽略只读限制",
"required": false
}
},
"canHaveChildren": false,
"canHaveParent": true,
"hasError": false
},
{
"id": "log_status",
"type": "log-action",
"name": "状态报告",
"icon": "📝",
"x": 1940,
"y": 540,
"children": [],
"properties": {
"message": {
"name": "日志消息",
"type": "string",
"value": "状态报告 - 当前: {{state}}, 上次动作: {{lastAction}}, 防御中: {{defendActive}}",
"description": "完整的黑板变量引用示例,显示多个变量值",
"required": true
},
"logLevel": {
"name": "日志级别",
"type": "select",
"value": "debug",
"description": "日志输出级别",
"options": [
"debug",
"info",
"warn",
"error"
],
"required": false
}
},
"canHaveChildren": false,
"canHaveParent": true,
"hasError": false
},
{
"id": "wait_idle",
"type": "wait-action",
"name": "闲置等待",
"icon": "⏰",
"x": 2160,
"y": 540,
"children": [],
"properties": {
"waitTime": {
"name": "等待时间",
"type": "number",
"value": 3,
"description": "等待时间必须大于0",
"required": true
},
"useExternalTime": {
"name": "使用外部时间",
"type": "boolean",
"value": false,
"description": "是否使用上下文提供的deltaTime否则使用内部时间计算",
"required": false
}
},
"canHaveChildren": false,
"canHaveParent": true,
"hasError": false
}
],
"connections": [
{
"id": "root_1-selector_main",
"sourceId": "root_1",
"targetId": "selector_main",
"path": "M 1349.21875 128 C 1349.21875 158 1359.21875 152 1359.21875 182",
"active": false
},
{
"id": "selector_main-repeater_patrol",
"sourceId": "selector_main",
"targetId": "repeater_patrol",
"path": "M 1359.21875 278 C 1359.21875 320 590 320 590 362",
"active": false
},
{
"id": "selector_main-selector_combat",
"sourceId": "selector_main",
"targetId": "selector_combat",
"path": "M 1359.21875 278 C 1359.21875 320 1250 320 1250 362",
"active": false
},
{
"id": "selector_main-sequence_idle",
"sourceId": "selector_main",
"targetId": "sequence_idle",
"path": "M 1359.21875 278 C 1359.21875 320 2019.21875 320 2019.21875 362",
"active": false
},
{
"id": "repeater_patrol-sequence_patrol",
"sourceId": "repeater_patrol",
"targetId": "sequence_patrol",
"path": "M 590 458 C 590 520 590 520 590 582",
"active": false
},
{
"id": "sequence_patrol-decorator_patrol_check",
"sourceId": "sequence_patrol",
"targetId": "decorator_patrol_check",
"path": "M 590 678 C 590 720 510 720 510 762",
"active": false
},
{
"id": "sequence_patrol-action_patrol",
"sourceId": "sequence_patrol",
"targetId": "action_patrol",
"path": "M 590 678 C 590 720 700 720 700 762",
"active": false
},
{
"id": "decorator_patrol_check-log_patrolling",
"sourceId": "decorator_patrol_check",
"targetId": "log_patrolling",
"path": "M 510 942.078125 C 510 972.078125 480 972 480 1002",
"active": false
},
{
"id": "selector_combat-sequence_attack",
"sourceId": "selector_combat",
"targetId": "sequence_attack",
"path": "M 1250 458 C 1250 500 1030 500 1030 542",
"active": false
},
{
"id": "selector_combat-sequence_defend",
"sourceId": "selector_combat",
"targetId": "sequence_defend",
"path": "M 1250 458 C 1250 500 1470 500 1470 542",
"active": false
},
{
"id": "sequence_attack-inverter_enemy",
"sourceId": "sequence_attack",
"targetId": "inverter_enemy",
"path": "M 1030 638 C 1030 680 920 680 920 722",
"active": false
},
{
"id": "sequence_attack-action_attack",
"sourceId": "sequence_attack",
"targetId": "action_attack",
"path": "M 1030 638 C 1030 680 1140 680 1140 722",
"active": false
},
{
"id": "inverter_enemy-condition_enemy",
"sourceId": "inverter_enemy",
"targetId": "condition_enemy",
"path": "M 920 798 C 920 840 920 840 920 882",
"active": false
},
{
"id": "sequence_defend-wait_defend",
"sourceId": "sequence_defend",
"targetId": "wait_defend",
"path": "M 1470 638 C 1470 680 1360 680 1360 722",
"active": false
},
{
"id": "sequence_defend-action_defend",
"sourceId": "sequence_defend",
"targetId": "action_defend",
"path": "M 1470 638 C 1470 680 1580 680 1580 722",
"active": false
},
{
"id": "sequence_idle-action_idle",
"sourceId": "sequence_idle",
"targetId": "action_idle",
"path": "M 2019.21875 458 C 2019.21875 500 1800 500 1800 542",
"active": false
},
{
"id": "sequence_idle-log_status",
"sourceId": "sequence_idle",
"targetId": "log_status",
"path": "M 2019.21875 458 C 2019.21875 500 2019.21875 500 2019.21875 542",
"active": false
},
{
"id": "sequence_idle-wait_idle",
"sourceId": "sequence_idle",
"targetId": "wait_idle",
"path": "M 2019.21875 458 C 2019.21875 500 2238.4375 500 2238.4375 542",
"active": false
}
],
"metadata": {
"name": "behavior-tree-examples-guide.bt",
"created": "2025-06-19T04:28:44.589Z",
"version": "1.0"
},
"blackboard": [
{
"name": "state",
"type": "string",
"value": "patrol",
"defaultValue": "idle",
"description": "当前状态",
"group": "核心状态",
"readOnly": false,
"constraints": {
"allowedValues": [
"idle",
"patrol",
"combat",
"defend"
]
}
},
{
"name": "lastAction",
"type": "string",
"value": "",
"defaultValue": "",
"description": "最后执行的动作",
"group": "核心状态",
"readOnly": false,
"constraints": {}
},
{
"name": "defendActive",
"type": "boolean",
"value": false,
"defaultValue": false,
"description": "是否正在防御",
"group": "战斗状态",
"readOnly": false,
"constraints": {}
},
{
"name": "health",
"type": "number",
"value": 100,
"defaultValue": 100,
"description": "生命值",
"group": "属性",
"readOnly": false,
"constraints": {
"min": 0,
"max": 100,
"step": 1
}
},
{
"name": "energy",
"type": "number",
"value": 50,
"defaultValue": 100,
"description": "能量值",
"group": "属性",
"readOnly": false,
"constraints": {
"min": 0,
"max": 100,
"step": 1
}
}
]
}

View File

@@ -0,0 +1,11 @@
{
"ver": "2.0.1",
"importer": "json",
"imported": true,
"uuid": "ba6c564a-c5c5-4dc7-ba95-9f0279e0bd66",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "e7a0c4c4-f555-4dc5-be34-83ae26b4eb35",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,495 @@
[
{
"__type__": "cc.SceneAsset",
"_name": "scene",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"scene": {
"__id__": 1
}
},
{
"__type__": "cc.Scene",
"_name": "scene",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
},
{
"__id__": 5
},
{
"__id__": 7
}
],
"_active": true,
"_components": [],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"autoReleaseAssets": false,
"_globals": {
"__id__": 9
},
"_id": "ff354f0b-c2f5-4dea-8ffb-0152d175d11c"
},
{
"__type__": "cc.Node",
"_name": "Main Light",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 3
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": -0.06397656665577071,
"y": -0.44608233363525845,
"z": -0.8239028751062036,
"w": -0.3436591377065261
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": -117.894,
"y": -194.909,
"z": 38.562
},
"_id": "c0y6F5f+pAvI805TdmxIjx"
},
{
"__type__": "cc.DirectionalLight",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": null,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 250,
"b": 240,
"a": 255
},
"_useColorTemperature": false,
"_colorTemperature": 6550,
"_staticSettings": {
"__id__": 4
},
"_visibility": -325058561,
"_illuminanceHDR": 65000,
"_illuminance": 65000,
"_illuminanceLDR": 1.6927083333333335,
"_shadowEnabled": false,
"_shadowPcf": 0,
"_shadowBias": 0.00001,
"_shadowNormalBias": 0,
"_shadowSaturation": 1,
"_shadowDistance": 50,
"_shadowInvisibleOcclusionRange": 200,
"_csmLevel": 4,
"_csmLayerLambda": 0.75,
"_csmOptimizationMode": 2,
"_csmAdvancedOptions": false,
"_csmLayersTransition": false,
"_csmTransitionRange": 0.05,
"_shadowFixedArea": false,
"_shadowNear": 0.1,
"_shadowFar": 10,
"_shadowOrthoSize": 5,
"_id": "597uMYCbhEtJQc0ffJlcgA"
},
{
"__type__": "cc.StaticLightSettings",
"_baked": false,
"_editorOnly": false,
"_castShadow": false
},
{
"__type__": "cc.Node",
"_name": "Main Camera",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 6
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": -10,
"y": 10,
"z": 10
},
"_lrot": {
"__type__": "cc.Quat",
"x": -0.27781593346944056,
"y": -0.36497167621709875,
"z": -0.11507512748638377,
"w": 0.8811195706053617
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": -35,
"y": -45,
"z": 0
},
"_id": "c9DMICJLFO5IeO07EPon7U"
},
{
"__type__": "cc.Camera",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 5
},
"_enabled": true,
"__prefab": null,
"_projection": 1,
"_priority": 0,
"_fov": 45,
"_fovAxis": 0,
"_orthoHeight": 10,
"_near": 1,
"_far": 1000,
"_color": {
"__type__": "cc.Color",
"r": 51,
"g": 51,
"b": 51,
"a": 255
},
"_depth": 1,
"_stencil": 0,
"_clearFlags": 14,
"_rect": {
"__type__": "cc.Rect",
"x": 0,
"y": 0,
"width": 1,
"height": 1
},
"_aperture": 19,
"_shutter": 7,
"_iso": 0,
"_screenScale": 1,
"_visibility": 1822425087,
"_targetTexture": null,
"_postProcess": null,
"_usePostProcess": false,
"_cameraType": -1,
"_trackingType": 0,
"_id": "7dWQTpwS5LrIHnc1zAPUtf"
},
{
"__type__": "cc.Node",
"_name": "ECSManager",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 8
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "43H+4zZ5xK2rblF/7Lha6k"
},
{
"__type__": "c82e7kJAeZJyraNnumIN+I4",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 7
},
"_enabled": true,
"__prefab": null,
"debugMode": true,
"_id": "40G/Xl9EBLJ7amO+29wrkO"
},
{
"__type__": "cc.SceneGlobals",
"ambient": {
"__id__": 10
},
"shadows": {
"__id__": 11
},
"_skybox": {
"__id__": 12
},
"fog": {
"__id__": 13
},
"octree": {
"__id__": 14
},
"skin": {
"__id__": 15
},
"lightProbeInfo": {
"__id__": 16
},
"postSettings": {
"__id__": 17
},
"bakedWithStationaryMainLight": false,
"bakedWithHighpLightmap": false
},
{
"__type__": "cc.AmbientInfo",
"_skyColorHDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.5,
"z": 0.8,
"w": 0.520833125
},
"_skyColor": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.5,
"z": 0.8,
"w": 0.520833125
},
"_skyIllumHDR": 20000,
"_skyIllum": 20000,
"_groundAlbedoHDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.2,
"z": 0.2,
"w": 1
},
"_groundAlbedo": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.2,
"z": 0.2,
"w": 1
},
"_skyColorLDR": {
"__type__": "cc.Vec4",
"x": 0.452588,
"y": 0.607642,
"z": 0.755699,
"w": 0
},
"_skyIllumLDR": 0.8,
"_groundAlbedoLDR": {
"__type__": "cc.Vec4",
"x": 0.618555,
"y": 0.577848,
"z": 0.544564,
"w": 0
}
},
{
"__type__": "cc.ShadowsInfo",
"_enabled": false,
"_type": 0,
"_normal": {
"__type__": "cc.Vec3",
"x": 0,
"y": 1,
"z": 0
},
"_distance": 0,
"_planeBias": 1,
"_shadowColor": {
"__type__": "cc.Color",
"r": 76,
"g": 76,
"b": 76,
"a": 255
},
"_maxReceived": 4,
"_size": {
"__type__": "cc.Vec2",
"x": 1024,
"y": 1024
}
},
{
"__type__": "cc.SkyboxInfo",
"_envLightingType": 0,
"_envmapHDR": {
"__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0",
"__expectedType__": "cc.TextureCube"
},
"_envmap": {
"__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0",
"__expectedType__": "cc.TextureCube"
},
"_envmapLDR": {
"__uuid__": "6f01cf7f-81bf-4a7e-bd5d-0afc19696480@b47c0",
"__expectedType__": "cc.TextureCube"
},
"_diffuseMapHDR": null,
"_diffuseMapLDR": null,
"_enabled": true,
"_useHDR": true,
"_editableMaterial": null,
"_reflectionHDR": null,
"_reflectionLDR": null,
"_rotationAngle": 0
},
{
"__type__": "cc.FogInfo",
"_type": 0,
"_fogColor": {
"__type__": "cc.Color",
"r": 200,
"g": 200,
"b": 200,
"a": 255
},
"_enabled": false,
"_fogDensity": 0.3,
"_fogStart": 0.5,
"_fogEnd": 300,
"_fogAtten": 5,
"_fogTop": 1.5,
"_fogRange": 1.2,
"_accurate": false
},
{
"__type__": "cc.OctreeInfo",
"_enabled": false,
"_minPos": {
"__type__": "cc.Vec3",
"x": -1024,
"y": -1024,
"z": -1024
},
"_maxPos": {
"__type__": "cc.Vec3",
"x": 1024,
"y": 1024,
"z": 1024
},
"_depth": 8
},
{
"__type__": "cc.SkinInfo",
"_enabled": true,
"_blurRadius": 0.01,
"_sssIntensity": 3
},
{
"__type__": "cc.LightProbeInfo",
"_giScale": 1,
"_giSamples": 1024,
"_bounces": 2,
"_reduceRinging": 0,
"_showProbe": true,
"_showWireframe": true,
"_showConvex": false,
"_data": null,
"_lightProbeSphereVolume": 1
},
{
"__type__": "cc.PostSettingsInfo",
"_toneMappingType": 0
}
]

View File

@@ -0,0 +1,11 @@
{
"ver": "1.1.50",
"importer": "scene",
"imported": true,
"uuid": "ff354f0b-c2f5-4dea-8ffb-0152d175d11c",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "1bf5f009-19d9-42b9-b6bb-b44efe349b09",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "6f9217a1-dff6-4460-b5da-eb01cf29c03c",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,99 @@
import { Core } from '@esengine/ecs-framework';
import { Component, _decorator } from 'cc';
import { ExampleGameScene } from './scenes/ExampleGameScene';
const { ccclass, property } = _decorator;
/**
* ECS管理器 - Cocos Creator组件
* 将此组件添加到场景中的任意节点上即可启动ECS框架
*
* 使用说明:
* 1. 在Cocos Creator场景中创建一个空节点
* 2. 将此ECSManager组件添加到该节点
* 3. 运行场景即可自动启动ECS框架
*/
@ccclass('ECSManager')
export class ECSManager extends Component {
@property({
tooltip: '是否启用调试模式(建议开发阶段开启)'
})
public debugMode: boolean = true;
private isInitialized: boolean = false;
/**
* 组件启动时初始化ECS
*/
start() {
this.initializeECS();
}
/**
* 初始化ECS框架
*/
private initializeECS(): void {
if (this.isInitialized) return;
console.log('🎮 正在初始化ECS框架...');
try {
// 1. 创建Core实例启用调试功能
if (this.debugMode) {
Core.create({
debugConfig: {
enabled: true,
websocketUrl: 'ws://localhost:8080/ecs-debug',
autoReconnect: true,
updateInterval: 1000,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
}
});
console.log('🔧 ECS调试模式已启用可在Cocos Creator扩展面板中查看调试信息');
} else {
Core.create(false);
}
// 2. 创建游戏场景
const gameScene = new ExampleGameScene();
// 3. 设置为当前场景会自动调用scene.begin()
Core.scene = gameScene;
this.isInitialized = true;
console.log('✅ ECS框架初始化成功');
console.log('📖 请查看 assets/scripts/ecs/README.md 了解如何添加组件和系统');
} catch (error) {
console.error('❌ ECS框架初始化失败:', error);
}
}
/**
* 每帧更新ECS框架
*/
update(deltaTime: number) {
if (this.isInitialized) {
// 更新ECS核心系统
Core.update(deltaTime);
}
}
/**
* 组件销毁时清理ECS
*/
onDestroy() {
if (this.isInitialized) {
console.log('🧹 清理ECS框架...');
// ECS框架会自动处理场景清理
this.isInitialized = false;
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "c82e7909-01e6-49ca-b68d-9ee98837e238",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,153 @@
# ECS框架启动模板
欢迎使用ECS框架这是一个最基础的启动模板帮助您快速开始ECS项目开发。
## 📁 项目结构
```
ecs/
├── components/ # 组件目录(请在此添加您的组件)
├── systems/ # 系统目录(请在此添加您的系统)
├── scenes/ # 场景目录
│ └── GameScene.ts # 主游戏场景
├── ECSManager.ts # ECS管理器组件
└── README.md # 本文档
```
## 🚀 快速开始
### 1. 启动ECS框架
ECS框架已经配置完成您只需要
1. 在Cocos Creator中打开您的场景
2. 创建一个空节点(例如命名为"ECSManager"
3.`ECSManager` 组件添加到该节点
4. 运行场景ECS框架将自动启动
### 2. 查看控制台输出
如果一切正常,您将在控制台看到:
```
🎮 正在初始化ECS框架...
🔧 ECS调试模式已启用可在Cocos Creator扩展面板中查看调试信息
🎯 游戏场景已创建
✅ ECS框架初始化成功
🚀 游戏场景已启动
```
### 3. 使用调试面板
ECS框架已启用调试功能您可以
1. 在Cocos Creator编辑器菜单中选择 "扩展" → "ECS Framework" → "调试面板"
2. 调试面板将显示实时的ECS运行状态
- 实体数量和状态
- 系统执行信息
- 性能监控数据
- 组件统计信息
**注意**:调试功能会消耗一定性能,正式发布时建议关闭调试模式。
## 📚 下一步开发
### 创建您的第一个组件
`components/` 目录下创建组件:
```typescript
// components/PositionComponent.ts
import { Component } from '@esengine/ecs-framework';
import { Vec3 } from 'cc';
export class PositionComponent extends Component {
public position: Vec3 = new Vec3();
constructor(x: number = 0, y: number = 0, z: number = 0) {
super();
this.position.set(x, y, z);
}
}
```
### 创建您的第一个系统
`systems/` 目录下创建系统:
```typescript
// systems/MovementSystem.ts
import { EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
import { PositionComponent } from '../components/PositionComponent';
export class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(PositionComponent));
}
protected process(entities: Entity[]): void {
for (const entity of entities) {
const position = entity.getComponent(PositionComponent);
if (position) {
// TODO: 在这里编写移动逻辑
console.log(`实体 ${entity.name} 位置: ${position.position}`);
}
}
}
}
```
### 在场景中注册系统
`scenes/GameScene.ts``initialize()` 方法中添加:
```typescript
import { MovementSystem } from '../systems/MovementSystem';
public initialize(): void {
super.initialize();
this.name = "MainGameScene";
// 添加系统
this.addEntityProcessor(new MovementSystem());
// 创建测试实体
const testEntity = this.createEntity("TestEntity");
testEntity.addComponent(new PositionComponent(0, 0, 0));
}
```
## 🔗 学习资源
- [ECS框架完整文档](https://github.com/esengine/ecs-framework)
- [ECS概念详解](https://github.com/esengine/ecs-framework/blob/master/docs/concepts-explained.md)
- [新手教程](https://github.com/esengine/ecs-framework/blob/master/docs/beginner-tutorials.md)
- [组件设计指南](https://github.com/esengine/ecs-framework/blob/master/docs/component-design-guide.md)
- [系统开发指南](https://github.com/esengine/ecs-framework/blob/master/docs/system-guide.md)
## 💡 开发提示
1. **组件只存储数据**:避免在组件中编写复杂逻辑
2. **系统处理逻辑**:所有业务逻辑应该在系统中实现
3. **使用Matcher过滤实体**系统通过Matcher指定需要处理的实体类型
4. **性能优化**:大量实体时考虑使用位掩码查询和组件索引
## ❓ 常见问题
### Q: 如何创建实体?
A: 在场景中使用 `this.createEntity("实体名称")`
### Q: 如何给实体添加组件?
A: 使用 `entity.addComponent(new YourComponent())`
### Q: 如何获取实体的组件?
A: 使用 `entity.getComponent(YourComponent)`
### Q: 如何删除实体?
A: 使用 `entity.destroy()``this.destroyEntity(entity)`
---
🎮 **开始您的ECS开发之旅吧**
如有问题请查阅官方文档或提交Issue。

View File

@@ -0,0 +1,11 @@
{
"ver": "1.0.1",
"importer": "text",
"imported": true,
"uuid": "0932496e-f7fe-4cb9-86e2-ebd7d2a3d047",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "3d8cbc91-5bc5-4d17-b53a-01fda26e4660",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,102 @@
import { Component } from '@esengine/ecs-framework';
/**
* 生命值组件 - 管理实体的生命值和相关状态
*
* 展示游戏逻辑组件的设计:
* 1. 包含生命值的核心数据
* 2. 提供简单的查询方法
* 3. 复杂的伤害处理逻辑留给系统处理
*/
export class HealthComponent extends Component {
/** 最大生命值 */
public maxHealth: number;
/** 当前生命值 */
public currentHealth: number;
/** 生命值回复速度(每秒回复量) */
public regenRate: number = 0;
/** 最后受到伤害的时间(用于延迟回血等机制) */
public lastDamageTime: number = 0;
/** 是否无敌 */
public invincible: boolean = false;
/** 无敌持续时间 */
public invincibleDuration: number = 0;
constructor(maxHealth: number = 100, regenRate: number = 0) {
super();
this.maxHealth = maxHealth;
this.currentHealth = maxHealth;
this.regenRate = regenRate;
}
/**
* 检查是否死亡
*/
isDead(): boolean {
return this.currentHealth <= 0;
}
/**
* 检查是否满血
*/
isFullHealth(): boolean {
return this.currentHealth >= this.maxHealth;
}
/**
* 获取生命值百分比0-1
*/
getHealthPercentage(): number {
return this.currentHealth / this.maxHealth;
}
/**
* 检查生命值是否低于指定百分比
*/
isHealthBelowPercentage(percentage: number): boolean {
return this.getHealthPercentage() < percentage;
}
/**
* 设置生命值(不超过最大值)
*/
setHealth(health: number) {
this.currentHealth = Math.max(0, Math.min(health, this.maxHealth));
}
/**
* 增加生命值(治疗)
*/
heal(amount: number) {
this.currentHealth = Math.min(this.currentHealth + amount, this.maxHealth);
}
/**
* 减少生命值(受伤)
* 注意:这里只修改数据,具体的伤害逻辑(如死亡处理)应该在系统中实现
*/
takeDamage(damage: number) {
if (this.invincible) return;
this.currentHealth = Math.max(0, this.currentHealth - damage);
this.lastDamageTime = Date.now();
}
/**
* 设置无敌状态
*/
setInvincible(duration: number) {
this.invincible = true;
this.invincibleDuration = duration;
}
/**
* 重置到满血状态
*/
reset() {
this.currentHealth = this.maxHealth;
this.invincible = false;
this.invincibleDuration = 0;
this.lastDamageTime = 0;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "0f20d48a-7b30-4081-a9de-709432b6737b",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,124 @@
import { Component } from '@esengine/ecs-framework';
import { Vec2 } from 'cc';
/**
* 玩家输入组件 - 存储玩家的输入状态
*
* 标记组件示例:
* 1. 标识这是一个玩家控制的实体
* 2. 存储输入状态数据
* 3. 输入处理逻辑在InputSystem中实现
*/
export class PlayerInputComponent extends Component {
/** 移动输入方向(-1到1 */
public moveDirection: Vec2 = new Vec2();
/** 按键状态 */
public keys: { [key: string]: boolean } = {};
/** 鼠标位置 */
public mousePosition: Vec2 = new Vec2();
/** 鼠标按键状态 */
public mouseButtons: { left: boolean; right: boolean; middle: boolean } = {
left: false,
right: false,
middle: false
};
/** 是否启用输入 */
public inputEnabled: boolean = true;
/** 输入敏感度 */
public sensitivity: number = 1.0;
constructor() {
super();
}
/**
* 设置移动方向
*/
setMoveDirection(x: number, y: number) {
this.moveDirection.set(x, y);
// 标准化方向向量(对角线移动不应该更快)
if (this.moveDirection.lengthSqr() > 1) {
this.moveDirection.normalize();
}
}
/**
* 设置按键状态
*/
setKey(key: string, pressed: boolean) {
this.keys[key] = pressed;
}
/**
* 检查按键是否按下
*/
isKeyPressed(key: string): boolean {
return this.keys[key] || false;
}
/**
* 检查是否有移动输入
*/
hasMovementInput(): boolean {
return this.moveDirection.lengthSqr() > 0.01;
}
/**
* 获取标准化的移动方向
*/
getNormalizedMoveDirection(): Vec2 {
const result = new Vec2(this.moveDirection);
if (result.lengthSqr() > 0) {
result.normalize();
}
return result;
}
/**
* 设置鼠标位置
*/
setMousePosition(x: number, y: number) {
this.mousePosition.set(x, y);
}
/**
* 设置鼠标按键状态
*/
setMouseButton(button: 'left' | 'right' | 'middle', pressed: boolean) {
this.mouseButtons[button] = pressed;
}
/**
* 检查鼠标按键是否按下
*/
isMouseButtonPressed(button: 'left' | 'right' | 'middle'): boolean {
return this.mouseButtons[button];
}
/**
* 清除所有输入状态
*/
clearInput() {
this.moveDirection.set(0, 0);
this.keys = {};
this.mouseButtons.left = false;
this.mouseButtons.right = false;
this.mouseButtons.middle = false;
}
/**
* 禁用输入
*/
disableInput() {
this.inputEnabled = false;
this.clearInput();
}
/**
* 启用输入
*/
enableInput() {
this.inputEnabled = true;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "ab10dc4c-c8a3-4fd2-83d6-433d4195966b",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,62 @@
import { Component } from '@esengine/ecs-framework';
import { Vec3 } from 'cc';
/**
* 位置组件 - 存储实体的空间位置信息
*
* 这是最基础的组件示例展示了ECS组件的设计原则
* 1. 主要存储数据,少量辅助方法
* 2. 单一职责:只负责位置相关的数据
* 3. 可复用:任何需要位置信息的实体都可以使用
*/
export class PositionComponent extends Component {
/** 3D位置坐标 */
public position: Vec3 = new Vec3();
/** 上一帧的位置(用于计算移动距离) */
public lastPosition: Vec3 = new Vec3();
constructor(x: number = 0, y: number = 0, z: number = 0) {
super();
this.position.set(x, y, z);
this.lastPosition.set(x, y, z);
}
/**
* 设置位置
*/
setPosition(x: number, y: number, z: number = 0) {
this.lastPosition.set(this.position);
this.position.set(x, y, z);
}
/**
* 移动位置
*/
move(deltaX: number, deltaY: number, deltaZ: number = 0) {
this.lastPosition.set(this.position);
this.position.x += deltaX;
this.position.y += deltaY;
this.position.z += deltaZ;
}
/**
* 计算到另一个位置的距离
*/
distanceTo(other: PositionComponent): number {
return Vec3.distance(this.position, other.position);
}
/**
* 获取本帧移动的距离
*/
getMovementDistance(): number {
return Vec3.distance(this.position, this.lastPosition);
}
/**
* 检查是否在指定范围内
*/
isWithinRange(target: PositionComponent, range: number): boolean {
return this.distanceTo(target) <= range;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "e6ee57d6-d0eb-43f2-a601-9b7a2812de66",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,94 @@
import { Component } from '@esengine/ecs-framework';
import { Vec3 } from 'cc';
/**
* 速度组件 - 存储实体的运动速度信息
*
* 设计原则展示:
* 1. 与PositionComponent分离遵循单一职责原则
* 2. 包含速度限制:避免无限加速
* 3. 提供常用的速度操作方法
*/
export class VelocityComponent extends Component {
/** 当前速度向量 */
public velocity: Vec3 = new Vec3();
/** 最大速度限制 */
public maxSpeed: number = 100;
/** 阻尼系数0-11为无阻尼 */
public damping: number = 1.0;
constructor(x: number = 0, y: number = 0, z: number = 0, maxSpeed: number = 100) {
super();
this.velocity.set(x, y, z);
this.maxSpeed = maxSpeed;
}
/**
* 设置速度
*/
setVelocity(x: number, y: number, z: number = 0) {
this.velocity.set(x, y, z);
this.clampToMaxSpeed();
}
/**
* 添加速度(加速度效果)
*/
addVelocity(x: number, y: number, z: number = 0) {
this.velocity.x += x;
this.velocity.y += y;
this.velocity.z += z;
this.clampToMaxSpeed();
}
/**
* 应用阻尼
*/
applyDamping(deltaTime: number) {
if (this.damping < 1.0) {
const dampingFactor = Math.pow(this.damping, deltaTime);
this.velocity.multiplyScalar(dampingFactor);
}
}
/**
* 限制速度不超过最大值
*/
private clampToMaxSpeed() {
const speed = this.velocity.length();
if (speed > this.maxSpeed) {
this.velocity.normalize();
this.velocity.multiplyScalar(this.maxSpeed);
}
}
/**
* 获取当前速度大小
*/
getSpeed(): number {
return this.velocity.length();
}
/**
* 获取速度方向(单位向量)
*/
getDirection(): Vec3 {
const result = new Vec3();
Vec3.normalize(result, this.velocity);
return result;
}
/**
* 停止移动
*/
stop() {
this.velocity.set(0, 0, 0);
}
/**
* 检查是否在移动
*/
isMoving(): boolean {
return this.velocity.lengthSqr() > 0.01; // 避免浮点数精度问题
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "784d7c28-2b72-427c-8b04-da0fcf775acf",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "6a2d6231-acf9-47b8-a020-d45a7433a95d",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "80dcbaf5-21f7-4bb1-aff4-2cdbb0b5d364",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "879b4e07-dd6b-4445-adb2-a970b97c6d6f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,316 @@
import { Scene } from '@esengine/ecs-framework';
import { Entity } from '@esengine/ecs-framework';
// 导入组件
import { PositionComponent } from '../components/PositionComponent';
import { VelocityComponent } from '../components/VelocityComponent';
import { HealthComponent } from '../components/HealthComponent';
import { PlayerInputComponent } from '../components/PlayerInputComponent';
// 导入系统
import { MovementSystem } from '../systems/MovementSystem';
import { PlayerInputSystem } from '../systems/PlayerInputSystem';
import { HealthSystem } from '../systems/HealthSystem';
/**
* 示例游戏场景 - 完整的ECS应用示例
*
* 这个场景展示了:
* 1. 如何创建和配置各种实体
* 2. 如何添加和组织系统
* 3. 如何实现完整的游戏逻辑
* 4. 如何进行调试和监控
*/
export class ExampleGameScene extends Scene {
// 场景中的重要实体引用
private player: Entity | null;
private enemies: Entity[];
private gameConfig: {
maxEnemies: number;
enemySpawnInterval: number;
gameArea: { width: number; height: number };
};
constructor() {
super();
// 在构造函数中初始化属性
this.player = null;
this.enemies = [];
this.gameConfig = {
maxEnemies: 5,
enemySpawnInterval: 3000, // 3秒生成一个敌人
gameArea: { width: 800, height: 600 }
};
}
/**
* 场景初始化(构造时调用)
*/
public initialize(): void {
super.initialize();
this.name = "ExampleGameScene";
console.log("📋 ExampleGameScene 构造完成");
}
/**
* 场景开始时的回调(所有构造函数执行完毕后调用)
*/
public onStart(): void {
super.onStart();
console.log("🎮 开始初始化示例游戏场景...");
// 1. 添加系统(注意顺序很重要)
this.setupSystems();
// 2. 创建游戏实体
this.createGameEntities();
// 3. 设置定时器和事件
this.setupGameLogic();
console.log("✅ 示例游戏场景初始化完成!");
this.printSceneInfo();
}
/**
* 设置游戏系统
*/
private setupSystems(): void {
console.log("🔧 添加游戏系统...");
// 输入系统(最先处理输入)
this.addEntityProcessor(new PlayerInputSystem());
// 移动系统(处理所有移动逻辑)
this.addEntityProcessor(new MovementSystem());
// 生命值系统(处理生命值、死亡等)
this.addEntityProcessor(new HealthSystem());
console.log("✅ 系统添加完成");
}
/**
* 创建游戏实体
*/
private createGameEntities(): void {
console.log("🏗️ 创建游戏实体...");
// 创建玩家
this.createPlayer();
// 创建初始敌人
this.createInitialEnemies();
// 创建环境实体(可选)
this.createEnvironmentEntities();
console.log("✅ 实体创建完成");
}
/**
* 创建玩家实体
*/
private createPlayer(): void {
this.player = this.createEntity("Player");
// 添加玩家组件
this.player.addComponent(new PositionComponent(0, 0, 0));
this.player.addComponent(new VelocityComponent(0, 0, 0, 250)); // 最大速度250
this.player.addComponent(new HealthComponent(100, 5)); // 100血每秒回5血
this.player.addComponent(new PlayerInputComponent());
console.log("🎯 玩家创建完成 - 使用WASD或方向键移动空格键攻击");
}
/**
* 创建初始敌人
*/
private createInitialEnemies(): void {
for (let i = 0; i < 3; i++) {
this.createEnemy(i);
}
}
/**
* 创建单个敌人
*/
private createEnemy(index: number): Entity {
const enemy = this.createEntity(`Enemy_${index}`);
// 随机位置
const x = (Math.random() - 0.5) * this.gameConfig.gameArea.width;
const y = (Math.random() - 0.5) * this.gameConfig.gameArea.height;
// 随机速度
const velocityX = (Math.random() - 0.5) * 100;
const velocityY = (Math.random() - 0.5) * 100;
// 添加敌人组件
enemy.addComponent(new PositionComponent(x, y, 0));
enemy.addComponent(new VelocityComponent(velocityX, velocityY, 0, 150));
enemy.addComponent(new HealthComponent(50, 0)); // 50血不回血
// 添加到敌人列表
this.enemies.push(enemy);
return enemy;
}
/**
* 创建环境实体(演示不同类型的实体)
*/
private createEnvironmentEntities(): void {
// 创建一些静态的环境对象
for (let i = 0; i < 5; i++) {
const obstacle = this.createEntity(`Obstacle_${i}`);
const x = (Math.random() - 0.5) * this.gameConfig.gameArea.width * 0.8;
const y = (Math.random() - 0.5) * this.gameConfig.gameArea.height * 0.8;
// 只有位置,没有速度和生命值
obstacle.addComponent(new PositionComponent(x, y, 0));
}
console.log("🌲 环境实体创建完成");
}
/**
* 设置游戏逻辑和定时器
*/
private setupGameLogic(): void {
console.log("⚙️ 设置游戏逻辑...");
// 敌人生成定时器
this.setupEnemySpawner();
// 游戏状态监控
this.setupGameMonitoring();
console.log("✅ 游戏逻辑设置完成");
}
/**
* 设置敌人生成器
*/
private setupEnemySpawner(): void {
setInterval(() => {
if (this.enemies.length < this.gameConfig.maxEnemies) {
const newEnemy = this.createEnemy(this.enemies.length);
}
}, this.gameConfig.enemySpawnInterval);
}
/**
* 设置游戏监控
*/
private setupGameMonitoring(): void {
// 每10秒清理已死亡的敌人引用
setInterval(() => {
this.cleanupDeadEnemies();
}, 10000);
}
/**
* 打印游戏状态(按需调用)
*/
private printGameStatus(): void {
const totalEntities = this.entities.count;
const aliveEnemies = this.enemies.filter(e => !e.isDestroyed).length;
console.log("📊 游戏状态报告:");
console.log(` - 总实体数: ${totalEntities}`);
console.log(` - 存活敌人: ${aliveEnemies}`);
if (this.player && !this.player.isDestroyed) {
const playerHealth = this.player.getComponent(HealthComponent);
const playerPos = this.player.getComponent(PositionComponent);
console.log(` - 玩家生命值: ${playerHealth?.currentHealth}/${playerHealth?.maxHealth}`);
console.log(` - 玩家位置: (${playerPos?.position.x.toFixed(1)}, ${playerPos?.position.y.toFixed(1)})`);
}
}
/**
* 清理已死亡的敌人引用
*/
private cleanupDeadEnemies(): void {
const initialCount = this.enemies.length;
this.enemies = this.enemies.filter(enemy => !enemy.isDestroyed);
const removedCount = initialCount - this.enemies.length;
if (removedCount > 0) {
console.log(`🧹 清理了 ${removedCount} 个已死亡的敌人引用`);
}
}
/**
* 打印场景信息
*/
private printSceneInfo(): void {
console.log("\n📋 场景信息:");
console.log(` 场景名: ${this.name}`);
console.log(` 实体数: ${this.entities.count}`);
console.log(` 系统数: ${this.entityProcessors.count}`);
console.log(` 玩家: ${this.player?.name || '未创建'}`);
console.log(` 敌人: ${this.enemies.length}`);
console.log("\n🎮 控制说明:");
console.log(" - WASD 或 方向键: 移动");
console.log(" - 空格: 攻击/行动");
console.log(" - ESC: 暂停");
console.log("\n💡 学习要点:");
console.log(" 1. 观察控制台输出了解ECS运行过程");
console.log(" 2. 打开调试面板查看性能数据");
console.log(" 3. 尝试修改组件参数观察变化");
console.log(" 4. 查看代码学习ECS设计模式\n");
}
/**
* 获取玩家实体(供其他系统使用)
*/
public getPlayer(): Entity | null {
return this.player;
}
/**
* 获取所有敌人(供其他系统使用)
*/
public getEnemies(): Entity[] {
return this.enemies.filter(enemy => !enemy.isDestroyed);
}
/**
* 游戏重置方法
*/
public resetGame(): void {
console.log("🔄 重置游戏...");
// 销毁所有实体
if (this.player) {
this.player.destroy();
this.player = null;
}
this.enemies.forEach(enemy => enemy.destroy());
this.enemies = [];
// 重新创建实体
this.createGameEntities();
console.log("✅ 游戏重置完成");
}
/**
* 场景卸载时调用
*/
public unload(): void {
console.log("🧹 清理示例游戏场景...");
// 清理引用
this.player = null;
this.enemies = [];
super.unload();
console.log("✅ 场景清理完成");
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "da87facc-89a0-47da-a0ef-423255200a51",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,57 @@
import { Scene } from '@esengine/ecs-framework';
/**
* 游戏场景
*
* 这是您的主游戏场景。在这里可以:
* - 添加游戏系统
* - 创建初始实体
* - 设置场景参数
*/
export class GameScene extends Scene {
/**
* 场景初始化
* 在场景创建时调用,用于设置基础配置
*/
public initialize(): void {
super.initialize();
// 设置场景名称
this.name = "MainGameScene";
console.log('🎯 游戏场景已创建');
// TODO: 在这里添加您的游戏系统
// 例如this.addEntityProcessor(new MovementSystem());
// TODO: 在这里创建初始实体
// 例如this.createEntity("Player");
}
/**
* 场景开始运行
* 在场景开始时调用,用于执行启动逻辑
*/
public onStart(): void {
super.onStart();
console.log('🚀 游戏场景已启动');
// TODO: 在这里添加场景启动逻辑
// 例如创建UI、播放音乐、初始化游戏状态等
}
/**
* 场景卸载
* 在场景结束时调用,用于清理资源
*/
public unload(): void {
console.log('🛑 游戏场景已结束');
// TODO: 在这里添加清理逻辑
// 例如:清理缓存、释放资源等
super.unload();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "8fee85be-2224-4200-a898-d3ae2406fb1d",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "a5e3a8c9-3d0b-4a36-9d20-6f70f1380131",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,228 @@
import { EntitySystem, Entity, Matcher, Time } from '@esengine/ecs-framework';
import { HealthComponent } from '../components/HealthComponent';
/**
* 生命值系统 - 处理生命值相关的逻辑
*
* 展示生命值管理:
* 1. 自动回血
* 2. 无敌状态管理
* 3. 死亡处理
* 4. 事件触发
*/
export class HealthSystem extends EntitySystem {
/** 回血延迟时间(受伤后多久开始回血,毫秒) */
private regenDelay: number = 3000;
constructor() {
// 只处理拥有HealthComponent的实体
super(Matcher.empty().all(HealthComponent));
}
public initialize(): void {
super.initialize();
console.log("HealthSystem 已初始化 - 开始处理生命值逻辑");
}
/**
* 每帧处理:更新生命值相关逻辑
*/
protected process(entities: Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(HealthComponent);
// 处理无敌状态
this.processInvincibility(health);
// 处理生命值回复
this.processHealthRegeneration(entity, health);
// 检查死亡状态
this.checkDeathStatus(entity, health);
}
}
/**
* 处理无敌状态
*/
private processInvincibility(health: HealthComponent): void {
if (health.invincible && health.invincibleDuration > 0) {
health.invincibleDuration -= Time.deltaTime;
// 无敌时间结束
if (health.invincibleDuration <= 0) {
health.invincible = false;
health.invincibleDuration = 0;
console.log("无敌状态结束");
}
}
}
/**
* 处理生命值回复
*/
private processHealthRegeneration(entity: Entity, health: HealthComponent): void {
// 如果已经满血或者没有回复速度,则不处理
if (health.isFullHealth() || health.regenRate <= 0) {
return;
}
// 检查是否超过了回血延迟时间
const currentTime = Date.now();
if (currentTime - health.lastDamageTime < this.regenDelay) {
return;
}
// 计算回血量
const regenAmount = health.regenRate * Time.deltaTime;
const oldHealth = health.currentHealth;
// 执行回血
health.heal(regenAmount);
// 如果实际回了血,输出日志
if (health.currentHealth > oldHealth) {
console.log(`${entity.name} 回血: ${oldHealth.toFixed(1)} -> ${health.currentHealth.toFixed(1)} (${health.getHealthPercentage() * 100}%)`);
}
}
/**
* 检查死亡状态
*/
private checkDeathStatus(entity: Entity, health: HealthComponent): void {
if (health.isDead()) {
this.handleEntityDeath(entity, health);
}
}
/**
* 处理实体死亡
*/
private handleEntityDeath(entity: Entity, health: HealthComponent): void {
console.log(`💀 ${entity.name} 已死亡!`);
// 触发死亡事件(如果有事件系统)
this.triggerDeathEvent(entity);
// 可以在这里添加死亡效果、掉落物品等逻辑
this.createDeathEffect(entity);
// 标记实体为死亡状态(而不是立即销毁)
// 这样其他系统可以处理死亡相关的逻辑
entity.addComponent(new DeadMarkerComponent());
// 可选:延迟销毁实体
setTimeout(() => {
if (entity && !entity.isDestroyed) {
entity.destroy();
console.log(`${entity.name} 已被销毁`);
}
}, 1000); // 1秒后销毁
}
/**
* 触发死亡事件
*/
private triggerDeathEvent(entity: Entity): void {
// 如果项目中有事件系统,可以在这里发送死亡事件
console.log(`触发死亡事件: ${entity.name}`);
// 示例事件数据
const deathEventData = {
entityId: entity.id,
entityName: entity.name,
deathTime: Date.now(),
position: this.getEntityPosition(entity)
};
// 这里可以调用事件系统发送事件
// eventBus.emit('entity:died', deathEventData);
}
/**
* 创建死亡效果
*/
private createDeathEffect(entity: Entity): void {
console.log(`💥 为 ${entity.name} 创建死亡效果`);
// 在实际游戏中,这里可能会:
// 1. 播放死亡动画
// 2. 播放死亡音效
// 3. 创建粒子效果
// 4. 掉落物品
}
/**
* 获取实体位置(辅助方法)
*/
private getEntityPosition(entity: Entity): { x: number; y: number; z: number } {
// 尝试获取位置组件
const position = entity.getComponent(PositionComponent);
if (position) {
return {
x: position.position.x,
y: position.position.y,
z: position.position.z
};
}
return { x: 0, y: 0, z: 0 };
}
/**
* 公共方法:对实体造成伤害
* 这个方法可以被其他系统调用
*/
public damageEntity(entity: Entity, damage: number, source?: Entity): boolean {
const health = entity.getComponent(HealthComponent);
if (!health || health.invincible) {
return false; // 无生命值组件或处于无敌状态
}
const oldHealth = health.currentHealth;
health.takeDamage(damage);
console.log(`⚔️ ${entity.name} 受到 ${damage} 点伤害: ${oldHealth.toFixed(1)} -> ${health.currentHealth.toFixed(1)}`);
// 如果有伤害来源,可以记录或处理
if (source) {
console.log(`伤害来源: ${source.name}`);
}
return true;
}
/**
* 公共方法:治疗实体
*/
public healEntity(entity: Entity, healAmount: number): boolean {
const health = entity.getComponent(HealthComponent);
if (!health || health.isFullHealth()) {
return false;
}
const oldHealth = health.currentHealth;
health.heal(healAmount);
console.log(`💚 ${entity.name} 恢复 ${healAmount} 点生命值: ${oldHealth.toFixed(1)} -> ${health.currentHealth.toFixed(1)}`);
return true;
}
}
/**
* 死亡标记组件 - 标记已死亡的实体
* 这是一个简单的标记组件,用于标识死亡状态
*/
class DeadMarkerComponent extends Component {
public deathTime: number;
constructor() {
super();
this.deathTime = Date.now();
}
}
// 导入位置组件(用于获取实体位置)
import { PositionComponent } from '../components/PositionComponent';
import { Component } from '@esengine/ecs-framework';

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "455c12d1-52a8-41ac-b1b5-0d2b93c079aa",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,99 @@
import { EntitySystem, Entity, Matcher, Time } from '@esengine/ecs-framework';
import { PositionComponent } from '../components/PositionComponent';
import { VelocityComponent } from '../components/VelocityComponent';
/**
* 移动系统 - 处理实体的移动逻辑
*
* EntitySystem示例
* 1. 使用Matcher指定需要的组件Position + Velocity
* 2. 每帧更新所有移动实体的位置
* 3. 展示组件间的协作
*/
export class MovementSystem extends EntitySystem {
constructor() {
// 只处理同时拥有PositionComponent和VelocityComponent的实体
super(Matcher.empty().all(PositionComponent, VelocityComponent));
}
/**
* 每帧执行:更新所有移动实体的位置
*/
protected process(entities: Entity[]): void {
for (const entity of entities) {
const position = entity.getComponent(PositionComponent);
const velocity = entity.getComponent(VelocityComponent);
// 基本移动:位置 = 当前位置 + 速度 * 时间
position.move(
velocity.velocity.x * Time.deltaTime,
velocity.velocity.y * Time.deltaTime,
velocity.velocity.z * Time.deltaTime
);
// 应用阻尼(摩擦力)
velocity.applyDamping(Time.deltaTime);
// 可选:添加边界检查
this.checkBoundaries(position, velocity);
}
}
/**
* 边界检查(可选功能)
* 这个方法演示了如何在系统中实现额外的游戏逻辑
*/
private checkBoundaries(position: PositionComponent, velocity: VelocityComponent) {
const bounds = {
left: -400,
right: 400,
top: 300,
bottom: -300
};
// 检查X轴边界
if (position.position.x < bounds.left) {
position.position.x = bounds.left;
velocity.velocity.x = Math.abs(velocity.velocity.x); // 反弹
} else if (position.position.x > bounds.right) {
position.position.x = bounds.right;
velocity.velocity.x = -Math.abs(velocity.velocity.x); // 反弹
}
// 检查Y轴边界
if (position.position.y < bounds.bottom) {
position.position.y = bounds.bottom;
velocity.velocity.y = Math.abs(velocity.velocity.y); // 反弹
} else if (position.position.y > bounds.top) {
position.position.y = bounds.top;
velocity.velocity.y = -Math.abs(velocity.velocity.y); // 反弹
}
}
/**
* 系统初始化时调用
* 可以在这里设置系统级别的配置
*/
public initialize(): void {
super.initialize();
console.log("MovementSystem 已初始化 - 开始处理实体移动");
}
/**
* 获取系统统计信息(用于调试)
*/
public getStats(): { processedEntities: number; totalMovement: number } {
let totalMovement = 0;
const entities = this.entities;
for (const entity of entities) {
const position = entity.getComponent(PositionComponent);
totalMovement += position.getMovementDistance();
}
return {
processedEntities: entities.length,
totalMovement: totalMovement
};
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "a8712467-efe0-46ec-a246-a9fa07d203d9",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,173 @@
import { EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
import { PlayerInputComponent } from '../components/PlayerInputComponent';
import { VelocityComponent } from '../components/VelocityComponent';
import { input, Input, EventKeyboard, KeyCode } from 'cc';
/**
* 玩家输入系统 - 处理玩家输入并转换为游戏行为
*
* 展示系统的职责:
* 1. 收集输入事件
* 2. 更新输入组件状态
* 3. 根据输入修改其他组件(如速度)
*/
export class PlayerInputSystem extends EntitySystem {
private moveSpeed: number = 200; // 移动速度
constructor() {
// 只处理拥有PlayerInputComponent的实体
super(Matcher.empty().all(PlayerInputComponent));
}
public initialize(): void {
super.initialize();
console.log("PlayerInputSystem 已初始化 - 开始监听玩家输入");
// 注册键盘事件监听器
this.setupInputListeners();
}
/**
* 设置输入事件监听器
*/
private setupInputListeners(): void {
// 键盘按下事件
input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
// 键盘抬起事件
input.on(Input.EventType.KEY_UP, this.onKeyUp, this);
}
/**
* 键盘按下处理
*/
private onKeyDown(event: EventKeyboard): void {
const keyCode = event.keyCode;
const keyName = this.getKeyName(keyCode);
// 更新所有玩家实体的输入状态
for (const entity of this.entities) {
const playerInput = entity.getComponent(PlayerInputComponent);
if (playerInput && playerInput.inputEnabled) {
playerInput.setKey(keyName, true);
}
}
}
/**
* 键盘抬起处理
*/
private onKeyUp(event: EventKeyboard): void {
const keyCode = event.keyCode;
const keyName = this.getKeyName(keyCode);
// 更新所有玩家实体的输入状态
for (const entity of this.entities) {
const playerInput = entity.getComponent(PlayerInputComponent);
if (playerInput && playerInput.inputEnabled) {
playerInput.setKey(keyName, false);
}
}
}
/**
* 每帧处理:根据输入状态更新实体行为
*/
protected process(entities: Entity[]): void {
for (const entity of entities) {
const playerInput = entity.getComponent(PlayerInputComponent);
if (!playerInput || !playerInput.inputEnabled) {
continue;
}
// 处理移动输入
this.processMovementInput(entity, playerInput);
// 处理其他输入(如攻击、跳跃等)
this.processActionInput(entity, playerInput);
}
}
/**
* 处理移动输入
*/
private processMovementInput(entity: Entity, playerInput: PlayerInputComponent): void {
const velocity = entity.getComponent(VelocityComponent);
if (!velocity) return;
// 根据按键状态计算移动方向
let moveX = 0;
let moveY = 0;
if (playerInput.isKeyPressed('A') || playerInput.isKeyPressed('ArrowLeft')) {
moveX -= 1;
}
if (playerInput.isKeyPressed('D') || playerInput.isKeyPressed('ArrowRight')) {
moveX += 1;
}
if (playerInput.isKeyPressed('W') || playerInput.isKeyPressed('ArrowUp')) {
moveY += 1;
}
if (playerInput.isKeyPressed('S') || playerInput.isKeyPressed('ArrowDown')) {
moveY -= 1;
}
// 更新输入组件的移动方向
playerInput.setMoveDirection(moveX, moveY);
// 将输入转换为速度
const normalizedDirection = playerInput.getNormalizedMoveDirection();
velocity.setVelocity(
normalizedDirection.x * this.moveSpeed * playerInput.sensitivity,
normalizedDirection.y * this.moveSpeed * playerInput.sensitivity,
0
);
}
/**
* 处理动作输入(攻击、技能等)
*/
private processActionInput(entity: Entity, playerInput: PlayerInputComponent): void {
// 空格键 - 跳跃或攻击
if (playerInput.isKeyPressed('Space')) {
console.log(`玩家 ${entity.name} 执行动作:攻击/跳跃`);
// 这里可以触发攻击组件或添加跳跃效果
}
// ESC键 - 暂停游戏
if (playerInput.isKeyPressed('Escape')) {
console.log("玩家请求暂停游戏");
// 可以发送暂停事件给游戏管理系统
}
}
/**
* 将键码转换为字符串
*/
private getKeyName(keyCode: KeyCode): string {
const keyMap: { [key: number]: string } = {
[KeyCode.KEY_A]: 'A',
[KeyCode.KEY_D]: 'D',
[KeyCode.KEY_S]: 'S',
[KeyCode.KEY_W]: 'W',
[KeyCode.ARROW_LEFT]: 'ArrowLeft',
[KeyCode.ARROW_RIGHT]: 'ArrowRight',
[KeyCode.ARROW_UP]: 'ArrowUp',
[KeyCode.ARROW_DOWN]: 'ArrowDown',
[KeyCode.SPACE]: 'Space',
[KeyCode.ESCAPE]: 'Escape'
};
return keyMap[keyCode] || `Key_${keyCode}`;
}
/**
* 系统清理
*/
public onDestroy(): void {
// 移除事件监听器
input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.off(Input.EventType.KEY_UP, this.onKeyUp, this);
console.log("PlayerInputSystem 已清理");
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "7b69a39f-926a-4260-94ba-e15e31b324b5",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,54 @@
{
"codeGeneration": {
"template": "typescript",
"useStrictMode": true,
"generateComments": true,
"generateImports": true,
"componentSuffix": "Component",
"systemSuffix": "System",
"indentStyle": "spaces",
"indentSize": 4
},
"performance": {
"enableMonitoring": true,
"warningThreshold": 16.67,
"criticalThreshold": 33.33,
"memoryWarningMB": 100,
"memoryCriticalMB": 200,
"maxRecentSamples": 60,
"enableFpsMonitoring": true,
"targetFps": 120
},
"debugging": {
"enableDebugMode": true,
"showEntityCount": true,
"showSystemExecutionTime": true,
"enablePerformanceWarnings": true,
"logLevel": "info",
"enableDetailedLogs": false
},
"editor": {
"autoRefreshAssets": true,
"showWelcomePanelOnStartup": true,
"enableAutoUpdates": false,
"updateChannel": "stable",
"enableNotifications": true
},
"template": {
"defaultEntityName": "ModifiedEntity",
"defaultComponentName": "TestComponent",
"defaultSystemName": "TestSystem",
"createExampleFiles": true,
"includeDocumentation": true,
"useFactoryPattern": true
},
"events": {
"enableEventSystem": true,
"defaultEventPriority": 0,
"enableAsyncEvents": true,
"enableEventBatching": false,
"batchSize": 10,
"batchDelay": 16,
"maxEventListeners": 100
}
}

View File

@@ -0,0 +1,81 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"description": "面板数据 / Panel data",
"additionalProperties": false,
"patternProperties": {
"^[a-zA-Z0-9_-]+$": {
"type": "object",
"description": "面板名 / Panel name",
"properties": {
"title": {
"type": "string",
"default": "Default Panel",
"description": "面板标题,支持 i18n:key / Panel title, support for i18n:key (required)"
},
"main": {
"type": "string",
"default": "dist/panels/default/index.js",
"description": "入口函数 / Entry function (required)"
},
"icon": {
"type": "string",
"description": "面板图标存放相对目录 / Relative directory for panel icon storage"
},
"type": {
"type": "string",
"enum": ["dockable", "simple"],
"default": "dockable",
"description": "面板类型dockable | simple / Panel type (dockable | simple)"
},
"flags": {
"type": "object",
"properties": {
"resizable": {
"type": "boolean",
"default": true,
"description": "是否可以改变大小,默认 true / Whether the size can be changed, default true"
},
"save": {
"type": "boolean",
"default": true,
"description": "是否需要保存,默认 false / Whether to save, default false"
},
"alwaysOnTop": {
"type": "boolean",
"default": true,
"description": "是否保持顶层显示,默认 false / Whether to keep the top level display, default false"
}
}
},
"size": {
"type": "object",
"description": "面板大小信息 / Panel size information",
"properties": {
"min-width": {
"type": "number",
"default": 200,
"description": "面板最小宽度 / Minimum panel width"
},
"min-height": {
"type": "number",
"default": 200,
"description": "面板最小高度 / Minimum panel height"
},
"width": {
"type": "number",
"default": 400,
"description": " 面板默认宽度 / Panel Default Width"
},
"height": {
"type": "number",
"default": 600,
"description": "面板默认高度 / Panel Default Height"
}
}
}
},
"required": ["title", "main"]
}
}
}

View File

@@ -0,0 +1,9 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"description": "其他扩展插件的扩展配置 / Extended configuration for other extension plugins",
"properties": {
},
"required": []
}

View File

@@ -0,0 +1,64 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"description": "插件定义文件 / Extension definition file",
"properties": {
"author": {
"type": "string",
"description": "作者 / Author",
"default": "Cocos Creator Developer"
},
"contributions": {
"$ref": "./contributions/index.json"
},
"dependencies": {
"type": "object",
"description": "发布时所需的依赖库 / Dependencies required for publishing"
},
"description": {
"type": "string",
"description": "简要介绍扩展关键特性、用途,支持 i18n / Brief introduction of the key features and uses of the extension, supporting i18n"
},
"devDependencies": {
"type": "object",
"description": "开发时所需的依赖库 / Dependencies required for development"
},
"editor": {
"type": "string",
"description": "支持的 Cocos Creator 编辑器版本,支持 semver 格式 / Supported Cocos Creator editor version, supporting semver format"
},
"main": {
"type": "string",
"description": "入口函数 / Entry function",
"default": "./dist/index.js"
},
"name": {
"type": "string",
"description": "不能以 _ 或 . 开头、不能含有大写字母,也不能含有 URL 的非法字符例如 .、' 和 ,。 / Cannot start with _ or., cannot contain uppercase letters, and cannot contain URL illegal characters such as.,'and,",
"default": "Custom Extension"
},
"package_version": {
"type": "number",
"description": "扩展系统预留版本号 / Extension system reserved version number",
"default": 2
},
"panels": {
"$ref": "./base/panels.json"
},
"scripts": {
"type": "object",
"description": "NPM 脚本 / NPM scripts"
},
"version": {
"type": "string",
"description": "版本号字符串 / Version number string",
"default": "1.0.0"
}
},
"required": [
"author",
"name",
"package_version",
"version"
]
}

View File

@@ -0,0 +1,81 @@
# ECS Framework for Cocos Creator - 开发扩展插件
专业的ECS框架开发助手为Cocos Creator提供完整的实体组件系统(ECS)开发工具链。
## 🎯 主要功能
### 📦 一键安装管理
- **自动检测**实时检测ECS框架安装状态和版本信息
- **一键安装**:快速安装 `@esengine/ecs-framework` 到当前项目
- **版本管理**:自动检查更新,支持一键更新到最新版本
- **智能卸载**:安全卸载框架,保护项目完整性
### 🚀 代码生成器
- **智能生成**:输入功能名称,自动生成对应的组件和系统代码
- **多种系统类型**支持EntitySystem、ProcessingSystem、IntervalSystem、PassiveSystem
- **组件配置**:可选择添加属性、注释等定制化选项
- **组件过滤**:支持生成带组件过滤的高级系统
### 🛠️ 项目模板
- **快速启动**一键生成完整的ECS项目结构
- **预设组件**包含位置、速度、Cocos节点等常用组件
- **系统示例**:提供移动系统、节点同步系统等实用示例
- **工厂模式**:包含实体工厂和场景管理器模板
### 🔍 调试工具
- **实时监控**查看ECS框架运行状态和性能数据
- **组件池监控**:实时监控组件对象池使用情况
- **性能分析**:提供详细的性能统计和优化建议
## 📋 面板介绍
### 欢迎面板
- ECS框架安装状态检测
- 一键安装、更新、卸载操作
- 项目模板生成
- 快速访问文档和GitHub
### 代码生成器
- 可视化代码生成界面
- 实时预览生成的代码结构
- 支持批量生成多个文件
### 调试面板
- 实时性能监控
- 组件池状态查看
- 系统运行统计
## 🔧 开发环境
- **Cocos Creator**: >= 3.8.6
- **Node.js**: >= 14.0.0
- **依赖框架**: @esengine/ecs-framework
## 📥 安装使用
1. 将插件复制到项目的 `extensions` 目录
2. 在Cocos Creator中启用插件
3. 通过菜单 `面板 -> ECS Framework -> 欢迎面板` 打开主界面
4. 按照界面提示安装ECS框架并开始开发
## 🚀 快速开始
1. **安装框架**:在欢迎面板点击"安装 ECS Framework"
2. **创建模板**:点击"创建ECS模板"生成项目结构
3. **生成代码**:使用代码生成器快速创建组件和系统
4. **开始开发**基于生成的模板开始您的ECS游戏开发
## 📚 更多资源
- **GitHub仓库**[https://github.com/esengine/ecs-framework](https://github.com/esengine/ecs-framework)
- **完整文档**包含详细的API文档和教程
- **技术交流**加入QQ群获取技术支持和交流
## ⭐ 特色优势
- **零配置**:开箱即用,无需复杂配置
- **可视化**:图形化界面,操作简单直观
- **高效率**:大幅减少重复代码编写
- **专业性**基于成熟的ECS框架设计模式
让ECS开发变得简单高效专注于游戏逻辑而非框架配置

View File

@@ -0,0 +1,22 @@
{
"$schema": "https://schemastore.azurewebsites.net/schemas/json/tsconfig.json",
"compilerOptions": {
"target": "ES2017",
"module": "CommonJS",
"moduleResolution": "node",
"inlineSourceMap": true,
"inlineSources": true,
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./source",
"types": [
"node",
"@cocos/creator-types/editor",
]
}
}

View File

@@ -0,0 +1,15 @@
"use strict";
module.exports = {
description: "Professional ECS Framework Development Assistant: One-click installation of @esengine/ecs-framework, intelligent code generator for quick creation of components and systems, project template generation, real-time status detection and version management. Provides welcome panel, debug panel, code generator and behavior tree AI component library to make ECS development in Cocos Creator more efficient and convenient.",
open_panel: "Default Panel",
send_to_panel: "Send message to panel",
menu: {
panel: "Panel",
develop: "Develop",
create: "Create",
open: "Open"
}
};

View File

@@ -0,0 +1,18 @@
"use strict";
module.exports = {
// 插件描述
description: "专业的ECS框架开发助手一键安装@esengine/ecs-framework智能代码生成器快速创建组件和系统项目模板生成实时状态检测和版本管理。提供欢迎面板、调试面板、代码生成器和行为树AI组件库让Cocos Creator的ECS开发更高效便捷。",
// 面板相关
open_panel: "默认面板",
send_to_panel: "发送消息给面板",
// 菜单相关
menu: {
panel: "面板",
develop: "开发",
create: "创建",
open: "打开"
}
};

View File

@@ -0,0 +1,318 @@
{
"name": "cocos-ecs-extension",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cocos-ecs-extension",
"version": "1.0.0",
"hasInstallScript": true,
"dependencies": {
"fs-extra": "^10.0.0",
"vue": "^3.1.4",
"ws": "^8.14.2"
},
"devDependencies": {
"@cocos/creator-types": "^3.8.6",
"@types/fs-extra": "^9.0.5",
"@types/node": "^18.17.1",
"@types/ws": "^8.5.10",
"typescript": "^5.8.2"
}
},
"node_modules/@babel/parser": {
"version": "7.23.0",
"license": "MIT",
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@cocos/creator-types": {
"version": "3.8.6",
"resolved": "https://registry.npmjs.org/@cocos/creator-types/-/creator-types-3.8.6.tgz",
"integrity": "sha512-hyZ4aoqqLxoRtKbBLSJM5RgtK3oGOlTEryHDcyH4znq3h9cFk+MSbQC2aJHvK5/bMlJzsZ641/hD77RGSrvo8Q==",
"dev": true
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"license": "MIT"
},
"node_modules/@types/fs-extra": {
"version": "9.0.13",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
"integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "18.19.111",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.111.tgz",
"integrity": "sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@vue/compiler-core": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.21.3",
"@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.3.4",
"@vue/shared": "3.3.4"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.20.15",
"@vue/compiler-core": "3.3.4",
"@vue/compiler-dom": "3.3.4",
"@vue/compiler-ssr": "3.3.4",
"@vue/reactivity-transform": "3.3.4",
"@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.0",
"postcss": "^8.1.10",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.3.4",
"@vue/shared": "3.3.4"
}
},
"node_modules/@vue/reactivity": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.3.4"
}
},
"node_modules/@vue/reactivity-transform": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.20.15",
"@vue/compiler-core": "3.3.4",
"@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.0"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.3.4",
"@vue/shared": "3.3.4"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@vue/runtime-core": "3.3.4",
"@vue/shared": "3.3.4",
"csstype": "^3.1.1"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.3.4",
"@vue/shared": "3.3.4"
},
"peerDependencies": {
"vue": "3.3.4"
}
},
"node_modules/@vue/shared": {
"version": "3.3.4",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.1.2",
"license": "MIT"
},
"node_modules/estree-walker": {
"version": "2.0.2",
"license": "MIT"
},
"node_modules/fs-extra": {
"version": "10.1.0",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"license": "ISC"
},
"node_modules/jsonfile": {
"version": "6.1.0",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/magic-string": {
"version": "0.30.3",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"engines": {
"node": ">=12"
}
},
"node_modules/nanoid": {
"version": "3.3.6",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.4.30",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/universalify": {
"version": "2.0.0",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/vue": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.3.4",
"@vue/compiler-sfc": "3.3.4",
"@vue/runtime-dom": "3.3.4",
"@vue/server-renderer": "3.3.4",
"@vue/shared": "3.3.4"
}
},
"node_modules/ws": {
"version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

@@ -0,0 +1,212 @@
{
"$schema": "./@types/schema/package/index.json",
"package_version": 2,
"name": "cocos-ecs-extension",
"version": "1.0.0",
"author": "esengine",
"editor": ">=3.8.6",
"scripts": {
"preinstall": "node ./scripts/preinstall.js",
"build": "npx tsc"
},
"description": "i18n:cocos-ecs-extension.description",
"main": "./dist/main.js",
"dependencies": {
"vue": "^3.1.4",
"fs-extra": "^10.0.0",
"ws": "^8.14.2"
},
"devDependencies": {
"@cocos/creator-types": "^3.8.6",
"@types/fs-extra": "^9.0.5",
"@types/node": "^18.17.1",
"@types/ws": "^8.5.10",
"typescript": "^5.8.2"
},
"panels": {
"default": {
"title": "ECS Framework - 欢迎面板",
"type": "dockable",
"main": "dist/panels/default/index.js",
"size": {
"min-width": 450,
"min-height": 600,
"width": 850,
"height": 800
}
},
"debug": {
"title": "ECS Framework - 调试面板",
"type": "dockable",
"main": "dist/panels/debug/index.js",
"size": {
"min-width": 400,
"min-height": 500,
"width": 500,
"height": 600
}
},
"generator": {
"title": "ECS Framework - 代码生成器",
"type": "dockable",
"main": "dist/panels/generator/index.js",
"size": {
"min-width": 600,
"min-height": 500,
"width": 900,
"height": 700
}
},
"behavior-tree": {
"title": "ECS Framework - 行为树AI组件库",
"type": "dockable",
"main": "dist/panels/behavior-tree/index.js",
"size": {
"min-width": 700,
"min-height": 600,
"width": 1000,
"height": 800
}
}
},
"contributions": {
"scene": {
"script": "./dist/scene.js"
},
"menu": [
{
"path": "i18n:menu.panel/ECS Framework",
"label": "欢迎面板",
"message": "open-panel"
},
{
"path": "i18n:menu.panel/ECS Framework",
"label": "调试面板",
"message": "open-debug"
},
{
"path": "i18n:menu.panel/ECS Framework",
"label": "代码生成器",
"message": "open-generator"
},
{
"path": "i18n:menu.panel/ECS Framework",
"label": "行为树AI组件库",
"message": "open-behavior-tree"
},
{
"path": "i18n:menu.develop/ECS Framework",
"label": "ECS 开发工具",
"message": "open-panel"
}
],
"assets": {
"menu": {
"methods": "./dist/assets-menu.js",
"assetMenu": "onAssetMenu"
}
},
"messages": {
"open-panel": {
"methods": [
"openPanel"
]
},
"install-ecs-framework": {
"methods": [
"install-ecs-framework"
]
},
"update-ecs-framework": {
"methods": [
"update-ecs-framework"
]
},
"uninstall-ecs-framework": {
"methods": [
"uninstall-ecs-framework"
]
},
"open-documentation": {
"methods": [
"open-documentation"
]
},
"create-ecs-template": {
"methods": [
"create-ecs-template"
]
},
"open-github": {
"methods": [
"open-github"
]
},
"open-qq-group": {
"methods": [
"open-qq-group"
]
},
"open-debug": {
"methods": [
"open-debug"
]
},
"open-generator": {
"methods": [
"open-generator"
]
},
"open-behavior-tree": {
"methods": [
"open-behavior-tree"
]
},
"install-behavior-tree": {
"methods": [
"install-behavior-tree"
]
},
"update-behavior-tree": {
"methods": [
"update-behavior-tree"
]
},
"check-behavior-tree-installed": {
"methods": [
"check-behavior-tree-installed"
]
},
"open-behavior-tree-docs": {
"methods": [
"open-behavior-tree-docs"
]
},
"create-behavior-tree-file": {
"methods": [
"create-behavior-tree-file"
]
},
"load-behavior-tree-file": {
"methods": [
"load-behavior-tree-file"
]
},
"create-behavior-tree-from-editor": {
"methods": [
"create-behavior-tree-from-editor"
]
},
"overwrite-behavior-tree-file": {
"methods": [
"overwrite-behavior-tree-file"
]
},
"behavior-tree-panel-load-file": {
"methods": [
"behavior-tree.loadBehaviorTreeFile"
]
}
}
}
}

View File

@@ -0,0 +1 @@
const readFileSync=require("fs")["readFileSync"],join=require("path")["join"],spawnSync=require("child_process")["spawnSync"],PATH={packageJSON:join(__dirname,"../package.json")};function checkCreatorTypesVersion(e){var o="win32"===process.platform?"npm.cmd":"npm";let n=spawnSync(o,["view","@cocos/creator-types","versions"]).stdout.toString();try{n=JSON.parse(listString)}catch(e){}return!!n.includes(e)}try{const e=readFileSync(PATH.packageJSON,"utf8"),f=JSON.parse(e),g=f.devDependencies["@cocos/creator-types"].replace(/^[^\d]+/,"");checkCreatorTypesVersion(g)||(console.log("Warning:"),console.log(" @en"),console.log(" Version check of @cocos/creator-types failed."),console.log(` The definition of ${g} has not been released yet. Please export the definition to the ./node_modules directory by selecting "Developer -> Export Interface Definition" in the menu of the Creator editor.`),console.log(" The definition of the corresponding version will be released on npm after the editor is officially released."),console.log(" @zh"),console.log(" @cocos/creator-types 版本检查失败。"),console.log(` ${g} 定义还未发布,请先通过 Creator 编辑器菜单 "开发者 -> 导出接口定义",导出定义到 ./node_modules 目录。`),console.log(" 对应版本的定义会在编辑器正式发布后同步发布到 npm 上。"))}catch(e){console.error(e)}

View File

@@ -0,0 +1,274 @@
import { ensureDir, writeFile } from 'fs-extra';
import { join } from 'path';
/**
* 代码生成器工具类
* 用于生成基础的ECS框架代码
*/
interface ComponentOptions {
includeComments: boolean;
addProperties: string[];
}
interface SystemOptions {
includeComments: boolean;
systemType: 'EntitySystem' | 'ProcessingSystem' | 'IntervalSystem' | 'PassiveSystem';
requiredComponents: string[];
}
export class CodeGenerator {
/**
* 生成组件代码
*/
public async generateComponent(
name: string,
targetDir: string,
options: ComponentOptions = {
includeComments: true,
addProperties: []
}
): Promise<void> {
const className = `${name}Component`;
const fileName = `${className}.ts`;
const filePath = join(targetDir, fileName);
await ensureDir(targetDir);
const comments = options.includeComments ? this.generateComponentComments(className) : '';
const properties = this.generateComponentProperties(options.addProperties);
const content = `import { Component } from '@esengine/ecs-framework';
${comments}
export class ${className} extends Component {
${properties}
constructor() {
super();
}
/**
* 重置组件状态
*/
public reset(): void {
// 重置组件属性到默认值
${this.generateResetCode(options.addProperties)}
}
}
`;
await writeFile(filePath, content, 'utf-8');
}
/**
* 生成系统代码
*/
public async generateSystem(
name: string,
targetDir: string,
options: SystemOptions = {
includeComments: true,
systemType: 'EntitySystem',
requiredComponents: []
}
): Promise<void> {
const className = `${name}System`;
const fileName = `${className}.ts`;
const filePath = join(targetDir, fileName);
await ensureDir(targetDir);
const comments = options.includeComments ? this.generateSystemComments(className, options.systemType) : '';
const imports = this.getSystemImports(options.systemType, options.requiredComponents);
const matcherSetup = options.requiredComponents.length > 0 ?
`Matcher.empty().all(${options.requiredComponents.join(', ')})` :
`Matcher.empty()`;
const processMethod = this.generateProcessMethod(options.systemType, options.requiredComponents, className);
const content = `${imports}
${comments}
export class ${className} extends ${options.systemType} {
constructor() {
super(${matcherSetup}${options.systemType === 'IntervalSystem' ? ', 1000 / 60' : ''})${options.systemType === 'IntervalSystem' ? '; // 60fps' : ';'}
}
${processMethod}
/**
* 系统开始时调用
*/
public begin(): void {
super.begin();
// 添加系统初始化逻辑
}
/**
* 系统结束时调用
*/
public end(): void {
// 添加系统清理逻辑
super.end();
}
}
`;
await writeFile(filePath, content, 'utf-8');
}
// ============ 辅助方法 ============
private generateComponentComments(className: string): string {
return `/**
* ${className}
*
* 组件描述
*
* @example
* \`\`\`typescript
* const entity = scene.createEntity("Example");
* const component = entity.addComponent(new ${className}());
* \`\`\`
*/`;
}
private generateSystemComments(className: string, systemType: string): string {
const descriptions = {
'EntitySystem': '处理拥有特定组件的实体',
'ProcessingSystem': '执行全局游戏逻辑',
'IntervalSystem': '按时间间隔处理实体',
'PassiveSystem': '被动响应事件或手动调用'
};
return `/**
* ${className}
*
* ${descriptions[systemType as keyof typeof descriptions] || '处理游戏逻辑'}
*
* @example
* \`\`\`typescript
* const system = new ${className}();
* scene.addEntityProcessor(system);
* \`\`\`
*/`;
}
private generateComponentProperties(properties: string[]): string {
if (properties.length === 0) {
return ' // 添加组件属性\n // public value: number = 0;';
}
return properties.map(prop => {
const [name, type = 'number', defaultValue = '0'] = prop.split(':');
return ` public ${name}: ${type} = ${defaultValue};`;
}).join('\n');
}
private generateResetCode(properties: string[]): string {
if (properties.length === 0) {
return ' // this.value = 0;';
}
return properties.map(prop => {
const [name, , defaultValue = '0'] = prop.split(':');
return ` this.${name} = ${defaultValue};`;
}).join('\n');
}
private getSystemImports(systemType: string, requiredComponents: string[]): string {
const imports = [systemType, 'Entity'];
// 所有系统类型都可能需要Matcher来过滤组件
if (requiredComponents.length > 0 || systemType === 'EntitySystem' || systemType === 'IntervalSystem' || systemType === 'PassiveSystem') {
imports.push('Matcher');
}
return `import { ${imports.join(', ')} } from '@esengine/ecs-framework';${requiredComponents.length > 0 ? '\n' + this.generateComponentImports(requiredComponents) : ''}`;
}
private generateComponentImports(components: string[]): string {
return components.map(comp => `import { ${comp} } from '../components/${comp}';`).join('\n');
}
private generateProcessMethod(systemType: string, requiredComponents: string[], className: string): string {
switch (systemType) {
case 'EntitySystem':
return ` protected process(entities: Entity[]): void {
for (const entity of entities) {
this.processEntity(entity);
}
}
private processEntity(entity: Entity): void {
${this.generateProcessingLogic(requiredComponents)}
}`;
case 'ProcessingSystem':
return ` public processSystem(): void {
// 添加全局系统逻辑
console.log('${className} processSystem called');
}`;
case 'IntervalSystem':
return ` protected process(entities: Entity[]): void {
const intervalDelta = this.getIntervalDelta();
console.log(\`${className} executing with interval delta: \${intervalDelta}\`);
for (const entity of entities) {
this.processEntity(entity, intervalDelta);
}
}
private processEntity(entity: Entity, delta: number): void {
${this.generateProcessingLogic(requiredComponents)}
}`;
case 'PassiveSystem':
return ` /**
* 被动系统不主动处理实体
* 通常用于响应事件或被其他系统调用
*/
public processEntity(entity: Entity): void {
${this.generateProcessingLogic(requiredComponents)}
}
/**
* 手动触发处理
*/
public trigger(): void {
for (const entity of this.entities) {
this.processEntity(entity);
}
}`;
default:
return '';
}
}
private generateProcessingLogic(requiredComponents: string[]): string {
if (requiredComponents.length === 0) {
return ' // 添加处理逻辑';
}
const componentVars = requiredComponents.map((comp: string) => {
const varName = comp.replace('Component', '').toLowerCase();
return ` const ${varName} = entity.getComponent(${comp});`;
}).join('\n');
const nullCheck = requiredComponents.map((comp: string) => {
const varName = comp.replace('Component', '').toLowerCase();
return varName;
}).join(' && ');
return `${componentVars}
if (${nullCheck}) {
// 添加处理逻辑
}`;
}
}

View File

@@ -0,0 +1,436 @@
import * as path from 'path';
import * as fs from 'fs';
/**
* ECS启动模板生成器
* 生成最基础的ECS框架启动模板不包含业务逻辑
*/
export class TemplateGenerator {
private projectPath: string;
private ecsDir: string;
constructor(projectPath: string) {
this.projectPath = projectPath;
this.ecsDir = path.join(projectPath, 'assets', 'scripts', 'ecs');
}
/**
* 检查是否已经存在ECS模板
*/
public checkTemplateExists(): boolean {
return fs.existsSync(this.ecsDir);
}
/**
* 获取已存在的文件列表
*/
public getExistingFiles(): string[] {
if (!this.checkTemplateExists()) return [];
const files: string[] = [];
this.scanDirectory(this.ecsDir, '', files);
return files;
}
private scanDirectory(dirPath: string, relativePath: string, files: string[]): void {
if (!fs.existsSync(dirPath)) return;
const items = fs.readdirSync(dirPath);
for (const item of items) {
const fullPath = path.join(dirPath, item);
const relativeFilePath = relativePath ? `${relativePath}/${item}` : item;
if (fs.statSync(fullPath).isDirectory()) {
this.scanDirectory(fullPath, relativeFilePath, files);
} else {
files.push(relativeFilePath);
}
}
}
/**
* 删除现有的ECS模板
*/
public removeExistingTemplate(): void {
if (fs.existsSync(this.ecsDir)) {
fs.rmSync(this.ecsDir, { recursive: true, force: true });
console.log('Removed existing ECS template');
}
}
/**
* 创建ECS启动模板
*/
public createTemplate(): void {
// 创建目录结构
this.createDirectories();
// 创建ECS启动管理器
this.createECSManager();
// 创建基础游戏场景
this.createBaseGameScene();
// 创建README文档
this.createReadme();
console.log('ECS启动模板创建成功');
}
/**
* 创建目录结构
*/
private createDirectories(): void {
const dirs = [
this.ecsDir,
path.join(this.ecsDir, 'scenes'),
path.join(this.ecsDir, 'components'),
path.join(this.ecsDir, 'systems')
];
dirs.forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
console.log(`Created directory: ${path.relative(this.projectPath, dir)}`);
}
});
}
/**
* 创建ECS管理器
*/
private createECSManager(): void {
this.writeFile(path.join(this.ecsDir, 'ECSManager.ts'), `import { Core } from '@esengine/ecs-framework';
import { Component, _decorator } from 'cc';
import { GameScene } from './scenes/GameScene';
const { ccclass, property } = _decorator;
/**
* ECS管理器 - Cocos Creator组件
* 将此组件添加到场景中的任意节点上即可启动ECS框架
*
* 使用说明:
* 1. 在Cocos Creator场景中创建一个空节点
* 2. 将此ECSManager组件添加到该节点
* 3. 运行场景即可自动启动ECS框架
*/
@ccclass('ECSManager')
export class ECSManager extends Component {
@property({
tooltip: '是否启用调试模式(建议开发阶段开启)'
})
public debugMode: boolean = true;
private isInitialized: boolean = false;
/**
* 组件启动时初始化ECS
*/
start() {
this.initializeECS();
}
/**
* 初始化ECS框架
*/
private initializeECS(): void {
if (this.isInitialized) return;
console.log('🎮 正在初始化ECS框架...');
try {
// 1. 创建Core实例启用调试功能
if (this.debugMode) {
Core.create({
debugConfig: {
enabled: true,
websocketUrl: 'ws://localhost:8080/ecs-debug',
autoReconnect: true,
updateInterval: 100,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
}
});
console.log('🔧 ECS调试模式已启用可在Cocos Creator扩展面板中查看调试信息');
} else {
Core.create(false);
}
// 2. 创建游戏场景
const gameScene = new GameScene();
// 3. 设置为当前场景会自动调用scene.begin()
Core.scene = gameScene;
this.isInitialized = true;
console.log('✅ ECS框架初始化成功');
console.log('📖 请查看 assets/scripts/ecs/README.md 了解如何添加组件和系统');
} catch (error) {
console.error('❌ ECS框架初始化失败:', error);
}
}
/**
* 每帧更新ECS框架
*/
update(deltaTime: number) {
if (this.isInitialized) {
// 更新ECS核心系统
Core.update(deltaTime);
}
}
/**
* 组件销毁时清理ECS
*/
onDestroy() {
if (this.isInitialized) {
console.log('🧹 清理ECS框架...');
// ECS框架会自动处理场景清理
this.isInitialized = false;
}
}
}
`);
}
/**
* 创建基础游戏场景
*/
private createBaseGameScene(): void {
this.writeFile(path.join(this.ecsDir, 'scenes', 'GameScene.ts'), `import { Scene } from '@esengine/ecs-framework';
/**
* 游戏场景
*
* 这是您的主游戏场景。在这里可以:
* - 添加游戏系统
* - 创建初始实体
* - 设置场景参数
*/
export class GameScene extends Scene {
/**
* 场景初始化
* 在场景创建时调用,用于设置基础配置
*/
public initialize(): void {
super.initialize();
// 设置场景名称
this.name = "MainGameScene";
console.log('🎯 游戏场景已创建');
// TODO: 在这里添加您的游戏系统
// 例如this.addEntityProcessor(new MovementSystem());
// TODO: 在这里创建初始实体
// 例如this.createEntity("Player");
}
/**
* 场景开始运行
* 在场景开始时调用,用于执行启动逻辑
*/
public onStart(): void {
super.onStart();
console.log('🚀 游戏场景已启动');
// TODO: 在这里添加场景启动逻辑
// 例如创建UI、播放音乐、初始化游戏状态等
}
/**
* 场景卸载
* 在场景结束时调用,用于清理资源
*/
public unload(): void {
console.log('🛑 游戏场景已结束');
// TODO: 在这里添加清理逻辑
// 例如:清理缓存、释放资源等
super.unload();
}
}
`);
}
/**
* 创建README文档
*/
private createReadme(): void {
this.writeFile(path.join(this.ecsDir, 'README.md'), `# ECS框架启动模板
欢迎使用ECS框架这是一个最基础的启动模板帮助您快速开始ECS项目开发。
## 📁 项目结构
\`\`\`
ecs/
├── components/ # 组件目录(请在此添加您的组件)
├── systems/ # 系统目录(请在此添加您的系统)
├── scenes/ # 场景目录
│ └── GameScene.ts # 主游戏场景
├── ECSManager.ts # ECS管理器组件
└── README.md # 本文档
\`\`\`
## 🚀 快速开始
### 1. 启动ECS框架
ECS框架已经配置完成您只需要
1. 在Cocos Creator中打开您的场景
2. 创建一个空节点(例如命名为"ECSManager"
3. 将 \`ECSManager\` 组件添加到该节点
4. 运行场景ECS框架将自动启动
### 2. 查看控制台输出
如果一切正常,您将在控制台看到:
\`\`\`
🎮 正在初始化ECS框架...
🔧 ECS调试模式已启用可在Cocos Creator扩展面板中查看调试信息
🎯 游戏场景已创建
✅ ECS框架初始化成功
🚀 游戏场景已启动
\`\`\`
### 3. 使用调试面板
ECS框架已启用调试功能您可以
1. 在Cocos Creator编辑器菜单中选择 "扩展" → "ECS Framework" → "调试面板"
2. 调试面板将显示实时的ECS运行状态
- 实体数量和状态
- 系统执行信息
- 性能监控数据
- 组件统计信息
**注意**:调试功能会消耗一定性能,正式发布时建议关闭调试模式。
## 📚 下一步开发
### 创建您的第一个组件
\`components/\` 目录下创建组件:
\`\`\`typescript
// components/PositionComponent.ts
import { Component } from '@esengine/ecs-framework';
import { Vec3 } from 'cc';
export class PositionComponent extends Component {
public position: Vec3 = new Vec3();
constructor(x: number = 0, y: number = 0, z: number = 0) {
super();
this.position.set(x, y, z);
}
}
\`\`\`
### 创建您的第一个系统
\`systems/\` 目录下创建系统:
\`\`\`typescript
// systems/MovementSystem.ts
import { EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
import { PositionComponent } from '../components/PositionComponent';
export class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(PositionComponent));
}
protected process(entities: Entity[]): void {
for (const entity of entities) {
const position = entity.getComponent(PositionComponent);
if (position) {
// TODO: 在这里编写移动逻辑
console.log(\`实体 \${entity.name} 位置: \${position.position}\`);
}
}
}
}
\`\`\`
### 在场景中注册系统
\`scenes/GameScene.ts\`\`initialize()\` 方法中添加:
\`\`\`typescript
import { MovementSystem } from '../systems/MovementSystem';
public initialize(): void {
super.initialize();
this.name = "MainGameScene";
// 添加系统
this.addEntityProcessor(new MovementSystem());
// 创建测试实体
const testEntity = this.createEntity("TestEntity");
testEntity.addComponent(new PositionComponent(0, 0, 0));
}
\`\`\`
## 🔗 学习资源
- [ECS框架完整文档](https://github.com/esengine/ecs-framework)
- [ECS概念详解](https://github.com/esengine/ecs-framework/blob/master/docs/concepts-explained.md)
- [新手教程](https://github.com/esengine/ecs-framework/blob/master/docs/beginner-tutorials.md)
- [组件设计指南](https://github.com/esengine/ecs-framework/blob/master/docs/component-design-guide.md)
- [系统开发指南](https://github.com/esengine/ecs-framework/blob/master/docs/system-guide.md)
## 💡 开发提示
1. **组件只存储数据**:避免在组件中编写复杂逻辑
2. **系统处理逻辑**:所有业务逻辑应该在系统中实现
3. **使用Matcher过滤实体**系统通过Matcher指定需要处理的实体类型
4. **性能优化**:大量实体时考虑使用位掩码查询和组件索引
## ❓ 常见问题
### Q: 如何创建实体?
A: 在场景中使用 \`this.createEntity("实体名称")\`
### Q: 如何给实体添加组件?
A: 使用 \`entity.addComponent(new YourComponent())\`
### Q: 如何获取实体的组件?
A: 使用 \`entity.getComponent(YourComponent)\`
### Q: 如何删除实体?
A: 使用 \`entity.destroy()\`\`this.destroyEntity(entity)\`
---
🎮 **开始您的ECS开发之旅吧**
如有问题请查阅官方文档或提交Issue。
`);
}
/**
* 写入文件
*/
private writeFile(filePath: string, content: string): void {
fs.writeFileSync(filePath, content, 'utf8');
console.log(`Created file: ${path.relative(this.projectPath, filePath)}`);
}
}

View File

@@ -0,0 +1,46 @@
export function onAssetMenu(assetInfo: any) {
console.log('[AssetMenu] onAssetMenu 被调用,资源信息:', assetInfo);
console.log('[AssetMenu] assetInfo 完整结构:', JSON.stringify(assetInfo, null, 2));
const menuItems = [];
// 检查是否为行为树文件
const isTargetFile = (assetInfo && assetInfo.name && assetInfo.name.endsWith('.bt.json')) ||
(assetInfo && assetInfo.file && assetInfo.file.endsWith('.bt.json'));
if (isTargetFile) {
console.log('[AssetMenu] 发现 .bt.json 文件,添加菜单项');
menuItems.push({
label: '用行为树编辑器打开',
click() {
console.log('[AssetMenu] 菜单项被点击,文件信息:', assetInfo);
// 直接调用主进程的方法,不需要复杂的序列化
try {
Editor.Message.send('cocos-ecs-extension', 'load-behavior-tree-file', assetInfo);
console.log('[AssetMenu] 消息发送成功');
} catch (error) {
console.error('[AssetMenu] 消息发送失败:', error);
}
}
});
}
// 在目录中添加创建选项
if (assetInfo && assetInfo.isDirectory) {
menuItems.push({
label: '创建行为树文件',
click() {
console.log('[AssetMenu] 在目录中创建行为树文件:', assetInfo);
try {
Editor.Message.send('cocos-ecs-extension', 'create-behavior-tree-file');
} catch (error) {
console.error('[AssetMenu] 创建消息发送失败:', error);
}
}
});
}
console.log('[AssetMenu] 返回菜单项数量:', menuItems.length);
return menuItems;
}

View File

@@ -0,0 +1,351 @@
import { exec } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import * as fsExtra from 'fs-extra';
/**
* 行为树相关的处理器
*/
export class BehaviorTreeHandler {
/**
* 安装行为树AI系统
*/
static async install(): Promise<boolean> {
const projectPath = Editor.Project.path;
const command = 'npm install @esengine/ai';
return new Promise((resolve) => {
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
if (error) {
console.error('AI系统安装失败:', error.message);
resolve(false);
} else {
// 验证安装是否成功
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ai');
const installSuccess = fs.existsSync(nodeModulesPath);
if (!installSuccess) {
console.warn('安装完成但未找到AI系统目录请检查网络连接');
}
resolve(installSuccess);
}
});
});
}
/**
* 更新行为树AI系统
*/
static async update(): Promise<boolean> {
const projectPath = Editor.Project.path;
const command = 'npm update @esengine/ai';
return new Promise((resolve) => {
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
if (error) {
console.error('AI系统更新失败:', error.message);
resolve(false);
} else {
// 验证更新是否成功
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ai');
const updateSuccess = fs.existsSync(nodeModulesPath);
if (!updateSuccess) {
console.warn('更新完成但未找到AI系统目录');
}
resolve(updateSuccess);
}
});
});
}
/**
* 检查行为树AI是否已安装
*/
static checkInstalled(): boolean {
try {
const projectPath = Editor.Project.path;
const packageJsonPath = path.join(projectPath, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return false;
}
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
return '@esengine/ai' in dependencies;
} catch (error) {
console.error('检查AI系统安装状态失败:', error);
return false;
}
}
/**
* 打开行为树文档
*/
static openDocumentation(): void {
const url = 'https://github.com/esengine/ai/blob/master/README.md';
try {
const { shell } = require('electron');
shell.openExternal(url);
console.log('Behavior Tree documentation opened successfully');
} catch (error) {
console.error('Failed to open Behavior Tree documentation:', error);
Editor.Dialog.info('打开行为树文档', {
detail: `请手动访问以下链接查看文档:\n\n${url}`,
});
}
}
/**
* 创建行为树文件
*/
static async createFile(assetInfo?: any): Promise<void> {
try {
const projectPath = Editor.Project.path;
const assetsPath = path.join(projectPath, 'assets');
// 生成唯一文件名
let fileName = 'NewBehaviorTree';
let counter = 1;
let filePath = path.join(assetsPath, `${fileName}.bt.json`);
while (fs.existsSync(filePath)) {
fileName = `NewBehaviorTree_${counter}`;
filePath = path.join(assetsPath, `${fileName}.bt.json`);
counter++;
}
// 创建默认的行为树配置
const defaultConfig = {
version: "1.0.0",
type: "behavior-tree",
metadata: {
createdAt: new Date().toISOString(),
nodeCount: 1
},
tree: {
id: "root",
type: "sequence",
namespace: "behaviourTree/composites",
properties: {},
children: []
}
};
// 写入文件
await fsExtra.writeFile(filePath, JSON.stringify(defaultConfig, null, 2));
// 刷新资源管理器 - 使用正确的资源路径
const relativeAssetPath = path.relative(projectPath, filePath).replace(/\\/g, '/');
const dbAssetPath = 'db://' + relativeAssetPath;
await Editor.Message.request('asset-db', 'refresh-asset', dbAssetPath);
console.log(`Behavior tree file created: ${filePath}`);
Editor.Dialog.info('创建成功', {
detail: `行为树文件 "${fileName}.bt.json" 已创建完成!\n\n文件位置assets/${fileName}.bt.json\n\n您可以右键点击文件选择"用行为树编辑器打开"来编辑它。`,
});
} catch (error) {
console.error('Failed to create behavior tree file:', error);
Editor.Dialog.error('创建失败', {
detail: `创建行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`,
});
}
}
/**
* 打开行为树文件
*/
static async openFile(assetInfo: any): Promise<void> {
try {
if (!assetInfo || !assetInfo.file) {
throw new Error('无效的文件信息');
}
const filePath = assetInfo.file;
const fileData = await this.loadFileData(filePath);
await this.openPanel();
await this.sendDataToPanel(fileData);
} catch (error) {
Editor.Dialog.error('打开失败', {
detail: `打开行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`
});
}
}
/**
* 读取并解析文件数据
*/
private static async loadFileData(filePath: string): Promise<any> {
try {
let assetPath = filePath;
if (path.isAbsolute(filePath)) {
const projectPath = Editor.Project.path;
if (filePath.startsWith(projectPath)) {
assetPath = path.relative(projectPath, filePath);
assetPath = assetPath.replace(/\\/g, '/');
}
}
if (!assetPath.startsWith('db://')) {
assetPath = 'db://' + assetPath;
}
try {
const assetInfo = await Editor.Message.request('asset-db', 'query-asset-info', assetPath);
if (assetInfo && assetInfo.source) {
const content = await fsExtra.readFile(assetInfo.source, 'utf8');
let fileContent: any;
try {
fileContent = JSON.parse(content);
} catch (parseError) {
fileContent = {
version: "1.0.0",
type: "behavior-tree",
rawContent: content
};
}
const fileData = {
...fileContent,
_fileInfo: {
fileName: path.basename(assetInfo.source, path.extname(assetInfo.source)),
filePath: assetInfo.source,
assetPath: assetPath
}
};
return fileData;
}
} catch (assetError) {
// 资源系统读取失败,尝试直接文件读取
}
const actualFilePath = path.isAbsolute(filePath) ? filePath : path.join(Editor.Project.path, filePath);
if (!fs.existsSync(actualFilePath)) {
throw new Error(`文件不存在: ${actualFilePath}`);
}
const content = await fsExtra.readFile(actualFilePath, 'utf8');
let fileContent: any;
try {
fileContent = JSON.parse(content);
} catch (parseError) {
fileContent = {
version: "1.0.0",
type: "behavior-tree",
rawContent: content
};
}
const fileData = {
...fileContent,
_fileInfo: {
fileName: path.basename(actualFilePath, path.extname(actualFilePath)),
filePath: actualFilePath
}
};
return fileData;
} catch (error) {
throw new Error(`文件读取失败: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* 打开行为树面板
*/
private static async openPanel(): Promise<void> {
await Editor.Panel.open('cocos-ecs-extension.behavior-tree');
await new Promise(resolve => setTimeout(resolve, 300));
}
/**
* 发送数据到面板
*/
private static async sendDataToPanel(fileData: any): Promise<void> {
try {
const result = await Editor.Message.request('cocos-ecs-extension.behavior-tree', 'loadBehaviorTreeFile', fileData);
} catch (error) {
setTimeout(() => {
try {
Editor.Message.send('cocos-ecs-extension.behavior-tree', 'loadBehaviorTreeFile', fileData);
} catch (delayError) {
// 静默失败
}
}, 100);
}
}
/**
* 从编辑器创建行为树文件
*/
static async createFromEditor(data: { fileName: string, content: string }): Promise<void> {
try {
const projectPath = Editor.Project.path;
const assetsPath = path.join(projectPath, 'assets');
let fileName = data.fileName;
let counter = 1;
let filePath = path.join(assetsPath, `${fileName}.bt.json`);
while (fs.existsSync(filePath)) {
fileName = `${data.fileName}_${counter}`;
filePath = path.join(assetsPath, `${fileName}.bt.json`);
counter++;
}
await fsExtra.writeFile(filePath, data.content);
const relativeAssetPath = path.relative(projectPath, filePath).replace(/\\/g, '/');
const dbAssetPath = 'db://' + relativeAssetPath;
await Editor.Message.request('asset-db', 'refresh-asset', dbAssetPath);
Editor.Dialog.info('保存成功', {
detail: `行为树文件 "${fileName}.bt.json" 已保存到 assets 目录中!`,
});
} catch (error) {
Editor.Dialog.error('保存失败', {
detail: `保存行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`,
});
}
}
/**
* 覆盖现有行为树文件
*/
static async overwriteFile(data: { filePath: string, content: string }): Promise<void> {
try {
await fsExtra.writeFile(data.filePath, data.content);
const projectPath = Editor.Project.path;
const relativeAssetPath = path.relative(projectPath, data.filePath).replace(/\\/g, '/');
const dbAssetPath = 'db://' + relativeAssetPath;
await Editor.Message.request('asset-db', 'refresh-asset', dbAssetPath);
const fileName = path.basename(data.filePath, path.extname(data.filePath));
Editor.Dialog.info('覆盖成功', {
detail: `行为树文件 "${fileName}.bt.json" 已更新!`,
});
} catch (error) {
Editor.Dialog.error('覆盖失败', {
detail: `覆盖行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`,
});
}
}
}

View File

@@ -0,0 +1,234 @@
import { exec } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import { TemplateGenerator } from '../TemplateGenerator';
/**
* ECS框架相关的处理器
*/
export class EcsFrameworkHandler {
/**
* 安装ECS Framework
*/
static async install(): Promise<void> {
const projectPath = Editor.Project.path;
const command = 'npm install @esengine/ecs-framework';
console.log(`Installing ECS Framework to project: ${projectPath}`);
return new Promise((resolve, reject) => {
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
console.log('Install stdout:', stdout);
if (stderr) console.log('Install stderr:', stderr);
if (error) {
console.error('Installation failed:', error);
reject(error);
} else {
console.log('Installation completed successfully');
// 验证安装是否成功
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
const installSuccess = fs.existsSync(nodeModulesPath);
if (installSuccess) {
console.log('ECS Framework installed successfully');
resolve();
} else {
console.warn('ECS Framework directory not found after install');
reject(new Error('安装验证失败'));
}
}
});
});
}
/**
* 更新ECS Framework
*/
static async update(targetVersion?: string): Promise<void> {
const projectPath = Editor.Project.path;
const version = targetVersion ? `@${targetVersion}` : '@latest';
const command = `npm install @esengine/ecs-framework${version}`;
console.log(`Updating ECS Framework to ${version} in project: ${projectPath}`);
return new Promise((resolve, reject) => {
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
console.log('Update stdout:', stdout);
if (stderr) console.log('Update stderr:', stderr);
if (error) {
console.error('Update failed:', error);
reject(error);
} else {
console.log('Update completed successfully');
// 验证更新是否成功
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
const updateSuccess = fs.existsSync(nodeModulesPath);
if (updateSuccess) {
console.log(`ECS Framework updated successfully to ${version}`);
resolve();
} else {
console.warn('ECS Framework directory not found after update');
reject(new Error('更新验证失败'));
}
}
});
});
}
/**
* 卸载ECS Framework
*/
static async uninstall(): Promise<void> {
const projectPath = Editor.Project.path;
const command = 'npm uninstall @esengine/ecs-framework';
console.log(`Uninstalling ECS Framework from project: ${projectPath}`);
return new Promise((resolve, reject) => {
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
console.log('Uninstall stdout:', stdout);
if (stderr) console.log('Uninstall stderr:', stderr);
if (error) {
console.error('Uninstall failed:', error);
reject(error);
} else {
console.log('Uninstall completed successfully');
// 检查是否真的卸载了
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
const stillExists = fs.existsSync(nodeModulesPath);
if (stillExists) {
console.warn('ECS Framework directory still exists after uninstall');
reject(new Error('卸载验证失败'));
} else {
console.log('ECS Framework uninstalled successfully');
resolve();
}
}
});
});
}
/**
* 打开文档
*/
static openDocumentation(): void {
const url = 'https://github.com/esengine/ecs-framework/blob/master/README.md';
try {
// 使用Electron的shell模块打开外部链接
const { shell } = require('electron');
shell.openExternal(url);
console.log('Documentation link opened successfully');
} catch (error) {
console.error('Failed to open documentation:', error);
Editor.Dialog.info('打开文档', {
detail: `请手动访问以下链接查看文档:\n\n${url}`,
});
}
}
/**
* 创建ECS模板
*/
static createTemplate(): void {
const projectPath = Editor.Project.path;
console.log(`Creating ECS template in project: ${projectPath}`);
try {
const templateGenerator = new TemplateGenerator(projectPath);
// 检查是否已存在模板
if (templateGenerator.checkTemplateExists()) {
const existingFiles = templateGenerator.getExistingFiles();
const fileList = existingFiles.length > 0 ? existingFiles.join('\n• ') : '未检测到具体文件';
Editor.Dialog.warn('模板已存在', {
detail: `检测到已存在ECS模板包含以下文件\n\n• ${fileList}\n\n是否要覆盖现有模板`,
buttons: ['覆盖', '取消'],
}).then((result: any) => {
if (result.response === 0) {
// 用户选择覆盖
console.log('User chose to overwrite existing template');
templateGenerator.removeExistingTemplate();
templateGenerator.createTemplate();
this.showTemplateCreatedDialog();
} else {
console.log('User cancelled template creation');
}
});
return;
}
// 创建新模板
templateGenerator.createTemplate();
console.log('ECS template created successfully');
this.showTemplateCreatedDialog();
} catch (error) {
console.error('Failed to create ECS template:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
Editor.Dialog.error('模板创建失败', {
detail: `创建ECS模板时发生错误\n\n${errorMessage}\n\n请检查项目权限和目录结构。`,
});
}
}
/**
* 显示模板创建成功的对话框
*/
private static showTemplateCreatedDialog(): void {
Editor.Dialog.info('模板创建成功', {
detail: '✅ ECS项目模板已创建完成\n\n已为您的Cocos Creator项目生成了完整的ECS架构模板包括\n\n' +
'• 位置、速度、Cocos节点组件\n' +
'• 移动系统和节点同步系统\n' +
'• 实体工厂和场景管理器\n' +
'• ECS管理器组件(可直接添加到节点)\n' +
'• 完整的使用文档\n\n' +
'请刷新资源管理器查看新创建的文件。',
});
}
/**
* 打开GitHub仓库
*/
static openGitHub(): void {
const url = 'https://github.com/esengine/ecs-framework';
try {
const { shell } = require('electron');
shell.openExternal(url);
console.log('GitHub repository opened successfully');
} catch (error) {
console.error('Failed to open GitHub repository:', error);
Editor.Dialog.info('打开GitHub', {
detail: `请手动访问以下链接:\n\n${url}`,
});
}
}
/**
* 打开QQ群
*/
static openQQGroup(): void {
const url = 'https://qm.qq.com/cgi-bin/qm/qr?k=your-qq-group-key';
try {
const { shell } = require('electron');
shell.openExternal(url);
console.log('QQ group opened successfully');
} catch (error) {
console.error('Failed to open QQ group:', error);
Editor.Dialog.info('QQ群', {
detail: '请手动搜索QQ群号或访问相关链接加入讨论群。',
});
}
}
}

View File

@@ -0,0 +1,64 @@
/**
* 面板管理相关的处理器
*/
export class PanelHandler {
/**
* 打开默认面板
*/
static openDefaultPanel(): void {
try {
Editor.Panel.open('cocos-ecs-extension');
console.log('Default panel opened successfully');
} catch (error) {
console.error('Failed to open default panel:', error);
Editor.Dialog.error('打开面板失败', {
detail: `无法打开面板:\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
});
}
}
/**
* 打开调试面板
*/
static openDebugPanel(): void {
try {
Editor.Panel.open('cocos-ecs-extension.debug');
console.log('Debug panel opened successfully');
} catch (error) {
console.error('Failed to open debug panel:', error);
Editor.Dialog.error('打开调试面板失败', {
detail: `无法打开调试面板:\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
});
}
}
/**
* 打开代码生成器面板
*/
static openGeneratorPanel(): void {
try {
Editor.Panel.open('cocos-ecs-extension.generator');
console.log('Generator panel opened successfully');
} catch (error) {
console.error('Failed to open generator panel:', error);
Editor.Dialog.error('打开代码生成器失败', {
detail: `无法打开代码生成器面板:\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
});
}
}
/**
* 打开行为树面板
*/
static openBehaviorTreePanel(): void {
try {
Editor.Panel.open('cocos-ecs-extension.behavior-tree');
console.log('Behavior Tree panel opened successfully');
} catch (error) {
console.error('Failed to open behavior tree panel:', error);
Editor.Dialog.error('打开行为树面板失败', {
detail: `无法打开行为树AI组件库面板\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
});
}
}
}

View File

@@ -0,0 +1,3 @@
export { EcsFrameworkHandler } from './EcsFrameworkHandler';
export { BehaviorTreeHandler } from './BehaviorTreeHandler';
export { PanelHandler } from './PanelHandler';

View File

@@ -0,0 +1,198 @@
// @ts-ignore
import packageJSON from '../package.json';
import { EcsFrameworkHandler, BehaviorTreeHandler, PanelHandler } from './handlers';
import { readJSON } from 'fs-extra';
import * as path from 'path';
import { AssetInfo } from '@cocos/creator-types/editor/packages/asset-db/@types/public';
/**
* @en Registration method for the main process of Extension
* @zh 为扩展的主进程的注册方法
*/
export const methods: { [key: string]: (...any: any) => any } = {
// ================ 面板管理 ================
/**
* 打开默认面板
*/
openPanel() {
PanelHandler.openDefaultPanel();
},
/**
* 打开调试面板
*/
'open-debug'() {
PanelHandler.openDebugPanel();
},
/**
* 打开代码生成器面板
*/
'open-generator'() {
PanelHandler.openGeneratorPanel();
},
/**
* 打开行为树面板
*/
'open-behavior-tree'() {
PanelHandler.openBehaviorTreePanel();
},
// ================ ECS框架管理 ================
/**
* 安装ECS Framework
*/
'install-ecs-framework'() {
EcsFrameworkHandler.install();
},
/**
* 更新ECS Framework
*/
'update-ecs-framework'() {
EcsFrameworkHandler.update();
},
/**
* 卸载ECS Framework
*/
'uninstall-ecs-framework'() {
EcsFrameworkHandler.uninstall();
},
/**
* 打开文档
*/
'open-documentation'() {
EcsFrameworkHandler.openDocumentation();
},
/**
* 创建ECS模板
*/
'create-ecs-template'() {
EcsFrameworkHandler.createTemplate();
},
/**
* 打开GitHub仓库
*/
'open-github'() {
EcsFrameworkHandler.openGitHub();
},
/**
* 打开QQ群
*/
'open-qq-group'() {
EcsFrameworkHandler.openQQGroup();
},
// ================ 行为树管理 ================
/**
* 安装行为树AI系统
*/
async 'install-behavior-tree'() {
try {
return await BehaviorTreeHandler.install();
} catch (error) {
console.error('安装行为树AI系统失败:', error);
return false;
}
},
/**
* 更新行为树AI系统
*/
async 'update-behavior-tree'() {
try {
return await BehaviorTreeHandler.update();
} catch (error) {
console.error('更新行为树AI系统失败:', error);
return false;
}
},
/**
* 检查行为树AI是否已安装
*/
'check-behavior-tree-installed'() {
return BehaviorTreeHandler.checkInstalled();
},
/**
* 打开行为树文档
*/
'open-behavior-tree-docs'() {
BehaviorTreeHandler.openDocumentation();
},
/**
* 创建行为树文件
*/
'create-behavior-tree-file'() {
BehaviorTreeHandler.createFile();
},
/**
* 加载行为树文件到编辑器
*/
async 'load-behavior-tree-file'(...args: any[]) {
const assetInfo = args.length >= 2 ? args[1] : args[0];
try {
if (!assetInfo || (!assetInfo.file && !assetInfo.path)) {
throw new Error('无效的文件信息');
}
await Editor.Panel.open('cocos-ecs-extension.behavior-tree');
await new Promise(resolve => setTimeout(resolve, 500));
const result = await Editor.Message.request('cocos-ecs-extension', 'behavior-tree-panel-load-file', assetInfo);
} catch (error) {
Editor.Dialog.error('打开失败', {
detail: `打开行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`
});
}
},
/**
* 从编辑器创建行为树文件
*/
'create-behavior-tree-from-editor'(event: any, data: any) {
BehaviorTreeHandler.createFromEditor(data);
},
/**
* 覆盖现有行为树文件
*/
'overwrite-behavior-tree-file'(...args: any[]) {
const data = args.length >= 2 ? args[1] : args[0];
if (data && data.filePath) {
BehaviorTreeHandler.overwriteFile(data);
} else {
throw new Error('文件路径不存在或数据无效');
}
},
};
/**
* @en Method triggered when the extension is started
* @zh 启动扩展时触发的方法
*/
export function load() {
console.log('[Cocos ECS Extension] 扩展已加载');
}
/**
* @en Method triggered when the extension is uninstalled
* @zh 卸载扩展时触发的方法
*/
export function unload() {
console.log('[Cocos ECS Extension] 扩展已卸载');
}

View File

@@ -0,0 +1,124 @@
import { ref } from 'vue';
import { TreeNode, DragState, Connection } from '../types';
import { allNodeTemplates as nodeTemplates } from '../data/nodeTemplates';
/**
* 应用状态管理
*/
export function useAppState() {
// 安装状态
const checkingStatus = ref(true);
const isInstalled = ref(false);
const version = ref<string | null>(null);
const isInstalling = ref(false);
// 编辑器状态
const nodeTemplates_ = ref(nodeTemplates);
const treeNodes = ref<TreeNode[]>([]);
const selectedNodeId = ref<string | null>(null);
const selectedConditionNodeId = ref<string | null>(null); // 选中的条件节点ID
const nodeSearchText = ref('');
// 调试:检查条件节点模板
console.log('🔍 条件节点模板检查:');
nodeTemplates.filter(t => t.category === 'condition').forEach(template => {
console.log(` ${template.name}: isDraggableCondition=${template.isDraggableCondition}`);
});
console.log('🎭 装饰器节点模板检查:');
nodeTemplates.filter(t => t.category === 'decorator').forEach(template => {
console.log(` ${template.name}: type=${template.type}`);
});
// 画布状态
const canvasWidth = ref(800);
const canvasHeight = ref(600);
const zoomLevel = ref(1);
const panX = ref(0);
const panY = ref(0);
const dragState = ref<DragState>({
isDraggingCanvas: false,
isDraggingNode: false,
isConnecting: false,
dragStartX: 0,
dragStartY: 0,
dragNodeId: null,
dragNodeStartX: 0,
dragNodeStartY: 0,
connectionStart: null,
connectionEnd: { x: 0, y: 0 }
});
// 连接状态
const connections = ref<Connection[]>([]);
const tempConnection = ref({ path: '' });
// UI状态
const showExportModal = ref(false);
const exportFormat = ref('json'); // 默认JSON格式TypeScript暂时禁用
// 工具函数
const getNodeByIdLocal = (id: string): TreeNode | undefined => {
return treeNodes.value.find(node => node.id === id);
};
const selectNode = (nodeId: string) => {
selectedNodeId.value = nodeId;
};
const newBehaviorTree = () => {
treeNodes.value = [];
selectedNodeId.value = null;
connections.value = [];
tempConnection.value.path = '';
};
const updateCanvasSize = () => {
const canvasArea = document.querySelector('.canvas-area') as HTMLElement;
if (canvasArea) {
const rect = canvasArea.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
canvasWidth.value = Math.max(rect.width, 800);
canvasHeight.value = Math.max(rect.height, 600);
}
}
};
return {
// 安装状态
checkingStatus,
isInstalled,
version,
isInstalling,
// 编辑器状态
nodeTemplates: nodeTemplates_,
treeNodes,
selectedNodeId,
selectedConditionNodeId,
nodeSearchText,
// 画布状态
canvasWidth,
canvasHeight,
zoomLevel,
panX,
panY,
dragState,
// 连接状态
connections,
tempConnection,
// UI状态
showExportModal,
exportFormat,
// 工具函数
getNodeByIdLocal,
selectNode,
newBehaviorTree,
updateCanvasSize
};
}

View File

@@ -0,0 +1,474 @@
import { ref, computed, reactive } from 'vue';
export interface BlackboardVariable {
name: string;
type: 'string' | 'number' | 'boolean' | 'vector2' | 'vector3' | 'object' | 'array';
value: any;
defaultValue: any;
description?: string;
group?: string;
readOnly?: boolean;
constraints?: {
min?: number;
max?: number;
step?: number;
allowedValues?: string[];
};
}
export interface BlackboardModalData {
name: string;
type: 'string' | 'number' | 'boolean' | 'vector2' | 'vector3' | 'object' | 'array';
defaultValue: any;
description: string;
group: string;
readOnly: boolean;
constraints: {
min?: number;
max?: number;
step?: number;
};
useAllowedValues: boolean;
allowedValuesText: string;
}
export function useBlackboard() {
const blackboardVariables = ref<Map<string, BlackboardVariable>>(new Map());
const expandedGroups = ref<Set<string>>(new Set(['未分组']));
const selectedVariable = ref<BlackboardVariable | null>(null);
const showBlackboardModal = ref(false);
const editingBlackboardVariable = ref<BlackboardVariable | null>(null);
const blackboardModalData = reactive<BlackboardModalData>({
name: '',
type: 'string',
defaultValue: '',
description: '',
group: '',
readOnly: false,
constraints: {},
useAllowedValues: false,
allowedValuesText: ''
});
const blackboardCollapsed = ref(false);
const blackboardTransparent = ref(true);
const blackboardVariablesArray = computed(() => {
return Array.from(blackboardVariables.value.values());
});
const blackboardVariableGroups = computed(() => {
const groups: Record<string, BlackboardVariable[]> = {};
blackboardVariables.value.forEach(variable => {
const groupName = variable.group || '未分组';
if (!groups[groupName]) {
groups[groupName] = [];
}
groups[groupName].push(variable);
});
const sortedGroups: Record<string, BlackboardVariable[]> = {};
const groupNames = Object.keys(groups).sort((a, b) => {
if (a === '未分组') return -1;
if (b === '未分组') return 1;
return a.localeCompare(b);
});
groupNames.forEach(groupName => {
groups[groupName].sort((a, b) => a.name.localeCompare(b.name));
sortedGroups[groupName] = groups[groupName];
});
return sortedGroups;
});
const groupedBlackboardVariables = () => {
return Object.entries(blackboardVariableGroups.value);
};
const isGroupExpanded = (groupName: string): boolean => {
return expandedGroups.value.has(groupName);
};
const toggleGroup = (groupName: string) => {
if (expandedGroups.value.has(groupName)) {
expandedGroups.value.delete(groupName);
} else {
expandedGroups.value.add(groupName);
}
};
const getVariableTypeIcon = (type: string): string => {
const iconMap: Record<string, string> = {
string: '📝',
number: '🔢',
boolean: '☑️',
vector2: '📐',
vector3: '🧊',
object: '📦',
array: '📋'
};
return iconMap[type] || '❓';
};
const formatBlackboardValue = (variable: BlackboardVariable): string => {
if (variable.value === null || variable.value === undefined) {
return 'null';
}
switch (variable.type) {
case 'boolean':
return variable.value ? 'true' : 'false';
case 'string':
return `"${variable.value}"`;
case 'number':
return variable.value.toString();
default:
return String(variable.value);
}
};
const hasVisibleConstraints = (variable: BlackboardVariable): boolean => {
if (!variable.constraints) return false;
return !!(
variable.constraints.min !== undefined ||
variable.constraints.max !== undefined ||
variable.constraints.allowedValues?.length
);
};
const formatConstraints = (constraints: BlackboardVariable['constraints']): string => {
const parts: string[] = [];
if (constraints?.min !== undefined) {
parts.push(`最小: ${constraints.min}`);
}
if (constraints?.max !== undefined) {
parts.push(`最大: ${constraints.max}`);
}
if (constraints?.allowedValues?.length) {
parts.push(`可选: ${constraints.allowedValues.join(', ')}`);
}
return parts.join(', ');
};
const getTypeDisplayName = (type: string): string => {
const typeMap: Record<string, string> = {
string: 'STR',
number: 'NUM',
boolean: 'BOOL',
vector2: 'VEC2',
vector3: 'VEC3',
object: 'OBJ',
array: 'ARR'
};
return typeMap[type] || type.toUpperCase();
};
const getDisplayValue = (variable: BlackboardVariable): string => {
if (variable.value === null || variable.value === undefined) {
return 'null';
}
switch (variable.type) {
case 'string':
return String(variable.value);
case 'number':
return variable.value.toString();
case 'boolean':
return variable.value ? 'true' : 'false';
case 'vector2':
if (typeof variable.value === 'object' && variable.value.x !== undefined && variable.value.y !== undefined) {
return `(${variable.value.x}, ${variable.value.y})`;
}
return String(variable.value);
case 'vector3':
if (typeof variable.value === 'object' && variable.value.x !== undefined && variable.value.y !== undefined && variable.value.z !== undefined) {
return `(${variable.value.x}, ${variable.value.y}, ${variable.value.z})`;
}
return String(variable.value);
case 'object':
case 'array':
try {
const jsonStr = JSON.stringify(variable.value);
return jsonStr.length > 20 ? jsonStr.substring(0, 17) + '...' : jsonStr;
} catch {
return String(variable.value);
}
default:
return String(variable.value);
}
};
const saveBlackboardVariable = () => {
if (!blackboardModalData.name.trim()) {
alert('请输入变量名称');
return;
}
let finalValue = blackboardModalData.defaultValue;
if (blackboardModalData.type === 'object' || blackboardModalData.type === 'array') {
try {
if (typeof blackboardModalData.defaultValue === 'string') {
finalValue = blackboardModalData.defaultValue ? JSON.parse(blackboardModalData.defaultValue) : (blackboardModalData.type === 'array' ? [] : {});
}
} catch (error) {
alert('JSON格式错误请检查输入');
return;
}
}
const constraints: BlackboardVariable['constraints'] = {};
if (blackboardModalData.constraints.min !== undefined) constraints.min = blackboardModalData.constraints.min;
if (blackboardModalData.constraints.max !== undefined) constraints.max = blackboardModalData.constraints.max;
if (blackboardModalData.constraints.step !== undefined) constraints.step = blackboardModalData.constraints.step;
if (blackboardModalData.useAllowedValues && blackboardModalData.allowedValuesText.trim()) {
constraints.allowedValues = blackboardModalData.allowedValuesText
.split('\n')
.map(val => val.trim())
.filter(val => val.length > 0);
}
const variable: BlackboardVariable = {
name: blackboardModalData.name,
type: blackboardModalData.type,
value: finalValue,
defaultValue: finalValue,
description: blackboardModalData.description,
group: blackboardModalData.group || undefined,
readOnly: blackboardModalData.readOnly,
constraints: Object.keys(constraints).length > 0 ? constraints : undefined
};
blackboardVariables.value.set(variable.name, variable);
const groupName = variable.group || '未分组';
expandedGroups.value.add(groupName);
showBlackboardModal.value = false;
editingBlackboardVariable.value = null;
Object.assign(blackboardModalData, {
name: '',
type: 'string',
defaultValue: '',
description: '',
group: '',
readOnly: false,
constraints: {},
useAllowedValues: false,
allowedValuesText: ''
});
};
const deleteBlackboardVariable = (variableName: string) => {
if (confirm(`确定要删除变量 "${variableName}" 吗?`)) {
blackboardVariables.value.delete(variableName);
}
};
const updateBlackboardVariable = (variableName: string, newValue: any) => {
const variable = blackboardVariables.value.get(variableName);
if (!variable) return;
if (variable.readOnly) {
alert('该变量为只读,无法修改');
return;
}
const updatedVariable = { ...variable, value: newValue };
blackboardVariables.value.set(variableName, updatedVariable);
};
const selectVariable = (variable: BlackboardVariable) => {
selectedVariable.value = variable;
};
const clearBlackboard = () => {
if (confirm('确定要清空所有变量吗?此操作不可恢复。')) {
blackboardVariables.value.clear();
selectedVariable.value = null;
}
};
const resetBlackboardToDefaults = () => {
if (confirm('确定要重置所有变量到默认值吗?')) {
blackboardVariables.value.forEach((variable, name) => {
if (variable.defaultValue !== undefined) {
variable.value = variable.defaultValue;
blackboardVariables.value.set(name, { ...variable });
}
});
}
};
const exportBlackboard = () => {
const data = Array.from(blackboardVariables.value.values());
const json = JSON.stringify(data, null, 2);
try {
navigator.clipboard.writeText(json);
alert('Blackboard配置已复制到剪贴板');
} catch (error) {
console.error('复制失败:', error);
}
};
const loadBlackboardFromArray = (variables: BlackboardVariable[]) => {
blackboardVariables.value.clear();
variables.forEach(variable => {
if (variable.name && variable.type) {
blackboardVariables.value.set(variable.name, variable);
// 展开变量所在的组
const groupName = variable.group || '未分组';
expandedGroups.value.add(groupName);
}
});
};
const importBlackboard = () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target?.result as string);
if (!Array.isArray(data)) {
throw new Error('格式错误:期望数组格式');
}
let importCount = 0;
data.forEach(varData => {
if (varData.name && varData.type) {
blackboardVariables.value.set(varData.name, varData);
importCount++;
}
});
alert(`成功导入 ${importCount} 个变量`);
} catch (error) {
alert('导入失败:' + (error as Error).message);
}
};
reader.readAsText(file);
};
input.click();
};
const onVariableDragStart = (event: DragEvent, variable: BlackboardVariable) => {
if (!event.dataTransfer) {
return;
}
const dragData = {
name: variable.name,
type: variable.type,
value: variable.value
};
event.dataTransfer.setData('application/blackboard-variable', JSON.stringify(dragData));
event.dataTransfer.effectAllowed = 'copy';
// 添加视觉反馈
const dragElement = event.currentTarget as HTMLElement;
if (dragElement) {
dragElement.style.opacity = '0.8';
setTimeout(() => {
dragElement.style.opacity = '1';
}, 100);
}
};
const editVariable = (variable: BlackboardVariable) => {
editingBlackboardVariable.value = variable;
Object.assign(blackboardModalData, {
name: variable.name,
type: variable.type,
defaultValue: (variable.type === 'object' || variable.type === 'array') ? JSON.stringify(variable.value, null, 2) : variable.value,
description: variable.description || '',
group: variable.group || '',
readOnly: variable.readOnly || false,
constraints: {
min: variable.constraints?.min,
max: variable.constraints?.max,
step: variable.constraints?.step
},
useAllowedValues: !!(variable.constraints?.allowedValues?.length),
allowedValuesText: variable.constraints?.allowedValues?.join('\n') || ''
});
showBlackboardModal.value = true;
};
const addBlackboardVariable = () => {
editingBlackboardVariable.value = null;
Object.assign(blackboardModalData, {
name: '',
type: 'string',
defaultValue: '',
description: '',
group: '',
readOnly: false,
constraints: {},
useAllowedValues: false,
allowedValuesText: ''
});
showBlackboardModal.value = true;
};
return {
blackboardVariables: blackboardVariablesArray,
selectedVariable,
showBlackboardModal,
editingBlackboardVariable,
blackboardModalData,
expandedGroups,
blackboardVariableGroups,
blackboardCollapsed,
blackboardTransparent,
groupedBlackboardVariables,
isGroupExpanded,
toggleGroup,
getVariableTypeIcon,
formatBlackboardValue,
hasVisibleConstraints,
formatConstraints,
getTypeDisplayName,
getDisplayValue,
addBlackboardVariable,
saveBlackboardVariable,
deleteBlackboardVariable,
removeBlackboardVariable: deleteBlackboardVariable,
updateBlackboardVariable,
editVariable,
selectVariable,
clearBlackboard,
resetBlackboardToDefaults,
loadBlackboardFromArray,
exportBlackboard,
importBlackboard,
onBlackboardDragStart: onVariableDragStart,
editBlackboardVariable: editVariable,
onBlackboardValueChange: (variable: BlackboardVariable) => {
updateBlackboardVariable(variable.name, variable.value);
}
};
}

View File

@@ -0,0 +1,203 @@
import { Ref, ref } from 'vue';
import { TreeNode, DragState } from '../types';
/**
* 画布管理功能
*/
export function useCanvasManager(
panX: Ref<number>,
panY: Ref<number>,
zoomLevel: Ref<number>,
treeNodes: Ref<TreeNode[]>,
selectedNodeId: Ref<string | null>,
canvasAreaRef: Ref<HTMLElement | null>,
updateConnections: () => void
) {
// 画布尺寸 - 使用默认值或从DOM获取
const canvasWidth = ref(800);
const canvasHeight = ref(600);
// 拖拽状态
const dragState = ref<DragState>({
isDraggingCanvas: false,
isDraggingNode: false,
dragNodeId: null,
dragStartX: 0,
dragStartY: 0,
dragNodeStartX: 0,
dragNodeStartY: 0,
isConnecting: false,
connectionStart: null,
connectionEnd: { x: 0, y: 0 }
});
// 如果有canvas引用更新尺寸
if (canvasAreaRef.value) {
const rect = canvasAreaRef.value.getBoundingClientRect();
canvasWidth.value = rect.width;
canvasHeight.value = rect.height;
}
// 画布操作功能
const onCanvasWheel = (event: WheelEvent) => {
event.preventDefault();
const zoomSpeed = 0.1;
const delta = event.deltaY > 0 ? -zoomSpeed : zoomSpeed;
const newZoom = Math.max(0.1, Math.min(3, zoomLevel.value + delta));
zoomLevel.value = newZoom;
};
const onCanvasMouseDown = (event: MouseEvent) => {
// 只在空白区域开始画布拖拽
if (event.target === event.currentTarget) {
dragState.value.isDraggingCanvas = true;
dragState.value.dragStartX = event.clientX;
dragState.value.dragStartY = event.clientY;
document.addEventListener('mousemove', onCanvasMouseMove);
document.addEventListener('mouseup', onCanvasMouseUp);
}
};
const onCanvasMouseMove = (event: MouseEvent) => {
if (dragState.value.isDraggingCanvas) {
const deltaX = event.clientX - dragState.value.dragStartX;
const deltaY = event.clientY - dragState.value.dragStartY;
panX.value += deltaX;
panY.value += deltaY;
dragState.value.dragStartX = event.clientX;
dragState.value.dragStartY = event.clientY;
}
};
const onCanvasMouseUp = (event: MouseEvent) => {
if (dragState.value.isDraggingCanvas) {
dragState.value.isDraggingCanvas = false;
document.removeEventListener('mousemove', onCanvasMouseMove);
document.removeEventListener('mouseup', onCanvasMouseUp);
}
};
// 缩放控制
const zoomIn = () => {
zoomLevel.value = Math.min(3, zoomLevel.value + 0.1);
};
const zoomOut = () => {
zoomLevel.value = Math.max(0.1, zoomLevel.value - 0.1);
};
const resetZoom = () => {
zoomLevel.value = 1;
};
const centerView = () => {
if (treeNodes.value.length === 0) {
panX.value = 0;
panY.value = 0;
return;
}
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
treeNodes.value.forEach(node => {
// 尝试从DOM获取实际节点尺寸否则使用默认值
const nodeElement = document.querySelector(`[data-node-id="${node.id}"]`);
let nodeWidth = 150;
let nodeHeight = 80; // 使用基础高度
if (nodeElement) {
const rect = nodeElement.getBoundingClientRect();
nodeWidth = rect.width / zoomLevel.value;
nodeHeight = rect.height / zoomLevel.value;
}
minX = Math.min(minX, node.x);
minY = Math.min(minY, node.y);
maxX = Math.max(maxX, node.x + nodeWidth);
maxY = Math.max(maxY, node.y + nodeHeight);
});
const centerX = (minX + maxX) / 2;
const centerY = (minY + maxY) / 2;
panX.value = canvasWidth.value / 2 - centerX * zoomLevel.value;
panY.value = canvasHeight.value / 2 - centerY * zoomLevel.value;
};
// 网格样式计算
const gridStyle = () => {
const gridSize = 20 * zoomLevel.value;
return {
backgroundSize: `${gridSize}px ${gridSize}px`,
backgroundPosition: `${panX.value % gridSize}px ${panY.value % gridSize}px`
};
};
// 节点拖拽功能
const startNodeDrag = (event: MouseEvent, node: any) => {
event.preventDefault();
event.stopPropagation();
dragState.value.isDraggingNode = true;
dragState.value.dragNodeId = node.id;
dragState.value.dragStartX = event.clientX;
dragState.value.dragStartY = event.clientY;
dragState.value.dragNodeStartX = node.x;
dragState.value.dragNodeStartY = node.y;
const nodeElement = event.currentTarget as HTMLElement;
nodeElement.classList.add('dragging');
document.addEventListener('mousemove', onNodeDrag);
document.addEventListener('mouseup', onNodeDragEnd);
};
const onNodeDrag = (event: MouseEvent) => {
if (!dragState.value.isDraggingNode || !dragState.value.dragNodeId) return;
const deltaX = (event.clientX - dragState.value.dragStartX) / zoomLevel.value;
const deltaY = (event.clientY - dragState.value.dragStartY) / zoomLevel.value;
const node = treeNodes.value.find(n => n.id === dragState.value.dragNodeId);
if (node) {
node.x = dragState.value.dragNodeStartX + deltaX;
node.y = dragState.value.dragNodeStartY + deltaY;
updateConnections();
}
};
const onNodeDragEnd = (event: MouseEvent) => {
if (dragState.value.isDraggingNode) {
const draggingNodes = document.querySelectorAll('.tree-node.dragging');
draggingNodes.forEach(node => node.classList.remove('dragging'));
dragState.value.isDraggingNode = false;
dragState.value.dragNodeId = null;
updateConnections();
document.removeEventListener('mousemove', onNodeDrag);
document.removeEventListener('mouseup', onNodeDragEnd);
}
};
return {
onCanvasWheel,
onCanvasMouseDown,
onCanvasMouseMove,
onCanvasMouseUp,
zoomIn,
zoomOut,
resetZoom,
centerView,
gridStyle,
startNodeDrag
};
}

View File

@@ -0,0 +1,490 @@
import { Ref } from 'vue';
import { TreeNode } from '../types';
import { NodeTemplate } from '../data/nodeTemplates';
/**
* 代码生成管理
*/
export function useCodeGeneration(
treeNodes: Ref<TreeNode[]>,
nodeTemplates: Ref<NodeTemplate[]>,
getNodeByIdLocal: (id: string) => TreeNode | undefined,
rootNode: () => TreeNode | null,
blackboardVariables?: Ref<Map<string, any>>
) {
// 生成行为树配置JSON
const generateBehaviorTreeConfig = () => {
const root = rootNode();
if (!root) {
return null;
}
const config: any = {
version: "1.0.0",
type: "behavior-tree",
metadata: {
createdAt: new Date().toISOString(),
hasECSNodes: hasECSNodes(),
nodeCount: treeNodes.value.length
},
tree: generateNodeConfig(root)
};
// 包含黑板数据
if (blackboardVariables && blackboardVariables.value.size > 0) {
config.blackboard = Array.from(blackboardVariables.value.values());
}
return config;
};
// 生成可读的配置JSON字符串
const generateConfigJSON = (): string => {
const config = generateBehaviorTreeConfig();
if (!config) {
return '// 请先添加根节点';
}
return JSON.stringify(config, null, 2);
};
// 生成TypeScript构建代码用于运行时从配置创建行为树
const generateTypeScriptCode = (): string => {
const config = generateBehaviorTreeConfig();
if (!config) {
return '// 请先添加根节点';
}
const { behaviorTreeImports, ecsImports } = getRequiredImports();
let importsCode = '';
if (behaviorTreeImports.length > 0) {
importsCode += `import { ${behaviorTreeImports.join(', ')}, BehaviorTreeBuilder } from '@esengine/ai';\n`;
}
if (ecsImports.length > 0) {
importsCode += `import { ${ecsImports.join(', ')} } from '@esengine/ecs-framework';\n`;
}
const contextType = hasECSNodes() ? 'Entity' : 'any';
const configString = JSON.stringify(config, null, 4);
return `${importsCode}
// 行为树配置
const behaviorTreeConfig = ${configString};
// 从配置创建行为树
export function createBehaviorTree<T extends ${contextType}>(context?: T): BehaviorTree<T> {
return BehaviorTreeBuilder.fromConfig<T>(behaviorTreeConfig, context);
}
// 直接导出配置(用于序列化保存)
export const config = behaviorTreeConfig;`;
};
const getRequiredImports = (): { behaviorTreeImports: string[], ecsImports: string[] } => {
const behaviorTreeImports = new Set<string>();
const ecsImports = new Set<string>();
// 总是需要这些基础类
behaviorTreeImports.add('BehaviorTree');
behaviorTreeImports.add('TaskStatus');
treeNodes.value.forEach(node => {
const template = nodeTemplates.value.find(t => t.className === node.type || t.type === node.type);
if (template?.className) {
if (template.namespace?.includes('ecs-integration')) {
behaviorTreeImports.add(template.className);
ecsImports.add('Entity');
ecsImports.add('Component');
} else {
behaviorTreeImports.add(template.className);
}
}
});
return {
behaviorTreeImports: Array.from(behaviorTreeImports),
ecsImports: Array.from(ecsImports)
};
};
const hasECSNodes = (): boolean => {
return treeNodes.value.some(node => {
const template = nodeTemplates.value.find(t => t.className === node.type || t.type === node.type);
return template?.namespace?.includes('ecs-integration');
});
};
// 生成节点配置对象
const generateNodeConfig = (node: TreeNode): any => {
const template = nodeTemplates.value.find(t => t.className === node.type || t.type === node.type);
if (!template || !template.className) {
return {
type: node.type,
error: "未知节点类型"
};
}
const nodeConfig: any = {
id: node.id,
type: template.className,
namespace: template.namespace || 'behaviourTree',
properties: {}
};
// 处理节点属性
if (node.properties) {
Object.entries(node.properties).forEach(([key, prop]) => {
if (prop.value !== undefined && prop.value !== '') {
nodeConfig.properties[key] = {
type: prop.type,
value: prop.value
};
}
});
}
// 处理子节点
if (node.children && node.children.length > 0) {
nodeConfig.children = node.children
.map(childId => getNodeByIdLocal(childId))
.filter(Boolean)
.map(child => generateNodeConfig(child!));
}
return nodeConfig;
};
const generateNodeCode = (node: TreeNode, indent: number = 0): string => {
const spaces = ' '.repeat(indent);
const template = nodeTemplates.value.find(t => t.className === node.type || t.type === node.type);
if (!template || !template.className) {
return `${spaces}// 未知节点类型: ${node.type}`;
}
let code = `${spaces}new ${template.className}(`;
const params: string[] = [];
// 处理特定节点的构造函数参数
if (template.namespace?.includes('ecs-integration')) {
// ECS节点的特殊处理
switch (template.className) {
case 'HasComponentCondition':
case 'AddComponentAction':
case 'RemoveComponentAction':
case 'ModifyComponentAction':
if (node.properties?.componentType?.value) {
params.push(node.properties.componentType.value);
}
if (template.className === 'AddComponentAction' && node.properties?.componentFactory?.value) {
params.push(node.properties.componentFactory.value);
}
if (template.className === 'ModifyComponentAction' && node.properties?.modifierCode?.value) {
params.push(node.properties.modifierCode.value);
}
break;
case 'HasTagCondition':
if (node.properties?.tag?.value !== undefined) {
params.push(node.properties.tag.value.toString());
}
break;
case 'IsActiveCondition':
if (node.properties?.checkHierarchy?.value !== undefined) {
params.push(node.properties.checkHierarchy.value.toString());
}
break;
case 'WaitTimeAction':
if (node.properties?.waitTime?.value !== undefined) {
params.push(node.properties.waitTime.value.toString());
}
break;
}
} else {
// 普通行为树节点的处理
switch (template.className) {
case 'ExecuteAction':
case 'ExecuteActionConditional':
if (node.properties?.actionCode?.value || node.properties?.conditionCode?.value) {
const code = node.properties.actionCode?.value || node.properties.conditionCode?.value;
params.push(code);
if (node.properties?.actionName?.value) {
params.push(`{ name: "${node.properties.actionName.value}" }`);
}
}
break;
case 'WaitAction':
if (node.properties?.waitTime?.value !== undefined) {
params.push(node.properties.waitTime.value.toString());
}
break;
case 'LogAction':
if (node.properties?.message?.value) {
params.push(`"${node.properties.message.value}"`);
}
break;
case 'Repeater':
if (node.properties?.repeatCount?.value !== undefined) {
params.push(node.properties.repeatCount.value.toString());
}
break;
case 'Sequence':
case 'Selector':
if (node.properties?.abortType?.value && node.properties.abortType.value !== 'None') {
params.push(`AbortTypes.${node.properties.abortType.value}`);
}
break;
}
}
code += params.join(', ');
code += ')';
// 处理子节点(对于复合节点和装饰器)
if (template.canHaveChildren && node.children && node.children.length > 0) {
const children = node.children
.map(childId => getNodeByIdLocal(childId))
.filter(Boolean)
.map(child => generateNodeCode(child!, indent + 1));
if (children.length > 0) {
const className = template.className; // 保存到局部变量
if (template.category === 'decorator') {
// 装饰器只有一个子节点
code = code.slice(0, -1); // 移除最后的 ')'
const varName = className.toLowerCase();
code += `;\n${spaces}${varName}.child = ${children[0].trim()};\n${spaces}return ${varName}`;
} else if (template.category === 'composite') {
// 复合节点需要添加子节点
code = code.slice(0, -1); // 移除最后的 ')'
code += `;\n`;
children.forEach(child => {
code += `${spaces}${className.toLowerCase()}.addChild(${child.trim()});\n`;
});
code += `${spaces}return ${className.toLowerCase()}`;
}
}
}
return code;
};
// 从配置创建行为树节点
const createTreeFromConfig = (config: any): TreeNode[] => {
console.log('createTreeFromConfig被调用接收到的配置:', config);
console.log('nodeTemplates当前数量:', nodeTemplates.value.length);
// 处理两种不同的文件格式
if (config.nodes && Array.isArray(config.nodes)) {
console.log('使用nodes格式处理节点数量:', config.nodes.length);
const result = createTreeFromNodesFormat(config);
console.log('nodes格式处理结果:', result);
return result;
} else if (config.tree) {
console.log('使用tree格式处理');
const result = createTreeFromTreeFormat(config);
console.log('tree格式处理结果:', result);
return result;
} else {
console.log('配置格式不匹配,返回空数组');
return [];
}
};
// 处理新格式nodes数组格式
const createTreeFromNodesFormat = (config: any): TreeNode[] => {
console.log('createTreeFromNodesFormat开始处理');
if (!config.nodes || !Array.isArray(config.nodes)) {
console.log('nodes数据无效');
return [];
}
const nodes: TreeNode[] = [];
config.nodes.forEach((nodeConfig: any, index: number) => {
console.log(`处理第${index + 1}个节点:`, nodeConfig);
const template = findTemplateByType(nodeConfig.type);
console.log(`为节点类型 "${nodeConfig.type}" 找到的模板:`, template);
if (!template) {
console.warn(`未找到节点类型 "${nodeConfig.type}" 的模板`);
return;
}
const node: TreeNode = {
id: nodeConfig.id || generateNodeId(),
type: template.type,
name: nodeConfig.name || template.name,
icon: nodeConfig.icon || template.icon,
description: nodeConfig.description || template.description,
canHaveChildren: template.canHaveChildren,
canHaveParent: template.canHaveParent,
x: nodeConfig.x || 400,
y: nodeConfig.y || 100,
properties: {},
children: nodeConfig.children || [],
parent: nodeConfig.parent,
hasError: false
};
// 恢复属性
if (nodeConfig.properties && template.properties) {
Object.entries(nodeConfig.properties).forEach(([key, propConfig]: [string, any]) => {
if (template.properties![key]) {
node.properties![key] = {
...template.properties![key],
value: propConfig.value !== undefined ? propConfig.value : template.properties![key].value
};
}
});
}
// 确保所有模板属性都有默认值
if (template.properties) {
Object.entries(template.properties).forEach(([key, propDef]) => {
if (!node.properties![key]) {
node.properties![key] = { ...propDef };
}
});
}
console.log(`创建的节点:`, node);
nodes.push(node);
});
console.log(`createTreeFromNodesFormat完成总共创建了${nodes.length}个节点`);
return nodes;
};
// 处理旧格式tree对象格式
const createTreeFromTreeFormat = (config: any): TreeNode[] => {
if (!config.tree) {
return [];
}
const nodes: TreeNode[] = [];
const processNode = (nodeConfig: any, parent?: TreeNode): TreeNode => {
const template = findTemplateByType(nodeConfig.type);
if (!template) {
throw new Error(`未知节点类型: ${nodeConfig.type}`);
}
const node: TreeNode = {
id: nodeConfig.id || generateNodeId(),
type: template.type,
name: template.name,
icon: template.icon,
description: template.description,
canHaveChildren: template.canHaveChildren,
canHaveParent: template.canHaveParent,
x: 400,
y: 100,
properties: {},
children: [],
parent: parent?.id,
hasError: false
};
// 恢复属性
if (nodeConfig.properties && template.properties) {
Object.entries(nodeConfig.properties).forEach(([key, propConfig]: [string, any]) => {
if (template.properties![key]) {
node.properties![key] = {
...template.properties![key],
value: propConfig.value !== undefined ? propConfig.value : template.properties![key].value
};
}
});
}
// 确保所有模板属性都有默认值
if (template.properties) {
Object.entries(template.properties).forEach(([key, propDef]) => {
if (!node.properties![key]) {
node.properties![key] = { ...propDef };
}
});
}
nodes.push(node);
// 处理子节点
if (nodeConfig.children && Array.isArray(nodeConfig.children)) {
nodeConfig.children.forEach((childConfig: any) => {
const childNode = processNode(childConfig, node);
node.children!.push(childNode.id);
});
}
return node;
};
processNode(config.tree);
return nodes;
};
// 通过类型名查找模板(支持多种匹配方式)
const findTemplateByType = (typeName: string): NodeTemplate | undefined => {
// 直接匹配 type 字段
let template = nodeTemplates.value.find(t => t.type === typeName);
if (template) return template;
// 匹配 className 字段
template = nodeTemplates.value.find(t => t.className === typeName);
if (template) return template;
// 大小写不敏感匹配 type
template = nodeTemplates.value.find(t => t.type.toLowerCase() === typeName.toLowerCase());
if (template) return template;
// 大小写不敏感匹配 className
template = nodeTemplates.value.find(t => t.className && t.className.toLowerCase() === typeName.toLowerCase());
if (template) return template;
// 特殊映射处理
const typeMapping: Record<string, string> = {
'Sequence': 'sequence',
'Selector': 'selector',
'Parallel': 'parallel',
'Inverter': 'inverter',
'Repeater': 'repeater',
'AlwaysSucceed': 'always-succeed',
'AlwaysFail': 'always-fail',
'UntilSuccess': 'until-success',
'UntilFail': 'until-fail',
'ExecuteAction': 'execute-action',
'LogAction': 'log-action',
'WaitAction': 'wait-action'
};
const mappedType = typeMapping[typeName];
if (mappedType) {
template = nodeTemplates.value.find(t => t.type === mappedType);
if (template) return template;
}
return undefined;
};
// 生成唯一节点ID
const generateNodeId = (): string => {
return 'node_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
};
return {
generateBehaviorTreeConfig,
generateConfigJSON,
generateTypeScriptCode,
generateNodeCode,
generateNodeConfig,
createTreeFromConfig,
getRequiredImports
};
}

View File

@@ -0,0 +1,372 @@
import { Ref, computed } from 'vue';
import { TreeNode } from '../types';
import { NodeTemplate } from '../data/nodeTemplates';
import { getRootNode } from '../utils/nodeUtils';
import { getInstallStatusText, getInstallStatusClass } from '../utils/installUtils';
import { getGridStyle } from '../utils/canvasUtils';
/**
* 计算属性管理
*/
export function useComputedProperties(
nodeTemplates: Ref<NodeTemplate[]>,
nodeSearchText: Ref<string>,
treeNodes: Ref<TreeNode[]>,
selectedNodeId: Ref<string | null>,
selectedConditionNodeId: Ref<string | null>,
checkingStatus: Ref<boolean>,
isInstalling: Ref<boolean>,
isInstalled: Ref<boolean>,
version: Ref<string | null>,
exportFormat: Ref<string>,
panX: Ref<number>,
panY: Ref<number>,
zoomLevel: Ref<number>,
getNodeByIdLocal: (id: string) => TreeNode | undefined,
codeGeneration?: {
generateConfigJSON: () => string;
generateTypeScriptCode: () => string;
}
) {
// 过滤节点
const filteredRootNodes = () => {
return nodeTemplates.value.filter(node =>
node.category === 'root' &&
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
);
};
const filteredCompositeNodes = () => {
return nodeTemplates.value.filter(node =>
node.category === 'composite' &&
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
);
};
const filteredDecoratorNodes = () => {
return nodeTemplates.value.filter(node =>
node.category === 'decorator' &&
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
);
};
const filteredActionNodes = () => {
return nodeTemplates.value.filter(node =>
node.category === 'action' &&
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
);
};
const filteredConditionNodes = () => {
return nodeTemplates.value.filter(node =>
node.category === 'condition' &&
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
);
};
const filteredECSNodes = () => {
return nodeTemplates.value.filter(node =>
node.category === 'ecs' &&
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
);
};
// 选中的节点 - 使用computed确保响应式更新
const selectedNode = computed(() => {
if (!selectedNodeId.value) return null;
// 直接从treeNodes数组中查找确保获取最新的节点状态
const node = treeNodes.value.find(n => n.id === selectedNodeId.value);
return node || null;
});
// 当前选中的条件节点(用于编辑条件属性)
const selectedConditionNode = computed(() => {
if (!selectedConditionNodeId.value) return null;
const decoratorNode = treeNodes.value.find(n => n.id === selectedConditionNodeId.value);
if (!decoratorNode || !decoratorNode.attachedCondition) return null;
// 根据条件类型重新构建属性结构
const conditionProperties = reconstructConditionProperties(
decoratorNode.attachedCondition.type,
decoratorNode.properties || {}
);
// 创建一个虚拟的条件节点对象,用于属性编辑
return {
id: decoratorNode.id + '_condition',
name: decoratorNode.attachedCondition.name + '(条件)',
type: decoratorNode.attachedCondition.type,
icon: decoratorNode.attachedCondition.icon,
properties: conditionProperties,
isConditionNode: true,
parentDecorator: decoratorNode
};
});
/**
* 根据条件类型重新构建属性结构
* 将装饰器的扁平属性转换回条件模板的属性结构
*/
const reconstructConditionProperties = (conditionType: string, decoratorProperties: Record<string, any>) => {
switch (conditionType) {
case 'condition-random':
return {
successProbability: {
type: 'number',
name: '成功概率',
value: decoratorProperties.successProbability || 0.5,
description: '条件成功的概率 (0.0 - 1.0)'
}
};
case 'condition-component':
return {
componentType: {
type: 'string',
name: '组件类型',
value: decoratorProperties.componentType || '',
description: '要检查的组件类型名称'
}
};
case 'condition-tag':
return {
tagValue: {
type: 'number',
name: '标签值',
value: decoratorProperties.tagValue || 0,
description: '要检查的标签值'
}
};
case 'condition-active':
return {
checkHierarchy: {
type: 'boolean',
name: '检查层级激活',
value: decoratorProperties.checkHierarchy || false,
description: '是否检查整个层级的激活状态'
}
};
case 'condition-numeric':
return {
propertyPath: {
type: 'string',
name: '属性路径',
value: decoratorProperties.propertyPath || 'context.someValue',
description: '要比较的数值属性路径'
},
compareOperator: {
type: 'select',
name: '比较操作符',
value: decoratorProperties.compareOperator || 'greater',
options: ['greater', 'less', 'equal', 'greaterEqual', 'lessEqual', 'notEqual'],
description: '数值比较的操作符'
},
compareValue: {
type: 'number',
name: '比较值',
value: decoratorProperties.compareValue || 0,
description: '用于比较的目标值'
}
};
case 'condition-property':
return {
propertyPath: {
type: 'string',
name: '属性路径',
value: decoratorProperties.propertyPath || 'context.someProperty',
description: '要检查的属性路径'
}
};
case 'condition-custom':
return {
conditionCode: {
type: 'code',
name: '条件代码',
value: decoratorProperties.conditionCode || '(context) => true',
description: '自定义条件判断函数'
}
};
// Blackboard相关条件使用实际的模板类型名
case 'blackboard-variable-exists':
return {
variableName: {
type: 'string',
name: '变量名',
value: decoratorProperties.variableName || '',
description: '要检查的黑板变量名'
},
invert: {
type: 'boolean',
name: '反转结果',
value: decoratorProperties.invert || false,
description: '是否反转检查结果'
}
};
case 'blackboard-value-comparison':
return {
variableName: {
type: 'string',
name: '变量名',
value: decoratorProperties.variableName || '',
description: '要比较的黑板变量名'
},
operator: {
type: 'select',
name: '比较操作符',
value: decoratorProperties.operator || 'equal',
options: ['equal', 'notEqual', 'greater', 'greaterOrEqual', 'less', 'lessOrEqual', 'contains', 'notContains'],
description: '比较操作类型'
},
compareValue: {
type: 'string',
name: '比较值',
value: decoratorProperties.compareValue || '',
description: '用于比较的值(留空则使用比较变量)'
},
compareVariable: {
type: 'string',
name: '比较变量名',
value: decoratorProperties.compareVariable || '',
description: '用于比较的另一个黑板变量名'
}
};
case 'blackboard-variable-type-check':
return {
variableName: {
type: 'string',
name: '变量名',
value: decoratorProperties.variableName || '',
description: '要检查的黑板变量名'
},
expectedType: {
type: 'select',
name: '期望类型',
value: decoratorProperties.expectedType || 'string',
options: ['string', 'number', 'boolean', 'vector2', 'vector3', 'object', 'array'],
description: '期望的变量类型'
}
};
case 'blackboard-variable-range-check':
return {
variableName: {
type: 'string',
name: '变量名',
value: decoratorProperties.variableName || '',
description: '要检查的数值型黑板变量名'
},
minValue: {
type: 'number',
name: '最小值',
value: decoratorProperties.minValue || 0,
description: '范围的最小值(包含)'
},
maxValue: {
type: 'number',
name: '最大值',
value: decoratorProperties.maxValue || 100,
description: '范围的最大值(包含)'
}
};
default:
// 对于未知的条件类型,尝试从装饰器属性中推断
const reconstructed: Record<string, any> = {};
Object.keys(decoratorProperties).forEach(key => {
if (key !== 'conditionType') {
reconstructed[key] = {
type: typeof decoratorProperties[key] === 'number' ? 'number' :
typeof decoratorProperties[key] === 'boolean' ? 'boolean' : 'string',
name: key,
value: decoratorProperties[key],
description: `${key}参数`
};
}
});
return reconstructed;
}
};
// 当前显示在属性面板的节点(普通节点或条件节点)
const activeNode = computed(() => selectedConditionNode.value || selectedNode.value);
// 根节点
const rootNode = () => {
return getRootNode(treeNodes.value);
};
// 安装状态
const installStatusClass = () => {
return getInstallStatusClass(isInstalling.value, isInstalled.value);
};
const installStatusText = () => {
return getInstallStatusText(
checkingStatus.value,
isInstalling.value,
isInstalled.value,
version.value
);
};
// 验证结果
const validationResult = () => {
if (treeNodes.value.length === 0) {
return { isValid: false, message: '行为树为空' };
}
const root = rootNode();
if (!root) {
return { isValid: false, message: '缺少根节点' };
}
return { isValid: true, message: '行为树结构有效' };
};
// 导出代码
const exportedCode = () => {
if (!codeGeneration) {
return '// 代码生成器未初始化';
}
try {
if (exportFormat.value === 'json') {
return codeGeneration.generateConfigJSON();
} else {
return codeGeneration.generateTypeScriptCode();
}
} catch (error) {
return `// 代码生成失败: ${error}`;
}
};
// 网格样式
const gridStyle = () => {
return getGridStyle(panX.value, panY.value, zoomLevel.value);
};
return {
filteredRootNodes,
filteredCompositeNodes,
filteredDecoratorNodes,
filteredActionNodes,
filteredConditionNodes,
filteredECSNodes,
selectedNode,
selectedConditionNode,
activeNode,
rootNode,
installStatusClass,
installStatusText,
validationResult,
exportedCode,
gridStyle
};
}

View File

@@ -0,0 +1,482 @@
import { ref, reactive, Ref } from 'vue';
import { TreeNode } from '../types';
import { NodeTemplate } from '../data/nodeTemplates';
/**
* 拖拽状态
*/
interface DragState {
isDraggingCondition: boolean;
conditionTemplate: NodeTemplate | null;
mousePosition: { x: number, y: number } | null;
hoveredDecoratorId: string | null;
}
/**
* 条件节点吸附功能
*/
export function useConditionAttachment(
treeNodes: Ref<TreeNode[]>,
getNodeByIdLocal: (id: string) => TreeNode | undefined
) {
const dragState = reactive<DragState>({
isDraggingCondition: false,
conditionTemplate: null,
mousePosition: null,
hoveredDecoratorId: null
});
/**
* 检查节点是否为条件装饰器
*/
const isConditionalDecorator = (node: TreeNode): boolean => {
return node.type === 'conditional-decorator';
};
/**
* 开始拖拽条件节点
*/
const startConditionDrag = (event: DragEvent, template: NodeTemplate) => {
console.log('🎯 开始条件拖拽:', template.name, template.isDraggableCondition);
if (!template.isDraggableCondition) {
console.warn('节点不是可拖拽条件:', template.name);
return;
}
dragState.isDraggingCondition = true;
dragState.conditionTemplate = template;
if (event.dataTransfer) {
event.dataTransfer.setData('application/json', JSON.stringify({
...template,
isConditionDrag: true
}));
event.dataTransfer.effectAllowed = 'copy';
}
console.log('✅ 条件拖拽状态已设置:', dragState);
};
/**
* 处理拖拽悬停在装饰器上
*/
const handleDecoratorDragOver = (event: DragEvent, decoratorNode: TreeNode) => {
console.log('🔀 装饰器拖拽悬停:', decoratorNode.name, decoratorNode.type, 'isDragging:', dragState.isDraggingCondition);
// 检查传输数据
const transferData = event.dataTransfer?.getData('application/json');
if (transferData) {
try {
const data = JSON.parse(transferData);
console.log('📦 传输数据:', data.isConditionDrag, data.isDraggableCondition, data.name);
} catch (e) {
console.log('❌ 传输数据解析失败:', transferData);
}
}
if (!dragState.isDraggingCondition || !isConditionalDecorator(decoratorNode)) {
console.log('❌ 不符合条件:', {
isDragging: dragState.isDraggingCondition,
isDecorator: isConditionalDecorator(decoratorNode),
nodeType: decoratorNode.type
});
return false;
}
event.preventDefault();
event.stopPropagation();
dragState.hoveredDecoratorId = decoratorNode.id;
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'copy';
}
console.log('✅ 装饰器可接受拖拽:', decoratorNode.name);
return true;
};
/**
* 处理拖拽离开装饰器
*/
const handleDecoratorDragLeave = (decoratorNode: TreeNode) => {
if (dragState.hoveredDecoratorId === decoratorNode.id) {
dragState.hoveredDecoratorId = null;
}
};
/**
* 条件到装饰器属性的映射
*/
const mapConditionToDecoratorProperties = (conditionTemplate: NodeTemplate): Record<string, any> => {
const baseConfig = {
conditionType: getConditionTypeFromTemplate(conditionTemplate),
shouldReevaluate: true
};
switch (conditionTemplate.type) {
case 'condition-random':
return {
...baseConfig,
successProbability: conditionTemplate.properties?.successProbability?.value || 0.5
};
case 'condition-component':
return {
...baseConfig,
componentType: conditionTemplate.properties?.componentType?.value || 'Component'
};
case 'condition-tag':
return {
...baseConfig,
tagValue: conditionTemplate.properties?.tagValue?.value || 0
};
case 'condition-active':
return {
...baseConfig,
checkHierarchy: conditionTemplate.properties?.checkHierarchy?.value || true
};
case 'condition-numeric':
return {
...baseConfig,
propertyPath: conditionTemplate.properties?.propertyPath?.value || 'context.someValue',
compareOperator: conditionTemplate.properties?.compareOperator?.value || 'greater',
compareValue: conditionTemplate.properties?.compareValue?.value || 0
};
case 'condition-property':
return {
...baseConfig,
propertyPath: conditionTemplate.properties?.propertyPath?.value || 'context.someProperty'
};
case 'condition-custom':
return {
...baseConfig,
conditionCode: conditionTemplate.properties?.conditionCode?.value || '(context) => true'
};
// Blackboard相关条件支持
case 'blackboard-variable-exists':
return {
...baseConfig,
variableName: conditionTemplate.properties?.variableName?.value || '',
invert: conditionTemplate.properties?.invert?.value || false
};
case 'blackboard-value-comparison':
return {
...baseConfig,
variableName: conditionTemplate.properties?.variableName?.value || '',
operator: conditionTemplate.properties?.operator?.value || 'equal',
compareValue: conditionTemplate.properties?.compareValue?.value || '',
compareVariable: conditionTemplate.properties?.compareVariable?.value || ''
};
case 'blackboard-variable-type-check':
return {
...baseConfig,
variableName: conditionTemplate.properties?.variableName?.value || '',
expectedType: conditionTemplate.properties?.expectedType?.value || 'string'
};
case 'blackboard-variable-range-check':
return {
...baseConfig,
variableName: conditionTemplate.properties?.variableName?.value || '',
minValue: conditionTemplate.properties?.minValue?.value || 0,
maxValue: conditionTemplate.properties?.maxValue?.value || 100
};
default:
return baseConfig;
}
};
/**
* 获取条件类型字符串
*/
const getConditionTypeFromTemplate = (template: NodeTemplate): string => {
const typeMap: Record<string, string> = {
'condition-random': 'random',
'condition-component': 'hasComponent',
'condition-tag': 'hasTag',
'condition-active': 'isActive',
'condition-numeric': 'numericCompare',
'condition-property': 'propertyExists',
'condition-custom': 'custom',
// Blackboard相关条件
'blackboard-variable-exists': 'blackboardExists',
'blackboard-value-comparison': 'blackboardCompare',
'blackboard-variable-type-check': 'blackboardTypeCheck',
'blackboard-variable-range-check': 'blackboardRangeCheck'
};
return typeMap[template.type] || 'custom';
};
/**
* 执行条件吸附到装饰器
*/
const attachConditionToDecorator = (
event: DragEvent,
decoratorNode: TreeNode
): boolean => {
event.preventDefault();
event.stopPropagation();
if (!dragState.isDraggingCondition || !dragState.conditionTemplate) {
return false;
}
if (!isConditionalDecorator(decoratorNode)) {
return false;
}
// 获取条件配置
const conditionConfig = mapConditionToDecoratorProperties(dragState.conditionTemplate);
// 更新装饰器属性
if (!decoratorNode.properties) {
decoratorNode.properties = {};
}
Object.assign(decoratorNode.properties, conditionConfig);
// 标记装饰器已附加条件
decoratorNode.attachedCondition = {
type: dragState.conditionTemplate.type,
name: dragState.conditionTemplate.name,
icon: dragState.conditionTemplate.icon
};
// 初始化为收缩状态
if (decoratorNode.conditionExpanded === undefined) {
decoratorNode.conditionExpanded = false;
}
// 重置拖拽状态
resetDragState();
return true;
};
/**
* 处理画布拖拽事件(阻止条件节点创建为独立节点)
*/
const handleCanvasDrop = (event: DragEvent): boolean => {
const templateData = event.dataTransfer?.getData('application/json');
if (!templateData) return false;
try {
const data = JSON.parse(templateData);
// 如果是条件拖拽,阻止创建独立节点
if (data.isConditionDrag || data.isDraggableCondition) {
event.preventDefault();
resetDragState();
return true;
}
} catch (error) {
// 忽略解析错误
}
return false;
};
/**
* 重置拖拽状态
*/
const resetDragState = () => {
dragState.isDraggingCondition = false;
dragState.conditionTemplate = null;
dragState.mousePosition = null;
dragState.hoveredDecoratorId = null;
};
/**
* 获取条件显示文本(简化版始终显示条件名称)
*/
const getConditionDisplayText = (decoratorNode: TreeNode, expanded: boolean = false): string => {
if (!decoratorNode.attachedCondition) {
return '';
}
// 始终返回条件名称,不管是否展开
return decoratorNode.attachedCondition.name;
};
/**
* 获取条件的可见属性(用于展开时显示)
*/
const getConditionProperties = (decoratorNode: TreeNode): Record<string, any> => {
if (!decoratorNode.attachedCondition || !decoratorNode.properties) {
return {};
}
const conditionType = decoratorNode.attachedCondition.type;
const visibleProps: Record<string, any> = {};
// 根据条件类型筛选相关属性
switch (conditionType) {
case 'condition-random':
if ('successProbability' in decoratorNode.properties) {
visibleProps['成功概率'] = `${(decoratorNode.properties.successProbability * 100).toFixed(1)}%`;
}
break;
case 'condition-component':
if ('componentType' in decoratorNode.properties) {
visibleProps['组件类型'] = decoratorNode.properties.componentType;
}
break;
case 'condition-tag':
if ('tagValue' in decoratorNode.properties) {
visibleProps['标签值'] = decoratorNode.properties.tagValue;
}
break;
case 'condition-active':
if ('checkHierarchy' in decoratorNode.properties) {
visibleProps['检查层级'] = decoratorNode.properties.checkHierarchy ? '是' : '否';
}
break;
case 'condition-numeric':
if ('propertyPath' in decoratorNode.properties) {
visibleProps['属性路径'] = decoratorNode.properties.propertyPath;
}
if ('compareOperator' in decoratorNode.properties) {
visibleProps['比较操作'] = decoratorNode.properties.compareOperator;
}
if ('compareValue' in decoratorNode.properties) {
visibleProps['比较值'] = decoratorNode.properties.compareValue;
}
break;
case 'condition-property':
if ('propertyPath' in decoratorNode.properties) {
visibleProps['属性路径'] = decoratorNode.properties.propertyPath;
}
break;
case 'blackboard-variable-exists':
if ('variableName' in decoratorNode.properties) {
visibleProps['变量名'] = decoratorNode.properties.variableName;
}
if ('invert' in decoratorNode.properties) {
visibleProps['反转结果'] = decoratorNode.properties.invert ? '是' : '否';
}
break;
case 'blackboard-value-comparison':
if ('variableName' in decoratorNode.properties) {
visibleProps['变量名'] = decoratorNode.properties.variableName;
}
if ('operator' in decoratorNode.properties) {
visibleProps['操作符'] = decoratorNode.properties.operator;
}
if ('compareValue' in decoratorNode.properties) {
visibleProps['比较值'] = decoratorNode.properties.compareValue;
}
if ('compareVariable' in decoratorNode.properties) {
visibleProps['比较变量'] = decoratorNode.properties.compareVariable;
}
break;
case 'blackboard-variable-type-check':
if ('variableName' in decoratorNode.properties) {
visibleProps['变量名'] = decoratorNode.properties.variableName;
}
if ('expectedType' in decoratorNode.properties) {
visibleProps['期望类型'] = decoratorNode.properties.expectedType;
}
break;
case 'blackboard-variable-range-check':
if ('variableName' in decoratorNode.properties) {
visibleProps['变量名'] = decoratorNode.properties.variableName;
}
if ('minValue' in decoratorNode.properties) {
visibleProps['最小值'] = decoratorNode.properties.minValue;
}
if ('maxValue' in decoratorNode.properties) {
visibleProps['最大值'] = decoratorNode.properties.maxValue;
}
break;
}
return visibleProps;
};
/**
* 切换条件展开状态
*/
const toggleConditionExpanded = (decoratorNode: TreeNode) => {
decoratorNode.conditionExpanded = !decoratorNode.conditionExpanded;
};
/**
* 移除装饰器的条件
*/
const removeConditionFromDecorator = (decoratorNode: TreeNode) => {
if (decoratorNode.attachedCondition) {
// 删除附加的条件信息
delete decoratorNode.attachedCondition;
// 重置展开状态
decoratorNode.conditionExpanded = false;
// 保留装饰器的基础属性,只删除条件相关的属性
const preservedProperties: Record<string, any> = {};
// 条件装饰器的基础属性
const baseDecoratorProperties = [
'executeWhenTrue',
'executeWhenFalse',
'checkInterval',
'abortType'
];
// 保留基础属性
if (decoratorNode.properties) {
baseDecoratorProperties.forEach(key => {
if (key in decoratorNode.properties!) {
preservedProperties[key] = decoratorNode.properties![key];
}
});
}
// 重置为只包含基础属性的对象
decoratorNode.properties = preservedProperties;
}
};
/**
* 检查装饰器是否可以接受条件吸附
*/
const canAcceptCondition = (decoratorNode: TreeNode): boolean => {
return isConditionalDecorator(decoratorNode);
};
return {
dragState,
startConditionDrag,
handleDecoratorDragOver,
handleDecoratorDragLeave,
attachConditionToDecorator,
handleCanvasDrop,
resetDragState,
getConditionDisplayText,
removeConditionFromDecorator,
canAcceptCondition,
isConditionalDecorator,
toggleConditionExpanded,
getConditionProperties
};
}

View File

@@ -0,0 +1,610 @@
import { Ref } from 'vue';
import { TreeNode, Connection, ConnectionState } from '../types';
/**
* 连接线管理功能
*/
export function useConnectionManager(
treeNodes: Ref<TreeNode[]>,
connections: Ref<Connection[]>,
connectionState: ConnectionState,
canvasAreaRef: Ref<HTMLElement | null>,
svgRef: Ref<SVGElement | null>,
panX: Ref<number>,
panY: Ref<number>,
zoomLevel: Ref<number>
) {
const getPortPosition = (nodeId: string, portType: 'input' | 'output') => {
const node = treeNodes.value.find(n => n.id === nodeId);
if (!node) return null;
const canvasArea = canvasAreaRef.value;
if (!canvasArea) {
return getCalculatedPortPosition(node, portType);
}
const selectors = [
`[data-node-id="${nodeId}"]`,
`.tree-node[data-node-id="${nodeId}"]`,
`div[data-node-id="${nodeId}"]`
];
let nodeElement: HTMLElement | null = null;
for (const selector of selectors) {
try {
const doc = canvasArea.ownerDocument || document;
const foundElement = doc.querySelector(selector);
if (foundElement && canvasArea.contains(foundElement)) {
nodeElement = foundElement as HTMLElement;
break;
}
} catch (error) {
continue;
}
}
if (!nodeElement) {
try {
const allTreeNodes = canvasArea.querySelectorAll('.tree-node');
for (let i = 0; i < allTreeNodes.length; i++) {
const el = allTreeNodes[i] as HTMLElement;
const dataNodeId = el.getAttribute('data-node-id');
if (dataNodeId === nodeId) {
nodeElement = el;
break;
}
}
} catch (error) {
// Fallback to calculated position
}
}
if (!nodeElement) {
return getCalculatedPortPosition(node, portType);
}
const portSelectors = [
`.port.port-${portType}`,
`.port-${portType}`,
`.port.${portType}`,
`.${portType}-port`
];
let portElement: HTMLElement | null = null;
for (const portSelector of portSelectors) {
try {
portElement = nodeElement.querySelector(portSelector) as HTMLElement;
if (portElement) {
break;
}
} catch (error) {
continue;
}
}
if (!portElement) {
return getNodeEdgePortPosition(nodeElement, node, portType);
}
const portRect = portElement.getBoundingClientRect();
const canvasRect = canvasAreaRef.value?.getBoundingClientRect();
if (!canvasRect) {
return getCalculatedPortPosition(node, portType);
}
const relativeX = portRect.left + portRect.width / 2 - canvasRect.left;
const relativeY = portRect.top + portRect.height / 2 - canvasRect.top;
const svgX = (relativeX - panX.value) / zoomLevel.value;
const svgY = (relativeY - panY.value) / zoomLevel.value;
return { x: svgX, y: svgY };
};
const getCalculatedPortPosition = (node: any, portType: 'input' | 'output') => {
let nodeWidth = 150;
let nodeHeight = 80;
if (node.properties) {
const propertyCount = Object.keys(node.properties).length;
if (propertyCount > 0) {
nodeHeight += propertyCount * 20 + 20;
nodeWidth = Math.max(150, nodeWidth + 50);
}
}
const portX = node.x + nodeWidth / 2;
const portY = portType === 'input'
? node.y - 8
: node.y + nodeHeight + 8;
return { x: portX, y: portY };
};
const getNodeEdgePortPosition = (nodeElement: HTMLElement, node: any, portType: 'input' | 'output') => {
const nodeRect = nodeElement.getBoundingClientRect();
const canvasRect = canvasAreaRef.value?.getBoundingClientRect();
if (!canvasRect) {
return getCalculatedPortPosition(node, portType);
}
// 计算节点在SVG坐标系中的实际大小和位置
const nodeWidth = nodeRect.width / zoomLevel.value;
const nodeHeight = nodeRect.height / zoomLevel.value;
// 端口位于节点的水平中心
const portX = node.x + nodeWidth / 2;
const portY = portType === 'input'
? node.y - 5
: node.y + nodeHeight + 5;
return { x: portX, y: portY };
};
const startConnection = (event: MouseEvent, nodeId: string, portType: 'input' | 'output') => {
event.preventDefault();
event.stopPropagation();
connectionState.isConnecting = true;
connectionState.startNodeId = nodeId;
connectionState.startPortType = portType;
connectionState.currentMousePos = { x: event.clientX, y: event.clientY };
const startPos = getPortPosition(nodeId, portType);
if (startPos) {
connectionState.startPortPos = startPos;
}
document.addEventListener('mousemove', onConnectionDrag);
document.addEventListener('mouseup', onConnectionEnd);
if (canvasAreaRef.value) {
canvasAreaRef.value.classList.add('connecting');
}
};
// 连接拖拽
const onConnectionDrag = (event: MouseEvent) => {
if (!connectionState.isConnecting || !connectionState.startNodeId || !connectionState.startPortType) return;
connectionState.currentMousePos = { x: event.clientX, y: event.clientY };
const svgPos = clientToSVGCoordinates(event.clientX, event.clientY);
const startPos = getPortPosition(connectionState.startNodeId, connectionState.startPortType);
if (startPos && svgPos) {
const controlOffset = Math.abs(svgPos.y - startPos.y) * 0.5;
let path: string;
if (connectionState.startPortType === 'output') {
path = `M ${startPos.x} ${startPos.y} C ${startPos.x} ${startPos.y + controlOffset} ${svgPos.x} ${svgPos.y - controlOffset} ${svgPos.x} ${svgPos.y}`;
} else {
path = `M ${startPos.x} ${startPos.y} C ${startPos.x} ${startPos.y - controlOffset} ${svgPos.x} ${svgPos.y + controlOffset} ${svgPos.x} ${svgPos.y}`;
}
if ('tempPath' in connectionState) {
(connectionState as any).tempPath = path;
}
}
const targetPort = findTargetPort(event.clientX, event.clientY);
if (targetPort && targetPort.nodeId !== connectionState.startNodeId) {
connectionState.hoveredPort = targetPort;
} else {
connectionState.hoveredPort = null;
}
};
// 结束连接
const onConnectionEnd = (event: MouseEvent) => {
if (!connectionState.isConnecting) return;
// 检查是否落在有效的端口上
const targetPort = findTargetPort(event.clientX, event.clientY);
if (targetPort && connectionState.startNodeId && connectionState.startPortType) {
const canConnectResult = canConnect(
connectionState.startNodeId,
connectionState.startPortType,
targetPort.nodeId,
targetPort.portType
);
if (canConnectResult) {
let parentId: string, childId: string;
if (connectionState.startPortType === 'output') {
parentId = connectionState.startNodeId;
childId = targetPort.nodeId;
} else {
parentId = targetPort.nodeId;
childId = connectionState.startNodeId;
}
createConnection(parentId, childId);
}
}
// 清理连接状态
cancelConnection();
};
// 取消连接
const cancelConnection = () => {
connectionState.isConnecting = false;
connectionState.startNodeId = null;
connectionState.startPortType = null;
connectionState.currentMousePos = null;
connectionState.startPortPos = null;
connectionState.hoveredPort = null;
if ('tempPath' in connectionState) {
(connectionState as any).tempPath = '';
}
document.removeEventListener('mousemove', onConnectionDrag);
document.removeEventListener('mouseup', onConnectionEnd);
if (canvasAreaRef.value) {
canvasAreaRef.value.classList.remove('connecting');
}
// 清除画布内的拖拽目标样式
if (canvasAreaRef.value) {
const allPorts = canvasAreaRef.value.querySelectorAll('.port.drag-target');
allPorts.forEach(port => port.classList.remove('drag-target'));
}
};
const clientToSVGCoordinates = (clientX: number, clientY: number) => {
if (!canvasAreaRef.value) return null;
try {
// 获取canvas容器的边界
const canvasRect = canvasAreaRef.value.getBoundingClientRect();
// 转换为相对于canvas的坐标
const canvasX = clientX - canvasRect.left;
const canvasY = clientY - canvasRect.top;
// 撤销SVG的transform转换为SVG坐标
// SVG transform: translate(panX, panY) scale(zoomLevel)
const svgX = (canvasX - panX.value) / zoomLevel.value;
const svgY = (canvasY - panY.value) / zoomLevel.value;
return { x: svgX, y: svgY };
} catch (e) {
return null;
}
};
// 查找目标端口
const findTargetPort = (clientX: number, clientY: number) => {
if (!canvasAreaRef.value) return null;
try {
const elementAtPoint = document.elementFromPoint(clientX, clientY);
if (elementAtPoint?.classList.contains('port') && canvasAreaRef.value.contains(elementAtPoint)) {
return getPortInfo(elementAtPoint as HTMLElement);
}
} catch (error) {
// 查询出错时静默处理
}
const allPorts = canvasAreaRef.value.querySelectorAll('.port');
for (const port of allPorts) {
const rect = port.getBoundingClientRect();
const margin = 10;
if (clientX >= rect.left - margin && clientX <= rect.right + margin &&
clientY >= rect.top - margin && clientY <= rect.bottom + margin) {
return getPortInfo(port as HTMLElement);
}
}
return null;
};
// 从端口元素获取端口信息
const getPortInfo = (portElement: HTMLElement) => {
const nodeElement = portElement.closest('.tree-node');
if (!nodeElement) return null;
const nodeId = nodeElement.getAttribute('data-node-id');
const portType = portElement.classList.contains('port-input') ? 'input' : 'output' as 'input' | 'output';
return nodeId ? { nodeId, portType } : null;
};
// 端口悬停处理
const onPortHover = (nodeId: string, portType: 'input' | 'output') => {
if (connectionState.isConnecting && connectionState.startNodeId !== nodeId) {
connectionState.hoveredPort = { nodeId, portType };
if (canConnect(connectionState.startNodeId!, connectionState.startPortType!, nodeId, portType)) {
// 在画布区域内查找端口元素
if (canvasAreaRef.value) {
const portElement = canvasAreaRef.value.querySelector(`[data-node-id="${nodeId}"] .port.port-${portType}`);
if (portElement) {
portElement.classList.add('drag-target');
}
}
}
}
};
const onPortLeave = () => {
if (connectionState.isConnecting) {
connectionState.hoveredPort = null;
// 清除画布内的拖拽目标样式
if (canvasAreaRef.value) {
const allPorts = canvasAreaRef.value.querySelectorAll('.port.drag-target');
allPorts.forEach(port => port.classList.remove('drag-target'));
}
}
};
// 验证连接目标是否有效
const isValidConnectionTarget = (nodeId: string, portType: 'input' | 'output') => {
if (!connectionState.isConnecting || !connectionState.startNodeId || connectionState.startNodeId === nodeId) {
return false;
}
return canConnect(connectionState.startNodeId, connectionState.startPortType!, nodeId, portType);
};
// 检查是否可以连接
const canConnect = (sourceNodeId: string, sourcePortType: string, targetNodeId: string, targetPortType: string) => {
if (sourceNodeId === targetNodeId) return false;
if (sourcePortType === targetPortType) return false;
let parentNodeId: string, childNodeId: string;
if (sourcePortType === 'output') {
parentNodeId = sourceNodeId;
childNodeId = targetNodeId;
} else {
parentNodeId = targetNodeId;
childNodeId = sourceNodeId;
}
const childNode = treeNodes.value.find(n => n.id === childNodeId);
if (childNode && childNode.parent && childNode.parent !== parentNodeId) {
return false;
}
const parentNode = treeNodes.value.find(n => n.id === parentNodeId);
if (!parentNode || !parentNode.canHaveChildren) return false;
if (!childNode || !childNode.canHaveParent) return false;
// 检查子节点数量限制
if (parentNode.maxChildren !== undefined) {
const currentChildrenCount = parentNode.children ? parentNode.children.length : 0;
if (currentChildrenCount >= parentNode.maxChildren) {
return false; // 已达到最大子节点数量
}
}
// 检查根节点限制:根节点不能有父节点
if (childNode.type === 'root') {
return false; // 根节点不能作为其他节点的子节点
}
// 检查是否只能有一个根节点
if (parentNode.type === 'root') {
// 根节点只能连接一个子节点
const rootNodes = treeNodes.value.filter(n => n.type === 'root');
if (rootNodes.length > 1) {
return false; // 不能有多个根节点
}
}
if (wouldCreateCycle(parentNodeId, childNodeId)) return false;
if (isDescendant(childNodeId, parentNodeId)) return false;
return true;
};
// 检查是否会创建循环
const wouldCreateCycle = (parentId: string, childId: string) => {
return isDescendant(parentId, childId);
};
const isDescendant = (ancestorId: string, descendantId: string): boolean => {
const visited = new Set<string>();
function checkPath(currentId: string): boolean {
if (currentId === ancestorId) return true;
if (visited.has(currentId)) return false;
visited.add(currentId);
const currentNode = treeNodes.value.find(n => n.id === currentId);
if (currentNode?.children) {
for (const childId of currentNode.children) {
if (checkPath(childId)) return true;
}
}
return false;
}
return checkPath(descendantId);
};
// 创建连接
const createConnection = (parentId: string, childId: string) => {
const parentNode = treeNodes.value.find(n => n.id === parentId);
const childNode = treeNodes.value.find(n => n.id === childId);
if (!parentNode || !childNode) return;
// 移除子节点的旧父子关系
if (childNode.parent) {
const oldParent = treeNodes.value.find(n => n.id === childNode.parent);
if (oldParent) {
const index = oldParent.children.indexOf(childId);
if (index > -1) {
oldParent.children.splice(index, 1);
}
}
}
// 建立新的父子关系
childNode.parent = parentId;
if (!parentNode.children.includes(childId)) {
parentNode.children.push(childId);
}
updateConnections();
};
// 改进的连接线更新方法
const updateConnections = () => {
// 立即清空现有连接
connections.value.length = 0;
// 创建新的连接数据
const newConnections: Connection[] = [];
// 遍历所有节点建立连接
treeNodes.value.forEach(node => {
if (node.children && node.children.length > 0) {
node.children.forEach(childId => {
const childNode = treeNodes.value.find(n => n.id === childId);
if (childNode) {
// 尝试获取端口位置
const parentPos = getPortPosition(node.id, 'output');
const childPos = getPortPosition(childId, 'input');
if (parentPos && childPos) {
// 计算贝塞尔曲线路径
const deltaY = Math.abs(childPos.y - parentPos.y);
const controlOffset = Math.max(30, Math.min(deltaY * 0.5, 80));
const path = `M ${parentPos.x} ${parentPos.y} C ${parentPos.x} ${parentPos.y + controlOffset} ${childPos.x} ${childPos.y - controlOffset} ${childPos.x} ${childPos.y}`;
newConnections.push({
id: `${node.id}-${childId}`,
sourceId: node.id,
targetId: childId,
path: path,
active: false
});
} else {
// 如果无法获取实际位置,使用计算位置作为后备
const fallbackParentPos = getCalculatedPortPosition(node, 'output');
const fallbackChildPos = getCalculatedPortPosition(childNode, 'input');
const deltaY = Math.abs(fallbackChildPos.y - fallbackParentPos.y);
const controlOffset = Math.max(30, Math.min(deltaY * 0.5, 80));
const path = `M ${fallbackParentPos.x} ${fallbackParentPos.y} C ${fallbackParentPos.x} ${fallbackParentPos.y + controlOffset} ${fallbackChildPos.x} ${fallbackChildPos.y - controlOffset} ${fallbackChildPos.x} ${fallbackChildPos.y}`;
newConnections.push({
id: `${node.id}-${childId}`,
sourceId: node.id,
targetId: childId,
path: path,
active: false
});
}
}
});
}
});
// 批量更新连接
connections.value.push(...newConnections);
// 如果有DOM元素进行二次精确更新
if (canvasAreaRef.value) {
setTimeout(() => {
// 二次更新使用实际DOM位置
const updatedConnections: Connection[] = [];
treeNodes.value.forEach(node => {
if (node.children && node.children.length > 0) {
node.children.forEach(childId => {
const childNode = treeNodes.value.find(n => n.id === childId);
if (childNode) {
const parentPos = getPortPosition(node.id, 'output');
const childPos = getPortPosition(childId, 'input');
if (parentPos && childPos) {
const deltaY = Math.abs(childPos.y - parentPos.y);
const controlOffset = Math.max(30, Math.min(deltaY * 0.5, 80));
const path = `M ${parentPos.x} ${parentPos.y} C ${parentPos.x} ${parentPos.y + controlOffset} ${childPos.x} ${childPos.y - controlOffset} ${childPos.x} ${childPos.y}`;
updatedConnections.push({
id: `${node.id}-${childId}`,
sourceId: node.id,
targetId: childId,
path: path,
active: false
});
}
}
});
}
});
// 如果二次更新得到了有效结果,替换连接数据
if (updatedConnections.length > 0) {
connections.value.length = 0;
connections.value.push(...updatedConnections);
}
}, 100); // 100ms延迟确保DOM完全渲染
}
};
// 删除连接线
const removeConnection = (connectionId: string) => {
const connection = connections.value.find(conn => conn.id === connectionId);
if (!connection) return;
const parentNode = treeNodes.value.find(n => n.id === connection.sourceId);
const childNode = treeNodes.value.find(n => n.id === connection.targetId);
if (parentNode && childNode) {
// 从父节点的children中移除
const index = parentNode.children.indexOf(connection.targetId);
if (index > -1) {
parentNode.children.splice(index, 1);
}
// 清除子节点的parent
childNode.parent = undefined;
// 更新连接线
updateConnections();
}
};
// 连接线点击事件处理
const onConnectionClick = (event: MouseEvent, connectionId: string) => {
event.preventDefault();
event.stopPropagation();
// 询问用户是否要删除连接
if (confirm('确定要删除这条连接线吗?')) {
removeConnection(connectionId);
}
};
return {
getPortPosition,
startConnection,
cancelConnection,
updateConnections,
removeConnection,
onConnectionClick,
onPortHover,
onPortLeave,
isValidConnectionTarget
};
}

View File

@@ -0,0 +1,538 @@
import { Ref, ref, watch } from 'vue';
import { TreeNode, Connection } from '../types';
interface FileOperationOptions {
treeNodes: Ref<TreeNode[]>;
selectedNodeId: Ref<string | null>;
connections: Ref<Connection[]>;
tempConnection: Ref<{ path: string }>;
showExportModal: Ref<boolean>;
codeGeneration?: {
createTreeFromConfig: (config: any) => TreeNode[];
};
updateConnections?: () => void;
blackboardOperations?: {
getBlackboardVariables: () => any[];
loadBlackboardVariables: (variables: any[]) => void;
clearBlackboard: () => void;
};
}
interface FileData {
nodes: TreeNode[];
connections: Connection[];
blackboard?: any[];
metadata: {
name: string;
created: string;
version: string;
};
}
export function useFileOperations(options: FileOperationOptions) {
const {
treeNodes,
selectedNodeId,
connections,
tempConnection,
showExportModal,
codeGeneration,
updateConnections,
blackboardOperations
} = options;
const hasUnsavedChanges = ref(false);
const lastSavedState = ref<string>('');
const currentFileName = ref('');
const currentFilePath = ref('');
const updateUnsavedStatus = () => {
const currentState = JSON.stringify({
nodes: treeNodes.value,
connections: connections.value
});
hasUnsavedChanges.value = currentState !== lastSavedState.value;
};
watch([treeNodes, connections], updateUnsavedStatus, { deep: true });
const markAsSaved = () => {
const currentState = JSON.stringify({
nodes: treeNodes.value,
connections: connections.value
});
lastSavedState.value = currentState;
hasUnsavedChanges.value = false;
};
const setCurrentFile = (fileName: string, filePath: string = '') => {
currentFileName.value = fileName;
currentFilePath.value = filePath;
markAsSaved();
};
const clearCurrentFile = () => {
currentFileName.value = '';
currentFilePath.value = '';
};
const exportBehaviorTreeData = (): FileData => {
const data: FileData = {
nodes: treeNodes.value,
connections: connections.value,
metadata: {
name: currentFileName.value || 'untitled',
created: new Date().toISOString(),
version: '1.0'
}
};
// 包含黑板数据
if (blackboardOperations) {
const blackboardVariables = blackboardOperations.getBlackboardVariables();
if (blackboardVariables.length > 0) {
data.blackboard = blackboardVariables;
}
}
return data;
};
const showMessage = (message: string, type: 'success' | 'error' = 'success') => {
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
background: ${type === 'success' ? '#4caf50' : '#f44336'};
color: white;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
z-index: 10001;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '1';
toast.style.transform = 'translateX(0)';
}, 10);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(100%)';
setTimeout(() => {
if (document.body.contains(toast)) {
document.body.removeChild(toast);
}
}, 300);
}, 3000);
};
const sendToMain = (message: string, data: any): Promise<void> => {
return new Promise((resolve, reject) => {
try {
Editor.Message.request('cocos-ecs-extension', message, data)
.then((result) => {
resolve();
})
.catch((error) => {
reject(error);
});
} catch (error) {
reject(error);
}
});
};
const checkUnsavedChanges = (): Promise<boolean> => {
return new Promise((resolve) => {
if (!hasUnsavedChanges.value) {
resolve(true);
return;
}
const result = confirm(
'当前行为树有未保存的更改,是否要保存?\n\n' +
'点击"确定"保存更改\n' +
'点击"取消"丢弃更改'
);
if (result) {
saveBehaviorTree().then(() => {
resolve(true);
}).catch(() => {
resolve(false);
});
} else {
resolve(true);
}
});
};
const newBehaviorTree = async () => {
const canProceed = await checkUnsavedChanges();
if (canProceed) {
treeNodes.value = [];
selectedNodeId.value = null;
connections.value = [];
tempConnection.value.path = '';
// 清空黑板
if (blackboardOperations) {
blackboardOperations.clearBlackboard();
}
clearCurrentFile();
markAsSaved();
}
};
const saveBehaviorTree = async (): Promise<boolean> => {
if (currentFilePath.value) {
return await saveToCurrentFile();
} else {
return await saveAsBehaviorTree();
}
};
const saveToCurrentFile = async (): Promise<boolean> => {
if (!currentFilePath.value) {
return await saveAsBehaviorTree();
}
try {
const data = exportBehaviorTreeData();
const jsonString = JSON.stringify(data, null, 2);
await sendToMain('overwrite-behavior-tree-file', {
filePath: currentFilePath.value,
content: jsonString
});
markAsSaved();
showMessage('保存成功!');
return true;
} catch (error) {
showMessage('保存失败: ' + error, 'error');
return false;
}
};
const saveAsBehaviorTree = async (): Promise<boolean> => {
try {
const data = exportBehaviorTreeData();
const jsonString = JSON.stringify(data, null, 2);
const result = await Editor.Dialog.save({
title: '保存行为树文件',
filters: [
{ name: '行为树文件', extensions: ['bt.json', 'json'] },
{ name: '所有文件', extensions: ['*'] }
]
});
if (result.canceled || !result.filePath) {
return false;
}
const fs = require('fs-extra');
await fs.writeFile(result.filePath, jsonString);
const path = require('path');
const fileName = path.basename(result.filePath, path.extname(result.filePath));
setCurrentFile(fileName, result.filePath);
showMessage(`保存成功!文件: ${result.filePath}`);
return true;
} catch (error) {
showMessage('另存为失败: ' + error, 'error');
return false;
}
};
const saveToFile = async (fileName: string, jsonString: string): Promise<boolean> => {
try {
await sendToMain('create-behavior-tree-from-editor', {
fileName: fileName + '.json',
content: jsonString
});
setCurrentFile(fileName, `assets/${fileName}.bt.json`);
showMessage(`保存成功!文件名: ${fileName}.json`);
return true;
} catch (error) {
showMessage('保存失败: ' + error, 'error');
return false;
}
};
const getFileNameFromUser = (): Promise<string | null> => {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
`;
const dialog = document.createElement('div');
dialog.style.cssText = `
background: #2d2d2d;
color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
min-width: 300px;
`;
dialog.innerHTML = `
<h3 style="margin: 0 0 15px 0; color: #ffffff;">保存行为树</h3>
<p style="margin: 0 0 15px 0; color: #cccccc;">请输入文件名(不含扩展名):</p>
<input type="text" id="filename-input" value="${currentFileName.value || 'behavior_tree'}"
style="width: 100%; padding: 8px; border: 1px solid #555; background: #1a1a1a; color: #ffffff; border-radius: 4px; margin-bottom: 15px;">
<div style="text-align: right;">
<button id="cancel-btn" style="padding: 8px 16px; margin-right: 8px; background: #555; color: #fff; border: none; border-radius: 4px; cursor: pointer;">取消</button>
<button id="save-btn" style="padding: 8px 16px; background: #007acc; color: #fff; border: none; border-radius: 4px; cursor: pointer;">保存</button>
</div>
`;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
const input = dialog.querySelector('#filename-input') as HTMLInputElement;
const saveBtn = dialog.querySelector('#save-btn') as HTMLButtonElement;
const cancelBtn = dialog.querySelector('#cancel-btn') as HTMLButtonElement;
input.focus();
input.select();
const cleanup = () => {
document.body.removeChild(overlay);
};
saveBtn.onclick = () => {
const fileName = input.value.trim();
cleanup();
resolve(fileName || null);
};
cancelBtn.onclick = () => {
cleanup();
resolve(null);
};
input.onkeydown = (e) => {
if (e.key === 'Enter') {
const fileName = input.value.trim();
cleanup();
resolve(fileName || null);
} else if (e.key === 'Escape') {
cleanup();
resolve(null);
}
};
});
};
const loadFileContent = (fileData: any, filePath: string = '') => {
try {
if (!fileData) {
return;
}
let parsedData = fileData;
if (fileData.rawContent) {
try {
parsedData = JSON.parse(fileData.rawContent);
} catch (e) {
parsedData = {
nodes: [],
connections: []
};
}
}
if (parsedData.nodes && Array.isArray(parsedData.nodes)) {
treeNodes.value = parsedData.nodes.map((node: any) => ({
...node,
x: node.x || 0,
y: node.y || 0,
children: node.children || [],
properties: node.properties || {},
canHaveChildren: node.canHaveChildren !== false,
canHaveParent: node.canHaveParent !== false,
hasError: node.hasError || false
}));
} else if (parsedData.tree) {
const treeNode = parsedData.tree;
const nodes = [treeNode];
const extractNodes = (node: any): any[] => {
const allNodes = [node];
if (node.children && Array.isArray(node.children)) {
node.children.forEach((child: any) => {
if (typeof child === 'object') {
allNodes.push(...extractNodes(child));
}
});
}
return allNodes;
};
const allNodes = extractNodes(treeNode);
treeNodes.value = allNodes.map((node: any, index: number) => ({
...node,
x: node.x || (300 + index * 150),
y: node.y || (100 + Math.floor(index / 3) * 200),
children: Array.isArray(node.children)
? node.children.filter((child: any) => typeof child === 'string')
: [],
properties: node.properties || {},
canHaveChildren: true,
canHaveParent: node.id !== 'root',
hasError: false
}));
} else {
treeNodes.value = [];
}
if (parsedData.connections && Array.isArray(parsedData.connections)) {
connections.value = parsedData.connections.map((conn: any) => ({
id: conn.id || Math.random().toString(36).substr(2, 9),
sourceId: conn.sourceId,
targetId: conn.targetId,
path: conn.path || '',
active: conn.active || false
}));
} else {
connections.value = [];
}
if (fileData._fileInfo) {
const fileName = fileData._fileInfo.fileName || 'untitled';
const fullPath = fileData._fileInfo.filePath || filePath;
setCurrentFile(fileName, fullPath);
} else if (parsedData.metadata?.name) {
setCurrentFile(parsedData.metadata.name, filePath);
} else {
setCurrentFile('untitled', filePath);
}
// 加载黑板数据
if (blackboardOperations && parsedData.blackboard && Array.isArray(parsedData.blackboard)) {
blackboardOperations.loadBlackboardVariables(parsedData.blackboard);
}
selectedNodeId.value = null;
tempConnection.value.path = '';
if (updateConnections) {
setTimeout(() => {
updateConnections();
}, 100);
}
} catch (error) {
console.error('文件加载失败:', error);
showMessage('文件加载失败: ' + error, 'error');
treeNodes.value = [];
connections.value = [];
selectedNodeId.value = null;
setCurrentFile('untitled', '');
}
};
const loadBehaviorTree = async () => {
const canProceed = await checkUnsavedChanges();
if (!canProceed) return;
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json,.bt';
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
try {
const configText = event.target?.result as string;
const config = JSON.parse(configText);
if (codeGeneration) {
const newNodes = codeGeneration.createTreeFromConfig(config);
treeNodes.value = newNodes;
selectedNodeId.value = null;
if (config.connections && Array.isArray(config.connections)) {
connections.value = config.connections.map((conn: any) => ({
id: conn.id,
sourceId: conn.sourceId,
targetId: conn.targetId,
path: conn.path || '',
active: conn.active || false
}));
} else {
connections.value = [];
}
tempConnection.value.path = '';
// 加载黑板数据
if (blackboardOperations && config.blackboard && Array.isArray(config.blackboard)) {
blackboardOperations.loadBlackboardVariables(config.blackboard);
}
const fileName = file.name.replace(/\.(json|bt)$/, '');
setCurrentFile(fileName, '');
setTimeout(() => {
if (updateConnections) {
updateConnections();
}
}, 100);
} else {
showMessage('代码生成器未初始化', 'error');
}
} catch (error) {
showMessage('配置文件格式错误', 'error');
}
};
reader.readAsText(file);
}
};
input.click();
};
const exportConfig = () => {
showExportModal.value = true;
};
return {
newBehaviorTree,
saveBehaviorTree,
saveAsBehaviorTree,
loadBehaviorTree,
loadFileContent,
exportConfig,
hasUnsavedChanges,
markAsSaved,
setCurrentFile,
clearCurrentFile,
currentFileName,
currentFilePath
};
}

View File

@@ -0,0 +1,60 @@
import { Ref } from 'vue';
import { checkBehaviorTreeInstalled, installBehaviorTreeAI } from '../utils/installUtils';
/**
* 安装管理
*/
export function useInstallation(
checkingStatus: Ref<boolean>,
isInstalled: Ref<boolean>,
version: Ref<string | null>,
isInstalling: Ref<boolean>
) {
// 检查安装状态
const checkInstallStatus = async () => {
checkingStatus.value = true;
try {
const result = await checkBehaviorTreeInstalled(Editor.Project.path);
isInstalled.value = result.installed;
version.value = result.version;
} catch (error) {
console.error('检查AI系统安装状态失败:', error);
isInstalled.value = false;
version.value = null;
} finally {
checkingStatus.value = false;
}
};
// 处理安装
const handleInstall = async () => {
isInstalling.value = true;
try {
const result = await installBehaviorTreeAI(Editor.Project.path);
if (result) {
// 等待文件系统更新
await new Promise(resolve => setTimeout(resolve, 2000));
await checkInstallStatus();
// 如果第一次检查失败,再次尝试
if (!isInstalled.value) {
await new Promise(resolve => setTimeout(resolve, 1000));
await checkInstallStatus();
}
} else {
console.error('AI系统安装失败');
}
} catch (error) {
console.error('安装AI系统时发生错误:', error);
} finally {
isInstalling.value = false;
}
};
return {
checkInstallStatus,
handleInstall
};
}

View File

@@ -0,0 +1,86 @@
/**
* 节点显示管理功能
*/
export function useNodeDisplay() {
// 检查节点是否有可见属性
const hasVisibleProperties = (node: any) => {
if (!node.properties) return false;
return Object.keys(getVisibleProperties(node)).length > 0;
};
// 获取可见属性
const getVisibleProperties = (node: any) => {
if (!node.properties) return {};
const visibleProps: any = {};
for (const [key, prop] of Object.entries(node.properties)) {
if (shouldShowProperty(prop as any, key)) {
visibleProps[key] = prop;
}
}
return visibleProps;
};
// 判断属性是否应该显示
const shouldShowProperty = (prop: any, key: string) => {
// 总是显示这些重要属性
const alwaysShow = ['abortType', 'repeatCount', 'priority'];
if (alwaysShow.includes(key)) {
return true;
}
// 对于其他属性,只在非默认值时显示
if (prop.type === 'string' && prop.value && prop.value.trim() !== '') {
return true;
}
if (prop.type === 'number' && prop.value !== 0 && prop.value !== -1) {
return true;
}
if (prop.type === 'boolean' && prop.value === true) {
return true;
}
if (prop.type === 'select' && prop.value !== 'None' && prop.value !== '') {
return true;
}
if (prop.type === 'code' && prop.value && prop.value.trim() !== '' && prop.value !== '(context) => true') {
return true;
}
return false;
};
// 格式化属性值显示
const formatPropertyValue = (prop: any) => {
switch (prop.type) {
case 'boolean':
return prop.value ? '✓' : '✗';
case 'number':
return prop.value.toString();
case 'select':
return prop.value;
case 'string':
return prop.value.length > 15 ? prop.value.substring(0, 15) + '...' : prop.value;
case 'code':
const code = prop.value || '';
if (code.length > 20) {
// 尝试提取函数体的关键部分
const bodyMatch = code.match(/=>\s*(.+)/) || code.match(/{\s*(.+?)\s*}/);
if (bodyMatch) {
const body = bodyMatch[1].trim();
return body.length > 15 ? body.substring(0, 15) + '...' : body;
}
return code.substring(0, 20) + '...';
}
return code;
default:
return prop.value?.toString() || '';
}
};
return {
hasVisibleProperties,
getVisibleProperties,
formatPropertyValue
};
}

View File

@@ -0,0 +1,190 @@
import { Ref, nextTick } from 'vue';
import { TreeNode, Connection } from '../types';
import { NodeTemplate } from '../data/nodeTemplates';
import { createNodeFromTemplate } from '../utils/nodeUtils';
import { getCanvasCoordinates } from '../utils/canvasUtils';
/**
* 节点操作管理
*/
export function useNodeOperations(
treeNodes: Ref<TreeNode[]>,
selectedNodeId: Ref<string | null>,
connections: Ref<Connection[]>,
panX: Ref<number>,
panY: Ref<number>,
zoomLevel: Ref<number>,
getNodeByIdLocal: (id: string) => TreeNode | undefined,
updateConnections?: () => void
) {
// 获取相对于画布的坐标(用于节点拖放等操作)
const getCanvasCoords = (event: MouseEvent, canvasElement: HTMLElement | null) => {
return getCanvasCoordinates(event, canvasElement, panX.value, panY.value, zoomLevel.value);
};
// 拖拽事件处理
const onNodeDragStart = (event: DragEvent, template: NodeTemplate) => {
if (event.dataTransfer) {
// 检查是否为条件节点,如果是则标记为条件拖拽
const dragData = {
...template,
isConditionDrag: template.isDraggableCondition || false
};
event.dataTransfer.setData('application/json', JSON.stringify(dragData));
event.dataTransfer.effectAllowed = 'copy';
}
};
const onCanvasDragOver = (event: DragEvent) => {
event.preventDefault();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'copy';
}
};
const onCanvasDrop = (event: DragEvent) => {
event.preventDefault();
const templateData = event.dataTransfer?.getData('application/json');
if (!templateData) return;
try {
const dragData = JSON.parse(templateData);
// 如果是条件节点拖拽,阻止创建独立节点
if (dragData.isConditionDrag || dragData.isDraggableCondition) {
return; // 条件节点不能作为独立节点创建
}
const template: NodeTemplate = dragData;
const canvasElement = event.currentTarget as HTMLElement;
const { x, y } = getCanvasCoords(event, canvasElement);
const newNode = createNodeFromTemplate(template, x, y);
treeNodes.value.push(newNode);
selectedNodeId.value = newNode.id;
} catch (error) {
// 节点创建失败时静默处理
}
};
// 节点删除(递归删除子节点)
const deleteNode = (nodeId: string) => {
const deleteRecursive = (id: string) => {
const node = getNodeByIdLocal(id);
if (!node) return;
// 递归删除子节点
node.children.forEach(childId => deleteRecursive(childId));
// 从父节点的children中移除
if (node.parent) {
const parent = getNodeByIdLocal(node.parent);
if (parent) {
const index = parent.children.indexOf(id);
if (index > -1) {
parent.children.splice(index, 1);
}
}
}
// 移除连接
connections.value = connections.value.filter(conn =>
conn.sourceId !== id && conn.targetId !== id
);
// 从树中移除节点
const nodeIndex = treeNodes.value.findIndex(n => n.id === id);
if (nodeIndex > -1) {
treeNodes.value.splice(nodeIndex, 1);
}
};
deleteRecursive(nodeId);
if (selectedNodeId.value === nodeId) {
selectedNodeId.value = null;
}
// 更新连接线
if (updateConnections) {
updateConnections();
}
};
// 通用的属性更新方法
const setNestedProperty = (obj: any, path: string, value: any) => {
const keys = path.split('.');
let current = obj;
// 导航到目标属性的父对象
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
current[key] = {};
}
current = current[key];
}
// 设置最终值
const finalKey = keys[keys.length - 1];
current[finalKey] = value;
};
// 节点属性更新
const updateNodeProperty = (path: string, value: any) => {
const selectedNode = selectedNodeId.value ? getNodeByIdLocal(selectedNodeId.value) : null;
if (!selectedNode) return;
// 检查是否是条件节点的属性更新
if (selectedNode.isConditionNode && selectedNode.parentDecorator) {
// 条件节点的属性更新需要同步到装饰器
updateConditionNodeProperty(selectedNode.parentDecorator, path, value);
} else {
// 普通节点的属性更新
setNestedProperty(selectedNode, path, value);
// 强制触发响应式更新
const nodeIndex = treeNodes.value.findIndex(n => n.id === selectedNode.id);
if (nodeIndex > -1) {
const newNodes = [...treeNodes.value];
newNodes[nodeIndex] = { ...selectedNode };
treeNodes.value = newNodes;
}
}
};
// 更新条件节点属性到装饰器
const updateConditionNodeProperty = (decoratorNode: TreeNode, path: string, value: any) => {
// 解析属性路径,例如 "properties.variableName.value" -> "variableName"
const pathParts = path.split('.');
if (pathParts[0] === 'properties' && pathParts[2] === 'value') {
const propertyName = pathParts[1];
// 直接更新装饰器的属性
if (!decoratorNode.properties) {
decoratorNode.properties = {};
}
decoratorNode.properties[propertyName] = value;
// 强制触发响应式更新
const nodeIndex = treeNodes.value.findIndex(n => n.id === decoratorNode.id);
if (nodeIndex > -1) {
const newNodes = [...treeNodes.value];
newNodes[nodeIndex] = { ...decoratorNode };
treeNodes.value = newNodes;
}
}
};
return {
getCanvasCoords,
onNodeDragStart,
onCanvasDragOver,
onCanvasDrop,
deleteNode,
updateNodeProperty
};
}

View File

@@ -0,0 +1,180 @@
import { readFileSync } from 'fs-extra';
import { join } from 'path';
import { createApp, App, defineComponent } from 'vue';
import { useBehaviorTreeEditor } from './composables/useBehaviorTreeEditor';
import { EventManager } from './utils/EventManager';
// Vue应用实例
let panelDataMap = new WeakMap<any, any>();
// 待处理的文件队列
let pendingFileData: any = null;
// Vue应用是否已挂载完成
let vueAppMounted: boolean = false;
// 存储面板实例用于访问面板的DOM元素
let currentPanelInstance: any = null;
/**
* 面板定义
*/
const panelDefinition = {
template: readFileSync(join(__dirname, '../../../static/template/behavior-tree/index.html'), 'utf-8'),
style: [
readFileSync(join(__dirname, '../../../static/style/behavior-tree/base.css'), 'utf-8'),
readFileSync(join(__dirname, '../../../static/style/behavior-tree/toolbar.css'), 'utf-8'),
readFileSync(join(__dirname, '../../../static/style/behavior-tree/panels.css'), 'utf-8'),
readFileSync(join(__dirname, '../../../static/style/behavior-tree/canvas.css'), 'utf-8'),
readFileSync(join(__dirname, '../../../static/style/behavior-tree/nodes.css'), 'utf-8'),
readFileSync(join(__dirname, '../../../static/style/behavior-tree/conditions.css'), 'utf-8'),
readFileSync(join(__dirname, '../../../static/style/behavior-tree/modals.css'), 'utf-8')
].join('\n'),
$: {
app: '#behavior-tree-app',
},
methods: {
async loadBehaviorTreeFile(assetInfo: any) {
try {
const filePath = assetInfo?.file || assetInfo?.path;
if (!filePath) {
throw new Error('无法获取文件路径');
}
const fs = require('fs-extra');
const path = require('path');
if (!fs.existsSync(filePath)) {
throw new Error(`文件不存在: ${filePath}`);
}
const content = await fs.readFile(filePath, 'utf8');
let fileContent: any;
try {
fileContent = JSON.parse(content);
} catch (parseError) {
fileContent = {
version: "1.0.0",
type: "behavior-tree",
tree: { id: "root", type: "sequence", children: [] }
};
}
const fileInfo = {
...fileContent,
_fileInfo: {
fileName: path.basename(filePath, path.extname(filePath)),
filePath: filePath
}
};
const notifyVueComponent = () => {
const appContainer = currentPanelInstance?.$.app;
if (appContainer && vueAppMounted) {
if (typeof (appContainer as any).loadFileContent === 'function') {
(appContainer as any).loadFileContent(fileInfo);
} else {
const event = new CustomEvent('load-behavior-tree-file', { detail: fileInfo });
document.dispatchEvent(event);
}
} else {
pendingFileData = fileInfo;
}
};
notifyVueComponent();
if (pendingFileData) {
setTimeout(() => {
if (pendingFileData) {
notifyVueComponent();
}
}, 500);
}
return { success: true, message: '文件加载成功' };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const event = new CustomEvent('file-load-error', { detail: { error: errorMessage } });
document.dispatchEvent(event);
return { success: false, error: errorMessage };
}
},
},
ready() {
currentPanelInstance = this;
if (this.$.app) {
try {
const BehaviorTreeEditor = defineComponent({
setup() {
const editor = useBehaviorTreeEditor();
return editor;
},
template: readFileSync(join(__dirname, '../../../static/template/behavior-tree/BehaviorTreeEditor.html'), 'utf-8')
});
const app = createApp(BehaviorTreeEditor);
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('ui-');
app.config.errorHandler = (err, instance, info) => {
console.error('[BehaviorTreePanel] Vue错误:', err, info);
};
app.component('tree-node-item', defineComponent({
props: ['node', 'level', 'getNodeByIdLocal'],
emits: ['node-select'],
template: `
<div class="tree-node-item"
:class="'level-' + level"
@click="$emit('node-select', node)">
<span class="node-icon">{{ node.icon || '●' }}</span>
<span class="node-name">{{ node.name || node.type }}</span>
<span class="node-type">{{ node.type }}</span>
</div>
`
}));
app.mount(this.$.app);
panelDataMap.set(this, app);
vueAppMounted = true;
if (pendingFileData) {
const event = new CustomEvent('load-behavior-tree-file', { detail: pendingFileData });
document.dispatchEvent(event);
pendingFileData = null;
}
} catch (error) {
console.error('[BehaviorTreePanel] 初始化失败:', error);
}
}
},
/**
* 面板关闭时调用
*/
close() {
try {
const app = panelDataMap.get(this);
if (app) {
app.unmount();
panelDataMap.delete(this);
}
EventManager.getInstance().cleanup();
} catch (error) {
console.error('[BehaviorTreePanel] 清理资源时发生错误:', error);
}
}
};
// 导出面板定义 - 使用Editor.Panel.define()包装
module.exports = Editor.Panel.define(panelDefinition);

View File

@@ -0,0 +1,81 @@
import { PropertyDefinition } from '../data/nodeTemplates';
export interface TreeNode {
id: string;
type: string;
name: string;
icon: string;
description: string;
x: number;
y: number;
children: string[];
parent?: string;
properties?: Record<string, any>; // 改为any以支持动态属性值
canHaveChildren: boolean;
canHaveParent: boolean;
maxChildren?: number; // 最大子节点数量限制
minChildren?: number; // 最小子节点数量要求
hasError?: boolean;
// 条件装饰器相关
attachedCondition?: {
type: string;
name: string;
icon: string;
};
// 条件节点相关(用于虚拟条件节点)
isConditionNode?: boolean;
parentDecorator?: TreeNode;
// 条件显示状态
conditionExpanded?: boolean; // 条件是否展开显示详细信息
}
export interface Connection {
id: string;
sourceId: string;
targetId: string;
path: string;
active: boolean;
}
export interface DragState {
isDraggingCanvas: boolean;
isDraggingNode: boolean;
isConnecting: boolean;
dragStartX: number;
dragStartY: number;
dragNodeId: string | null;
dragNodeStartX: number;
dragNodeStartY: number;
connectionStart: { nodeId: string; portType: string } | null;
connectionEnd: { x: number; y: number };
}
export interface InstallStatus {
installed: boolean;
version: string | null;
packageExists: boolean;
}
export interface ValidationResult {
isValid: boolean;
message: string;
}
export interface ConnectionPort {
nodeId: string;
portType: string;
}
export interface CanvasCoordinates {
x: number;
y: number;
}
export interface ConnectionState {
isConnecting: boolean;
startNodeId: string | null;
startPortType: 'input' | 'output' | null;
currentMousePos: { x: number; y: number } | null;
startPortPos: { x: number; y: number } | null;
hoveredPort: { nodeId: string; portType: 'input' | 'output' } | null;
}

View File

@@ -0,0 +1,104 @@
/**
* 事件管理器 - 统一处理面板的事件通信
*/
export class EventManager {
private static instance: EventManager;
private eventListeners: Map<string, EventListener[]> = new Map();
private constructor() {}
static getInstance(): EventManager {
if (!EventManager.instance) {
EventManager.instance = new EventManager();
}
return EventManager.instance;
}
/**
* 添加事件监听器
*/
addEventListener(eventType: string, listener: EventListener): void {
if (!this.eventListeners.has(eventType)) {
this.eventListeners.set(eventType, []);
}
const listeners = this.eventListeners.get(eventType)!;
listeners.push(listener);
// 添加到DOM
document.addEventListener(eventType, listener);
console.log(`[EventManager] 添加事件监听器: ${eventType}`);
}
/**
* 移除事件监听器
*/
removeEventListener(eventType: string, listener: EventListener): void {
const listeners = this.eventListeners.get(eventType);
if (listeners) {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
document.removeEventListener(eventType, listener);
console.log(`[EventManager] 移除事件监听器: ${eventType}`);
}
}
}
/**
* 移除特定类型的所有监听器
*/
removeAllListeners(eventType: string): void {
const listeners = this.eventListeners.get(eventType);
if (listeners) {
listeners.forEach(listener => {
document.removeEventListener(eventType, listener);
});
this.eventListeners.delete(eventType);
console.log(`[EventManager] 移除所有 ${eventType} 事件监听器`);
}
}
/**
* 清理所有事件监听器
*/
cleanup(): void {
this.eventListeners.forEach((listeners, eventType) => {
listeners.forEach(listener => {
document.removeEventListener(eventType, listener);
});
});
this.eventListeners.clear();
console.log('[EventManager] 清理所有事件监听器');
}
/**
* 发送消息到主进程
*/
static sendToMain(message: string, ...args: any[]): void {
try {
if (typeof (window as any).sendToMain === 'function') {
(window as any).sendToMain(message, ...args);
console.log(`[EventManager] 发送消息到主进程: ${message}`, args);
} else {
console.error('[EventManager] sendToMain 方法不可用');
}
} catch (error) {
console.error('[EventManager] 发送消息失败:', error);
}
}
/**
* 触发自定义事件
*/
static dispatch(eventType: string, detail?: any): void {
try {
const event = new CustomEvent(eventType, { detail });
document.dispatchEvent(event);
console.log(`[EventManager] 触发事件: ${eventType}`, detail);
} catch (error) {
console.error(`[EventManager] 触发事件失败: ${eventType}`, error);
}
}
}

View File

@@ -0,0 +1,109 @@
import { CanvasCoordinates } from '../types';
/**
* 获取相对于画布的坐标(考虑缩放和平移)
*/
export function getCanvasCoordinates(
event: MouseEvent,
canvasElement: HTMLElement | null,
panX: number,
panY: number,
zoomLevel: number
): CanvasCoordinates {
if (!canvasElement) {
return { x: 0, y: 0 };
}
try {
const rect = canvasElement.getBoundingClientRect();
const x = (event.clientX - rect.left - panX) / zoomLevel;
const y = (event.clientY - rect.top - panY) / zoomLevel;
return { x, y };
} catch (error) {
return { x: 0, y: 0 };
}
}
/**
* 计算网格样式
*/
export function getGridStyle(panX: number, panY: number, zoomLevel: number) {
const gridSize = 20 * zoomLevel;
return {
backgroundSize: `${gridSize}px ${gridSize}px`,
backgroundPosition: `${panX % gridSize}px ${panY % gridSize}px`
};
}
/**
* 计算视图居中的平移值
*/
export function calculateCenterView(
nodes: any[],
canvasWidth: number,
canvasHeight: number,
zoomLevel: number
): { panX: number; panY: number } {
if (nodes.length === 0) {
return { panX: 0, panY: 0 };
}
// 计算所有节点的边界
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
nodes.forEach(node => {
minX = Math.min(minX, node.x);
minY = Math.min(minY, node.y);
maxX = Math.max(maxX, node.x + 150);
maxY = Math.max(maxY, node.y + 100);
});
// 计算中心点
const centerX = (minX + maxX) / 2;
const centerY = (minY + maxY) / 2;
// 设置平移,使内容居中
const panX = canvasWidth / 2 - centerX * zoomLevel;
const panY = canvasHeight / 2 - centerY * zoomLevel;
return { panX, panY };
}
/**
* 约束缩放级别
*/
export function constrainZoom(zoom: number): number {
return Math.max(0.3, Math.min(zoom, 3));
}
/**
* 计算缩放后的坐标
*/
export function transformCoordinate(
x: number,
y: number,
panX: number,
panY: number,
zoomLevel: number
): { x: number; y: number } {
return {
x: x * zoomLevel + panX,
y: y * zoomLevel + panY
};
}
/**
* 计算逆向变换的坐标(从屏幕坐标到画布坐标)
*/
export function inverseTransformCoordinate(
screenX: number,
screenY: number,
panX: number,
panY: number,
zoomLevel: number
): { x: number; y: number } {
return {
x: (screenX - panX) / zoomLevel,
y: (screenY - panY) / zoomLevel
};
}

View File

@@ -0,0 +1,95 @@
import { InstallStatus } from '../types';
import * as fs from 'fs';
import * as path from 'path';
/**
* 检查行为树AI系统是否已安装
* 通过主进程检查项目中是否安装了@esengine/ai包
*/
export async function checkBehaviorTreeInstalled(projectPath: string): Promise<InstallStatus> {
try {
// 通过Editor.Message请求主进程检查安装状态
const isInstalled = await Editor.Message.request('cocos-ecs-extension', 'check-behavior-tree-installed');
if (isInstalled) {
// 如果已安装,读取版本信息
const packageJsonPath = path.join(projectPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
const aiPackage = dependencies['@esengine/ai'];
return {
installed: true,
version: aiPackage || null,
packageExists: true
};
}
}
return {
installed: false,
version: null,
packageExists: fs.existsSync(path.join(projectPath, 'package.json'))
};
} catch (error) {
return {
installed: false,
version: null,
packageExists: false
};
}
}
/**
* 格式化安装状态文本
*/
export function getInstallStatusText(
isChecking: boolean,
isInstalling: boolean,
isInstalled: boolean,
version: string | null
): string {
if (isChecking) return '检查状态中...';
if (isInstalling) return '正在安装AI系统...';
return isInstalled ? 'AI系统已安装' : 'AI系统未安装';
}
/**
* 获取安装状态CSS类
*/
export function getInstallStatusClass(
isInstalling: boolean,
isInstalled: boolean
): string {
if (isInstalling) return 'installing';
return isInstalled ? 'installed' : 'not-installed';
}
/**
* 安装行为树AI系统
* 通过发送消息到主进程来执行真实的npm安装命令
*/
export async function installBehaviorTreeAI(projectPath: string): Promise<boolean> {
try {
const result = await Editor.Message.request('cocos-ecs-extension', 'install-behavior-tree');
return Boolean(result);
} catch (error) {
console.error('请求安装AI系统失败:', error);
return false;
}
}
/**
* 更新行为树AI系统
* 通过发送消息到主进程来执行真实的npm更新命令
*/
export async function updateBehaviorTreeAI(projectPath: string): Promise<boolean> {
try {
const result = await Editor.Message.request('cocos-ecs-extension', 'update-behavior-tree');
return Boolean(result);
} catch (error) {
console.error('请求更新AI系统失败:', error);
return false;
}
}

View File

@@ -0,0 +1,230 @@
import { TreeNode, ValidationResult } from '../types';
import { NodeTemplate } from '../data/nodeTemplates';
/**
* 生成唯一的节点ID
*/
export function generateNodeId(): string {
return 'node_' + Math.random().toString(36).substr(2, 9);
}
/**
* 根据模板创建节点
*/
export function createNodeFromTemplate(template: NodeTemplate, x: number = 100, y: number = 100): TreeNode {
const nodeId = generateNodeId();
// 深拷贝 properties 以避免引用共享
let properties: any = {};
if (template.properties) {
for (const [key, prop] of Object.entries(template.properties)) {
properties[key] = {
name: prop.name,
type: prop.type,
value: prop.value,
description: prop.description,
options: prop.options ? [...prop.options] : undefined,
required: prop.required
};
}
}
const node: TreeNode = {
id: nodeId,
type: template.type,
name: template.name,
icon: template.icon,
description: template.description,
x: x,
y: y,
children: [],
properties: properties,
canHaveChildren: template.canHaveChildren,
canHaveParent: template.canHaveParent,
maxChildren: template.maxChildren,
minChildren: template.minChildren,
hasError: false
};
return node;
}
/**
* 根据ID查找节点
*/
export function getNodeById(nodes: TreeNode[], id: string): TreeNode | undefined {
return nodes.find(node => node.id === id);
}
/**
* 获取根节点
*/
export function getRootNode(nodes: TreeNode[]): TreeNode | undefined {
return nodes.find(node => !node.parent);
}
/**
* 递归删除节点及其子节点
*/
export function deleteNodeRecursive(
nodes: TreeNode[],
nodeId: string,
connections: any[],
onConnectionsUpdate: (connections: any[]) => void
): TreeNode[] {
const deleteRecursive = (id: string) => {
const node = getNodeById(nodes, id);
if (!node) return;
// 递归删除子节点
node.children.forEach(childId => deleteRecursive(childId));
// 从父节点的children中移除
if (node.parent) {
const parent = getNodeById(nodes, node.parent);
if (parent) {
const index = parent.children.indexOf(id);
if (index > -1) {
parent.children.splice(index, 1);
}
}
}
// 移除连接
const updatedConnections = connections.filter(conn =>
conn.sourceId !== id && conn.targetId !== id
);
onConnectionsUpdate(updatedConnections);
// 从树中移除节点
const nodeIndex = nodes.findIndex(n => n.id === id);
if (nodeIndex > -1) {
nodes.splice(nodeIndex, 1);
}
};
deleteRecursive(nodeId);
return [...nodes]; // 返回新数组以触发响应式更新
}
/**
* 验证行为树结构
*/
export function validateTree(nodes: TreeNode[]): ValidationResult {
if (nodes.length === 0) {
return { isValid: false, message: '行为树为空' };
}
// 检查根节点
const rootNodes = nodes.filter(node => !node.parent);
if (rootNodes.length === 0) {
return { isValid: false, message: '缺少根节点' };
}
if (rootNodes.length > 1) {
return { isValid: false, message: `发现多个根节点: ${rootNodes.map(n => n.name).join(', ')}` };
}
// 验证每个节点的子节点数量限制
for (const node of nodes) {
const childrenCount = node.children.length;
// 检查最小子节点数量
if (node.minChildren !== undefined && childrenCount < node.minChildren) {
return {
isValid: false,
message: `节点 "${node.name}" 需要至少 ${node.minChildren} 个子节点,当前只有 ${childrenCount}`
};
}
// 检查最大子节点数量
if (node.maxChildren !== undefined && childrenCount > node.maxChildren) {
return {
isValid: false,
message: `节点 "${node.name}" 最多只能有 ${node.maxChildren} 个子节点,当前有 ${childrenCount}`
};
}
// 检查装饰器节点的特殊限制
if (node.type.includes('decorator') || node.type.includes('Decorator')) {
if (childrenCount !== 1) {
return {
isValid: false,
message: `装饰器节点 "${node.name}" 必须有且只能有一个子节点,当前有 ${childrenCount}`
};
}
}
// 检查叶子节点不能有子节点
if (!node.canHaveChildren && childrenCount > 0) {
return {
isValid: false,
message: `叶子节点 "${node.name}" 不能有子节点,但当前有 ${childrenCount}`
};
}
}
// 检查循环引用
const visited = new Set<string>();
const recursionStack = new Set<string>();
function hasCycle(nodeId: string): boolean {
if (recursionStack.has(nodeId)) return true;
if (visited.has(nodeId)) return false;
visited.add(nodeId);
recursionStack.add(nodeId);
const node = getNodeById(nodes, nodeId);
if (node) {
for (const childId of node.children) {
if (hasCycle(childId)) return true;
}
}
recursionStack.delete(nodeId);
return false;
}
for (const node of nodes) {
if (hasCycle(node.id)) {
return { isValid: false, message: '检测到循环引用' };
}
}
return { isValid: true, message: '行为树结构有效' };
}
/**
* 更新节点属性
*/
export function updateNodeProperty(node: TreeNode, path: string, value: any): void {
if (!node.properties) return;
const keys = path.split('.');
let target: any = node.properties;
for (let i = 0; i < keys.length - 1; i++) {
target = target[keys[i]];
}
target[keys[keys.length - 1]] = value;
}
/**
* 计算节点的边界框
*/
export function getNodesBounds(nodes: TreeNode[]): { minX: number; minY: number; maxX: number; maxY: number } {
if (nodes.length === 0) {
return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
}
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
nodes.forEach(node => {
minX = Math.min(minX, node.x);
minY = Math.min(minY, node.y);
maxX = Math.max(maxX, node.x + 150); // 节点宽度
maxY = Math.max(maxY, node.y + 100); // 节点高度
});
return { minX, minY, maxX, maxY };
}

View File

@@ -0,0 +1,717 @@
import { readFileSync } from 'fs-extra';
import { join } from 'path';
import { createApp, App, defineComponent, ref, reactive, onMounted, onUnmounted } from 'vue';
import { WebSocketServer, WebSocket } from 'ws';
import { IncomingMessage } from 'http';
const panelDataMap = new WeakMap<any, App>();
/**
* 游戏实例信息
*/
interface GameInstance {
id: string;
name: string;
connectTime: number;
lastUpdateTime: number;
isActive: boolean;
debugData?: any;
ws?: WebSocket; // WebSocket连接
}
/**
* 详细的调试信息接口
*/
interface DetailedDebugInfo {
// 基础信息
instanceId: string;
instanceName: string;
isRunning: boolean;
frameworkLoaded: boolean;
currentScene: string;
uptime: number;
// 性能信息
performance: {
frameTime: number;
fps: number;
averageFrameTime: number;
minFrameTime: number;
maxFrameTime: number;
frameTimeHistory: number[];
engineFrameTime: number;
ecsPercentage: number;
};
// 内存信息
memory: {
totalMemory: number;
usedMemory: number;
freeMemory: number;
entityMemory: number;
componentMemory: number;
systemMemory: number;
pooledMemory: number;
gcCollections: number;
};
// 实体信息
entities: {
total: number;
active: number;
inactive: number;
pendingAdd: number;
pendingRemove: number;
entitiesPerArchetype: Array<{
signature: string;
count: number;
memory: number;
}>;
topEntitiesByComponents: Array<{
id: string;
name: string;
componentCount: number;
memory: number;
}>;
};
// 组件信息
components: {
totalTypes: number;
totalInstances: number;
componentStats: Array<{
typeName: string;
instanceCount: number;
memoryPerInstance: number;
totalMemory: number;
poolSize: number;
poolUtilization: number;
}>;
};
// 系统信息
systems: {
total: number;
systemStats: Array<{
name: string;
type: string;
entityCount: number;
averageExecutionTime: number;
minExecutionTime: number;
maxExecutionTime: number;
executionTimeHistory: number[];
memoryUsage: number;
updateOrder: number;
enabled: boolean;
percentage: number;
}>;
};
// 场景信息
scenes: {
currentScene: string;
sceneMemory: number;
sceneEntityCount: number;
sceneSystemCount: number;
sceneUptime: number;
};
}
/**
* ECS调试服务器
* 作为服务端,接收多个游戏实例的连接
*/
class ECSDebugServer {
private wss?: WebSocketServer;
private port: number = 8080;
private gameInstances = new Map<string, GameInstance>();
private isRunning: boolean = false;
constructor(port: number = 8080) {
this.port = port;
}
async start(): Promise<boolean> {
if (this.isRunning) return true;
try {
this.wss = new WebSocketServer({ port: this.port });
this.wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {
const instanceId = this.generateInstanceId();
const instance: GameInstance = {
id: instanceId,
name: `游戏实例-${instanceId.substring(0, 8)}`,
connectTime: Date.now(),
lastUpdateTime: Date.now(),
isActive: true,
debugData: null,
ws: ws
};
this.gameInstances.set(instanceId, instance);
console.log(`[ECS Debug Server] New instance connected: ${instance.name}`);
ws.on('message', (data: Buffer) => {
try {
const message = JSON.parse(data.toString());
this.handleMessage(instanceId, message);
} catch (error) {
console.error('[ECS Debug Server] Failed to parse message:', error);
}
});
ws.on('close', () => {
const instance = this.gameInstances.get(instanceId);
if (instance) {
instance.isActive = false;
console.log(`[ECS Debug Server] Instance disconnected: ${instance.name}`);
}
});
ws.on('error', (error: Error) => {
console.error(`[ECS Debug Server] WebSocket error for ${instanceId}:`, error);
});
// 发送连接确认
this.sendToInstance(instanceId, {
type: 'connection_confirmed',
instanceId: instanceId,
serverTime: Date.now()
});
});
this.isRunning = true;
console.log(`[ECS Debug Server] Started on port ${this.port}`);
return true;
} catch (error) {
console.error('[ECS Debug Server] Failed to start:', error);
return false;
}
}
stop(): void {
if (this.wss) {
this.wss.close();
this.wss = undefined;
}
this.gameInstances.clear();
this.isRunning = false;
console.log('[ECS Debug Server] Stopped');
}
private generateInstanceId(): string {
return Math.random().toString(36).substring(2) + Date.now().toString(36);
}
private handleMessage(instanceId: string, message: any): void {
const instance = this.gameInstances.get(instanceId);
if (!instance) return;
switch (message.type) {
case 'debug_data':
instance.debugData = message.data;
instance.lastUpdateTime = Date.now();
break;
case 'instance_info':
if (message.name) {
instance.name = message.name;
}
break;
case 'ping':
this.sendToInstance(instanceId, { type: 'pong', timestamp: Date.now() });
break;
}
}
private sendToInstance(instanceId: string, message: any): void {
const instance = this.gameInstances.get(instanceId);
if (instance && instance.ws && instance.ws.readyState === 1) {
instance.ws.send(JSON.stringify(message));
}
}
get running(): boolean {
return this.isRunning;
}
get instances(): GameInstance[] {
return Array.from(this.gameInstances.values());
}
getInstance(instanceId: string): GameInstance | undefined {
return this.gameInstances.get(instanceId);
}
getInstanceDebugData(instanceId: string): DetailedDebugInfo | null {
const instance = this.gameInstances.get(instanceId);
if (!instance || !instance.debugData) {
return null;
}
return this.transformToDetailedDebugInfo(instance, instance.debugData);
}
private transformToDetailedDebugInfo(instance: GameInstance, rawData: any): DetailedDebugInfo {
const uptime = (Date.now() - instance.connectTime) / 1000;
// 计算系统性能数据包括ECS占比
const systemBreakdown = rawData.performance?.systemBreakdown || [];
const systemPerformance = rawData.performance?.systemPerformance || [];
// 创建系统名称到占比的映射
const systemPercentageMap = new Map<string, number>();
systemBreakdown.forEach((sys: any) => {
systemPercentageMap.set(sys.systemName, sys.percentage || 0);
});
return {
instanceId: instance.id,
instanceName: instance.name,
isRunning: rawData.isRunning || false,
frameworkLoaded: rawData.frameworkLoaded || false,
currentScene: rawData.currentScene || '未知',
uptime: uptime,
performance: {
frameTime: rawData.performance?.frameTime || 0,
fps: rawData.performance?.fps || 0,
averageFrameTime: rawData.performance?.averageFrameTime || rawData.performance?.frameTime || 0,
minFrameTime: rawData.performance?.minFrameTime || rawData.performance?.frameTime || 0,
maxFrameTime: rawData.performance?.maxFrameTime || rawData.performance?.frameTime || 0,
frameTimeHistory: rawData.performance?.frameTimeHistory || [],
engineFrameTime: rawData.performance?.engineFrameTime || 0,
ecsPercentage: rawData.performance?.ecsPercentage || 0
},
memory: {
totalMemory: rawData.performance?.memoryDetails?.totalMemory || rawData.memory?.totalMemory || 512 * 1024 * 1024,
usedMemory: rawData.performance?.memoryDetails?.usedMemory || (rawData.performance?.memoryUsage ? rawData.performance.memoryUsage * 1024 * 1024 : 0),
freeMemory: rawData.performance?.memoryDetails?.freeMemory || 0,
entityMemory: rawData.performance?.memoryDetails?.entities || rawData.memory?.entityMemory || 0,
componentMemory: rawData.performance?.memoryDetails?.components || rawData.memory?.componentMemory || 0,
systemMemory: rawData.performance?.memoryDetails?.systems || rawData.memory?.systemMemory || 0,
pooledMemory: rawData.performance?.memoryDetails?.pooled || rawData.memory?.pooledMemory || 0,
gcCollections: rawData.performance?.memoryDetails?.gcCollections || rawData.memory?.gcCollections || 0
},
entities: {
total: rawData.entities?.totalEntities || 0,
active: rawData.entities?.activeEntities || 0,
inactive: (rawData.entities?.totalEntities || 0) - (rawData.entities?.activeEntities || 0),
pendingAdd: rawData.entities?.pendingAdd || 0,
pendingRemove: rawData.entities?.pendingRemove || 0,
entitiesPerArchetype: rawData.entities?.entitiesPerArchetype || [],
topEntitiesByComponents: rawData.entities?.topEntitiesByComponents || []
},
components: {
totalTypes: rawData.components?.componentTypes || 0,
totalInstances: rawData.components?.componentInstances || 0,
componentStats: (rawData.components?.componentStats || []).map((comp: any) => ({
typeName: comp.typeName,
instanceCount: comp.instanceCount || 0,
memoryPerInstance: comp.memoryPerInstance || 0,
totalMemory: comp.totalMemory || (comp.instanceCount || 0) * (comp.memoryPerInstance || 0),
poolSize: comp.poolSize || 0,
poolUtilization: comp.poolSize > 0 ? (comp.instanceCount / comp.poolSize * 100) : 0
}))
},
systems: {
total: rawData.systems?.totalSystems || 0,
systemStats: (rawData.systems?.systemsInfo || []).map((sys: any) => {
const systemName = sys.name;
const percentage = systemPercentageMap.get(systemName) || 0;
return {
name: systemName,
type: sys.type || 'Unknown',
entityCount: sys.entityCount || 0,
averageExecutionTime: sys.executionTime || 0,
minExecutionTime: sys.minExecutionTime || sys.executionTime || 0,
maxExecutionTime: sys.maxExecutionTime || sys.executionTime || 0,
executionTimeHistory: sys.executionTimeHistory || [],
memoryUsage: sys.memoryUsage || 0,
updateOrder: sys.updateOrder || 0,
enabled: sys.enabled !== false,
percentage: percentage
};
})
},
scenes: {
currentScene: rawData.currentScene || '未知',
sceneMemory: rawData.scenes?.sceneMemory || 0,
sceneEntityCount: rawData.entities?.totalEntities || 0,
sceneSystemCount: rawData.systems?.totalSystems || 0,
sceneUptime: rawData.scenes?.sceneUptime || uptime
}
};
}
}
/**
* 默认调试信息
*/
const defaultDebugInfo: DetailedDebugInfo = {
instanceId: '',
instanceName: '未选择实例',
isRunning: false,
frameworkLoaded: false,
currentScene: '未知',
uptime: 0,
performance: {
frameTime: 0,
fps: 0,
averageFrameTime: 0,
minFrameTime: 0,
maxFrameTime: 0,
frameTimeHistory: [],
engineFrameTime: 0,
ecsPercentage: 0
},
memory: {
totalMemory: 0,
usedMemory: 0,
freeMemory: 0,
entityMemory: 0,
componentMemory: 0,
systemMemory: 0,
pooledMemory: 0,
gcCollections: 0
},
entities: {
total: 0,
active: 0,
inactive: 0,
pendingAdd: 0,
pendingRemove: 0,
entitiesPerArchetype: [],
topEntitiesByComponents: []
},
components: {
totalTypes: 0,
totalInstances: 0,
componentStats: []
},
systems: {
total: 0,
systemStats: []
},
scenes: {
currentScene: '未知',
sceneMemory: 0,
sceneEntityCount: 0,
sceneSystemCount: 0,
sceneUptime: 0
}
};
// 全局调试服务器实例
let globalDebugServer: ECSDebugServer | null = null;
/**
* 启动调试服务器
*/
async function ensureDebugServer(): Promise<ECSDebugServer> {
if (!globalDebugServer) {
globalDebugServer = new ECSDebugServer(8080);
}
if (!globalDebugServer.running) {
await globalDebugServer.start();
}
return globalDebugServer;
}
module.exports = Editor.Panel.define({
listeners: {
show() { },
hide() { },
},
template: `<div id="app"></div>`,
style: readFileSync(join(__dirname, '../../../static/style/debug/index.css'), 'utf-8'),
$: {
app: '#app',
},
ready() {
if (this.$.app) {
const app = createApp(defineComponent({
setup() {
const debugInfo = reactive<DetailedDebugInfo>({ ...defaultDebugInfo });
const gameInstances = ref<GameInstance[]>([]);
const selectedInstanceId = ref<string>('');
const isAutoRefresh = ref(true);
const refreshInterval = ref(100);
const lastUpdateTime = ref('');
const showComponentPoolHelp = ref(false);
let intervalId: NodeJS.Timeout | null = null;
let debugServer: ECSDebugServer | null = null;
// 初始化调试服务器
const initializeServer = async () => {
try {
debugServer = await ensureDebugServer();
} catch (error) {
console.error('Failed to start debug server:', error);
}
};
// 更新游戏实例列表和调试信息
const updateDebugInfo = async () => {
if (!debugServer) return;
try {
// 更新实例列表
gameInstances.value = debugServer.instances;
// 如果有选中的实例,更新其调试信息
if (selectedInstanceId.value) {
const detailedInfo = debugServer.getInstanceDebugData(selectedInstanceId.value);
if (detailedInfo) {
Object.assign(debugInfo, detailedInfo);
} else {
// 实例已断开,重置选择
selectedInstanceId.value = '';
Object.assign(debugInfo, defaultDebugInfo);
}
}
lastUpdateTime.value = new Date().toLocaleTimeString();
} catch (error) {
console.error('Failed to update debug info:', error);
}
};
// 开始自动刷新
const startAutoRefresh = () => {
if (intervalId) clearInterval(intervalId);
if (isAutoRefresh.value) {
intervalId = setInterval(updateDebugInfo, refreshInterval.value);
}
};
// 停止自动刷新
const stopAutoRefresh = () => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
};
// 手动刷新
const manualRefresh = () => {
updateDebugInfo();
};
// 切换自动刷新
const toggleAutoRefresh = () => {
if (isAutoRefresh.value) {
startAutoRefresh();
} else {
stopAutoRefresh();
}
};
// 更改刷新间隔
const changeRefreshInterval = () => {
if (isAutoRefresh.value) {
startAutoRefresh();
}
};
// 实例选择改变
const onInstanceChanged = () => {
if (selectedInstanceId.value) {
updateDebugInfo();
} else {
Object.assign(debugInfo, defaultDebugInfo);
}
};
// 格式化运行时间
const formatUptime = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
// 格式化内存大小
const formatMemory = (bytes: number): string => {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB';
};
// 获取FPS颜色
const getFpsColor = (fps: number): string => {
if (fps >= 55) return 'good';
if (fps >= 30) return 'warning';
return 'critical';
};
// 获取内存颜色
const getMemoryColor = (percentage: number): string => {
if (percentage < 70) return 'good';
if (percentage < 85) return 'warning';
return 'critical';
};
// 获取ECS时间占比颜色
const getECSTimeColor = (percentage: number): string => {
if (!percentage) return 'good';
if (percentage <= 10) return 'good'; // ECS占用<=10%为绿色
if (percentage <= 30) return 'warning'; // ECS占用<=30%为黄色
return 'critical'; // ECS占用>30%为红色
};
// 获取执行时间颜色
const getExecutionTimeColor = (time: number): string => {
if (time < 1) return 'good'; // <1ms为绿色
if (time < 5) return 'warning'; // <5ms为黄色
return 'critical';
};
// 打开文档链接
const openDocumentation = (section: string): void => {
const urls: Record<string, string> = {
'component-pool': 'https://github.com/esengine/ecs-framework/tree/master/docs/component-design-guide.md#1-对象池优化',
'performance-optimization': 'https://github.com/esengine/ecs-framework/tree/master/docs/performance-optimization.md'
};
const url = urls[section];
if (!url) return;
try {
// 在Cocos Creator扩展环境中直接使用Electron的shell模块
const { shell } = require('electron');
shell.openExternal(url);
} catch (error) {
console.error('无法打开链接:', error);
// 如果失败,复制到剪贴板
copyUrlToClipboard(url);
}
};
// 复制链接到剪贴板的辅助函数
const copyUrlToClipboard = (url: string): void => {
try {
// 尝试使用现代的剪贴板API
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).then(() => {
console.log(`文档链接已复制到剪贴板: ${url}`);
// 如果可能的话,显示用户友好的提示
if (typeof alert !== 'undefined') {
alert(`文档链接已复制到剪贴板,请在浏览器中粘贴访问:\n${url}`);
}
}).catch(() => {
fallbackCopyText(url);
});
} else {
fallbackCopyText(url);
}
} catch (error) {
fallbackCopyText(url);
}
};
// 备用的复制文本方法
const fallbackCopyText = (text: string): void => {
try {
// 创建临时的文本区域
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
console.log(`文档链接已复制到剪贴板: ${text}`);
if (typeof alert !== 'undefined') {
alert(`文档链接已复制到剪贴板,请在浏览器中粘贴访问:\n${text}`);
}
} else {
console.log(`请手动复制文档链接: ${text}`);
if (typeof alert !== 'undefined') {
alert(`请手动复制文档链接:\n${text}`);
}
}
} catch (error) {
console.log(`请手动访问文档: ${text}`);
if (typeof alert !== 'undefined') {
alert(`请手动访问文档:\n${text}`);
}
}
};
// 组件挂载时初始化
onMounted(async () => {
await initializeServer();
updateDebugInfo();
startAutoRefresh();
});
// 组件卸载时清理
onUnmounted(() => {
stopAutoRefresh();
});
return {
debugInfo,
gameInstances,
selectedInstanceId,
isAutoRefresh,
refreshInterval,
lastUpdateTime,
showComponentPoolHelp,
manualRefresh,
toggleAutoRefresh,
changeRefreshInterval,
onInstanceChanged,
formatUptime,
formatMemory,
getFpsColor,
getMemoryColor,
getECSTimeColor,
getExecutionTimeColor,
openDocumentation
};
},
template: readFileSync(join(__dirname, '../../../static/template/debug/index.html'), 'utf-8'),
}));
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('ui-');
app.mount(this.$.app);
panelDataMap.set(this, app);
}
},
beforeClose() { },
close() {
const app = panelDataMap.get(this);
if (app) {
app.unmount();
panelDataMap.delete(this);
}
// 关闭调试服务器
if (globalDebugServer) {
globalDebugServer.stop();
globalDebugServer = null;
}
},
});

View File

@@ -0,0 +1,528 @@
/* eslint-disable vue/one-component-per-file */
import { readFileSync } from 'fs-extra';
import { join } from 'path';
import { createApp, App, defineComponent, ref, onMounted } from 'vue';
import * as fs from 'fs';
import * as path from 'path';
import { exec } from 'child_process';
const panelDataMap = new WeakMap<any, App>();
/**
* 检测ECS框架安装状态的工具函数
*/
async function checkEcsFrameworkStatus(projectPath: string) {
const packageJsonPath = path.join(projectPath, 'package.json');
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
try {
// 检查package.json是否存在
const packageJsonExists = fs.existsSync(packageJsonPath);
if (!packageJsonExists) {
return {
packageJsonExists: false,
ecsInstalled: false,
ecsVersion: null
};
}
// 读取package.json
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
// 检查依赖中是否包含ECS框架
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
const ecsInDeps = dependencies['@esengine/ecs-framework'];
// 检查node_modules中是否实际安装了ECS框架
const ecsActuallyInstalled = fs.existsSync(nodeModulesPath);
let ecsVersion = null;
if (ecsActuallyInstalled) {
try {
const ecsPackageJson = JSON.parse(
fs.readFileSync(path.join(nodeModulesPath, 'package.json'), 'utf-8')
);
ecsVersion = ecsPackageJson.version;
} catch (e) {
console.warn('Unable to read ECS framework version:', e);
}
}
return {
packageJsonExists: true,
ecsInstalled: ecsActuallyInstalled && !!ecsInDeps,
ecsVersion,
declaredVersion: ecsInDeps
};
} catch (error) {
console.error('Error checking ECS framework status:', error);
return {
packageJsonExists: false,
ecsInstalled: false,
ecsVersion: null
};
}
}
/**
* 检测ECS模板状态
*/
function checkEcsTemplateStatus(projectPath: string) {
const ecsDir = path.join(projectPath, 'assets', 'scripts', 'ecs');
try {
if (!fs.existsSync(ecsDir)) {
return {
templateExists: false,
existingFiles: []
};
}
// 扫描ECS目录中的文件
const existingFiles: string[] = [];
function scanDirectory(dirPath: string, relativePath: string = '') {
if (!fs.existsSync(dirPath)) return;
const items = fs.readdirSync(dirPath);
for (const item of items) {
const fullPath = path.join(dirPath, item);
const relativeFilePath = relativePath ? `${relativePath}/${item}` : item;
if (fs.statSync(fullPath).isDirectory()) {
scanDirectory(fullPath, relativeFilePath);
} else {
existingFiles.push(relativeFilePath);
}
}
}
scanDirectory(ecsDir);
return {
templateExists: existingFiles.length > 0,
existingFiles
};
} catch (error) {
console.error('Error checking ECS template status:', error);
return {
templateExists: false,
existingFiles: []
};
}
}
/**
* 获取ECS框架的最新版本
*/
function getLatestEcsVersion(): Promise<string | null> {
return new Promise((resolve) => {
exec('npm view @esengine/ecs-framework version', (error, stdout) => {
if (error) {
console.warn('Failed to get latest version:', error);
resolve(null);
} else {
resolve(stdout.trim());
}
});
});
}
/**
* 获取Node.js版本
*/
function getNodeVersion(): Promise<string> {
return new Promise((resolve) => {
exec('node --version', (error, stdout) => {
if (error) {
resolve('未知');
} else {
resolve(stdout.trim().replace('v', ''));
}
});
});
}
/**
* 比较版本号
*/
function compareVersions(current: string, latest: string): boolean {
try {
const currentParts = current.split('.').map(Number);
const latestParts = latest.split('.').map(Number);
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
const currentPart = currentParts[i] || 0;
const latestPart = latestParts[i] || 0;
if (latestPart > currentPart) {
return true; // 有更新
} else if (latestPart < currentPart) {
return false; // 当前版本更新
}
}
return false; // 版本相同
} catch (error) {
console.warn('Version comparison failed:', error);
return false;
}
}
/**
* @zh 如果希望兼容 3.3 之前的版本可以使用下方的代码
* @en You can add the code below if you want compatibility with versions prior to 3.3
*/
// Editor.Panel.define = Editor.Panel.define || function(options: any) { return options }
module.exports = Editor.Panel.define({
listeners: {
show() { console.log('ECS Welcome Panel Show'); },
hide() { console.log('ECS Welcome Panel Hide'); },
},
template: readFileSync(join(__dirname, '../../../static/template/default/index.html'), 'utf-8'),
style: readFileSync(join(__dirname, '../../../static/style/default/index.css'), 'utf-8'),
$: {
app: '#app',
},
methods: {
/**
* 向主进程发送消息的方法
*/
sendToMain(message: string, ...args: any[]) {
Editor.Message.send('cocos-ecs-extension', message, ...args);
}
},
ready() {
if (this.$.app) {
const app = createApp({});
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('ui-');
// ECS欢迎组件
app.component('EcsWelcome', defineComponent({
setup() {
// 响应式状态
const checkingStatus = ref(true);
const ecsInstalled = ref(false);
const ecsVersion = ref<string | null>(null);
const latestVersion = ref<string | null>(null);
const hasUpdate = ref(false);
const packageJsonExists = ref(false);
const nodeVersion = ref('检测中...');
const pluginVersion = ref('1.0.0');
const lastCheckTime = ref<string | null>(null);
// ECS模板状态
const templateExists = ref(false);
const existingFiles = ref<string[]>([]);
// 操作状态
const installing = ref(false);
const updating = ref(false);
const uninstalling = ref(false);
// 操作状态显示
const showOperationStatus = ref(false);
const operationStatusType = ref('loading');
const operationStatusMessage = ref('');
const operationStatusDetails = ref('');
// 显示操作状态
const setOperationStatus = (type: string, message: string, details?: string) => {
showOperationStatus.value = true;
operationStatusType.value = type;
operationStatusMessage.value = message;
operationStatusDetails.value = details || '';
// 自动隐藏成功和警告消息
if (type === 'success' || type === 'warning') {
setTimeout(() => {
showOperationStatus.value = false;
}, 5000);
}
};
// 获取状态图标
const getStatusIcon = (type: string) => {
switch (type) {
case 'loading': return '⏳';
case 'success': return '✅';
case 'error':
case 'failed': return '❌';
case 'warning': return '⚠️';
default: return '';
}
};
// 监听来自主进程的消息 - 暂时注释掉,使用定时刷新
const setupMessageListeners = () => {
// TODO: 使用正确的消息监听API
console.log('Message listeners setup - using polling instead');
};
// 定时检查状态(用于检测操作完成)
let statusCheckInterval: any = null;
const startStatusPolling = () => {
if (statusCheckInterval) clearInterval(statusCheckInterval);
statusCheckInterval = setInterval(() => {
if (installing.value || updating.value || uninstalling.value) {
checkStatus();
}
}, 3000); // 每3秒检查一次
};
const stopStatusPolling = () => {
if (statusCheckInterval) {
clearInterval(statusCheckInterval);
statusCheckInterval = null;
}
};
// 检测状态的方法
const checkStatus = async () => {
checkingStatus.value = true;
try {
// 获取当前项目路径
const projectPath = Editor.Project.path;
// 检测ECS框架状态
const status = await checkEcsFrameworkStatus(projectPath);
const prevInstalled = ecsInstalled.value;
const prevVersion = ecsVersion.value;
packageJsonExists.value = status.packageJsonExists;
ecsInstalled.value = status.ecsInstalled;
ecsVersion.value = status.ecsVersion;
// 检测ECS模板状态
const templateStatus = checkEcsTemplateStatus(projectPath);
templateExists.value = templateStatus.templateExists;
existingFiles.value = templateStatus.existingFiles;
// 检测操作完成
if (installing.value) {
if (status.ecsInstalled && !prevInstalled) {
installing.value = false;
setOperationStatus('success', 'ECS Framework 安装成功!');
stopStatusPolling();
} else if (!status.ecsInstalled) {
// 可能还在安装中,继续等待
}
}
if (updating.value) {
if (status.ecsVersion && status.ecsVersion !== prevVersion) {
updating.value = false;
setOperationStatus('success', `ECS Framework 更新成功到 v${status.ecsVersion}`);
stopStatusPolling();
}
}
if (uninstalling.value) {
if (!status.ecsInstalled && prevInstalled) {
uninstalling.value = false;
setOperationStatus('success', 'ECS Framework 卸载成功!');
stopStatusPolling();
}
}
// 获取Node.js版本
nodeVersion.value = await getNodeVersion();
// 检查更新
if (ecsInstalled.value && ecsVersion.value) {
await checkForUpdates();
}
// 更新检查时间
lastCheckTime.value = new Date().toLocaleString();
} catch (error) {
console.error('Status check failed:', error);
// 如果检查失败,停止操作状态
if (installing.value || updating.value || uninstalling.value) {
installing.value = false;
updating.value = false;
uninstalling.value = false;
setOperationStatus('error', '状态检查失败,请手动验证操作结果');
stopStatusPolling();
}
} finally {
checkingStatus.value = false;
}
};
// 检查更新
const checkForUpdates = async () => {
if (!ecsInstalled.value || !ecsVersion.value) {
setOperationStatus('warning', '请先安装 ECS Framework');
return;
}
try {
setOperationStatus('loading', '正在检查更新...');
const latest = await getLatestEcsVersion();
if (latest) {
latestVersion.value = latest;
const needsUpdate = compareVersions(ecsVersion.value, latest);
hasUpdate.value = needsUpdate;
if (needsUpdate) {
setOperationStatus('success', `发现新版本v${latest}当前v${ecsVersion.value}`);
} else {
setOperationStatus('success', `已是最新版本v${ecsVersion.value}`);
}
} else {
setOperationStatus('warning', '无法获取最新版本信息,请检查网络连接');
}
// 更新检查时间
lastCheckTime.value = new Date().toLocaleString();
} catch (error) {
console.warn('Failed to check updates:', error);
setOperationStatus('error', '检查更新失败,请检查网络连接');
}
};
// 操作方法
const installEcsFramework = () => {
if (!packageJsonExists.value || installing.value) return;
Editor.Dialog.info('安装 ECS Framework', {
detail: '即将安装@esengine/ecs-framework到当前项目...',
buttons: ['确定', '取消'],
default: 0,
}).then((result) => {
if (result.response === 0) {
installing.value = true;
setOperationStatus('loading', '正在安装 ECS Framework...');
startStatusPolling();
// 发送安装命令到主进程
Editor.Message.send('cocos-ecs-extension', 'install-ecs-framework');
}
});
};
const updateEcsFramework = () => {
if (!hasUpdate.value || updating.value) return;
Editor.Dialog.info('更新 ECS Framework', {
detail: `即将更新ECS框架从 v${ecsVersion.value} 到 v${latestVersion.value}`,
buttons: ['确定', '取消'],
default: 0,
}).then((result) => {
if (result.response === 0) {
updating.value = true;
setOperationStatus('loading', `正在更新 ECS Framework 到 v${latestVersion.value}...`);
startStatusPolling();
Editor.Message.send('cocos-ecs-extension', 'update-ecs-framework', latestVersion.value);
}
});
};
const uninstallEcsFramework = () => {
if (uninstalling.value) return;
Editor.Dialog.warn('卸载 ECS Framework', {
detail: '确定要卸载ECS框架吗这将删除项目中的ECS框架依赖。',
buttons: ['确定卸载', '取消'],
default: 1,
}).then((result) => {
if (result.response === 0) {
uninstalling.value = true;
setOperationStatus('loading', '正在卸载 ECS Framework...');
startStatusPolling();
Editor.Message.send('cocos-ecs-extension', 'uninstall-ecs-framework');
}
});
};
const openDocumentation = () => {
if (!ecsInstalled.value) return;
Editor.Message.send('cocos-ecs-extension', 'open-documentation');
};
const createEcsTemplate = () => {
if (!ecsInstalled.value || templateExists.value) return;
Editor.Dialog.info('创建 ECS 模板', {
detail: '即将创建基础的ECS项目结构和启动代码...',
buttons: ['确定', '取消'],
default: 0,
}).then((result) => {
if (result.response === 0) {
Editor.Message.send('cocos-ecs-extension', 'create-ecs-template');
}
});
};
const openGithub = () => {
Editor.Message.send('cocos-ecs-extension', 'open-github');
};
const joinQQGroup = () => {
Editor.Message.send('cocos-ecs-extension', 'open-qq-group');
};
const openGenerator = () => {
Editor.Message.send('cocos-ecs-extension', 'open-generator');
};
// 组件挂载后检测状态
onMounted(() => {
setupMessageListeners();
checkStatus();
});
return {
checkingStatus,
ecsInstalled,
ecsVersion,
latestVersion,
hasUpdate,
packageJsonExists,
nodeVersion,
pluginVersion,
lastCheckTime,
templateExists,
existingFiles,
installing,
updating,
uninstalling,
showOperationStatus,
operationStatusType,
operationStatusMessage,
operationStatusDetails,
getStatusIcon,
installEcsFramework,
updateEcsFramework,
uninstallEcsFramework,
checkForUpdates,
openDocumentation,
createEcsTemplate,
openGithub,
joinQQGroup,
openGenerator
};
},
template: readFileSync(join(__dirname, '../../../static/template/vue/welcome.html'), 'utf-8'),
}));
app.mount(this.$.app);
panelDataMap.set(this, app);
}
},
beforeClose() { },
close() {
const app = panelDataMap.get(this);
if (app) {
app.unmount();
}
},
});

View File

@@ -0,0 +1,240 @@
import { readFileSync } from 'fs-extra';
import { join } from 'path';
import * as path from 'path';
import { createApp, App, defineComponent, ref, reactive } from 'vue';
import { CodeGenerator } from '../../CodeGenerator';
const panelDataMap = new WeakMap<any, App>();
module.exports = Editor.Panel.define({
listeners: {
show() { },
hide() { },
},
template: `<div id="app"></div>`,
style: readFileSync(join(__dirname, '../../../static/style/generator/index.css'), 'utf-8'),
$: {
app: '#app',
},
ready() {
if (this.$.app) {
const app = createApp(defineComponent({
setup() {
const featureName = ref('');
const options = reactive({
generateComponent: true,
generateSystem: false
});
// 组件选项
const componentOptions = reactive({
includeComments: true,
addProperties: []
});
// 系统选项
const systemOptions = reactive({
systemType: 'EntitySystem' as 'EntitySystem' | 'ProcessingSystem' | 'IntervalSystem' | 'PassiveSystem',
includeComments: true,
requiredComponents: [],
filterByComponent: true
});
// 系统类型定义
const systemTypes = [
{
value: 'EntitySystem',
name: 'EntitySystem',
icon: '🔄',
description: '批量处理实体,适合需要遍历多个实体的逻辑',
usage: '适用场景:移动系统、渲染系统、物理碰撞系统'
},
{
value: 'ProcessingSystem',
name: 'ProcessingSystem',
icon: '⚡',
description: '执行全局逻辑,不依赖特定实体',
usage: '适用场景:输入处理、音效管理、场景切换'
},
{
value: 'IntervalSystem',
name: 'IntervalSystem',
icon: '⏰',
description: '按时间间隔执行,可控制执行频率',
usage: '适用场景AI决策、状态保存、定时清理'
},
{
value: 'PassiveSystem',
name: 'PassiveSystem',
icon: '🎯',
description: '被动响应,需要手动调用或事件触发',
usage: '适用场景:技能释放、道具使用、特殊效果'
}
];
const isGenerating = ref(false);
const previewCode = ref('');
const showPreview = ref(false);
// 选择系统类型
const selectSystemType = (type: string) => {
systemOptions.systemType = type as any;
updatePreview();
};
// 生成代码
const generateCode = async () => {
if (!featureName.value.trim()) {
Editor.Dialog.warn('请输入功能名称', {
detail: '请先输入一个有效的功能名称例如Health、Movement、Combat等'
});
return;
}
if (!options.generateComponent && !options.generateSystem) {
Editor.Dialog.warn('请选择生成内容', {
detail: '请至少选择一种要生成的代码类型(组件或系统)'
});
return;
}
isGenerating.value = true;
try {
const projectPath = Editor.Project.path;
const ecsDir = path.join(projectPath, 'assets', 'scripts', 'ecs');
// 检查ECS目录是否存在
const fs = require('fs');
if (!fs.existsSync(ecsDir)) {
Editor.Dialog.warn('ECS目录不存在', {
detail: '请先创建ECS模板后再生成代码。\n\n您可以在欢迎面板中点击"创建ECS模板"来创建基础结构。',
});
return;
}
const codeGenerator = new CodeGenerator();
const generatedFiles: string[] = [];
const baseName = featureName.value.trim();
// 生成组件
if (options.generateComponent) {
const componentDir = path.join(ecsDir, 'components');
await codeGenerator.generateComponent(baseName, componentDir, componentOptions);
generatedFiles.push(`📦 组件: ${baseName}Component.ts`);
}
// 生成系统
if (options.generateSystem) {
const systemDir = path.join(ecsDir, 'systems');
// 如果选择了组件过滤且生成了组件,自动添加组件过滤
const requiredComponents = (systemOptions.filterByComponent && options.generateComponent) ?
[`${baseName}Component`] : [];
const systemOpts = {
...systemOptions,
requiredComponents
};
await codeGenerator.generateSystem(
baseName,
systemDir,
systemOpts
);
generatedFiles.push(`⚙️ 系统: ${baseName}System.ts`);
}
// 成功提示
Editor.Dialog.info('代码生成成功', {
detail: `${baseName} 功能代码已生成完成!\n\n生成的文件\n${generatedFiles.join('\n')}\n\n请刷新资源管理器查看新创建的文件。`
});
// 清空输入
featureName.value = '';
} catch (error) {
console.error('Failed to generate code:', error);
Editor.Dialog.error('代码生成失败', {
detail: `生成代码时发生错误:\n\n${error}`
});
} finally {
isGenerating.value = false;
}
};
// 预览代码
const previewGeneration = () => {
if (!featureName.value.trim()) {
showPreview.value = false;
return;
}
const baseName = featureName.value.trim();
let preview = `将要生成的文件:\n\n`;
if (options.generateComponent) {
preview += `📦 组件: ${baseName}Component.ts\n`;
preview += ` - 位置: assets/scripts/ecs/components/\n`;
preview += ` - 基础组件模板\n\n`;
}
if (options.generateSystem) {
const selectedType = systemTypes.find(t => t.value === systemOptions.systemType);
preview += `⚙️ 系统: ${baseName}System.ts\n`;
preview += ` - 位置: assets/scripts/ecs/systems/\n`;
preview += ` - 类型: ${selectedType?.name || systemOptions.systemType}\n`;
if (systemOptions.filterByComponent && options.generateComponent) {
preview += ` - 过滤组件: ${baseName}Component\n`;
} else if (systemOptions.filterByComponent) {
preview += ` - 组件过滤: 需要手动配置\n`;
} else {
preview += ` - 组件过滤: 无\n`;
}
preview += `\n`;
}
previewCode.value = preview;
showPreview.value = true;
};
// 监听功能名称变化
const updatePreview = () => {
if (featureName.value.trim()) {
previewGeneration();
} else {
showPreview.value = false;
}
};
return {
featureName,
options,
componentOptions,
systemOptions,
systemTypes,
isGenerating,
previewCode,
showPreview,
generateCode,
updatePreview,
selectSystemType
};
},
template: readFileSync(join(__dirname, '../../../static/template/generator/index.html'), 'utf-8')
}));
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('ui-');
app.mount(this.$.app);
panelDataMap.set(this, app);
}
},
beforeClose() { },
close() {
const app = panelDataMap.get(this);
if (app) {
app.unmount();
panelDataMap.delete(this);
}
},
});

View File

@@ -0,0 +1,187 @@
import { join } from 'path';
// 添加编辑器内的模块搜索路径
module.paths.push(join(Editor.App.path, 'node_modules'));
export function load() {
console.log('ECS Debug Scene Script loaded');
}
export function unload() {
console.log('ECS Debug Scene Script unloaded');
}
export const methods = {
/**
* 获取预览状态
* @returns {object} 预览状态信息
*/
getPreviewState() {
try {
// 检查是否在游戏运行状态
const { director } = require('cc');
if (director && director.getScene && director.getScene()) {
return {
isRunning: true,
engineLoaded: true
};
}
return {
isRunning: false,
engineLoaded: false
};
} catch (error) {
console.warn('Failed to get preview state:', error);
return {
isRunning: false,
engineLoaded: false
};
}
},
/**
* 检查ECS框架是否已加载
* @returns {boolean} ECS框架加载状态
*/
isECSFrameworkLoaded() {
try {
// 检查是否有ECS框架的全局对象
return typeof window !== 'undefined' && !!(window as any).ECSFramework;
} catch (error) {
console.warn('Failed to check ECS framework status:', error);
return false;
}
},
/**
* 获取场景基本信息
* @returns {object} 场景信息
*/
getSceneBasicInfo() {
try {
const { director } = require('cc');
if (director && director.getScene) {
const scene = director.getScene();
return {
sceneName: scene ? (scene.name || '当前场景') : '未知场景',
nodeCount: scene ? this.countNodes(scene) : 0,
isValid: scene ? scene.isValid : false
};
}
return {
sceneName: '未知场景',
nodeCount: 0,
isValid: false
};
} catch (error) {
console.warn('Failed to get scene basic info:', error);
return {
sceneName: '获取失败',
nodeCount: 0,
isValid: false
};
}
},
/**
* 获取ECS框架的调试信息
* @returns {object|null} ECS调试数据或null如果框架未加载
*/
getECSDebugInfo() {
try {
// 检查是否有ECS框架的全局对象
if (typeof window !== 'undefined' && (window as any).ECSFramework) {
const ecs = (window as any).ECSFramework;
// 获取当前场景和实体管理器
if (ecs.Core && ecs.Core.getCurrentScene) {
const scene = ecs.Core.getCurrentScene();
if (scene && scene.entityManager) {
const entityManager = scene.entityManager;
const systemManager = scene.systemManager;
// 收集调试信息
const debugInfo = {
timestamp: new Date().toISOString(),
frameworkLoaded: true,
currentScene: scene.name || '当前场景',
totalEntities: entityManager.entityCount || 0,
activeEntities: entityManager.activeEntityCount || 0,
pendingAdd: 0, // 需要具体API
pendingRemove: 0, // 需要具体API
totalSystems: systemManager ? systemManager.getSystemCount() : 0,
systemsInfo: [],
frameTime: 0, // 需要性能监控
memoryUsage: 0, // 需要内存监控
componentTypes: 0, // 需要组件统计
componentInstances: 0 // 需要组件实例统计
};
// 获取系统信息
if (systemManager && systemManager.getSystems) {
const systems = systemManager.getSystems();
debugInfo.systemsInfo = systems.map((system: any, index: number) => ({
name: system.constructor.name || `System${index}`,
entityCount: system.entities ? system.entities.length : 0,
executionTime: system.lastExecutionTime || 0,
updateOrder: index + 1
}));
}
return debugInfo;
}
}
}
// 检查是否直接导入了ECS模块
try {
// 这里需要根据实际的ECS框架导入方式调整
const { Core } = require('ecs-framework');
if (Core) {
const scene = Core.getCurrentScene();
if (scene) {
return {
timestamp: new Date().toISOString(),
frameworkLoaded: true,
currentScene: scene.name || '当前场景',
totalEntities: scene.entityManager?.entityCount || 0,
activeEntities: scene.entityManager?.activeEntityCount || 0,
pendingAdd: 0,
pendingRemove: 0,
totalSystems: scene.systemManager?.getSystemCount() || 0,
systemsInfo: [],
frameTime: 0,
memoryUsage: 0,
componentTypes: 0,
componentInstances: 0
};
}
}
} catch (error) {
// ECS框架未导入或未初始化
}
return null;
} catch (error) {
console.warn('Failed to get ECS debug info:', error);
return null;
}
},
/**
* 递归计算节点数量
* @param {any} node
* @returns {number}
*/
countNodes(node: any): number {
if (!node) return 0;
let count = 1; // 当前节点
if (node.children) {
for (const child of node.children) {
count += this.countNodes(child);
}
}
return count;
}
};

Some files were not shown because too many files have changed in this diff Show More