Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73a882f75e | ||
|
|
310f5f2349 | ||
|
|
8c86d6b696 | ||
|
|
82cd163adc | ||
|
|
802ee25621 | ||
|
|
f48ebb65ba | ||
|
|
aaa2a8ed2c | ||
|
|
5a06f5420b | ||
|
|
343f5a44f2 | ||
|
|
92125aee3a | ||
|
|
96f651b7ca | ||
|
|
06ea01e928 | ||
|
|
577f1e429a | ||
|
|
7808f64fe5 | ||
|
|
e6789e49e4 | ||
|
|
797619aece | ||
|
|
1b5363611d | ||
|
|
103f773286 | ||
|
|
d9ef0b587e | ||
|
|
d5b98256f0 | ||
|
|
efcceaa898 | ||
|
|
e4aad11965 | ||
|
|
47207fad52 | ||
|
|
202bf82896 | ||
|
|
0e3274a743 | ||
|
|
b06174926d | ||
|
|
abb23a3c02 | ||
|
|
0c8f232282 | ||
|
|
ef023d27bf | ||
|
|
7a591825eb | ||
|
|
e71c49d596 | ||
|
|
e6ce8995ba | ||
|
|
f6250b6d5b | ||
|
|
757eff2937 | ||
|
|
996a7f3ddf | ||
|
|
94c050bacb | ||
|
|
3f4aa59a29 | ||
|
|
bee7cf4278 | ||
|
|
b9db6f0b40 | ||
|
|
8967cba3c7 | ||
|
|
d04ad2eea9 | ||
|
|
f2d3880a06 | ||
|
|
ec5f70ecfc | ||
|
|
40b3fe7165 | ||
|
|
4095f1e946 | ||
|
|
e219fc47ba | ||
|
|
6e2e7a4af5 | ||
|
|
2e7f764d6c | ||
|
|
ce64de5b3d | ||
|
|
35ca1dd7ea | ||
|
|
8d0ad6b871 | ||
|
|
0aa4791cf7 | ||
|
|
082c2b46d0 | ||
|
|
50420f9052 |
80
.gitignore
vendored
80
.gitignore
vendored
@@ -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
3
.gitmodules
vendored
Normal 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
40
.npmignore
Normal 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
570
README.md
@@ -3,293 +3,435 @@
|
||||
[](https://badge.fury.io/js/%40esengine%2Fecs-framework)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
一个轻量级的 TypeScript ECS(Entity-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)
|
||||
60
SECURITY.md
60
SECURITY.md
@@ -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
BIN
docs/beginner-tutorials.md
Normal file
Binary file not shown.
431
docs/bitmask-guide.md
Normal file
431
docs/bitmask-guide.md
Normal 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. **灵活的查询构建** - 支持复杂的组件组合查询
|
||||
|
||||
通过理解和正确使用位掩码,可以显著提升游戏的性能表现。记住要在游戏初始化时注册组件类型,预计算常用掩码,并合理管理缓存。
|
||||
692
docs/component-design-guide.md
Normal file
692
docs/component-design-guide.md
Normal 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
665
docs/concepts-explained.md
Normal 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个冗余接口** - 移除了所有未使用的接口定义
|
||||
- **完整的类型覆盖** - 为所有主要功能提供类型支持
|
||||
|
||||
这种设计确保了框架的类型安全性,同时保持了代码的简洁性和可维护性。
|
||||
@@ -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
370
docs/entity-guide.md
Normal 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: 不可以。实体与场景绑定,需要在新场景中重新创建。
|
||||
370
docs/entity-manager-example.md
Normal file
370
docs/entity-manager-example.md
Normal 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,可以构建高性能的游戏系统。记住要及时清理无用实体,缓存频繁查询的结果,并使用合适的查询方法来优化性能。
|
||||
496
docs/event-system-example.md
Normal file
496
docs/event-system-example.md
Normal 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
425
docs/performance-optimization.md
Normal file
425
docs/performance-optimization.md
Normal 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框架的性能表现。
|
||||
@@ -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[] = [];
|
||||
|
||||
734
docs/scene-management-guide.md
Normal file
734
docs/scene-management-guide.md
Normal 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
519
docs/system-guide.md
Normal 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
653
docs/timer-guide.md
Normal 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
600
docs/use-cases.md
Normal 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框架在不同场景下都发挥最佳性能。
|
||||
@@ -0,0 +1,2 @@
|
||||
[InternetShortcut]
|
||||
URL=https://docs.cocos.com/creator/manual/en/scripting/setup.html#custom-script-template
|
||||
54
extensions/cocos/cocos-ecs/.ecs-framework-settings.json
Normal file
54
extensions/cocos/cocos-ecs/.ecs-framework-settings.json
Normal 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
24
extensions/cocos/cocos-ecs/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
#///////////////////////////
|
||||
# Cocos Creator 3D Project
|
||||
#///////////////////////////
|
||||
library/
|
||||
temp/
|
||||
local/
|
||||
build/
|
||||
profiles/
|
||||
native
|
||||
#//////////////////////////
|
||||
# NPM
|
||||
#//////////////////////////
|
||||
node_modules/
|
||||
|
||||
#//////////////////////////
|
||||
# VSCode
|
||||
#//////////////////////////
|
||||
.vscode/
|
||||
|
||||
#//////////////////////////
|
||||
# WebStorm
|
||||
#//////////////////////////
|
||||
.idea/
|
||||
14
extensions/cocos/cocos-ecs/assets/resources.meta
Normal file
14
extensions/cocos/cocos-ecs/assets/resources.meta
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "2.0.1",
|
||||
"importer": "json",
|
||||
"imported": true,
|
||||
"uuid": "ba6c564a-c5c5-4dc7-ba95-9f0279e0bd66",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
extensions/cocos/cocos-ecs/assets/scenes.meta
Normal file
9
extensions/cocos/cocos-ecs/assets/scenes.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "e7a0c4c4-f555-4dc5-be34-83ae26b4eb35",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
495
extensions/cocos/cocos-ecs/assets/scenes/scene.scene
Normal file
495
extensions/cocos/cocos-ecs/assets/scenes/scene.scene
Normal 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
|
||||
}
|
||||
]
|
||||
11
extensions/cocos/cocos-ecs/assets/scenes/scene.scene.meta
Normal file
11
extensions/cocos/cocos-ecs/assets/scenes/scene.scene.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.1.50",
|
||||
"importer": "scene",
|
||||
"imported": true,
|
||||
"uuid": "ff354f0b-c2f5-4dea-8ffb-0152d175d11c",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
extensions/cocos/cocos-ecs/assets/scripts.meta
Normal file
9
extensions/cocos/cocos-ecs/assets/scripts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "1bf5f009-19d9-42b9-b6bb-b44efe349b09",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
extensions/cocos/cocos-ecs/assets/scripts/ecs.meta
Normal file
9
extensions/cocos/cocos-ecs/assets/scripts/ecs.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "6f9217a1-dff6-4460-b5da-eb01cf29c03c",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
99
extensions/cocos/cocos-ecs/assets/scripts/ecs/ECSManager.ts
Normal file
99
extensions/cocos/cocos-ecs/assets/scripts/ecs/ECSManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "c82e7909-01e6-49ca-b68d-9ee98837e238",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
153
extensions/cocos/cocos-ecs/assets/scripts/ecs/README.md
Normal file
153
extensions/cocos/cocos-ecs/assets/scripts/ecs/README.md
Normal 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。
|
||||
11
extensions/cocos/cocos-ecs/assets/scripts/ecs/README.md.meta
Normal file
11
extensions/cocos/cocos-ecs/assets/scripts/ecs/README.md.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"importer": "text",
|
||||
"imported": true,
|
||||
"uuid": "0932496e-f7fe-4cb9-86e2-ebd7d2a3d047",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "3d8cbc91-5bc5-4d17-b53a-01fda26e4660",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "0f20d48a-7b30-4081-a9de-709432b6737b",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "ab10dc4c-c8a3-4fd2-83d6-433d4195966b",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "e6ee57d6-d0eb-43f2-a601-9b7a2812de66",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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-1,1为无阻尼) */
|
||||
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; // 避免浮点数精度问题
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "784d7c28-2b72-427c-8b04-da0fcf775acf",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "6a2d6231-acf9-47b8-a020-d45a7433a95d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "80dcbaf5-21f7-4bb1-aff4-2cdbb0b5d364",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "879b4e07-dd6b-4445-adb2-a970b97c6d6f",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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("✅ 场景清理完成");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "da87facc-89a0-47da-a0ef-423255200a51",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "8fee85be-2224-4200-a898-d3ae2406fb1d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "a5e3a8c9-3d0b-4a36-9d20-6f70f1380131",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "455c12d1-52a8-41ac-b1b5-0d2b93c079aa",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "a8712467-efe0-46ec-a246-a9fa07d203d9",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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 已清理");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "7b69a39f-926a-4260-94ba-e15e31b324b5",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"description": "其他扩展插件的扩展配置 / Extended configuration for other extension plugins",
|
||||
"properties": {
|
||||
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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开发变得简单高效,专注于游戏逻辑而非框架配置!
|
||||
@@ -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",
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
};
|
||||
@@ -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: "打开"
|
||||
}
|
||||
};
|
||||
318
extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/package-lock.json
generated
Normal file
318
extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/package-lock.json
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("[33mWarning:[0m"),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)}
|
||||
@@ -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}) {
|
||||
// 添加处理逻辑
|
||||
}`;
|
||||
}
|
||||
}
|
||||
@@ -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)}`);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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群号或访问相关链接加入讨论群。',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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编辑器。`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { EcsFrameworkHandler } from './EcsFrameworkHandler';
|
||||
export { BehaviorTreeHandler } from './BehaviorTreeHandler';
|
||||
export { PanelHandler } from './PanelHandler';
|
||||
@@ -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] 扩展已卸载');
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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
Reference in New Issue
Block a user