From 8c86d6b696f0080a104155d556e5854b159f9adc Mon Sep 17 00:00:00 2001
From: YHH <359807859@qq.com>
Date: Thu, 19 Jun 2025 10:38:31 +0800
Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3=E5=8F=8A?=
=?UTF-8?q?=E4=BC=98=E5=8C=96=E8=A1=8C=E4=B8=BA=E6=A0=91=E7=BC=96=E8=BE=91?=
=?UTF-8?q?=E5=99=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 25 +-
docs/component-design-guide.md | 19 +-
docs/core-concepts.md | 58 +++-
docs/getting-started.md | 167 ++++++++++-
docs/system-guide.md | 30 +-
docs/timer-guide.md | 8 +-
.../behavior-tree/composables/useAppState.ts | 2 +-
.../composables/useBehaviorTreeEditor.ts | 281 +++++++++++++-----
.../composables/useBlackboard.ts | 18 +-
.../composables/useComputedProperties.ts | 200 ++++++++++++-
.../composables/useConditionAttachment.ts | 238 +++++++++++----
.../composables/useNodeOperations.ts | 51 +++-
.../behavior-tree/data/nodeTemplates.ts | 40 ++-
.../panels/behavior-tree/types/index.ts | 5 +
.../static/style/behavior-tree/conditions.css | 205 ++++++++++++-
.../static/style/behavior-tree/modals.css | 153 +++++++++-
.../static/style/behavior-tree/nodes.css | 9 +
.../static/style/behavior-tree/panels.css | 254 +++++++++++++---
.../behavior-tree/BehaviorTreeEditor.html | 129 ++++++--
thirdparty/BehaviourTree-ai | 2 +-
20 files changed, 1608 insertions(+), 286 deletions(-)
diff --git a/README.md b/README.md
index e5941cd9..5d890ba3 100644
--- a/README.md
+++ b/README.md
@@ -31,8 +31,27 @@ npm install @esengine/ecs-framework
```typescript
import { Core, Scene, Entity, Component, EntitySystem } from '@esengine/ecs-framework';
-// 创建核心实例
-const core = Core.create(true); // 调试模式
+// 创建核心实例 - 使用配置对象(推荐)
+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();
@@ -305,6 +324,7 @@ enum ECSEventType {
| 实体管理器 | ✅ 统一接口 | ❌ 低级 API | ✅ 高级接口 |
| 性能优化 | ✅ 多重优化 | ✅ 极致性能 | ✅ React 优化 |
| JavaScript引擎集成 | ✅ 专为JS引擎设计 | ✅ 通用设计 | ⚠️ 主要 React |
+| 可视化调试工具 | ✅ [Cocos插件](https://store.cocos.com/app/detail/7823) | ❌ 无官方工具 | ✅ React DevTools |
**选择指南:**
- 选择本框架:需要完整的游戏开发工具链和中文社区支持
@@ -331,6 +351,7 @@ ecs-framework/
### 🎯 新手入门
- **[📖 新手教程完整指南](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) - 不同类型游戏的具体应用案例
diff --git a/docs/component-design-guide.md b/docs/component-design-guide.md
index c9079a56..5d7f5809 100644
--- a/docs/component-design-guide.md
+++ b/docs/component-design-guide.md
@@ -387,16 +387,12 @@ class HealthComponent extends Component {
this.currentHealth -= damage;
// 发送事件,让其他系统响应
- Core.emitter.emit('health:damaged', {
- entity: this.entity,
- damage: damage,
- remainingHealth: this.currentHealth
- });
+ // 注意:需要在实际使用中获取EntityManager实例
+ // 示例:entityManager.eventBus.emit('health:damaged', {...});
if (this.currentHealth <= 0) {
- Core.emitter.emit('health:died', {
- entity: this.entity
- });
+ // 示例:entityManager.eventBus.emit('health:died', {...});
+ console.log('实体死亡');
}
}
}
@@ -406,12 +402,13 @@ class AnimationComponent extends Component {
onAddedToEntity() {
super.onAddedToEntity();
- // 监听受伤事件
- Core.emitter.addObserver('health:damaged', this.onDamaged, this);
+ // 监听受伤事件(需要在实际使用中获取EntityManager实例)
+ // 示例:entityManager.eventBus.on('health:damaged', this.onDamaged, { context: this });
}
onRemovedFromEntity() {
- Core.emitter.removeObserver('health:damaged', this.onDamaged, this);
+ // 事件监听会在组件移除时自动清理
+ // 如需手动清理,保存listenerId并调用eventBus.off()
super.onRemovedFromEntity();
}
diff --git a/docs/core-concepts.md b/docs/core-concepts.md
index c98d8e3d..0dd5b1f7 100644
--- a/docs/core-concepts.md
+++ b/docs/core-concepts.md
@@ -19,28 +19,64 @@ Core 是框架的核心管理类,负责游戏的生命周期管理。
### 创建和配置
```typescript
-import { Core } from '@esengine/ecs-framework';
+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 '@esengine/ecs-framework';
+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);
+ }
+}
```
### 定时器系统
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 2d37dfa7..d9fe0302 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -29,6 +29,145 @@ Core.update(deltaTime);
- 更精确的时间控制
- 统一的API,简化集成
+## Core配置
+
+### 基础配置
+
+ECS框架提供了灵活的配置选项来满足不同项目需求:
+
+```typescript
+import { Core, ICoreConfig } from '@esengine/ecs-framework';
+
+// 方式1:简化配置(向后兼容)
+Core.create(true); // 启用调试模式
+Core.create(false); // 发布模式
+Core.create(); // 默认调试模式
+
+// 方式2:详细配置(推荐)
+const config: ICoreConfig = {
+ debug: true, // 启用调试模式
+ enableEntitySystems: true, // 启用实体系统(默认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);
+```
+
+### 调试功能
+
+ECS框架内置了强大的调试功能,支持运行时监控和远程调试:
+
+#### Cocos Creator专用调试插件
+
+**🎯 对于Cocos Creator用户,我们提供了专门的可视化调试插件:**
+
+- **插件地址**:[cocos-ecs-framework 调试插件](https://store.cocos.com/app/detail/7823)
+- **插件版本**:v1.0.0
+- **支持版本**:Cocos Creator v3.0.0+
+- **支持平台**:Android | iOS | HTML5
+
+这个插件提供了完整的ECS可视化调试界面,包括实体查看器、组件编辑器、系统监控、性能分析等功能。
+
+#### 通用调试配置
+
+```typescript
+// 运行时启用调试
+Core.enableDebug({
+ enabled: true,
+ websocketUrl: 'ws://localhost:8080',
+ autoReconnect: true,
+ updateInterval: 500,
+ channels: {
+ entities: true,
+ systems: true,
+ performance: true,
+ components: false, // 可以选择性禁用某些通道
+ scenes: true
+ }
+});
+
+// 获取调试数据
+const debugData = Core.getDebugData();
+console.log('当前实体数量:', debugData?.entities?.totalEntities);
+
+// 禁用调试
+Core.disableDebug();
+
+// 检查调试状态
+if (Core.isDebugEnabled) {
+ console.log('调试模式已启用');
+}
+```
+
+### 生产环境配置建议
+
+```typescript
+// 开发环境 - Cocos Creator
+const devConfigForCocos: ICoreConfig = {
+ debug: true,
+ enableEntitySystems: true,
+ debugConfig: {
+ enabled: true,
+ websocketUrl: 'ws://localhost:8080', // 连接Cocos插件
+ autoReconnect: true,
+ updateInterval: 1000,
+ channels: {
+ entities: true,
+ systems: true,
+ performance: true,
+ components: true,
+ scenes: true
+ }
+ }
+};
+
+// 开发环境 - 其他平台
+const devConfig: 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 prodConfig: ICoreConfig = {
+ debug: false, // 关闭调试以提升性能
+ enableEntitySystems: true,
+ // debugConfig 可以省略或设为 undefined
+};
+
+const isDevelopment = process.env.NODE_ENV === 'development';
+Core.create(isDevelopment ? devConfig : prodConfig);
+```
+
+**💡 调试功能说明:**
+- **Cocos Creator**:推荐使用[官方调试插件](https://store.cocos.com/app/detail/7823)获得最佳调试体验
+- **其他平台**:可以通过WebSocket连接自定义调试工具
+- **生产环境**:建议关闭调试功能以获得最佳性能
+
## 平台集成
### Laya引擎
@@ -44,8 +183,10 @@ class LayaECSGame extends LayaScene {
constructor() {
super();
- // 初始化ECS框架
- Core.create(true);
+ // 初始化ECS框架(简化方式)
+ Core.create(true); // 启用调试模式
+ // 完整配置示例: Core.create({ debug: true, enableEntitySystems: true, debugConfig: {...} })
+
this.ecsScene = new ECSScene();
this.ecsScene.name = "LayaGameScene";
Core.scene = this.ecsScene;
@@ -117,8 +258,10 @@ export class ECSGameManager extends CocosComponent {
private entityManager: EntityManager;
start() {
- // 初始化ECS框架
- Core.create(true);
+ // 初始化ECS框架(简化方式)
+ Core.create(true); // 启用调试模式
+ // 完整配置示例: Core.create({ debug: true, enableEntitySystems: true, debugConfig: {...} })
+
this.ecsScene = new ECSScene();
this.ecsScene.name = "CocosGameScene";
Core.scene = this.ecsScene;
@@ -172,6 +315,12 @@ class CocosRenderSystem extends EntitySystem {
// 将ECSGameManager脚本挂载到场景根节点
```
+**🔧 Cocos Creator调试提示:**
+为了获得最佳的ECS调试体验,建议安装我们的专用调试插件:
+- 插件地址:[https://store.cocos.com/app/detail/7823](https://store.cocos.com/app/detail/7823)
+- 支持Cocos Creator v3.0.0+
+- 提供实体查看器、组件编辑器、系统监控等功能
+
### Node.js后端
```typescript
@@ -185,7 +334,10 @@ class ServerGameManager {
private lastUpdate: number = Date.now();
constructor() {
- Core.create(true);
+ // 初始化ECS框架(简化方式)
+ Core.create(true); // 启用调试模式
+ // 完整配置示例: Core.create({ debug: true, enableEntitySystems: true, debugConfig: {...} })
+
this.scene = new Scene();
this.scene.name = "ServerScene";
Core.scene = this.scene;
@@ -276,7 +428,10 @@ class BrowserGame {
private entityManager: EntityManager;
constructor() {
- Core.create(true);
+ // 初始化ECS框架(简化方式)
+ Core.create(true); // 启用调试模式
+ // 完整配置示例: Core.create({ debug: true, enableEntitySystems: true, debugConfig: {...} })
+
this.scene = new Scene();
this.scene.name = "BrowserScene";
Core.scene = this.scene;
diff --git a/docs/system-guide.md b/docs/system-guide.md
index 300d2a00..a333c399 100644
--- a/docs/system-guide.md
+++ b/docs/system-guide.md
@@ -69,7 +69,8 @@ class HealthSystem extends EntitySystem {
entity.addComponent(new DeadComponent());
// 触发死亡事件
- Core.emitter.emit('entity:died', {
+ const eventBus = this.scene.entityManager.eventBus;
+ eventBus.emit('entity:died', {
entityId: entity.id,
entityName: entity.name
});
@@ -235,7 +236,8 @@ class SpawnSystem extends IntervalSystem {
spawner.lastSpawnTime = Time.totalTime;
// 发送生成事件
- Core.emitter.emit('enemy:spawned', {
+ const eventBus = this.scene.entityManager.eventBus;
+ eventBus.emit('enemy:spawned', {
enemyId: enemy.id,
spawnPoint: spawnPoint,
spawnerEntity: spawnerEntity.id
@@ -270,18 +272,17 @@ class ScoreSystem extends PassiveSystem {
initialize() {
super.initialize();
- // 监听游戏事件(使用Core.emitter)
- Core.emitter.addObserver('enemy:killed', this.onEnemyKilled, this);
- Core.emitter.addObserver('item:collected', this.onItemCollected, this);
- Core.emitter.addObserver('combo:broken', this.onComboBroken, this);
+ // 监听游戏事件(使用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() {
- // 清理事件监听
- Core.emitter.removeObserver('enemy:killed', this.onEnemyKilled, this);
- Core.emitter.removeObserver('item:collected', this.onItemCollected, this);
- Core.emitter.removeObserver('combo:broken', this.onComboBroken, this);
+ // 事件监听会在系统销毁时自动清理
+ // 如需手动清理,可以保存listenerId并调用eventBus.off()
}
private onEnemyKilled(data: { enemyType: string; position: { x: number; y: number } }) {
@@ -305,7 +306,8 @@ class ScoreSystem extends PassiveSystem {
this.score += points;
// 发送分数更新事件
- Core.emitter.emit('score:updated', {
+ const eventBus = this.scene.entityManager.eventBus;
+ eventBus.emit('score:updated', {
score: this.score,
points: points,
multiplier: this.multiplier,
@@ -415,7 +417,8 @@ class CollisionSystem extends EntitySystem {
if (collision) {
// 发送碰撞事件,让其他系统响应
- Core.emitter.emit('collision:detected', {
+ const eventBus = this.scene.entityManager.eventBus;
+ eventBus.emit('collision:detected', {
entity1: collider1,
entity2: collider2,
collisionPoint: point
@@ -427,7 +430,8 @@ class CollisionSystem extends EntitySystem {
class HealthSystem extends PassiveSystem {
onAddedToScene() {
// 监听碰撞事件
- Core.emitter.addObserver('collision:detected', this.onCollision, this);
+ const eventBus = this.scene.entityManager.eventBus;
+ eventBus.on('collision:detected', this.onCollision, { context: this });
}
private onCollision(data: CollisionEventData) {
diff --git a/docs/timer-guide.md b/docs/timer-guide.md
index 221eebfa..b69e43d7 100644
--- a/docs/timer-guide.md
+++ b/docs/timer-guide.md
@@ -488,8 +488,9 @@ class LevelTimer {
console.log("⏰ 时间到!游戏结束");
- // 触发游戏结束
- Core.emitter.emit('level:timeout');
+ // 触发游戏结束(需要在实际使用中获取EntityManager实例)
+ // 示例:entityManager.eventBus.emit('level:timeout');
+ console.log('触发关卡超时事件');
}
completeLevel() {
@@ -509,7 +510,8 @@ class LevelTimer {
const bonus = Math.floor(timeLeft * 10); // 每秒剩余10分
if (bonus > 0) {
console.log(`时间奖励:${bonus} 分`);
- Core.emitter.emit('score:time_bonus', { bonus });
+ // 触发时间奖励事件(需要在实际使用中获取EntityManager实例)
+ // 示例:entityManager.eventBus.emit('score:time_bonus', { bonus });
}
}
diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useAppState.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useAppState.ts
index 68e0ca27..053e54b1 100644
--- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useAppState.ts
+++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useAppState.ts
@@ -56,7 +56,7 @@ export function useAppState() {
// UI状态
const showExportModal = ref(false);
- const exportFormat = ref('json');
+ const exportFormat = ref('json'); // 默认JSON格式,TypeScript暂时禁用
// 工具函数
const getNodeByIdLocal = (id: string): TreeNode | undefined => {
diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts
index 1375011f..f010ab3a 100644
--- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts
+++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts
@@ -159,23 +159,51 @@ export function useBehaviorTreeEditor() {
try {
const blackboardData = event.dataTransfer?.getData('application/blackboard-variable');
- if (!blackboardData) return;
+
+ if (!blackboardData) {
+ return;
+ }
const variable = JSON.parse(blackboardData);
- const activeNode = computedProps.activeNode.value;
- if (!activeNode || !activeNode.properties) return;
-
- const property = activeNode.properties[propertyKey];
- if (!property) return;
-
- // 设置Blackboard引用
- const referenceValue = `{{${variable.name}}}`;
- nodeOps.updateNodeProperty(`properties.${propertyKey}.value`, referenceValue);
-
- // 移除拖拽样式
- const element = event.currentTarget as HTMLElement;
- element.classList.remove('drag-over');
+ // 检查当前是否在编辑条件节点
+ if (appState.selectedConditionNodeId.value) {
+ // 条件节点:直接更新装饰器的属性
+ const decoratorNode = appState.getNodeByIdLocal(appState.selectedConditionNodeId.value);
+ if (decoratorNode) {
+ const referenceValue = `{{${variable.name}}}`;
+
+ if (!decoratorNode.properties) {
+ decoratorNode.properties = {};
+ }
+ decoratorNode.properties[propertyKey] = referenceValue;
+
+ // 强制触发响应式更新
+ const nodeIndex = appState.treeNodes.value.findIndex(n => n.id === decoratorNode.id);
+ if (nodeIndex > -1) {
+ const newNodes = [...appState.treeNodes.value];
+ newNodes[nodeIndex] = { ...decoratorNode };
+ appState.treeNodes.value = newNodes;
+ }
+ }
+ } else {
+ // 普通节点:使用原来的逻辑
+ const activeNode = computedProps.activeNode.value;
+
+ if (!activeNode || !activeNode.properties) {
+ return;
+ }
+
+ const property = activeNode.properties[propertyKey];
+
+ if (!property) {
+ return;
+ }
+
+ // 设置Blackboard引用
+ const referenceValue = `{{${variable.name}}}`;
+ nodeOps.updateNodeProperty(`properties.${propertyKey}.value`, referenceValue);
+ }
} catch (error) {
console.error('处理Blackboard拖拽失败:', error);
@@ -187,6 +215,7 @@ export function useBehaviorTreeEditor() {
event.stopPropagation();
const hasBlackboardData = event.dataTransfer?.types.includes('application/blackboard-variable');
+
if (hasBlackboardData) {
event.dataTransfer!.dropEffect = 'copy';
const element = event.currentTarget as HTMLElement;
@@ -200,7 +229,25 @@ export function useBehaviorTreeEditor() {
};
const clearBlackboardReference = (propertyKey: string) => {
- nodeOps.updateNodeProperty(`properties.${propertyKey}.value`, '');
+ // 检查当前是否在编辑条件节点
+ if (appState.selectedConditionNodeId.value) {
+ // 条件节点:直接清除装饰器的属性
+ const decoratorNode = appState.getNodeByIdLocal(appState.selectedConditionNodeId.value);
+ if (decoratorNode && decoratorNode.properties) {
+ decoratorNode.properties[propertyKey] = '';
+
+ // 强制触发响应式更新
+ const nodeIndex = appState.treeNodes.value.findIndex(n => n.id === decoratorNode.id);
+ if (nodeIndex > -1) {
+ const newNodes = [...appState.treeNodes.value];
+ newNodes[nodeIndex] = { ...decoratorNode };
+ appState.treeNodes.value = newNodes;
+ }
+ }
+ } else {
+ // 普通节点:使用原来的逻辑
+ nodeOps.updateNodeProperty(`properties.${propertyKey}.value`, '');
+ }
};
const startNodeDrag = (event: MouseEvent, node: any) => {
@@ -461,62 +508,128 @@ export function useBehaviorTreeEditor() {
}
};
- // 保存到文件
- const saveToFile = () => {
- const code = computedProps.exportedCode();
- const format = appState.exportFormat.value;
- const extension = format === 'json' ? '.json' : '.ts';
- const mimeType = format === 'json' ? 'application/json' : 'text/typescript';
-
- // 创建文件并下载
- const blob = new Blob([code], { type: mimeType });
- const url = URL.createObjectURL(blob);
-
- const a = document.createElement('a');
- a.href = url;
- a.download = `behavior_tree_config${extension}`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
-
- URL.revokeObjectURL(url);
-
- // 显示成功消息
- const toast = document.createElement('div');
- toast.style.cssText = `
- position: fixed;
- top: 20px;
- right: 20px;
- padding: 12px 20px;
- background: #4caf50;
- 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 = `文件已保存: behavior_tree_config${extension}`;
-
- 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%)';
+ // 保存到文件 - 使用Cocos Creator扩展API提供保存路径选择
+ const saveToFile = async () => {
+ try {
+ const code = computedProps.exportedCode();
+ const format = appState.exportFormat.value;
+ const extension = format === 'json' ? '.json' : '.ts';
+ const fileType = format === 'json' ? 'JSON配置文件' : 'TypeScript文件';
+
+ // 使用Cocos Creator的文件保存对话框
+ const result = await Editor.Dialog.save({
+ title: `保存${fileType}`,
+ filters: [
+ {
+ name: fileType,
+ extensions: extension === '.json' ? ['json'] : ['ts']
+ },
+ {
+ name: '所有文件',
+ extensions: ['*']
+ }
+ ]
+ });
+
+ if (result.canceled || !result.filePath) {
+ return; // 用户取消了保存
+ }
+
+ // 写入文件
+ const fs = require('fs-extra');
+ await fs.writeFile(result.filePath, code, 'utf8');
+
+ // 显示成功消息
+ const toast = document.createElement('div');
+ toast.style.cssText = `
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ padding: 12px 20px;
+ background: #4caf50;
+ 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;
+ max-width: 400px;
+ word-wrap: break-word;
+ `;
+
+ const path = require('path');
+ const fileName = path.basename(result.filePath);
+ toast.innerHTML = `
+
✅ 文件保存成功
+ 文件名: ${fileName}
+ 路径: ${result.filePath}
+ `;
+
+ document.body.appendChild(toast);
+
setTimeout(() => {
- if (document.body.contains(toast)) {
- document.body.removeChild(toast);
- }
- }, 300);
- }, 3000);
+ 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);
+ }, 4000);
+
+ } catch (error: any) {
+ console.error('保存文件失败:', error);
+
+ // 显示错误消息
+ const errorToast = document.createElement('div');
+ errorToast.style.cssText = `
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ padding: 12px 20px;
+ background: #f56565;
+ 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;
+ max-width: 400px;
+ word-wrap: break-word;
+ `;
+ errorToast.innerHTML = `
+ ❌ 保存失败
+ ${error?.message || error}
+ `;
+
+ document.body.appendChild(errorToast);
+
+ setTimeout(() => {
+ errorToast.style.opacity = '1';
+ errorToast.style.transform = 'translateX(0)';
+ }, 10);
+
+ setTimeout(() => {
+ errorToast.style.opacity = '0';
+ errorToast.style.transform = 'translateX(100%)';
+ setTimeout(() => {
+ if (document.body.contains(errorToast)) {
+ document.body.removeChild(errorToast);
+ }
+ }, 300);
+ }, 5000);
+ }
};
+
+
onMounted(() => {
// 自动检查安装状态
installation.checkInstallStatus();
@@ -634,23 +747,31 @@ export function useBehaviorTreeEditor() {
},
updateNodeProperty: (path: string, value: any) => {
if (appState.selectedConditionNodeId.value) {
+ // 条件节点的属性更新 - 需要同步到装饰器
const decoratorNode = appState.getNodeByIdLocal(appState.selectedConditionNodeId.value);
if (decoratorNode) {
- const keys = path.split('.');
- let current: any = decoratorNode;
-
- 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] = {};
+ // 解析路径,例如 "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 = appState.treeNodes.value.findIndex(n => n.id === decoratorNode.id);
+ if (nodeIndex > -1) {
+ const newNodes = [...appState.treeNodes.value];
+ newNodes[nodeIndex] = { ...decoratorNode };
+ appState.treeNodes.value = newNodes;
}
- current = current[key];
}
-
- const finalKey = keys[keys.length - 1];
- current[finalKey] = value;
}
} else {
+ // 普通节点属性更新
nodeOps.updateNodeProperty(path, value);
}
},
@@ -660,9 +781,11 @@ export function useBehaviorTreeEditor() {
handleDecoratorDragLeave: conditionAttachment.handleDecoratorDragLeave,
attachConditionToDecorator: conditionAttachment.attachConditionToDecorator,
getConditionDisplayText: conditionAttachment.getConditionDisplayText,
+ getConditionProperties: conditionAttachment.getConditionProperties,
removeConditionFromDecorator: conditionAttachment.removeConditionFromDecorator,
canAcceptCondition: conditionAttachment.canAcceptCondition,
resetDragState: conditionAttachment.resetDragState,
+ toggleConditionExpanded: conditionAttachment.toggleConditionExpanded,
handleCanvasDrop: (event: DragEvent) => {
if (conditionAttachment.handleCanvasDrop(event)) {
diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBlackboard.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBlackboard.ts
index 01d8a744..f9158fb9 100644
--- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBlackboard.ts
+++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBlackboard.ts
@@ -346,15 +346,27 @@ export function useBlackboard() {
};
const onVariableDragStart = (event: DragEvent, variable: BlackboardVariable) => {
- if (!event.dataTransfer) return;
+ if (!event.dataTransfer) {
+ return;
+ }
- event.dataTransfer.setData('application/blackboard-variable', JSON.stringify({
+ 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) => {
diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useComputedProperties.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useComputedProperties.ts
index 527c76e7..7385b080 100644
--- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useComputedProperties.ts
+++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useComputedProperties.ts
@@ -86,18 +86,216 @@ export function useComputedProperties(
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: decoratorNode.properties || {},
+ properties: conditionProperties,
isConditionNode: true,
parentDecorator: decoratorNode
};
});
+ /**
+ * 根据条件类型重新构建属性结构
+ * 将装饰器的扁平属性转换回条件模板的属性结构
+ */
+ const reconstructConditionProperties = (conditionType: string, decoratorProperties: Record) => {
+ 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 = {};
+ 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);
diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useConditionAttachment.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useConditionAttachment.ts
index 3eb835f6..c66c76ac 100644
--- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useConditionAttachment.ts
+++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useConditionAttachment.ts
@@ -161,6 +161,38 @@ export function useConditionAttachment(
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;
}
@@ -177,7 +209,12 @@ export function useConditionAttachment(
'condition-active': 'isActive',
'condition-numeric': 'numericCompare',
'condition-property': 'propertyExists',
- 'condition-custom': 'custom'
+ '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';
@@ -190,27 +227,19 @@ export function useConditionAttachment(
event: DragEvent,
decoratorNode: TreeNode
): boolean => {
- console.log('🎯 执行条件吸附:', decoratorNode.name, dragState.conditionTemplate?.name);
-
event.preventDefault();
event.stopPropagation();
if (!dragState.isDraggingCondition || !dragState.conditionTemplate) {
- console.log('❌ 拖拽状态无效:', {
- isDragging: dragState.isDraggingCondition,
- hasTemplate: !!dragState.conditionTemplate
- });
return false;
}
if (!isConditionalDecorator(decoratorNode)) {
- console.log('❌ 不是条件装饰器:', decoratorNode.type);
return false;
}
// 获取条件配置
const conditionConfig = mapConditionToDecoratorProperties(dragState.conditionTemplate);
- console.log('📝 条件配置:', conditionConfig);
// 更新装饰器属性
if (!decoratorNode.properties) {
@@ -225,8 +254,11 @@ export function useConditionAttachment(
name: dragState.conditionTemplate.name,
icon: dragState.conditionTemplate.icon
};
-
- console.log('✅ 条件吸附成功!', decoratorNode.attachedCondition);
+
+ // 初始化为收缩状态
+ if (decoratorNode.conditionExpanded === undefined) {
+ decoratorNode.conditionExpanded = false;
+ }
// 重置拖拽状态
resetDragState();
@@ -260,7 +292,6 @@ export function useConditionAttachment(
* 重置拖拽状态
*/
const resetDragState = () => {
- console.log('🔄 重置拖拽状态');
dragState.isDraggingCondition = false;
dragState.conditionTemplate = null;
dragState.mousePosition = null;
@@ -268,45 +299,126 @@ export function useConditionAttachment(
};
/**
- * 获取条件显示文本
+ * 获取条件显示文本(简化版始终显示条件名称)
*/
- const getConditionDisplayText = (decoratorNode: TreeNode): string => {
- if (!decoratorNode.attachedCondition || !decoratorNode.properties) {
+ const getConditionDisplayText = (decoratorNode: TreeNode, expanded: boolean = false): string => {
+ if (!decoratorNode.attachedCondition) {
return '';
}
- const conditionType = decoratorNode.properties.conditionType;
-
- switch (conditionType) {
- case 'random':
- const probability = decoratorNode.properties.successProbability || 0.5;
- return `${(probability * 100).toFixed(0)}%概率`;
-
- case 'hasComponent':
- return `有${decoratorNode.properties.componentType || 'Component'}`;
-
- case 'hasTag':
- return `标签=${decoratorNode.properties.tagValue || 0}`;
-
- case 'isActive':
- const checkHierarchy = decoratorNode.properties.checkHierarchy;
- return checkHierarchy ? '激活(含层级)' : '激活';
-
- case 'numericCompare':
- const path = decoratorNode.properties.propertyPath || 'value';
- const operator = decoratorNode.properties.compareOperator || '>';
- const value = decoratorNode.properties.compareValue || 0;
- return `${path} ${operator} ${value}`;
-
- case 'propertyExists':
- return `存在${decoratorNode.properties.propertyPath || 'property'}`;
-
- case 'custom':
- return '自定义条件';
-
- default:
- return decoratorNode.attachedCondition.name;
+ // 始终返回条件名称,不管是否展开
+ return decoratorNode.attachedCondition.name;
+ };
+
+ /**
+ * 获取条件的可见属性(用于展开时显示)
+ */
+ const getConditionProperties = (decoratorNode: TreeNode): Record => {
+ if (!decoratorNode.attachedCondition || !decoratorNode.properties) {
+ return {};
}
+
+ const conditionType = decoratorNode.attachedCondition.type;
+ const visibleProps: Record = {};
+
+ // 根据条件类型筛选相关属性
+ 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;
};
/**
@@ -314,10 +426,34 @@ export function useConditionAttachment(
*/
const removeConditionFromDecorator = (decoratorNode: TreeNode) => {
if (decoratorNode.attachedCondition) {
+ // 删除附加的条件信息
delete decoratorNode.attachedCondition;
- // 完全清空所有属性,回到初始空白状态
- decoratorNode.properties = {};
+ // 重置展开状态
+ decoratorNode.conditionExpanded = false;
+
+ // 保留装饰器的基础属性,只删除条件相关的属性
+ const preservedProperties: Record = {};
+
+ // 条件装饰器的基础属性
+ 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;
}
};
@@ -339,6 +475,8 @@ export function useConditionAttachment(
getConditionDisplayText,
removeConditionFromDecorator,
canAcceptCondition,
- isConditionalDecorator
+ isConditionalDecorator,
+ toggleConditionExpanded,
+ getConditionProperties
};
-}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useNodeOperations.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useNodeOperations.ts
index 3d190e24..4e68edab 100644
--- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useNodeOperations.ts
+++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useNodeOperations.ts
@@ -135,18 +135,47 @@ export function useNodeOperations(
// 节点属性更新
const updateNodeProperty = (path: string, value: any) => {
- const node = selectedNodeId.value ? getNodeByIdLocal(selectedNodeId.value) : null;
- if (!node) return;
+ const selectedNode = selectedNodeId.value ? getNodeByIdLocal(selectedNodeId.value) : null;
+ if (!selectedNode) return;
- // 使用通用方法更新属性
- setNestedProperty(node, path, value);
-
- // 强制触发响应式更新
- const nodeIndex = treeNodes.value.findIndex(n => n.id === node.id);
- if (nodeIndex > -1) {
- const newNodes = [...treeNodes.value];
- newNodes[nodeIndex] = { ...node };
- treeNodes.value = newNodes;
+ // 检查是否是条件节点的属性更新
+ 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;
+ }
}
};
diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/data/nodeTemplates.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/data/nodeTemplates.ts
index 632a7a02..3b4d7336 100644
--- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/data/nodeTemplates.ts
+++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/data/nodeTemplates.ts
@@ -292,7 +292,45 @@ export const nodeTemplates: NodeTemplate[] = [
minChildren: 1,
className: 'ConditionalDecorator',
namespace: 'behaviourTree/decorators',
- properties: {}
+ properties: {
+ conditionType: {
+ name: '条件类型',
+ type: 'select',
+ value: 'custom',
+ options: ['custom', 'random', 'hasComponent', 'hasTag', 'isActive', 'numericCompare', 'propertyExists'],
+ description: '装饰器使用的条件类型',
+ 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: 0,
+ description: '条件检查间隔时间(秒),0表示每帧检查',
+ required: false
+ },
+ abortType: {
+ name: '中止类型',
+ type: 'select',
+ value: 'None',
+ options: ['None', 'LowerPriority', 'Self', 'Both'],
+ description: '决定节点在何种情况下会被中止',
+ required: false
+ }
+ }
},
// 动作节点 (Actions) - 叶子节点,不能有子节点
diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/types/index.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/types/index.ts
index 209ca301..49a90d1c 100644
--- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/types/index.ts
+++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/types/index.ts
@@ -22,6 +22,11 @@ export interface TreeNode {
name: string;
icon: string;
};
+ // 条件节点相关(用于虚拟条件节点)
+ isConditionNode?: boolean;
+ parentDecorator?: TreeNode;
+ // 条件显示状态
+ conditionExpanded?: boolean; // 条件是否展开显示详细信息
}
export interface Connection {
diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/conditions.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/conditions.css
index 80ecfd56..c64f17e7 100644
--- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/conditions.css
+++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/conditions.css
@@ -28,6 +28,29 @@
}
}
+/* 条件装饰器 - 基础状态 */
+.conditional-decorator {
+ width: 200px;
+ min-width: 200px;
+ max-width: 350px; /* 增加最大宽度以容纳长的条件名称 */
+ min-height: 80px;
+ transition: all 0.3s ease;
+ word-wrap: break-word;
+}
+
+/* 条件装饰器 - 有附加条件状态 */
+.conditional-decorator.has-attached-condition {
+ width: auto; /* 自动调整宽度 */
+ min-width: 280px; /* 进一步增加最小宽度,确保较长的条件名称能完整显示 */
+ max-width: 400px; /* 增加最大宽度 */
+ min-height: 110px; /* 增加基础高度 */
+}
+
+/* 条件装饰器 - 展开状态 */
+.conditional-decorator.has-attached-condition .condition-properties {
+ min-height: 30px; /* 为展开内容预留空间 */
+}
+
/* 条件装饰器接受状态 */
.tree-node.can-accept-condition {
border: 2px dashed #ffd700;
@@ -51,7 +74,7 @@
transform: scale(1.02);
}
-/* 条件附加区域 */
+/* 条件吸附区域 */
.condition-attachment-area {
margin-top: 8px;
padding: 8px;
@@ -61,84 +84,214 @@
.condition-placeholder {
text-align: center;
- padding: 12px;
+ padding: 6px 8px;
border: 2px dashed #4a5568;
border-radius: 4px;
color: #a0aec0;
- font-size: 11px;
+ font-size: 10px;
transition: all 0.3s ease;
+ min-height: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
.tree-node.can-accept-condition .condition-placeholder {
border-color: #ffd700;
color: #ffd700;
background: rgba(255, 215, 0, 0.05);
+ animation: pulse-placeholder 2s infinite;
}
+@keyframes pulse-placeholder {
+ 0%, 100% {
+ background: rgba(255, 215, 0, 0.05);
+ }
+ 50% {
+ background: rgba(255, 215, 0, 0.15);
+ }
+}
+
+.placeholder-text {
+ font-size: 10px;
+ font-weight: 500;
+}
+
+/* 附加的条件 */
.attached-condition {
background: rgba(255, 215, 0, 0.1);
border: 1px solid #ffd700;
border-radius: 4px;
padding: 6px;
+ position: relative;
+ width: 100%;
+ box-sizing: border-box;
+ min-height: 36px;
}
+/* 条件信息区域 */
.condition-info {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
cursor: pointer;
- padding: 4px;
+ padding: 4px 6px 4px 6px;
+ padding-right: 40px; /* 为右侧两个按钮预留空间 */
border-radius: 3px;
transition: all 0.2s ease;
+ line-height: 1.3;
+ width: 100%;
+ box-sizing: border-box;
}
.condition-info:hover {
background: rgba(255, 215, 0, 0.2);
- transform: scale(1.02);
+ transform: scale(1.01);
}
.condition-info.condition-selected {
background: rgba(255, 215, 0, 0.3);
border: 1px solid #ffd700;
- box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.25);
+ box-shadow: 0 0 0 1px rgba(255, 215, 0, 0.25);
}
.condition-icon {
- font-size: 12px;
+ font-size: 11px;
+ flex-shrink: 0;
}
.condition-text {
flex: 1;
color: #ffd700;
font-weight: 500;
+ font-size: 11px;
+ word-wrap: break-word;
+ word-break: break-word;
+ line-height: 1.3;
+ min-width: 0;
+ max-width: calc(100% - 50px); /* 为图标、编辑提示和按钮预留空间 */
+ white-space: nowrap; /* 尽量保持一行显示 */
+ overflow: hidden;
+ text-overflow: ellipsis; /* 如果实在太长则显示省略号 */
}
.edit-hint {
- font-size: 10px;
+ font-size: 9px;
color: #a0aec0;
margin-left: auto;
transition: color 0.2s ease;
+ flex-shrink: 0;
}
.condition-info:hover .edit-hint {
color: #ffd700;
}
-.remove-condition-btn {
- background: none;
+/* 展开/收缩按钮 */
+.toggle-condition-btn {
+ position: absolute;
+ top: 4px;
+ right: 20px;
+ width: 14px;
+ height: 14px;
border: none;
- color: #e53e3e;
+ background: #4a5568;
+ color: #a0aec0;
cursor: pointer;
- font-size: 14px;
- padding: 2px 4px;
border-radius: 2px;
+ font-size: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
transition: all 0.2s ease;
+ line-height: 1;
+ z-index: 2;
+}
+
+.toggle-condition-btn:hover {
+ background: #2d3748;
+ color: #ffd700;
+ transform: scale(1.1);
+}
+
+/* 移除条件按钮 */
+.remove-condition-btn {
+ position: absolute;
+ top: -2px;
+ right: -2px;
+ background: #e53e3e;
+ border: none;
+ color: white;
+ cursor: pointer;
+ font-size: 10px;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ line-height: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+ opacity: 0.8;
+ z-index: 3;
}
.remove-condition-btn:hover {
- background: rgba(229, 62, 62, 0.2);
+ background: #c53030;
transform: scale(1.1);
+ opacity: 1;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+/* 条件属性展开区域 */
+.condition-properties {
+ margin-top: 6px;
+ padding-top: 6px;
+ width: 100%;
+ box-sizing: border-box;
+ padding-right: 6px; /* 为按钮预留一点空间 */
+}
+
+/* 属性分隔线 */
+.properties-divider {
+ width: calc(100% - 6px);
+ height: 1px;
+ background: linear-gradient(90deg, transparent 0%, #ffd700 20%, #ffd700 80%, transparent 100%);
+ margin-bottom: 6px;
+ opacity: 0.6;
+}
+
+/* 条件属性项 */
+.condition-property-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ padding: 2px 4px;
+ margin-bottom: 2px;
+ font-size: 10px;
+ line-height: 1.2;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.condition-property-label {
+ color: #a0aec0;
+ font-weight: 500;
+ flex-shrink: 0;
+ margin-right: 8px;
+ white-space: nowrap;
+}
+
+.condition-property-value {
+ color: #ffd700;
+ font-weight: 400;
+ text-align: right;
+ word-wrap: break-word;
+ word-break: break-word;
+ min-width: 0;
+ flex: 1;
+ white-space: normal; /* 允许值换行 */
}
/* 画布状态 */
@@ -151,9 +304,33 @@
}
/* 条件装饰器节点的特殊样式 */
+.tree-node.node-conditional-decorator {
+ /* 基础高度和宽度 */
+ min-height: 80px;
+ width: 200px; /* 增加基础宽度 */
+ transition: all 0.3s ease;
+}
+
+/* 附加了条件的装饰器节点需要更大的高度 */
+.tree-node.node-conditional-decorator.has-attached-condition {
+ min-height: 130px; /* 增加高度 */
+ width: 220px; /* 进一步增加宽度以容纳更多内容 */
+}
+
.tree-node.node-conditional-decorator .condition-attachment-area {
border: 1px solid #9f7aea;
background: rgba(159, 122, 234, 0.05);
+ margin-top: 4px;
+ min-height: 20px;
+ transition: all 0.3s ease;
+}
+
+/* 当有条件附加时,增加条件区域的高度 */
+.tree-node.node-conditional-decorator.has-attached-condition .condition-attachment-area {
+ min-height: 45px;
+ padding: 10px 12px;
+ background: rgba(255, 215, 0, 0.08);
+ border-color: #ffd700;
}
.tree-node.node-conditional-decorator.node-selected .condition-attachment-area {
diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/modals.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/modals.css
index a4f7bb01..63033634 100644
--- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/modals.css
+++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/modals.css
@@ -56,8 +56,10 @@
background: #2d3748;
border-radius: 12px;
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.6);
- max-width: 90vw;
+ max-width: 95vw;
max-height: 90vh;
+ min-width: 800px;
+ width: auto;
display: flex;
flex-direction: column;
border: 1px solid #4a5568;
@@ -138,18 +140,119 @@
overflow-y: auto;
padding: 24px;
background: #2d3748;
+ min-width: 750px;
}
+/* 导出选项样式增强 */
.export-options {
- margin-bottom: 20px;
+ margin-bottom: 16px;
+ padding: 16px;
+ background: rgba(255, 255, 255, 0.02);
+ border: 1px solid #4a5568;
+ border-radius: 8px;
}
.export-options label {
- display: block;
- margin-bottom: 8px;
- color: #e2e8f0;
- font-size: 14px;
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ margin-bottom: 12px;
+ padding: 12px 16px;
+ background: rgba(255, 255, 255, 0.02);
+ border: 1px solid #4a5568;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ font-size: 13px;
font-weight: 500;
+ position: relative;
+}
+
+.export-options label:hover {
+ background: rgba(255, 255, 255, 0.05);
+ border-color: #667eea;
+ transform: translateY(-1px);
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
+}
+
+.export-options label:has(input[type="radio"]:checked) {
+ background: rgba(102, 126, 234, 0.1);
+ border-color: #667eea;
+ box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
+}
+
+.export-options input[type="radio"] {
+ appearance: none;
+ width: 18px;
+ height: 18px;
+ border: 2px solid #4a5568;
+ border-radius: 50%;
+ background: #1a202c;
+ cursor: pointer;
+ position: relative;
+ transition: all 0.2s ease;
+ flex-shrink: 0;
+ margin-top: 2px;
+}
+
+.export-options input[type="radio"]:checked {
+ border-color: #667eea;
+ background: #667eea;
+}
+
+.export-options input[type="radio"]:checked::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 8px;
+ height: 8px;
+ background: white;
+ border-radius: 50%;
+}
+
+.export-options label span {
+ color: #e2e8f0;
+ font-weight: 600;
+ flex: 1;
+}
+
+.export-options label small {
+ display: block;
+ margin-top: 4px;
+ color: #a0aec0;
+ font-size: 11px;
+ font-weight: 400;
+ line-height: 1.3;
+}
+
+/* 禁用选项样式 */
+.export-options label.disabled-option {
+ opacity: 0.5;
+ cursor: not-allowed;
+ background: rgba(255, 255, 255, 0.01);
+ border-color: #374151;
+}
+
+.export-options label.disabled-option:hover {
+ background: rgba(255, 255, 255, 0.01);
+ border-color: #374151;
+ transform: none;
+ box-shadow: none;
+}
+
+.export-options label.disabled-option span {
+ color: #6b7280;
+}
+
+.export-options label.disabled-option input[type="radio"] {
+ border-color: #374151;
+ cursor: not-allowed;
+}
+
+.export-options label.disabled-option input[type="radio"]:disabled {
+ background: #1f2937;
}
.code-output {
@@ -217,22 +320,38 @@
transform: translateY(0);
}
-.modal-footer .save-btn {
- background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
+.modal-footer .copy-btn {
+ background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%);
+ border: 1px solid #3182ce;
}
-.modal-footer .save-btn:hover {
+.modal-footer .copy-btn:hover {
+ background: linear-gradient(135deg, #2c5282 0%, #2a4365 100%);
+ border-color: #2c5282;
+}
+
+.modal-footer .save-file-btn {
background: linear-gradient(135deg, #38a169 0%, #2f855a 100%);
+ border: 1px solid #38a169;
}
-.modal-footer .cancel-btn {
- background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
+.modal-footer .save-file-btn:hover {
+ background: linear-gradient(135deg, #2f855a 0%, #276749 100%);
+ border-color: #2f855a;
}
-.modal-footer .cancel-btn:hover {
- background: linear-gradient(135deg, #4b5563 0%, #374151 100%);
+.modal-footer .close-btn {
+ background: linear-gradient(135deg, #718096 0%, #4a5568 100%);
+ border: 1px solid #718096;
}
+.modal-footer .close-btn:hover {
+ background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
+ border-color: #4a5568;
+}
+
+
+
/* Blackboard模态框特定样式 */
.blackboard-modal {
width: 520px;
@@ -427,5 +546,11 @@
font-size: 11px;
color: #e2e8f0;
resize: none;
- min-height: 300px;
+ min-height: 400px;
+ width: 100%;
+ box-sizing: border-box;
+ line-height: 1.4;
+ overflow-y: auto;
+ white-space: pre;
+ word-wrap: break-word;
}
\ No newline at end of file
diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/nodes.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/nodes.css
index 74a6fc08..7dbc1f5a 100644
--- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/nodes.css
+++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/nodes.css
@@ -83,6 +83,15 @@
overflow: hidden;
}
+/* 条件装饰器节点的node-body需要更大的宽度 */
+.tree-node.node-conditional-decorator .node-body {
+ max-width: 176px; /* 基础状态 */
+}
+
+.tree-node.node-conditional-decorator.has-attached-condition .node-body {
+ max-width: 196px; /* 附加条件后更宽 */
+}
+
.node-description {
margin-bottom: 6px;
color: #cbd5e0;
diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/panels.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/panels.css
index 31fecad0..60c43707 100644
--- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/panels.css
+++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/panels.css
@@ -564,61 +564,73 @@
}
.blackboard-sidebar .variable-item {
- background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
- border: 1px solid #4a5568;
- border-radius: 8px;
- padding: 12px 16px;
- cursor: grab;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ padding: 8px;
+ margin: 4px 0;
+ background: #4a5568;
+ border-radius: 4px;
+ transition: all 0.2s ease;
position: relative;
- display: flex;
- align-items: center;
- gap: 12px;
- border-left-width: 0;
- min-height: 44px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
- backdrop-filter: blur(10px);
- overflow: hidden;
+ border-left: 3px solid transparent;
+ user-select: none;
}
.blackboard-sidebar .variable-item::before {
content: '';
position: absolute;
- left: 12px;
- top: 50%;
- transform: translateY(-50%);
- width: 8px;
- height: 8px;
- border-radius: 50%;
- transition: all 0.3s ease;
- box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1), 0 2px 4px rgba(0, 0, 0, 0.3);
-}
-
-.blackboard-sidebar .variable-item:hover {
- background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
- border-color: #667eea;
- transform: translateY(-2px) scale(1.02);
- box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(102, 126, 234, 0.3);
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 3px;
+ border-radius: 2px 0 0 2px;
+ transition: all 0.2s ease;
}
.blackboard-sidebar .variable-item:hover::before {
- transform: translateY(-50%) scale(1.2);
- box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.2), 0 0 12px currentColor, 0 4px 8px rgba(0, 0, 0, 0.4);
-}
-
-.blackboard-sidebar .variable-item:active {
- cursor: grabbing;
- transform: scale(0.98);
+ background: #ffd700;
}
.blackboard-sidebar .variable-header {
display: flex;
align-items: center;
justify-content: space-between;
- gap: 12px;
+ gap: 8px;
flex: 1;
min-width: 0;
- margin-left: 20px;
+}
+
+.blackboard-sidebar .variable-drag-area {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 1;
+ min-width: 0;
+ cursor: grab;
+ padding: 4px 8px;
+ border-radius: 4px;
+ transition: all 0.2s ease;
+ position: relative;
+}
+
+.blackboard-sidebar .variable-drag-area:hover {
+ background: rgba(102, 126, 234, 0.1);
+ transform: translateX(-2px);
+}
+
+.blackboard-sidebar .variable-drag-area:active {
+ cursor: grabbing;
+}
+
+.blackboard-sidebar .variable-drag-area .drag-hint {
+ opacity: 0;
+ font-size: 10px;
+ color: #ffd700;
+ transition: opacity 0.2s ease;
+ margin-left: auto;
+}
+
+.blackboard-sidebar .variable-drag-area:hover .drag-hint {
+ opacity: 1;
+ animation: pulse-hint 2s infinite;
}
.blackboard-sidebar .variable-info {
@@ -920,4 +932,170 @@
font-size: 8px;
padding: 4px;
}
+}
+
+/* Blackboard 拖拽目标区域样式 */
+.blackboard-drop-zone {
+ margin-bottom: 8px;
+ padding: 8px;
+ border: 2px dashed #4a5568;
+ border-radius: 6px;
+ background: rgba(74, 85, 104, 0.1);
+ transition: all 0.3s ease;
+ cursor: pointer;
+ min-height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+}
+
+.blackboard-drop-zone:hover {
+ border-color: #667eea;
+ background: rgba(102, 126, 234, 0.05);
+}
+
+.blackboard-drop-zone.drag-over {
+ border-color: #667eea;
+ background: rgba(102, 126, 234, 0.15);
+ transform: scale(1.02);
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
+}
+
+.blackboard-drop-zone.has-reference {
+ border-color: #48bb78;
+ background: rgba(72, 187, 120, 0.1);
+ border-style: solid;
+}
+
+.blackboard-drop-zone.has-reference:hover {
+ background: rgba(72, 187, 120, 0.15);
+}
+
+.drop-placeholder {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: #a0aec0;
+ font-size: 12px;
+ text-align: center;
+ flex-direction: column;
+ padding: 4px;
+}
+
+.drop-icon {
+ font-size: 16px;
+ opacity: 0.7;
+}
+
+.drop-text {
+ font-weight: 500;
+ letter-spacing: 0.5px;
+}
+
+.blackboard-drop-zone .drop-placeholder {
+ transition: all 0.3s ease;
+}
+
+.blackboard-drop-zone:hover .drop-placeholder {
+ color: #667eea;
+}
+
+.blackboard-drop-zone:hover .drop-icon {
+ opacity: 1;
+ transform: scale(1.1);
+}
+
+.blackboard-drop-zone.drag-over .drop-placeholder {
+ color: #667eea;
+ transform: scale(1.05);
+}
+
+.blackboard-drop-zone.drag-over .drop-icon {
+ animation: bounce-drop 0.6s ease-in-out infinite;
+}
+
+@keyframes bounce-drop {
+ 0%, 100% {
+ transform: scale(1.1) translateY(0);
+ }
+ 50% {
+ transform: scale(1.2) translateY(-2px);
+ }
+}
+
+/* 输入框的Blackboard集成样式 */
+.property-item input.with-blackboard {
+ border-top: 1px solid #4a5568;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+
+.property-item input.with-blackboard:focus {
+ border-top-color: #667eea;
+}
+
+@keyframes pulse-hint {
+ 0%, 100% {
+ opacity: 0.7;
+ transform: translateY(-50%) scale(1);
+ }
+ 50% {
+ opacity: 1;
+ transform: translateY(-50%) scale(1.05);
+ }
+}
+
+@keyframes bounce-drop {
+ 0%, 100% {
+ transform: scale(1.1) translateY(0);
+ }
+ 50% {
+ transform: scale(1.2) translateY(-2px);
+ }
+}
+
+.blackboard-usage-hint {
+ margin: 8px 12px;
+ padding: 8px 12px;
+ background: rgba(102, 126, 234, 0.1);
+ border: 1px solid rgba(102, 126, 234, 0.2);
+ border-radius: 6px;
+ font-size: 11px;
+}
+
+.hint-content {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ color: #667eea;
+}
+
+.hint-icon {
+ font-size: 12px;
+ animation: pulse-hint 3s infinite;
+}
+
+.hint-text {
+ font-weight: 500;
+ letter-spacing: 0.3px;
+}
+
+/* 调试信息样式 */
+.property-item.debug-info {
+ background: rgba(255, 193, 7, 0.1);
+ border-left: 3px solid #ffc107;
+ padding: 8px 12px;
+ margin: 4px 0;
+ border-radius: 4px;
+}
+
+.debug-value {
+ font-family: 'Courier New', monospace;
+ background: rgba(0, 0, 0, 0.1);
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-size: 11px;
+ color: #e83e8c;
+ font-weight: bold;
}
\ No newline at end of file
diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html
index ec4383cf..562ca145 100644
--- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html
+++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html
@@ -287,7 +287,8 @@
'node-error': node.hasError,
'dragging': dragState.dragNode && dragState.dragNode.id === node.id,
'condition-hover': conditionDragState.hoveredDecoratorId === node.id,
- 'can-accept-condition': canAcceptCondition(node) && conditionDragState.isDraggingCondition
+ 'can-accept-condition': canAcceptCondition(node) && conditionDragState.isDraggingCondition,
+ 'has-attached-condition': node.type === 'conditional-decorator' && node.attachedCondition
}
]"
:style="{
@@ -321,6 +322,25 @@
{{ getConditionDisplayText(node) }}
📝
+
+
+
+
+
+ {{ key }}:
+ {{ value }}
+
+
+
+