更新文档及优化行为树编辑器

This commit is contained in:
YHH
2025-06-19 10:38:31 +08:00
parent 82cd163adc
commit 8c86d6b696
20 changed files with 1608 additions and 286 deletions

View File

@@ -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) - 不同类型游戏的具体应用案例

View File

@@ -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();
}

View File

@@ -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);
}
}
```
### 定时器系统

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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 });
}
}

View File

@@ -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 => {

View File

@@ -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 = `
<div style="font-weight: bold; margin-bottom: 4px;">✅ 文件保存成功</div>
<div style="font-size: 12px; opacity: 0.9;">文件名: ${fileName}</div>
<div style="font-size: 11px; opacity: 0.7; margin-top: 2px;">路径: ${result.filePath}</div>
`;
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 = `
<div style="font-weight: bold; margin-bottom: 4px;">❌ 保存失败</div>
<div style="font-size: 12px;">${error?.message || error}</div>
`;
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)) {

View File

@@ -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) => {

View File

@@ -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<string, any>) => {
switch (conditionType) {
case 'condition-random':
return {
successProbability: {
type: 'number',
name: '成功概率',
value: decoratorProperties.successProbability || 0.5,
description: '条件成功的概率 (0.0 - 1.0)'
}
};
case 'condition-component':
return {
componentType: {
type: 'string',
name: '组件类型',
value: decoratorProperties.componentType || '',
description: '要检查的组件类型名称'
}
};
case 'condition-tag':
return {
tagValue: {
type: 'number',
name: '标签值',
value: decoratorProperties.tagValue || 0,
description: '要检查的标签值'
}
};
case 'condition-active':
return {
checkHierarchy: {
type: 'boolean',
name: '检查层级激活',
value: decoratorProperties.checkHierarchy || false,
description: '是否检查整个层级的激活状态'
}
};
case 'condition-numeric':
return {
propertyPath: {
type: 'string',
name: '属性路径',
value: decoratorProperties.propertyPath || 'context.someValue',
description: '要比较的数值属性路径'
},
compareOperator: {
type: 'select',
name: '比较操作符',
value: decoratorProperties.compareOperator || 'greater',
options: ['greater', 'less', 'equal', 'greaterEqual', 'lessEqual', 'notEqual'],
description: '数值比较的操作符'
},
compareValue: {
type: 'number',
name: '比较值',
value: decoratorProperties.compareValue || 0,
description: '用于比较的目标值'
}
};
case 'condition-property':
return {
propertyPath: {
type: 'string',
name: '属性路径',
value: decoratorProperties.propertyPath || 'context.someProperty',
description: '要检查的属性路径'
}
};
case 'condition-custom':
return {
conditionCode: {
type: 'code',
name: '条件代码',
value: decoratorProperties.conditionCode || '(context) => true',
description: '自定义条件判断函数'
}
};
// Blackboard相关条件使用实际的模板类型名
case 'blackboard-variable-exists':
return {
variableName: {
type: 'string',
name: '变量名',
value: decoratorProperties.variableName || '',
description: '要检查的黑板变量名'
},
invert: {
type: 'boolean',
name: '反转结果',
value: decoratorProperties.invert || false,
description: '是否反转检查结果'
}
};
case 'blackboard-value-comparison':
return {
variableName: {
type: 'string',
name: '变量名',
value: decoratorProperties.variableName || '',
description: '要比较的黑板变量名'
},
operator: {
type: 'select',
name: '比较操作符',
value: decoratorProperties.operator || 'equal',
options: ['equal', 'notEqual', 'greater', 'greaterOrEqual', 'less', 'lessOrEqual', 'contains', 'notContains'],
description: '比较操作类型'
},
compareValue: {
type: 'string',
name: '比较值',
value: decoratorProperties.compareValue || '',
description: '用于比较的值(留空则使用比较变量)'
},
compareVariable: {
type: 'string',
name: '比较变量名',
value: decoratorProperties.compareVariable || '',
description: '用于比较的另一个黑板变量名'
}
};
case 'blackboard-variable-type-check':
return {
variableName: {
type: 'string',
name: '变量名',
value: decoratorProperties.variableName || '',
description: '要检查的黑板变量名'
},
expectedType: {
type: 'select',
name: '期望类型',
value: decoratorProperties.expectedType || 'string',
options: ['string', 'number', 'boolean', 'vector2', 'vector3', 'object', 'array'],
description: '期望的变量类型'
}
};
case 'blackboard-variable-range-check':
return {
variableName: {
type: 'string',
name: '变量名',
value: decoratorProperties.variableName || '',
description: '要检查的数值型黑板变量名'
},
minValue: {
type: 'number',
name: '最小值',
value: decoratorProperties.minValue || 0,
description: '范围的最小值(包含)'
},
maxValue: {
type: 'number',
name: '最大值',
value: decoratorProperties.maxValue || 100,
description: '范围的最大值(包含)'
}
};
default:
// 对于未知的条件类型,尝试从装饰器属性中推断
const reconstructed: Record<string, any> = {};
Object.keys(decoratorProperties).forEach(key => {
if (key !== 'conditionType') {
reconstructed[key] = {
type: typeof decoratorProperties[key] === 'number' ? 'number' :
typeof decoratorProperties[key] === 'boolean' ? 'boolean' : 'string',
name: key,
value: decoratorProperties[key],
description: `${key}参数`
};
}
});
return reconstructed;
}
};
// 当前显示在属性面板的节点(普通节点或条件节点)
const activeNode = computed(() => selectedConditionNode.value || selectedNode.value);

View File

@@ -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<string, any> => {
if (!decoratorNode.attachedCondition || !decoratorNode.properties) {
return {};
}
const conditionType = decoratorNode.attachedCondition.type;
const visibleProps: Record<string, any> = {};
// 根据条件类型筛选相关属性
switch (conditionType) {
case 'condition-random':
if ('successProbability' in decoratorNode.properties) {
visibleProps['成功概率'] = `${(decoratorNode.properties.successProbability * 100).toFixed(1)}%`;
}
break;
case 'condition-component':
if ('componentType' in decoratorNode.properties) {
visibleProps['组件类型'] = decoratorNode.properties.componentType;
}
break;
case 'condition-tag':
if ('tagValue' in decoratorNode.properties) {
visibleProps['标签值'] = decoratorNode.properties.tagValue;
}
break;
case 'condition-active':
if ('checkHierarchy' in decoratorNode.properties) {
visibleProps['检查层级'] = decoratorNode.properties.checkHierarchy ? '是' : '否';
}
break;
case 'condition-numeric':
if ('propertyPath' in decoratorNode.properties) {
visibleProps['属性路径'] = decoratorNode.properties.propertyPath;
}
if ('compareOperator' in decoratorNode.properties) {
visibleProps['比较操作'] = decoratorNode.properties.compareOperator;
}
if ('compareValue' in decoratorNode.properties) {
visibleProps['比较值'] = decoratorNode.properties.compareValue;
}
break;
case 'condition-property':
if ('propertyPath' in decoratorNode.properties) {
visibleProps['属性路径'] = decoratorNode.properties.propertyPath;
}
break;
case 'blackboard-variable-exists':
if ('variableName' in decoratorNode.properties) {
visibleProps['变量名'] = decoratorNode.properties.variableName;
}
if ('invert' in decoratorNode.properties) {
visibleProps['反转结果'] = decoratorNode.properties.invert ? '是' : '否';
}
break;
case 'blackboard-value-comparison':
if ('variableName' in decoratorNode.properties) {
visibleProps['变量名'] = decoratorNode.properties.variableName;
}
if ('operator' in decoratorNode.properties) {
visibleProps['操作符'] = decoratorNode.properties.operator;
}
if ('compareValue' in decoratorNode.properties) {
visibleProps['比较值'] = decoratorNode.properties.compareValue;
}
if ('compareVariable' in decoratorNode.properties) {
visibleProps['比较变量'] = decoratorNode.properties.compareVariable;
}
break;
case 'blackboard-variable-type-check':
if ('variableName' in decoratorNode.properties) {
visibleProps['变量名'] = decoratorNode.properties.variableName;
}
if ('expectedType' in decoratorNode.properties) {
visibleProps['期望类型'] = decoratorNode.properties.expectedType;
}
break;
case 'blackboard-variable-range-check':
if ('variableName' in decoratorNode.properties) {
visibleProps['变量名'] = decoratorNode.properties.variableName;
}
if ('minValue' in decoratorNode.properties) {
visibleProps['最小值'] = decoratorNode.properties.minValue;
}
if ('maxValue' in decoratorNode.properties) {
visibleProps['最大值'] = decoratorNode.properties.maxValue;
}
break;
}
return visibleProps;
};
/**
* 切换条件展开状态
*/
const toggleConditionExpanded = (decoratorNode: TreeNode) => {
decoratorNode.conditionExpanded = !decoratorNode.conditionExpanded;
};
/**
@@ -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<string, any> = {};
// 条件装饰器的基础属性
const baseDecoratorProperties = [
'executeWhenTrue',
'executeWhenFalse',
'checkInterval',
'abortType'
];
// 保留基础属性
if (decoratorNode.properties) {
baseDecoratorProperties.forEach(key => {
if (key in decoratorNode.properties!) {
preservedProperties[key] = decoratorNode.properties![key];
}
});
}
// 重置为只包含基础属性的对象
decoratorNode.properties = preservedProperties;
}
};
@@ -339,6 +475,8 @@ export function useConditionAttachment(
getConditionDisplayText,
removeConditionFromDecorator,
canAcceptCondition,
isConditionalDecorator
isConditionalDecorator,
toggleConditionExpanded,
getConditionProperties
};
}
}

View File

@@ -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;
}
}
};

View File

@@ -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) - 叶子节点,不能有子节点

View File

@@ -22,6 +22,11 @@ export interface TreeNode {
name: string;
icon: string;
};
// 条件节点相关(用于虚拟条件节点)
isConditionNode?: boolean;
parentDecorator?: TreeNode;
// 条件显示状态
conditionExpanded?: boolean; // 条件是否展开显示详细信息
}
export interface Connection {

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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 @@
<span class="condition-text">{{ getConditionDisplayText(node) }}</span>
<span class="edit-hint">📝</span>
</div>
<!-- 展开时显示条件属性 -->
<div v-if="node.conditionExpanded" class="condition-properties">
<div class="properties-divider"></div>
<div
v-for="(value, key) in getConditionProperties(node)"
:key="key"
class="condition-property-item"
>
<span class="condition-property-label">{{ key }}:</span>
<span class="condition-property-value">{{ value }}</span>
</div>
</div>
<button
class="toggle-condition-btn"
@click.stop="toggleConditionExpanded(node)"
:title="node.conditionExpanded ? '收缩详情' : '展开详情'"
>{{ node.conditionExpanded ? '▲' : '▼' }}</button>
<button
class="remove-condition-btn"
@click.stop="removeConditionFromDecorator(node)"
@@ -440,13 +460,35 @@
>
<label>{{ prop.name }}:</label>
<div class="property-input-container">
<!-- Blackboard 拖拽目标区域 -->
<div
v-if="isBlackboardDroppable(prop)"
class="blackboard-drop-zone"
:class="{ 'has-reference': isBlackboardReference(prop.value) }"
@drop="handleBlackboardDrop($event, key)"
@dragover="handleBlackboardDragOver"
@dragleave="handleBlackboardDragLeave"
>
<div v-if="!isBlackboardReference(prop.value)" class="drop-placeholder">
<span class="drop-icon">📋</span>
<span class="drop-text">拖拽Blackboard变量到此处</span>
</div>
<div v-else class="blackboard-reference">
<span class="ref-icon">🔗</span>
<span class="ref-text">{{ prop.value }}</span>
<button class="clear-ref" @click="clearBlackboardReference(key)" title="清除引用">×</button>
</div>
</div>
<!-- 输入控件 -->
<input
v-if="prop.type === 'string'"
type="text"
:value="prop.value"
@input="updateNodeProperty('properties.' + key + '.value', $event.target.value)"
:key="activeNode.id + '_' + key + '_string'"
:placeholder="'拖拽Blackboard变量或直接输入'"
:placeholder="isBlackboardDroppable(prop) ? '或直接输入值' : '请输入值'"
:class="{ 'with-blackboard': isBlackboardDroppable(prop) }"
>
<input
v-else-if="prop.type === 'number'"
@@ -454,7 +496,8 @@
:value="prop.value"
@input="updateNodeProperty('properties.' + key + '.value', parseFloat($event.target.value) || 0)"
:key="activeNode.id + '_' + key + '_number'"
:placeholder="'拖拽Blackboard变量或直接输入'"
:placeholder="isBlackboardDroppable(prop) ? '或直接输入值' : '请输入值'"
:class="{ 'with-blackboard': isBlackboardDroppable(prop) }"
>
<input
v-else-if="prop.type === 'boolean'"
@@ -487,12 +530,6 @@
{{ option }}
</option>
</select>
<div v-if="isBlackboardReference(prop.value)" class="blackboard-reference">
<span class="ref-icon">🔗</span>
<span class="ref-text">{{ prop.value }}</span>
<button class="clear-ref" @click="clearBlackboardReference(key)" title="清除引用">×</button>
</div>
</div>
<p v-if="prop.description" class="property-help">{{ prop.description }}</p>
</div>
@@ -688,6 +725,14 @@
<button @click="clearBlackboard" v-if="blackboardVariables.length > 0" class="clear-btn">清空</button>
</div>
<!-- 使用提示 -->
<div v-if="blackboardVariables.length > 0" class="blackboard-usage-hint">
<div class="hint-content">
<span class="hint-icon">💡</span>
<span class="hint-text">拖拽变量名使用</span>
</div>
</div>
<div class="blackboard-scroll-area">
<div v-if="blackboardVariables.length > 0" class="blackboard-groups">
<div
@@ -702,16 +747,22 @@
:key="variable.name"
class="variable-item"
:class="variable.type"
:draggable="true"
@dragstart="onBlackboardDragStart($event, variable)"
:title="variable.description"
>
<div class="variable-header">
<span class="variable-name">{{ variable.name }}</span>
<span class="variable-type">{{ getTypeDisplayName(variable.type) }}</span>
<div
class="variable-drag-area"
:draggable="true"
@dragstart="onBlackboardDragStart($event, variable)"
@click.stop
>
<span class="variable-name">{{ variable.name }}</span>
<span class="variable-type">{{ getTypeDisplayName(variable.type) }}</span>
<span class="drag-hint">🎯</span>
</div>
<div class="variable-actions">
<button @click="editBlackboardVariable(variable)" class="edit-btn">✏️</button>
<button @click="removeBlackboardVariable(variable.name)" class="remove-btn">🗑️</button>
<button @click.stop="editBlackboardVariable(variable)" class="edit-btn">✏️</button>
<button @click.stop="removeBlackboardVariable(variable.name)" class="remove-btn">🗑️</button>
</div>
</div>
@@ -720,6 +771,7 @@
v-if="variable.constraints && variable.constraints.allowedValues"
v-model="variable.value"
@change="onBlackboardValueChange(variable)"
@click.stop
:disabled="variable.readOnly"
>
<option
@@ -755,28 +807,51 @@
<div v-if="showExportModal" class="modal-overlay" @click="showExportModal = false">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>导出配置</h3>
<h3>⚡ 导出行为树配置</h3>
<button @click="showExportModal = false">×</button>
</div>
<div class="modal-body">
<div class="export-options">
<h4 style="margin: 0 0 12px 0; color: #e2e8f0; font-size: 14px;">📄 选择导出格式:</h4>
<label>
<input type="radio" v-model="exportFormat" value="json"> JSON配置
<input type="radio" v-model="exportFormat" value="json">
<span>📄 JSON配置文件</span>
<small style="display: block; margin-left: 24px; color: #a0aec0; font-size: 11px;">适用于运行时动态加载行为树</small>
</label>
<label>
<input type="radio" v-model="exportFormat" value="typescript"> TypeScript代码
<label class="disabled-option">
<input type="radio" v-model="exportFormat" value="typescript" disabled>
<span>📝 TypeScript代码 (暂不可用)</span>
<small style="display: block; margin-left: 24px; color: #6b7280; font-size: 11px;">此功能正在开发中,敬请期待</small>
</label>
</div>
<textarea
class="export-code"
:value="exportedCode()"
readonly
></textarea>
<div class="preview-section">
<h4 style="margin: 0 0 8px 0; color: #e2e8f0; font-size: 13px;">📋 代码预览:</h4>
<textarea
class="export-code"
:value="exportedCode()"
readonly
></textarea>
</div>
<div class="usage-hint" style="margin-top: 16px; padding: 12px; background: rgba(102, 126, 234, 0.08); border: 1px solid rgba(102, 126, 234, 0.2); border-radius: 6px;">
<div style="color: #a0aec0; font-size: 12px; line-height: 1.4;">
<strong style="color: #e2e8f0;">💡 使用提示:</strong><br/>
<span v-if="exportFormat === 'json'">
• JSON配置可用于运行时动态加载行为树<br/>
• 使用 BehaviorTreeBuilder.fromConfig() 方法构建行为树<br/>
• 可以保存为 .json 文件在项目中使用
</span>
<span v-else>
• 当前仅支持JSON格式导出<br/>
• TypeScript代码生成功能正在开发中
</span>
</div>
</div>
</div>
<div class="modal-footer">
<button @click="copyToClipboard">复制到剪贴板</button>
<button @click="saveToFile">保存到文件</button>
<button @click="showExportModal = false">关闭</button>
<button @click="copyToClipboard" class="copy-btn">📋 复制到剪贴板</button>
<button @click="saveToFile" class="save-file-btn">💾 保存到文件</button>
<button @click="showExportModal = false" class="close-btn">关闭</button>
</div>
</div>
</div>