From 310f5f234946d143c62d62804a3d9798dbe3eab2 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Thu, 19 Jun 2025 15:00:14 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=85=88=E5=8A=A0=E5=85=A5?= =?UTF-8?q?=E5=AE=9E=E4=BD=93=E5=90=8E=E5=8A=A0=E5=85=A5=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E4=BB=A5=E8=AE=A9matcher=E8=BF=9B=E8=A1=8C=E5=AE=9E=E4=BD=93?= =?UTF-8?q?=E5=8C=B9=E9=85=8D/=E4=BC=98=E5=8C=96=E8=A1=8C=E4=B8=BA?= =?UTF-8?q?=E6=A0=91=E8=8A=82=E7=82=B9=E6=95=88=E6=9E=9C=E5=8F=8A=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../behavior-tree-examples-guide.bt.json | 818 ++++++++++++++++++ .../behavior-tree-examples-guide.bt.json.meta | 11 + .../behavior-tree/composables/useAppState.ts | 2 +- .../composables/useBehaviorTreeEditor.ts | 308 ++++++- .../composables/useBlackboard.ts | 26 + .../composables/useCodeGeneration.ts | 12 +- .../composables/useConnectionManager.ts | 83 +- .../composables/useFileOperations.ts | 37 +- .../behavior-tree/data/nodeTemplates.ts | 8 +- .../static/style/behavior-tree/modals.css | 66 +- .../static/style/behavior-tree/nodes.css | 64 +- .../static/style/behavior-tree/panels.css | 331 ++++++- .../static/style/behavior-tree/toolbar.css | 36 + .../behavior-tree/BehaviorTreeEditor.html | 64 +- src/Core.ts | 2 +- src/ECS/Component.ts | 2 +- src/ECS/Core/EntityManager.ts | 2 - src/ECS/Core/EventBus.ts | 2 +- src/ECS/Entity.ts | 2 - src/ECS/Systems/EntitySystem.ts | 13 +- src/Utils/DebugReporter.ts | 2 +- src/index.ts | 2 +- 22 files changed, 1809 insertions(+), 84 deletions(-) create mode 100644 extensions/cocos/cocos-ecs/assets/resources/behavior-tree-examples-guide.bt.json create mode 100644 extensions/cocos/cocos-ecs/assets/resources/behavior-tree-examples-guide.bt.json.meta diff --git a/extensions/cocos/cocos-ecs/assets/resources/behavior-tree-examples-guide.bt.json b/extensions/cocos/cocos-ecs/assets/resources/behavior-tree-examples-guide.bt.json new file mode 100644 index 00000000..1e02801d --- /dev/null +++ b/extensions/cocos/cocos-ecs/assets/resources/behavior-tree-examples-guide.bt.json @@ -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 + } + } + ] +} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/resources/behavior-tree-examples-guide.bt.json.meta b/extensions/cocos/cocos-ecs/assets/resources/behavior-tree-examples-guide.bt.json.meta new file mode 100644 index 00000000..9d746861 --- /dev/null +++ b/extensions/cocos/cocos-ecs/assets/resources/behavior-tree-examples-guide.bt.json.meta @@ -0,0 +1,11 @@ +{ + "ver": "2.0.1", + "importer": "json", + "imported": true, + "uuid": "ba6c564a-c5c5-4dc7-ba95-9f0279e0bd66", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useAppState.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useAppState.ts index 053e54b1..ce1406a6 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useAppState.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useAppState.ts @@ -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'; /** * 应用状态管理 diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts index f010ab3a..3e92a4ee 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts @@ -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 = { + '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(); - - 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(); + 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 diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBlackboard.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBlackboard.ts index f9158fb9..a425fffc 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBlackboard.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBlackboard.ts @@ -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, diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useCodeGeneration.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useCodeGeneration.ts index 79fab8da..755ad7ce 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useCodeGeneration.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useCodeGeneration.ts @@ -9,7 +9,8 @@ export function useCodeGeneration( treeNodes: Ref, nodeTemplates: Ref, getNodeByIdLocal: (id: string) => TreeNode | undefined, - rootNode: () => TreeNode | null + rootNode: () => TreeNode | null, + blackboardVariables?: Ref> ) { // 生成行为树配置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字符串 diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useConnectionManager.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useConnectionManager.ts index 50d26c27..805a7a77 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useConnectionManager.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useConnectionManager.ts @@ -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完全渲染 + } }; // 删除连接线 diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useFileOperations.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useFileOperations.ts index 26fb3dd9..aa4eca9b 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useFileOperations.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useFileOperations.ts @@ -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, ''); diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/data/nodeTemplates.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/data/nodeTemplates.ts index 3b4d7336..34b5bf83 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/data/nodeTemplates.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/data/nodeTemplates.ts @@ -1106,4 +1106,10 @@ export const nodeTemplates: NodeTemplate[] = [ } } } -]; \ No newline at end of file +]; + +// 导出所有节点模板 +export const allNodeTemplates: NodeTemplate[] = nodeTemplates; + +// 为了保持向后兼容,保留原来的导出 +export { nodeTemplates as default }; \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/modals.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/modals.css index 63033634..e290fb47 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/modals.css +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/modals.css @@ -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,'); + 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 { diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/nodes.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/nodes.css index 7dbc1f5a..cd5a6734 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/nodes.css +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/nodes.css @@ -328,4 +328,66 @@ opacity: 1; transform: scale(1); } -} \ No newline at end of file +} + +/* 节点类型提示 */ +.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; +} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/panels.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/panels.css index 60c43707..e762dbf9 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/panels.css +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/panels.css @@ -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,'); + 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,'); + 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; } \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/toolbar.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/toolbar.css index ca284af8..58058469 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/toolbar.css +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/toolbar.css @@ -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 { diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html index 562ca145..a52ba363 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html @@ -218,7 +218,7 @@
- +
@@ -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 @@ >
{{ node.icon }} - {{ node.name }} + {{ node.name }}
@@ -424,17 +425,60 @@
+ +
+

+ {{ activeNode.icon }} + 节点类型信息 + +

+
+
+ 原始类型: + + {{ getOriginalNodeName(activeNode.type) }} + + {{ getNodeCategory(activeNode.type) }} + + +
+
+ 节点ID: + {{ activeNode.type }} +
+
+ 原始描述: + {{ getNodeTemplate(activeNode.type).description }} +
+
+ ⚠️ 自定义名称: + {{ activeNode.name }} +
+
+
+

基本信息

- +
+ + ✏️ +
@@ -443,6 +487,7 @@ @input="updateNodeProperty('description', $event.target.value)" :key="activeNode.id + '_description'" :disabled="activeNode.isConditionNode" + :placeholder="getNodeTemplate(activeNode.type)?.description || '请输入节点描述...'" >
@@ -722,6 +767,7 @@
+
diff --git a/src/Core.ts b/src/Core.ts index 602031e3..3a9a24fa 100644 --- a/src/Core.ts +++ b/src/Core.ts @@ -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'; /** * 游戏引擎核心类 diff --git a/src/ECS/Component.ts b/src/ECS/Component.ts index 1c31f4ea..c76e7435 100644 --- a/src/ECS/Component.ts +++ b/src/ECS/Component.ts @@ -1,4 +1,4 @@ -import type { IComponent } from '../Types'; +import type { IComponent } from '../types'; /** * 游戏组件基类 diff --git a/src/ECS/Core/EntityManager.ts b/src/ECS/Core/EntityManager.ts index eda6ce35..e5854004 100644 --- a/src/ECS/Core/EntityManager.ts +++ b/src/ECS/Core/EntityManager.ts @@ -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'; /** * 实体查询构建器 diff --git a/src/ECS/Core/EventBus.ts b/src/ECS/Core/EventBus.ts index b58b9ca6..aac7828e 100644 --- a/src/ECS/Core/EventBus.ts +++ b/src/ECS/Core/EventBus.ts @@ -8,7 +8,7 @@ import { ISystemEventData, ISceneEventData, IPerformanceEventData -} from '../../Types'; +} from '../../types'; import { TypeSafeEventSystem, EventListenerConfig, diff --git a/src/ECS/Entity.ts b/src/ECS/Entity.ts index eb8d236b..78cd9d99 100644 --- a/src/ECS/Entity.ts +++ b/src/ECS/Entity.ts @@ -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'; /** * 实体比较器 diff --git a/src/ECS/Systems/EntitySystem.ts b/src/ECS/Systems/EntitySystem.ts index 74ad651d..a302b023 100644 --- a/src/ECS/Systems/EntitySystem.ts +++ b/src/ECS/Systems/EntitySystem.ts @@ -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); + } + } + + // 子类可以重写此方法进行额外初始化 } /** diff --git a/src/Utils/DebugReporter.ts b/src/Utils/DebugReporter.ts index 84fd75d2..d01eee19 100644 --- a/src/Utils/DebugReporter.ts +++ b/src/Utils/DebugReporter.ts @@ -6,7 +6,7 @@ import { IPerformanceDebugData, IComponentDebugData, ISceneDebugData -} from '../Types'; +} from '../types'; import { Core } from '../Core'; import { Time } from './Time'; diff --git a/src/index.ts b/src/index.ts index 9a2a357d..bd2ada6c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,4 +18,4 @@ export * from './ECS'; // 工具类和类型定义 export * from './Utils'; -export * from './Types'; \ No newline at end of file +export * from './types'; \ No newline at end of file