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 }} +
+
+ + + + + + - -
- 🔗 - {{ prop.value }} - -

{{ prop.description }}

@@ -688,6 +725,14 @@ + +
+
+ 💡 + 拖拽变量名使用 +
+
+
- {{ variable.name }} - {{ getTypeDisplayName(variable.type) }} +
+ {{ variable.name }} + {{ getTypeDisplayName(variable.type) }} + 🎯 +
- - + +
@@ -720,6 +771,7 @@ v-if="variable.constraints && variable.constraints.allowedValues" v-model="variable.value" @change="onBlackboardValueChange(variable)" + @click.stop :disabled="variable.readOnly" >
\ No newline at end of file diff --git a/thirdparty/BehaviourTree-ai b/thirdparty/BehaviourTree-ai index 73c1d773..93719af8 160000 --- a/thirdparty/BehaviourTree-ai +++ b/thirdparty/BehaviourTree-ai @@ -1 +1 @@ -Subproject commit 73c1d77324c5f0c23817f16af6105e8de8e97c47 +Subproject commit 93719af820d72e23584f56d0f6d86e269c338efb