新增根节点

This commit is contained in:
YHH
2025-06-18 15:53:48 +08:00
parent 92125aee3a
commit 343f5a44f2
8 changed files with 215 additions and 28 deletions

View File

@@ -8,6 +8,7 @@ import { useFileOperations } from './useFileOperations';
import { useConnectionManager } from './useConnectionManager';
import { useCanvasManager } from './useCanvasManager';
import { useNodeDisplay } from './useNodeDisplay';
import { validateTree as validateTreeStructure } from '../utils/nodeUtils';
/**
* 主要的行为树编辑器组合功能
@@ -248,32 +249,31 @@ export function useBehaviorTreeEditor() {
// 验证树结构
const validateTree = () => {
// 使用改进的验证函数
const validationResult = validateTreeStructure(appState.treeNodes.value);
const errors: string[] = [];
const warnings: string[] = [];
const rootNodes = appState.treeNodes.value.filter(node =>
!appState.treeNodes.value.some(otherNode =>
otherNode.children?.includes(node.id)
)
);
if (rootNodes.length === 0) {
errors.push('没有找到根节点');
} else if (rootNodes.length > 1) {
warnings.push(`找到多个根节点: ${rootNodes.map(n => n.name).join(', ')}`);
if (!validationResult.isValid) {
errors.push(validationResult.message);
}
// 检查孤立节点(除了根节点)
appState.treeNodes.value.forEach(node => {
const hasParent = appState.treeNodes.value.some(otherNode =>
otherNode.children?.includes(node.id)
);
const hasChildren = node.children && node.children.length > 0;
if (!hasParent && !hasChildren && appState.treeNodes.value.length > 1) {
warnings.push(`节点 "${node.name}" 是孤立节点`);
if (node.type !== 'root') {
const hasParent = appState.treeNodes.value.some(otherNode =>
otherNode.children?.includes(node.id)
);
const hasChildren = node.children && node.children.length > 0;
if (!hasParent && !hasChildren && appState.treeNodes.value.length > 1) {
warnings.push(`节点 "${node.name}" 是孤立节点`);
}
}
});
// 检查连接完整性
appState.connections.value.forEach(conn => {
const sourceNode = appState.treeNodes.value.find(n => n.id === conn.sourceId);
const targetNode = appState.treeNodes.value.find(n => n.id === conn.targetId);
@@ -286,7 +286,21 @@ export function useBehaviorTreeEditor() {
}
});
let message = '树结构验证完成!\n\n';
// 检查节点类型一致性
appState.treeNodes.value.forEach(node => {
if (node.type === 'root' && node.parent) {
errors.push(`根节点 "${node.name}" 不应该有父节点`);
}
// 检查装饰器节点的限制
if (node.type.includes('decorator') || node.type.includes('Decorator')) {
if (node.children.length > 1) {
warnings.push(`装饰器节点 "${node.name}" 建议只连接一个子节点,当前有 ${node.children.length}`);
}
}
});
let message = '🔍 树结构验证完成!\n\n';
if (errors.length > 0) {
message += `❌ 错误 (${errors.length}):\n${errors.map(e => `${e}`).join('\n')}\n\n`;
@@ -297,7 +311,9 @@ export function useBehaviorTreeEditor() {
}
if (errors.length === 0 && warnings.length === 0) {
message += '✅ 没有发现问题!';
message += '✅ 没有发现问题!树结构完全符合行为树规范。';
} else if (errors.length === 0) {
message += '✅ 树结构基本有效,但有一些建议优化的地方。';
}
alert(message);

View File

@@ -28,6 +28,13 @@ export function useComputedProperties(
}
) {
// 过滤节点
const filteredRootNodes = () => {
return nodeTemplates.value.filter(node =>
node.category === 'root' &&
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
);
};
const filteredCompositeNodes = () => {
return nodeTemplates.value.filter(node =>
node.category === 'composite' &&
@@ -126,6 +133,7 @@ export function useComputedProperties(
};
return {
filteredRootNodes,
filteredCompositeNodes,
filteredDecoratorNodes,
filteredActionNodes,

View File

@@ -380,6 +380,28 @@ export function useConnectionManager(
if (!parentNode || !parentNode.canHaveChildren) return false;
if (!childNode || !childNode.canHaveParent) return false;
// 检查子节点数量限制
if (parentNode.maxChildren !== undefined) {
const currentChildrenCount = parentNode.children ? parentNode.children.length : 0;
if (currentChildrenCount >= parentNode.maxChildren) {
return false; // 已达到最大子节点数量
}
}
// 检查根节点限制:根节点不能有父节点
if (childNode.type === 'root') {
return false; // 根节点不能作为其他节点的子节点
}
// 检查是否只能有一个根节点
if (parentNode.type === 'root') {
// 根节点只能连接一个子节点
const rootNodes = treeNodes.value.filter(n => n.type === 'root');
if (rootNodes.length > 1) {
return false; // 不能有多个根节点
}
}
if (wouldCreateCycle(parentNodeId, childNodeId)) return false;
if (isDescendant(childNodeId, parentNodeId)) return false;

View File

@@ -17,10 +17,12 @@ export interface NodeTemplate {
type: string;
name: string;
icon: string;
category: 'composite' | 'decorator' | 'action' | 'condition' | 'ecs';
category: 'composite' | 'decorator' | 'action' | 'condition' | 'ecs' | 'root';
description: string;
canHaveChildren: boolean;
canHaveParent: boolean;
maxChildren?: number; // 最大子节点数量限制
minChildren?: number; // 最小子节点数量要求
properties?: Record<string, PropertyDefinition>;
className?: string; // 对应的实际类名
namespace?: string; // 命名空间
@@ -30,6 +32,21 @@ export interface NodeTemplate {
* 基于项目实际行为树系统的节点模板定义
*/
export const nodeTemplates: NodeTemplate[] = [
// 根节点
{
type: 'root',
name: '根节点',
icon: '🌳',
category: 'root',
description: '行为树的根节点,每棵树只能有一个根节点',
canHaveChildren: true,
canHaveParent: false,
maxChildren: 1,
minChildren: 1,
className: 'BehaviorTree',
namespace: 'behaviourTree'
},
// 复合节点 (Composites)
{
type: 'sequence',
@@ -39,6 +56,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: '按顺序执行子节点,任一失败则整体失败',
canHaveChildren: true,
canHaveParent: true,
minChildren: 1,
className: 'Sequence',
namespace: 'behaviourTree/composites',
properties: {
@@ -60,6 +78,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: '按顺序执行子节点,任一成功则整体成功',
canHaveChildren: true,
canHaveParent: true,
minChildren: 1,
className: 'Selector',
namespace: 'behaviourTree/composites',
properties: {
@@ -81,6 +100,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: '并行执行所有子节点',
canHaveChildren: true,
canHaveParent: true,
minChildren: 2,
className: 'Parallel',
namespace: 'behaviourTree/composites'
},
@@ -92,6 +112,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: '并行执行子节点,任一成功则成功',
canHaveChildren: true,
canHaveParent: true,
minChildren: 2,
className: 'ParallelSelector',
namespace: 'behaviourTree/composites'
},
@@ -103,6 +124,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: '随机顺序执行子节点,任一成功则成功',
canHaveChildren: true,
canHaveParent: true,
minChildren: 2,
className: 'RandomSelector',
namespace: 'behaviourTree/composites'
},
@@ -114,11 +136,12 @@ export const nodeTemplates: NodeTemplate[] = [
description: '随机顺序执行子节点,任一失败则失败',
canHaveChildren: true,
canHaveParent: true,
minChildren: 2,
className: 'RandomSequence',
namespace: 'behaviourTree/composites'
},
// 装饰器节点 (Decorators)
// 装饰器节点 (Decorators) - 只能有一个子节点
{
type: 'repeater',
name: '重复器',
@@ -127,6 +150,8 @@ export const nodeTemplates: NodeTemplate[] = [
description: '重复执行子节点指定次数或无限次',
canHaveChildren: true,
canHaveParent: true,
maxChildren: 1,
minChildren: 1,
className: 'Repeater',
namespace: 'behaviourTree/decorators',
properties: {
@@ -154,6 +179,8 @@ export const nodeTemplates: NodeTemplate[] = [
description: '反转子节点的执行结果',
canHaveChildren: true,
canHaveParent: true,
maxChildren: 1,
minChildren: 1,
className: 'Inverter',
namespace: 'behaviourTree/decorators'
},
@@ -165,6 +192,8 @@ export const nodeTemplates: NodeTemplate[] = [
description: '无论子节点结果如何都返回成功',
canHaveChildren: true,
canHaveParent: true,
maxChildren: 1,
minChildren: 1,
className: 'AlwaysSucceed',
namespace: 'behaviourTree/decorators'
},
@@ -176,6 +205,8 @@ export const nodeTemplates: NodeTemplate[] = [
description: '无论子节点结果如何都返回失败',
canHaveChildren: true,
canHaveParent: true,
maxChildren: 1,
minChildren: 1,
className: 'AlwaysFail',
namespace: 'behaviourTree/decorators'
},
@@ -187,6 +218,8 @@ export const nodeTemplates: NodeTemplate[] = [
description: '重复执行子节点直到成功',
canHaveChildren: true,
canHaveParent: true,
maxChildren: 1,
minChildren: 1,
className: 'UntilSuccess',
namespace: 'behaviourTree/decorators'
},
@@ -198,6 +231,8 @@ export const nodeTemplates: NodeTemplate[] = [
description: '重复执行子节点直到失败',
canHaveChildren: true,
canHaveParent: true,
maxChildren: 1,
minChildren: 1,
className: 'UntilFail',
namespace: 'behaviourTree/decorators'
},
@@ -209,6 +244,8 @@ export const nodeTemplates: NodeTemplate[] = [
description: '基于条件执行子节点',
canHaveChildren: true,
canHaveParent: true,
maxChildren: 1,
minChildren: 1,
className: 'ConditionalDecorator',
namespace: 'behaviourTree/decorators',
properties: {
@@ -222,7 +259,7 @@ export const nodeTemplates: NodeTemplate[] = [
}
},
// 动作节点 (Actions)
// 动作节点 (Actions) - 叶子节点,不能有子节点
{
type: 'execute-action',
name: '执行动作',
@@ -231,6 +268,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: '执行自定义代码逻辑',
canHaveChildren: false,
canHaveParent: true,
maxChildren: 0,
className: 'ExecuteAction',
namespace: 'behaviourTree/actions',
properties: {
@@ -258,6 +296,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: '等待指定时间后完成',
canHaveChildren: false,
canHaveParent: true,
maxChildren: 0,
className: 'WaitAction',
namespace: 'behaviourTree/actions',
properties: {
@@ -285,6 +324,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: '输出日志信息',
canHaveChildren: false,
canHaveParent: true,
maxChildren: 0,
className: 'LogAction',
namespace: 'behaviourTree/actions',
properties: {
@@ -313,6 +353,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: '运行另一个行为树',
canHaveChildren: false,
canHaveParent: true,
maxChildren: 0,
className: 'BehaviorTreeReference',
namespace: 'behaviourTree/actions',
properties: {
@@ -326,7 +367,7 @@ export const nodeTemplates: NodeTemplate[] = [
}
},
// 条件节点 (基础条件)
// 条件节点 (基础条件) - 叶子节点,不能有子节点
{
type: 'execute-conditional',
name: '执行条件',
@@ -335,6 +376,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: '执行自定义条件判断',
canHaveChildren: false,
canHaveParent: true,
maxChildren: 0,
className: 'ExecuteActionConditional',
namespace: 'behaviourTree/conditionals',
properties: {
@@ -348,7 +390,7 @@ export const nodeTemplates: NodeTemplate[] = [
}
},
// ECS专用节点
// ECS专用节点 - 都是叶子节点
{
type: 'has-component',
name: '检查组件',
@@ -357,6 +399,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: '检查实体是否包含指定组件',
canHaveChildren: false,
canHaveParent: true,
maxChildren: 0,
className: 'HasComponentCondition',
namespace: 'ecs-integration/behaviors',
properties: {
@@ -377,6 +420,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: '为实体添加组件',
canHaveChildren: false,
canHaveParent: true,
maxChildren: 0,
className: 'AddComponentAction',
namespace: 'ecs-integration/behaviors',
properties: {
@@ -404,6 +448,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: '从实体移除组件',
canHaveChildren: false,
canHaveParent: true,
maxChildren: 0,
className: 'RemoveComponentAction',
namespace: 'ecs-integration/behaviors',
properties: {
@@ -424,6 +469,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: '修改实体组件的属性',
canHaveChildren: false,
canHaveParent: true,
maxChildren: 0,
className: 'ModifyComponentAction',
namespace: 'ecs-integration/behaviors',
properties: {
@@ -451,6 +497,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: '检查实体是否具有指定标签',
canHaveChildren: false,
canHaveParent: true,
maxChildren: 0,
className: 'HasTagCondition',
namespace: 'ecs-integration/behaviors',
properties: {
@@ -471,6 +518,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: '检查实体是否处于激活状态',
canHaveChildren: false,
canHaveParent: true,
maxChildren: 0,
className: 'IsActiveCondition',
namespace: 'ecs-integration/behaviors',
properties: {
@@ -491,6 +539,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: 'ECS优化的等待动作',
canHaveChildren: false,
canHaveParent: true,
maxChildren: 0,
className: 'WaitTimeAction',
namespace: 'ecs-integration/behaviors',
properties: {
@@ -511,6 +560,7 @@ export const nodeTemplates: NodeTemplate[] = [
description: '销毁当前实体',
canHaveChildren: false,
canHaveParent: true,
maxChildren: 0,
className: 'DestroyEntityAction',
namespace: 'ecs-integration/behaviors'
}

View File

@@ -13,6 +13,8 @@ export interface TreeNode {
properties?: Record<string, PropertyDefinition>;
canHaveChildren: boolean;
canHaveParent: boolean;
maxChildren?: number; // 最大子节点数量限制
minChildren?: number; // 最小子节点数量要求
hasError?: boolean;
}

View File

@@ -41,6 +41,8 @@ export function createNodeFromTemplate(template: NodeTemplate, x: number = 100,
properties: properties,
canHaveChildren: template.canHaveChildren,
canHaveParent: template.canHaveParent,
maxChildren: template.maxChildren,
minChildren: template.minChildren,
hasError: false
};
return node;
@@ -112,13 +114,81 @@ export function validateTree(nodes: TreeNode[]): ValidationResult {
return { isValid: false, message: '行为树为空' };
}
const root = getRootNode(nodes);
if (!root) {
// 检查根节点
const rootNodes = nodes.filter(node => !node.parent);
if (rootNodes.length === 0) {
return { isValid: false, message: '缺少根节点' };
}
if (rootNodes.length > 1) {
return { isValid: false, message: `发现多个根节点: ${rootNodes.map(n => n.name).join(', ')}` };
}
// 可以添加更多验证逻辑
// 例如:检查循环引用、孤立节点等
// 验证每个节点的子节点数量限制
for (const node of nodes) {
const childrenCount = node.children.length;
// 检查最小子节点数量
if (node.minChildren !== undefined && childrenCount < node.minChildren) {
return {
isValid: false,
message: `节点 "${node.name}" 需要至少 ${node.minChildren} 个子节点,当前只有 ${childrenCount}`
};
}
// 检查最大子节点数量
if (node.maxChildren !== undefined && childrenCount > node.maxChildren) {
return {
isValid: false,
message: `节点 "${node.name}" 最多只能有 ${node.maxChildren} 个子节点,当前有 ${childrenCount}`
};
}
// 检查装饰器节点的特殊限制
if (node.type.includes('decorator') || node.type.includes('Decorator')) {
if (childrenCount !== 1) {
return {
isValid: false,
message: `装饰器节点 "${node.name}" 必须有且只能有一个子节点,当前有 ${childrenCount}`
};
}
}
// 检查叶子节点不能有子节点
if (!node.canHaveChildren && childrenCount > 0) {
return {
isValid: false,
message: `叶子节点 "${node.name}" 不能有子节点,但当前有 ${childrenCount}`
};
}
}
// 检查循环引用
const visited = new Set<string>();
const recursionStack = new Set<string>();
function hasCycle(nodeId: string): boolean {
if (recursionStack.has(nodeId)) return true;
if (visited.has(nodeId)) return false;
visited.add(nodeId);
recursionStack.add(nodeId);
const node = getNodeById(nodes, nodeId);
if (node) {
for (const childId of node.children) {
if (hasCycle(childId)) return true;
}
}
recursionStack.delete(nodeId);
return false;
}
for (const node of nodes) {
if (hasCycle(node.id)) {
return { isValid: false, message: '检测到循环引用' };
}
}
return { isValid: true, message: '行为树结构有效' };
}

View File

@@ -190,6 +190,7 @@
cursor: grabbing;
}
.node-item.root { border-left: 3px solid #f6ad55; }
.node-item.composite { border-left: 3px solid #667eea; }
.node-item.decorator { border-left: 3px solid #9f7aea; }
.node-item.action { border-left: 3px solid #48bb78; }

View File

@@ -47,6 +47,24 @@
</div>
<div class="node-categories">
<!-- 根节点 -->
<div class="category">
<h4 class="category-title">🌳 根节点</h4>
<div class="node-list">
<div
v-for="node in filteredRootNodes()"
:key="node.type"
class="node-item root"
:draggable="true"
@dragstart="onNodeDragStart($event, node)"
:title="node.description"
>
<span class="node-icon">{{ node.icon }}</span>
<span class="node-name">{{ node.name }}</span>
</div>
</div>
</div>
<!-- 复合节点 -->
<div class="category">
<h4 class="category-title">🔗 复合节点</h4>