支持先加入实体后加入系统以让matcher进行实体匹配/优化行为树节点效果及逻辑

This commit is contained in:
YHH
2025-06-19 15:00:14 +08:00
parent 8c86d6b696
commit 310f5f2349
22 changed files with 1809 additions and 84 deletions

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { ref } from 'vue';
import { TreeNode, DragState, Connection } from '../types';
import { nodeTemplates } from '../data/nodeTemplates';
import { allNodeTemplates as nodeTemplates } from '../data/nodeTemplates';
/**
* 应用状态管理

View File

@@ -36,7 +36,11 @@ export function useBehaviorTreeEditor() {
appState.treeNodes,
appState.nodeTemplates,
appState.getNodeByIdLocal,
getRootNode
getRootNode,
computed(() => blackboard.blackboardVariables.value.reduce((map, variable) => {
map.set(variable.name, variable);
return map;
}, new Map()))
);
const computedProps = useComputedProperties(
@@ -115,7 +119,14 @@ export function useBehaviorTreeEditor() {
tempConnection: appState.tempConnection,
showExportModal: appState.showExportModal,
codeGeneration: codeGen,
updateConnections: connectionManager.updateConnections
updateConnections: connectionManager.updateConnections,
blackboardOperations: {
getBlackboardVariables: () => blackboard.blackboardVariables.value,
loadBlackboardVariables: (variables: any[]) => {
blackboard.loadBlackboardFromArray(variables);
},
clearBlackboard: blackboard.clearBlackboard
}
});
const canvasManager = useCanvasManager(
@@ -250,6 +261,51 @@ export function useBehaviorTreeEditor() {
}
};
// 节点类型识别相关方法
const getOriginalNodeName = (nodeType: string): string => {
const template = appState.nodeTemplates.value.find(t => t.type === nodeType);
return template?.name || nodeType;
};
const getNodeTemplate = (nodeType: string) => {
return appState.nodeTemplates.value.find(t => t.type === nodeType);
};
const getNodeCategory = (nodeType: string): string => {
const template = getNodeTemplate(nodeType);
if (!template) return 'unknown';
const category = template.category || 'unknown';
const categoryMap: Record<string, string> = {
'root': '根节点',
'composite': '组合',
'decorator': '装饰器',
'action': '动作',
'condition': '条件',
'ecs': 'ECS'
};
return categoryMap[category] || category;
};
const isNodeNameCustomized = (node: any): boolean => {
if (!node) return false;
const originalName = getOriginalNodeName(node.type);
return node.name !== originalName;
};
const resetNodeToOriginalName = () => {
if (!appState.selectedNodeId.value) return;
const selectedNode = appState.getNodeByIdLocal(appState.selectedNodeId.value);
if (!selectedNode) return;
const originalName = getOriginalNodeName(selectedNode.type);
nodeOps.updateNodeProperty('name', originalName);
console.log(`节点名称已重置为原始名称: ${originalName}`);
};
const startNodeDrag = (event: MouseEvent, node: any) => {
event.stopPropagation();
event.preventDefault();
@@ -306,12 +362,13 @@ export function useBehaviorTreeEditor() {
installation.handleInstall();
};
// 自动布局功能
// 紧凑子树布局算法 - 体现行为树的层次结构
const autoLayout = () => {
if (appState.treeNodes.value.length === 0) {
return;
}
// 找到根节点
const rootNode = appState.treeNodes.value.find(node =>
!appState.treeNodes.value.some(otherNode =>
otherNode.children?.includes(node.id)
@@ -319,57 +376,225 @@ export function useBehaviorTreeEditor() {
);
if (!rootNode) {
console.warn('未找到根节点,无法进行自动布局');
return;
}
const levelNodes: { [level: number]: any[] } = {};
const visited = new Set<string>();
const queue = [{ node: rootNode, level: 0 }];
while (queue.length > 0) {
const { node, level } = queue.shift()!;
// 计算节点尺寸
const getNodeSize = (node: any) => {
let width = 180;
let height = 100;
if (visited.has(node.id)) continue;
visited.add(node.id);
if (!levelNodes[level]) {
levelNodes[level] = [];
// 根据节点类型调整基础尺寸
switch (node.category || node.type) {
case 'root':
width = 200; height = 70;
break;
case 'composite':
width = 160; height = 90;
break;
case 'decorator':
width = 140; height = 80;
break;
case 'action':
width = 180; height = 100;
break;
case 'condition':
width = 150; height = 85;
break;
}
levelNodes[level].push(node);
if (node.children && Array.isArray(node.children)) {
// 根据属性数量动态调整
if (node.properties) {
const propertyCount = Object.keys(node.properties).length;
height += propertyCount * 20;
}
// 根据名称长度调整宽度
if (node.name) {
const nameWidth = node.name.length * 8 + 40;
width = Math.max(width, nameWidth);
}
return { width, height };
};
// 紧凑子树布局核心算法
const layoutSubtree = (node: any, parentX = 0, parentY = 0, depth = 0): { width: number, height: number } => {
const nodeSize = getNodeSize(node);
// 如果是叶子节点,直接返回自身尺寸
if (!node.children || node.children.length === 0) {
node.x = parentX;
node.y = parentY;
return { width: nodeSize.width, height: nodeSize.height };
}
// 递归布局所有子节点,收集子树信息
const childSubtrees: Array<{ node: any, width: number, height: number }> = [];
let totalChildrenWidth = 0;
let maxChildHeight = 0;
const childY = parentY + nodeSize.height + 60; // 子节点距离父节点的垂直间距
const siblingSpacing = 40; // 同级子节点间的水平间距
// 先计算每个子树的尺寸
node.children.forEach((childId: string) => {
const childNode = appState.treeNodes.value.find(n => n.id === childId);
if (childNode) {
const subtreeInfo = layoutSubtree(childNode, 0, childY, depth + 1);
childSubtrees.push({ node: childNode, ...subtreeInfo });
totalChildrenWidth += subtreeInfo.width;
maxChildHeight = Math.max(maxChildHeight, subtreeInfo.height);
}
});
// 添加子节点间的间距
if (childSubtrees.length > 1) {
totalChildrenWidth += (childSubtrees.length - 1) * siblingSpacing;
}
// 计算父节点的最终位置(在子节点的中心上方)
const subtreeWidth = Math.max(nodeSize.width, totalChildrenWidth);
node.x = parentX + subtreeWidth / 2 - nodeSize.width / 2;
node.y = parentY;
// 布局子节点(以父节点为中心分布)
let currentX = parentX + subtreeWidth / 2 - totalChildrenWidth / 2;
childSubtrees.forEach(({ node: childNode, width: childWidth }) => {
// 将子节点定位到其子树的中心
const childCenterOffset = childWidth / 2;
childNode.x = currentX + childCenterOffset - getNodeSize(childNode).width / 2;
// 递归调整子树中所有节点的位置
adjustSubtreePosition(childNode, currentX, childY);
currentX += childWidth + siblingSpacing;
});
// 返回整个子树的尺寸
const subtreeHeight = nodeSize.height + 60 + maxChildHeight;
return { width: subtreeWidth, height: subtreeHeight };
};
// 递归调整子树位置
const adjustSubtreePosition = (node: any, baseX: number, baseY: number) => {
const nodeSize = getNodeSize(node);
if (!node.children || node.children.length === 0) {
return;
}
// 计算子节点的总宽度
let totalChildrenWidth = 0;
const siblingSpacing = 40;
node.children.forEach((childId: string) => {
const childNode = appState.treeNodes.value.find(n => n.id === childId);
if (childNode) {
const childSubtreeWidth = calculateSubtreeWidth(childNode);
totalChildrenWidth += childSubtreeWidth;
}
});
if (node.children.length > 1) {
totalChildrenWidth += (node.children.length - 1) * siblingSpacing;
}
// 重新定位子节点
let currentX = baseX + Math.max(nodeSize.width, totalChildrenWidth) / 2 - totalChildrenWidth / 2;
const childY = baseY + nodeSize.height + 60;
node.children.forEach((childId: string) => {
const childNode = appState.treeNodes.value.find(n => n.id === childId);
if (childNode) {
const childSubtreeWidth = calculateSubtreeWidth(childNode);
const childCenterOffset = childSubtreeWidth / 2;
childNode.x = currentX + childCenterOffset - getNodeSize(childNode).width / 2;
childNode.y = childY;
adjustSubtreePosition(childNode, currentX, childY);
currentX += childSubtreeWidth + siblingSpacing;
}
});
};
// 计算子树宽度
const calculateSubtreeWidth = (node: any): number => {
const nodeSize = getNodeSize(node);
if (!node.children || node.children.length === 0) {
return nodeSize.width;
}
let totalChildrenWidth = 0;
const siblingSpacing = 40;
node.children.forEach((childId: string) => {
const childNode = appState.treeNodes.value.find(n => n.id === childId);
if (childNode) {
totalChildrenWidth += calculateSubtreeWidth(childNode);
}
});
if (node.children.length > 1) {
totalChildrenWidth += (node.children.length - 1) * siblingSpacing;
}
return Math.max(nodeSize.width, totalChildrenWidth);
};
// 开始布局 - 从根节点开始
const startX = 400; // 画布中心X
const startY = 50; // 顶部留白
const treeInfo = layoutSubtree(rootNode, startX, startY);
// 处理孤立节点
const connectedNodeIds = new Set<string>();
const collectConnectedNodes = (node: any) => {
connectedNodeIds.add(node.id);
if (node.children) {
node.children.forEach((childId: string) => {
const childNode = appState.treeNodes.value.find(n => n.id === childId);
if (childNode && !visited.has(childId)) {
queue.push({ node: childNode, level: level + 1 });
if (childNode) {
collectConnectedNodes(childNode);
}
});
}
};
collectConnectedNodes(rootNode);
const orphanNodes = appState.treeNodes.value.filter(node => !connectedNodeIds.has(node.id));
if (orphanNodes.length > 0) {
const orphanY = startY + treeInfo.height + 100;
orphanNodes.forEach((node, index) => {
node.x = startX + (index - orphanNodes.length / 2) * 200;
node.y = orphanY + Math.floor(index / 5) * 120;
});
}
const nodeWidth = 200;
const nodeHeight = 150;
const startX = 400;
const startY = 100;
Object.keys(levelNodes).forEach(levelStr => {
const level = parseInt(levelStr);
const nodes = levelNodes[level];
const totalWidth = (nodes.length - 1) * nodeWidth;
const offsetX = -totalWidth / 2;
nodes.forEach((node, index) => {
node.x = startX + offsetX + index * nodeWidth;
node.y = startY + level * nodeHeight;
});
});
setTimeout(() => {
// 强制更新连接线
const forceUpdateConnections = () => {
connectionManager.updateConnections();
}, 100);
nextTick(() => {
connectionManager.updateConnections();
setTimeout(() => {
connectionManager.updateConnections();
}, 150);
});
};
forceUpdateConnections();
console.log(`紧凑子树布局完成:${appState.treeNodes.value.length} 个节点已重新排列`);
};
// 验证树结构
const validateTree = () => {
// 使用改进的验证函数
@@ -830,6 +1055,13 @@ export function useBehaviorTreeEditor() {
handleBlackboardDragLeave,
clearBlackboardReference,
// 节点类型识别方法
getOriginalNodeName,
getNodeTemplate,
getNodeCategory,
isNodeNameCustomized,
resetNodeToOriginalName,
blackboardCollapsed: computed({
get: () => blackboardSidebarState.collapsed,
set: (value: boolean) => blackboardSidebarState.collapsed = value

View File

@@ -297,6 +297,17 @@ export function useBlackboard() {
}
};
const resetBlackboardToDefaults = () => {
if (confirm('确定要重置所有变量到默认值吗?')) {
blackboardVariables.value.forEach((variable, name) => {
if (variable.defaultValue !== undefined) {
variable.value = variable.defaultValue;
blackboardVariables.value.set(name, { ...variable });
}
});
}
};
const exportBlackboard = () => {
const data = Array.from(blackboardVariables.value.values());
const json = JSON.stringify(data, null, 2);
@@ -309,6 +320,19 @@ export function useBlackboard() {
}
};
const loadBlackboardFromArray = (variables: BlackboardVariable[]) => {
blackboardVariables.value.clear();
variables.forEach(variable => {
if (variable.name && variable.type) {
blackboardVariables.value.set(variable.name, variable);
// 展开变量所在的组
const groupName = variable.group || '未分组';
expandedGroups.value.add(groupName);
}
});
};
const importBlackboard = () => {
const input = document.createElement('input');
input.type = 'file';
@@ -436,6 +460,8 @@ export function useBlackboard() {
editVariable,
selectVariable,
clearBlackboard,
resetBlackboardToDefaults,
loadBlackboardFromArray,
exportBlackboard,
importBlackboard,

View File

@@ -9,7 +9,8 @@ export function useCodeGeneration(
treeNodes: Ref<TreeNode[]>,
nodeTemplates: Ref<NodeTemplate[]>,
getNodeByIdLocal: (id: string) => TreeNode | undefined,
rootNode: () => TreeNode | null
rootNode: () => TreeNode | null,
blackboardVariables?: Ref<Map<string, any>>
) {
// 生成行为树配置JSON
@@ -20,7 +21,7 @@ export function useCodeGeneration(
return null;
}
return {
const config: any = {
version: "1.0.0",
type: "behavior-tree",
metadata: {
@@ -30,6 +31,13 @@ export function useCodeGeneration(
},
tree: generateNodeConfig(root)
};
// 包含黑板数据
if (blackboardVariables && blackboardVariables.value.size > 0) {
config.blackboard = Array.from(blackboardVariables.value.values());
}
return config;
};
// 生成可读的配置JSON字符串

View File

@@ -462,25 +462,49 @@ export function useConnectionManager(
updateConnections();
};
// 更新连接线
// 改进的连接线更新方法
const updateConnections = () => {
// 立即清空现有连接
connections.value.length = 0;
// 添加一个小延迟确保DOM已经更新
setTimeout(() => {
treeNodes.value.forEach(node => {
if (node.children) {
// 创建新的连接数据
const newConnections: Connection[] = [];
// 遍历所有节点建立连接
treeNodes.value.forEach(node => {
if (node.children && node.children.length > 0) {
node.children.forEach(childId => {
const childNode = treeNodes.value.find(n => n.id === childId);
if (childNode) {
// 尝试获取端口位置
const parentPos = getPortPosition(node.id, 'output');
const childPos = getPortPosition(childId, 'input');
if (parentPos && childPos) {
const controlOffset = Math.abs(childPos.y - parentPos.y) * 0.5;
// 计算贝塞尔曲线路径
const deltaY = Math.abs(childPos.y - parentPos.y);
const controlOffset = Math.max(30, Math.min(deltaY * 0.5, 80));
const path = `M ${parentPos.x} ${parentPos.y} C ${parentPos.x} ${parentPos.y + controlOffset} ${childPos.x} ${childPos.y - controlOffset} ${childPos.x} ${childPos.y}`;
connections.value.push({
newConnections.push({
id: `${node.id}-${childId}`,
sourceId: node.id,
targetId: childId,
path: path,
active: false
});
} else {
// 如果无法获取实际位置,使用计算位置作为后备
const fallbackParentPos = getCalculatedPortPosition(node, 'output');
const fallbackChildPos = getCalculatedPortPosition(childNode, 'input');
const deltaY = Math.abs(fallbackChildPos.y - fallbackParentPos.y);
const controlOffset = Math.max(30, Math.min(deltaY * 0.5, 80));
const path = `M ${fallbackParentPos.x} ${fallbackParentPos.y} C ${fallbackParentPos.x} ${fallbackParentPos.y + controlOffset} ${fallbackChildPos.x} ${fallbackChildPos.y - controlOffset} ${fallbackChildPos.x} ${fallbackChildPos.y}`;
newConnections.push({
id: `${node.id}-${childId}`,
sourceId: node.id,
targetId: childId,
@@ -492,7 +516,50 @@ export function useConnectionManager(
});
}
});
}, 50); // 50ms延迟确保DOM渲染完成
// 批量更新连接
connections.value.push(...newConnections);
// 如果有DOM元素进行二次精确更新
if (canvasAreaRef.value) {
setTimeout(() => {
// 二次更新使用实际DOM位置
const updatedConnections: Connection[] = [];
treeNodes.value.forEach(node => {
if (node.children && node.children.length > 0) {
node.children.forEach(childId => {
const childNode = treeNodes.value.find(n => n.id === childId);
if (childNode) {
const parentPos = getPortPosition(node.id, 'output');
const childPos = getPortPosition(childId, 'input');
if (parentPos && childPos) {
const deltaY = Math.abs(childPos.y - parentPos.y);
const controlOffset = Math.max(30, Math.min(deltaY * 0.5, 80));
const path = `M ${parentPos.x} ${parentPos.y} C ${parentPos.x} ${parentPos.y + controlOffset} ${childPos.x} ${childPos.y - controlOffset} ${childPos.x} ${childPos.y}`;
updatedConnections.push({
id: `${node.id}-${childId}`,
sourceId: node.id,
targetId: childId,
path: path,
active: false
});
}
}
});
}
});
// 如果二次更新得到了有效结果,替换连接数据
if (updatedConnections.length > 0) {
connections.value.length = 0;
connections.value.push(...updatedConnections);
}
}, 100); // 100ms延迟确保DOM完全渲染
}
};
// 删除连接线

View File

@@ -11,11 +11,17 @@ interface FileOperationOptions {
createTreeFromConfig: (config: any) => TreeNode[];
};
updateConnections?: () => void;
blackboardOperations?: {
getBlackboardVariables: () => any[];
loadBlackboardVariables: (variables: any[]) => void;
clearBlackboard: () => void;
};
}
interface FileData {
nodes: TreeNode[];
connections: Connection[];
blackboard?: any[];
metadata: {
name: string;
created: string;
@@ -31,7 +37,8 @@ export function useFileOperations(options: FileOperationOptions) {
tempConnection,
showExportModal,
codeGeneration,
updateConnections
updateConnections,
blackboardOperations
} = options;
const hasUnsavedChanges = ref(false);
@@ -70,7 +77,7 @@ export function useFileOperations(options: FileOperationOptions) {
};
const exportBehaviorTreeData = (): FileData => {
return {
const data: FileData = {
nodes: treeNodes.value,
connections: connections.value,
metadata: {
@@ -79,6 +86,16 @@ export function useFileOperations(options: FileOperationOptions) {
version: '1.0'
}
};
// 包含黑板数据
if (blackboardOperations) {
const blackboardVariables = blackboardOperations.getBlackboardVariables();
if (blackboardVariables.length > 0) {
data.blackboard = blackboardVariables;
}
}
return data;
};
const showMessage = (message: string, type: 'success' | 'error' = 'success') => {
@@ -165,6 +182,12 @@ export function useFileOperations(options: FileOperationOptions) {
selectedNodeId.value = null;
connections.value = [];
tempConnection.value.path = '';
// 清空黑板
if (blackboardOperations) {
blackboardOperations.clearBlackboard();
}
clearCurrentFile();
markAsSaved();
}
@@ -409,6 +432,11 @@ export function useFileOperations(options: FileOperationOptions) {
setCurrentFile('untitled', filePath);
}
// 加载黑板数据
if (blackboardOperations && parsedData.blackboard && Array.isArray(parsedData.blackboard)) {
blackboardOperations.loadBlackboardVariables(parsedData.blackboard);
}
selectedNodeId.value = null;
tempConnection.value.path = '';
@@ -463,6 +491,11 @@ export function useFileOperations(options: FileOperationOptions) {
tempConnection.value.path = '';
// 加载黑板数据
if (blackboardOperations && config.blackboard && Array.isArray(config.blackboard)) {
blackboardOperations.loadBlackboardVariables(config.blackboard);
}
const fileName = file.name.replace(/\.(json|bt)$/, '');
setCurrentFile(fileName, '');

View File

@@ -1106,4 +1106,10 @@ export const nodeTemplates: NodeTemplate[] = [
}
}
}
];
];
// 导出所有节点模板
export const allNodeTemplates: NodeTemplate[] = nodeTemplates;
// 为了保持向后兼容,保留原来的导出
export { nodeTemplates as default };

View File

@@ -389,14 +389,15 @@
.form-textarea {
width: 100%;
padding: 12px 16px;
background: #1a202c;
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
border: 2px solid #4a5568;
border-radius: 8px;
color: white;
color: #e2e8f0;
font-size: 13px;
font-family: inherit;
transition: all 0.2s ease;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-sizing: border-box;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.form-input:focus,
@@ -404,14 +405,35 @@
.form-textarea:focus {
border-color: #667eea;
outline: none;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2), 0 4px 12px rgba(102, 126, 234, 0.15);
transform: translateY(-1px);
}
.form-input:hover,
.form-select:hover,
.form-textarea:hover {
border-color: #718096;
border-color: #667eea;
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
/* 美化表单下拉选择框 */
.form-select {
appearance: none;
background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 5"><path fill="%23718096" d="M2 0L0 2h4zm0 5L0 3h4z"/></svg>');
background-repeat: no-repeat;
background-position: right 16px center;
background-size: 12px;
padding-right: 48px;
cursor: pointer;
}
.form-select option {
background: #2d3748;
color: #e2e8f0;
padding: 8px 12px;
}
.form-input::placeholder,
@@ -522,8 +544,40 @@
}
.allowed-values textarea {
margin-top: 6px;
margin-top: 8px;
resize: vertical;
min-height: 100px;
font-family: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
font-size: 12px;
line-height: 1.5;
background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%);
border: 2px solid #4a5568;
border-radius: 8px;
color: #e2e8f0;
padding: 12px 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.allowed-values textarea:focus {
border-color: #667eea;
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2), 0 4px 12px rgba(102, 126, 234, 0.15);
transform: translateY(-1px);
outline: none;
}
.allowed-values textarea:hover {
border-color: #667eea;
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.allowed-values textarea::placeholder {
color: #718096;
font-style: italic;
opacity: 0.8;
}
.checkbox-label {

View File

@@ -328,4 +328,66 @@
opacity: 1;
transform: scale(1);
}
}
}
/* 节点类型提示 */
.tree-node[data-original-name]:not([data-original-name=""]):hover::after {
content: "原始: " attr(data-original-name);
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.9);
color: #f7fafc;
padding: 4px 8px;
border-radius: 4px;
font-size: 10px;
white-space: nowrap;
z-index: 1000;
pointer-events: none;
opacity: 0;
animation: tooltipFadeIn 0.3s ease forwards;
border: 1px solid #4a5568;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.tree-node.node-selected[data-original-name]:not([data-original-name=""]) .node-header::before {
content: "📝";
position: absolute;
top: -8px;
right: -8px;
background: #f59e0b;
color: white;
width: 16px;
height: 16px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
border: 2px solid #2d3748;
z-index: 5;
}
@keyframes tooltipFadeIn {
from {
opacity: 0;
transform: translateX(-50%) translateY(-5px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
/* 节点标题自定义指示器 */
.node-title.customized {
color: #f6ad55;
position: relative;
}
.node-title.customized::after {
content: " ✏️";
font-size: 10px;
opacity: 0.7;
}

View File

@@ -235,13 +235,24 @@
.property-item select {
width: 100%;
padding: 8px 12px;
background: #1a202c;
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
border: 1px solid #4a5568;
border-radius: 4px;
color: white;
border-radius: 6px;
color: #e2e8f0;
font-size: 12px;
font-family: inherit;
box-sizing: border-box;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.property-item input:hover,
.property-item textarea:hover,
.property-item select:hover {
border-color: #667eea;
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.property-item input:focus,
@@ -249,7 +260,26 @@
.property-item select:focus {
border-color: #667eea;
outline: none;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2), 0 4px 12px rgba(102, 126, 234, 0.15);
transform: translateY(-1px);
}
/* 美化下拉选择框箭头 */
.property-item select {
appearance: none;
background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 5"><path fill="%23718096" d="M2 0L0 2h4zm0 5L0 3h4z"/></svg>');
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 10px;
padding-right: 36px;
cursor: pointer;
}
.property-item select option {
background: #2d3748;
color: #e2e8f0;
padding: 8px 12px;
}
.property-item .code-input {
@@ -521,6 +551,16 @@
transform: translateY(-1px);
}
.blackboard-sidebar .reset-btn {
background: #ed8936;
color: white;
}
.blackboard-sidebar .reset-btn:hover {
background: #dd6b20;
transform: translateY(-1px);
}
.blackboard-sidebar .clear-btn {
background: #f56565;
color: white;
@@ -812,6 +852,112 @@
font-style: italic;
}
/* 黑板变量值区域样式 */
.blackboard-sidebar .variable-value {
margin-top: 6px;
min-height: 24px;
display: flex;
align-items: center;
}
/* 美化约束值下拉选择框 */
.blackboard-sidebar .variable-value select {
width: 100%;
padding: 6px 12px;
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
border: 1px solid #4a5568;
border-radius: 6px;
color: #e2e8f0;
font-family: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
appearance: none;
background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 5"><path fill="%23718096" d="M2 0L0 2h4zm0 5L0 3h4z"/></svg>');
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 8px;
padding-right: 28px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
/* 下拉框 hover 状态 */
.blackboard-sidebar .variable-value select:hover {
border-color: #667eea;
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
transform: translateY(-1px);
}
/* 下拉框 focus 状态 */
.blackboard-sidebar .variable-value select:focus {
border-color: #667eea;
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2), 0 4px 12px rgba(102, 126, 234, 0.15);
transform: translateY(-1px);
}
/* 下拉框选项样式 */
.blackboard-sidebar .variable-value select option {
background: #2d3748;
color: #e2e8f0;
padding: 8px 12px;
border: none;
font-size: 11px;
}
/* 不同类型变量的下拉框颜色 */
.blackboard-sidebar .variable-item.string .variable-value select {
border-left: 3px solid #f59e0b;
position: relative;
}
.blackboard-sidebar .variable-item.string .variable-value select:focus {
border-left-color: #f59e0b;
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.2), 0 4px 12px rgba(245, 158, 11, 0.15);
}
/* 约束值下拉框的特殊标识 */
.blackboard-sidebar .variable-value select::before {
content: '🔒';
position: absolute;
top: 50%;
left: 8px;
transform: translateY(-50%);
font-size: 8px;
opacity: 0.6;
pointer-events: none;
z-index: 1;
}
/* 添加约束值提示 */
.blackboard-sidebar .variable-item:has(.variable-value select) .variable-constraints {
color: #f59e0b;
font-weight: 500;
opacity: 0.9;
}
.blackboard-sidebar .variable-item:has(.variable-value select) .variable-constraints::before {
content: '🔐 ';
font-size: 10px;
margin-right: 4px;
}
/* 禁用状态样式 */
.blackboard-sidebar .variable-value select:disabled {
opacity: 0.6;
cursor: not-allowed;
background: #1a202c;
border-color: #2d3748;
}
.blackboard-sidebar .variable-value select:disabled:hover {
transform: none;
box-shadow: none;
}
.blackboard-sidebar .empty-blackboard {
text-align: center;
padding: 24px 16px;
@@ -1098,4 +1244,181 @@
font-size: 11px;
color: #e83e8c;
font-weight: bold;
}
/* 节点类型信息区域样式 */
.node-type-info {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 8px;
margin-bottom: 16px;
position: relative;
overflow: hidden;
}
.node-type-info::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
}
.node-type-info h4 {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0;
padding: 12px 16px 8px 16px;
color: #e2e8f0;
font-size: 14px;
font-weight: 600;
border-bottom: 1px solid rgba(102, 126, 234, 0.2);
}
.node-type-icon {
margin-right: 8px;
font-size: 16px;
}
.reset-name-btn {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
border: none;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 10px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 4px;
font-weight: 500;
}
.reset-name-btn:hover {
background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(217, 119, 6, 0.3);
}
.node-type-details {
padding: 12px 16px 16px 16px;
}
.type-info-row {
display: flex;
align-items: center;
margin-bottom: 8px;
font-size: 12px;
}
.type-info-row:last-child {
margin-bottom: 0;
}
.info-label {
min-width: 80px;
color: #a0aec0;
font-weight: 500;
flex-shrink: 0;
}
.info-value {
flex: 1;
color: #e2e8f0;
display: flex;
align-items: center;
gap: 8px;
}
.original-type {
font-weight: 600;
color: #667eea;
}
.node-id {
font-family: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
background: rgba(45, 55, 72, 0.8);
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
color: #9ca3af;
border: 1px solid #4a5568;
}
.original-description {
font-style: italic;
color: #cbd5e0;
line-height: 1.4;
}
.custom-name {
font-weight: 600;
color: #f59e0b;
}
.type-badge {
font-size: 9px;
padding: 2px 6px;
border-radius: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 700;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.badge-根节点 {
background: linear-gradient(135deg, #f6ad55 0%, #ed8936 100%);
color: white;
}
.badge-组合 {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.badge-装饰器 {
background: linear-gradient(135deg, #9f7aea 0%, #805ad5 100%);
color: white;
}
.badge-动作 {
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
color: white;
}
.badge-条件 {
background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%);
color: white;
}
.badge-ecs {
background: linear-gradient(135deg, #38b2ac 0%, #319795 100%);
color: white;
}
.badge-unknown {
background: linear-gradient(135deg, #718096 0%, #4a5568 100%);
color: white;
}
/* 名称输入容器样式 */
.name-input-container {
position: relative;
display: flex;
align-items: center;
}
.custom-indicator {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
color: #f59e0b;
font-size: 12px;
opacity: 0.8;
pointer-events: none;
}

View File

@@ -71,6 +71,42 @@
color: #e2e8f0;
}
/* 布局组样式 */
.layout-group {
display: flex;
gap: 4px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px;
padding: 2px;
align-items: center;
}
.layout-group button {
padding: 8px 12px;
background: transparent;
border: none;
color: rgba(255,255,255,0.8);
font-size: 12px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
min-width: 70px;
}
.layout-group button:hover {
background: rgba(255,255,255,0.1);
color: white;
transform: translateY(-1px);
}
.layout-group button:active {
transform: translateY(0);
background: rgba(255,255,255,0.15);
}
.tool-btn.has-changes {

View File

@@ -218,7 +218,7 @@
</div>
<div class="canvas-actions">
<button @click="centerView">居中</button>
<button @click="autoLayout">自动布局</button>
<button @click="autoLayout" title="紧凑子树布局 - 子节点紧贴父节点">自动布局</button>
<button @click="validateTree">验证</button>
<button @click="clearAllConnections" title="清除所有连接线" v-if="connections.length > 0">清除连线</button>
</div>
@@ -279,6 +279,7 @@
v-for="node in treeNodes"
:key="node.id"
:data-node-id="node.id"
:data-original-name="isNodeNameCustomized(node) ? getOriginalNodeName(node.type) : ''"
class="tree-node"
:class="[
'node-' + node.type,
@@ -303,7 +304,7 @@
>
<div class="node-header">
<span class="node-icon">{{ node.icon }}</span>
<span class="node-title">{{ node.name }}</span>
<span class="node-title" :class="{ 'customized': isNodeNameCustomized(node) }">{{ node.name }}</span>
<button class="node-delete" @click.stop="deleteNode(node.id)">×</button>
</div>
<div class="node-body">
@@ -424,17 +425,60 @@
</div>
<div v-if="activeNode" class="node-properties">
<!-- 节点类型信息区域 -->
<div class="property-section node-type-info">
<h4>
<span class="node-type-icon">{{ activeNode.icon }}</span>
节点类型信息
<button
class="reset-name-btn"
@click="resetNodeToOriginalName"
:title="'重置为原始名称: ' + getOriginalNodeName(activeNode.type)"
v-if="isNodeNameCustomized(activeNode)"
>
🔄 恢复原名
</button>
</h4>
<div class="node-type-details">
<div class="type-info-row">
<span class="info-label">原始类型:</span>
<span class="info-value original-type">
{{ getOriginalNodeName(activeNode.type) }}
<span class="type-badge" :class="'badge-' + getNodeCategory(activeNode.type)">
{{ getNodeCategory(activeNode.type) }}
</span>
</span>
</div>
<div class="type-info-row">
<span class="info-label">节点ID:</span>
<span class="info-value node-id">{{ activeNode.type }}</span>
</div>
<div class="type-info-row" v-if="getNodeTemplate(activeNode.type)">
<span class="info-label">原始描述:</span>
<span class="info-value original-description">{{ getNodeTemplate(activeNode.type).description }}</span>
</div>
<div class="type-info-row" v-if="isNodeNameCustomized(activeNode)">
<span class="info-label">⚠️ 自定义名称:</span>
<span class="info-value custom-name">{{ activeNode.name }}</span>
</div>
</div>
</div>
<div class="property-section">
<h4>基本信息</h4>
<div class="property-item">
<label>节点名称:</label>
<input
type="text"
:value="activeNode.name"
@input="updateNodeProperty('name', $event.target.value)"
:key="activeNode.id + '_name'"
:disabled="activeNode.isConditionNode"
>
<div class="name-input-container">
<input
type="text"
:value="activeNode.name"
@input="updateNodeProperty('name', $event.target.value)"
:key="activeNode.id + '_name'"
:disabled="activeNode.isConditionNode"
:placeholder="getOriginalNodeName(activeNode.type)"
>
<span v-if="isNodeNameCustomized(activeNode)" class="custom-indicator" title="已自定义名称">✏️</span>
</div>
</div>
<div class="property-item">
<label>描述:</label>
@@ -443,6 +487,7 @@
@input="updateNodeProperty('description', $event.target.value)"
:key="activeNode.id + '_description'"
:disabled="activeNode.isConditionNode"
:placeholder="getNodeTemplate(activeNode.type)?.description || '请输入节点描述...'"
></textarea>
</div>
</div>
@@ -722,6 +767,7 @@
<div class="blackboard-actions">
<button @click="showBlackboardModal = true" class="add-var-btn">+ 添加变量</button>
<button @click="resetBlackboardToDefaults" v-if="blackboardVariables.length > 0" class="reset-btn" title="重置所有变量到默认值">🔄 重置</button>
<button @click="clearBlackboard" v-if="blackboardVariables.length > 0" class="clear-btn">清空</button>
</div>

View File

@@ -7,7 +7,7 @@ import { PoolManager } from './Utils/Pool';
import { ECSFluentAPI, createECSAPI } from './ECS/Core/FluentAPI';
import { Scene } from './ECS/Scene';
import { DebugReporter } from './Utils/DebugReporter';
import { ICoreConfig, IECSDebugConfig } from './Types';
import { ICoreConfig, IECSDebugConfig } from './types';
/**
* 游戏引擎核心类

View File

@@ -1,4 +1,4 @@
import type { IComponent } from '../Types';
import type { IComponent } from '../types';
/**
* 游戏组件基类

View File

@@ -6,8 +6,6 @@ import { ComponentIndexManager, IndexType } from './ComponentIndex';
import { ArchetypeSystem } from './ArchetypeSystem';
import { DirtyTrackingSystem, DirtyFlag } from './DirtyTrackingSystem';
import { EventBus } from './EventBus';
import { ECSEventType } from '../CoreEvents';
import { IEntityEventData, IComponentEventData } from '../../Types';
/**
* 实体查询构建器

View File

@@ -8,7 +8,7 @@ import {
ISystemEventData,
ISceneEventData,
IPerformanceEventData
} from '../../Types';
} from '../../types';
import {
TypeSafeEventSystem,
EventListenerConfig,

View File

@@ -1,8 +1,6 @@
import { Component } from './Component';
import { ComponentRegistry, ComponentType } from './Core/ComponentStorage';
import { EventBus } from './Core/EventBus';
import { ECSEventType } from './CoreEvents';
import { IComponentEventData } from '../Types';
/**
* 实体比较器

View File

@@ -3,7 +3,7 @@ import { Core } from '../../Core';
import { Matcher } from '../Utils/Matcher';
import { PerformanceMonitor } from '../../Utils/PerformanceMonitor';
import type { Scene } from '../Scene';
import type { ISystemBase } from '../../Types';
import type { ISystemBase } from '../../types';
/**
* 实体系统的基类
@@ -115,10 +115,17 @@ export abstract class EntitySystem implements ISystemBase {
/**
* 系统初始化
*
* 在系统创建时调用,子类可以重写此方法进行初始化操作
* 在系统创建时调用,自动检查场景中已存在的实体是否匹配此系统
* 子类可以重写此方法进行额外的初始化操作。
*/
public initialize(): void {
// 子类可以重写此方法
if (this.scene?.entities?.buffer) {
for (const entity of this.scene.entities.buffer) {
this.onChanged(entity);
}
}
// 子类可以重写此方法进行额外初始化
}
/**

View File

@@ -6,7 +6,7 @@ import {
IPerformanceDebugData,
IComponentDebugData,
ISceneDebugData
} from '../Types';
} from '../types';
import { Core } from '../Core';
import { Time } from './Time';

View File

@@ -18,4 +18,4 @@ export * from './ECS';
// 工具类和类型定义
export * from './Utils';
export * from './Types';
export * from './types';