fix(editor): 修复行为树删除连接时children数组未同步清理的bug (#214)

This commit is contained in:
YHH
2025-11-03 09:57:18 +08:00
committed by GitHub
parent ddc7a7750e
commit 40cde9c050

View File

@@ -19,10 +19,17 @@ import '../styles/BehaviorTreeNode.css';
type NodeExecutionStatus = 'idle' | 'running' | 'success' | 'failure'; type NodeExecutionStatus = 'idle' | 'running' | 'success' | 'failure';
type ExecutionMode = 'idle' | 'running' | 'paused' | 'step'; type ExecutionMode = 'idle' | 'running' | 'paused' | 'step';
type BlackboardValue = string | number | boolean | null | undefined | Record<string, unknown> | unknown[];
type BlackboardVariables = Record<string, BlackboardValue>;
interface DraggedVariableData {
variableName: string;
}
interface BehaviorTreeEditorProps { interface BehaviorTreeEditorProps {
onNodeSelect?: (node: BehaviorTreeNode) => void; onNodeSelect?: (node: BehaviorTreeNode) => void;
onNodeCreate?: (template: NodeTemplate, position: { x: number; y: number }) => void; onNodeCreate?: (template: NodeTemplate, position: { x: number; y: number }) => void;
blackboardVariables?: Record<string, any>; blackboardVariables?: BlackboardVariables;
projectPath?: string | null; projectPath?: string | null;
} }
@@ -66,58 +73,6 @@ function generateUniqueId(): string {
return `${timestamp}${randomPart}`; return `${timestamp}${randomPart}`;
} }
/**
* 推断 JavaScript 值的类型
*/
function inferValueType(value: any): string {
if (value === null || value === undefined) {
return 'any';
}
const jsType = typeof value;
if (jsType === 'object') {
if (Array.isArray(value)) {
return 'array';
}
return 'object';
}
return jsType;
}
/**
* 检查两个类型是否兼容
* @param sourceType 源类型(黑板变量的实际类型)
* @param targetType 目标类型(节点属性期望的类型)
* @returns 是否兼容
*/
function areTypesCompatible(sourceType: string, targetType: string): boolean {
// 完全匹配
if (sourceType === targetType) {
return true;
}
// any 类型兼容一切
if (targetType === 'any' || targetType === 'blackboard' || targetType === 'variable') {
return true;
}
// number 可以转换为 string
if (sourceType === 'number' && targetType === 'string') {
return true;
}
// boolean 可以转换为 string
if (sourceType === 'boolean' && targetType === 'string') {
return true;
}
// string 可以转换为 select下拉选择
if (sourceType === 'string' && targetType === 'select') {
return true;
}
return false;
}
/** /**
* 行为树编辑器主组件 * 行为树编辑器主组件
* *
@@ -166,7 +121,6 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
boxSelectStart, boxSelectStart,
boxSelectEnd, boxSelectEnd,
dragDelta, dragDelta,
forceUpdateCounter: _forceUpdateCounter,
setNodes, setNodes,
setConnections, setConnections,
setSelectedNodeIds, setSelectedNodeIds,
@@ -286,11 +240,9 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
// 运行状态 // 运行状态
const [executionMode, setExecutionMode] = useState<ExecutionMode>('idle'); const [executionMode, setExecutionMode] = useState<ExecutionMode>('idle');
const [_executionHistory, setExecutionHistory] = useState<string[]>([]);
const [executionLogs, setExecutionLogs] = useState<ExecutionLog[]>([]); const [executionLogs, setExecutionLogs] = useState<ExecutionLog[]>([]);
const [executionSpeed, setExecutionSpeed] = useState<number>(1.0); const [executionSpeed, setExecutionSpeed] = useState<number>(1.0);
const [tickCount, setTickCount] = useState(0); const [tickCount, setTickCount] = useState(0);
const _executionTimerRef = useRef<number | null>(null);
const executionModeRef = useRef<ExecutionMode>('idle'); const executionModeRef = useRef<ExecutionMode>('idle');
const executorRef = useRef<BehaviorTreeExecutor | null>(null); const executorRef = useRef<BehaviorTreeExecutor | null>(null);
const animationFrameRef = useRef<number | null>(null); const animationFrameRef = useRef<number | null>(null);
@@ -298,7 +250,7 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
const executionSpeedRef = useRef<number>(1.0); const executionSpeedRef = useRef<number>(1.0);
const statusTimersRef = useRef<Map<string, number>>(new Map()); const statusTimersRef = useRef<Map<string, number>>(new Map());
// 保存设计时的初始黑板变量值(用于保存和停止后还原) // 保存设计时的初始黑板变量值(用于保存和停止后还原)
const initialBlackboardVariablesRef = useRef<Record<string, any>>({}); const initialBlackboardVariablesRef = useRef<BlackboardVariables>({});
// 跟踪运行时添加的节点(在运行中未生效的节点) // 跟踪运行时添加的节点(在运行中未生效的节点)
const [uncommittedNodeIds, setUncommittedNodeIds] = useState<Set<string>>(new Set()); const [uncommittedNodeIds, setUncommittedNodeIds] = useState<Set<string>>(new Set());
@@ -327,7 +279,6 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
connections: new Map(), connections: new Map(),
lastNodeStatus: new Map() lastNodeStatus: new Map()
}); });
const lastLogUpdateRef = useRef<number>(0);
// 键盘事件监听 - 删除选中节点 // 键盘事件监听 - 删除选中节点
useEffect(() => { useEffect(() => {
@@ -349,9 +300,22 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
// 优先删除选中的连线 // 优先删除选中的连线
if (selectedConnection) { if (selectedConnection) {
// 删除连接
removeConnections((conn: Connection) => removeConnections((conn: Connection) =>
!(conn.from === selectedConnection.from && conn.to === selectedConnection.to) !(conn.from === selectedConnection.from && conn.to === selectedConnection.to)
); );
// 同步更新父节点的children数组移除被删除的子节点引用
setNodes(nodes.map((node: BehaviorTreeNode) => {
if (node.id === selectedConnection.from) {
return {
...node,
children: node.children.filter((childId: string) => childId !== selectedConnection.to)
};
}
return node;
}));
setSelectedConnection(null); setSelectedConnection(null);
return; return;
} }
@@ -420,7 +384,7 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
// 检查是否是黑板变量 // 检查是否是黑板变量
const blackboardVariableData = e.dataTransfer.getData('application/blackboard-variable'); const blackboardVariableData = e.dataTransfer.getData('application/blackboard-variable');
if (blackboardVariableData) { if (blackboardVariableData) {
const variableData = JSON.parse(blackboardVariableData); const variableData = JSON.parse(blackboardVariableData) as DraggedVariableData;
// 创建黑板变量节点 // 创建黑板变量节点
const variableTemplate: NodeTemplate = { const variableTemplate: NodeTemplate = {
@@ -470,7 +434,7 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
return; return;
} }
const template: NodeTemplate = JSON.parse(templateData); const template = JSON.parse(templateData) as NodeTemplate;
const newNode: BehaviorTreeNode = { const newNode: BehaviorTreeNode = {
id: `node_${generateUniqueId()}`, id: `node_${generateUniqueId()}`,
@@ -848,39 +812,6 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
} }
} }
// 类型兼容性检查
const fromNode = nodes.find((n: BehaviorTreeNode) => n.id === actualFrom);
if (fromNode && toNode && actualFromProperty && actualToProperty) {
const isFromBlackboard = fromNode.data.nodeType === 'blackboard-variable';
if (isFromBlackboard) {
// 从黑板变量连接到节点属性
const variableName = fromNode.data.variableName;
const variableValue = blackboardVariables[variableName];
const sourceType = inferValueType(variableValue);
// 获取目标属性的期望类型
const targetProperty = toNode.template.properties.find(
(p: PropertyDefinition) => p.name === actualToProperty
);
if (targetProperty) {
const targetType = targetProperty.type;
if (!areTypesCompatible(sourceType, targetType)) {
showToast(
`类型不兼容: 黑板变量 "${variableName}" (${sourceType}) 无法连接到属性 "${targetProperty.label}" (${targetType})`,
'error',
5000
);
clearConnecting();
return;
}
}
}
}
setConnections([...connections, { setConnections([...connections, {
from: actualFrom, from: actualFrom,
to: actualTo, to: actualTo,
@@ -1195,69 +1126,14 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
return { x, y }; return { x, y };
}; };
// 估算节点的总高度
const estimateNodeHeight = (node: BehaviorTreeNode): number => {
const isBlackboard = node.data.nodeType === 'blackboard-variable';
if (isBlackboard) {
// 黑板变量节点结构简单
// padding: 12px * 2
// 标题 + 值显示区域
return 24 + 18 + 30; // 约72px
}
// 普通节点
const paddingVertical = 12 * 2; // padding top + bottom
const titleArea = 18 + 6; // icon + marginBottom
const categoryArea = 13;
const bottomPortSpace = 16; // 底部端口需要的空间
let propsArea = 0;
if (node.template.properties.length > 0) {
const propContainerHeader = 8 + 8 + 1; // marginTop + paddingTop + borderTop
const eachPropHeight = 22; // height 18px + marginBottom 4px
propsArea = propContainerHeader + (node.template.properties.length * eachPropHeight);
}
return paddingVertical + titleArea + categoryArea + propsArea + bottomPortSpace;
};
// 计算属性引脚的Y坐标偏移从节点中心算起
const _getPropertyPinYOffset = (node: BehaviorTreeNode, propertyIndex: number): number => {
// 从节点顶部开始的距离:
const paddingTop = 12;
const titleArea = 18 + 6; // icon高度 + marginBottom
const categoryArea = 13;
const propContainerMarginTop = 8;
const propContainerPaddingTop = 8;
const propContainerBorderTop = 1;
const eachPropHeight = 22; // height 18px + marginBottom 4px
const pinOffsetInRow = 9; // top 3px + 半个引脚 6px
const offsetFromTop = paddingTop + titleArea + categoryArea +
propContainerMarginTop + propContainerPaddingTop + propContainerBorderTop +
(propertyIndex * eachPropHeight) + pinOffsetInRow;
// 节点高度的一半
const nodeHalfHeight = estimateNodeHeight(node) / 2;
// 从节点中心到引脚的偏移 = 从顶部的距离 - 节点高度的一半
return offsetFromTop - nodeHalfHeight;
};
// 执行状态回调直接操作DOM不触发React重渲染 // 执行状态回调直接操作DOM不触发React重渲染
const handleExecutionStatusUpdate = ( const handleExecutionStatusUpdate = (
statuses: ExecutionStatus[], statuses: ExecutionStatus[],
logs: ExecutionLog[], logs: ExecutionLog[],
runtimeBlackboardVars?: Record<string, any> runtimeBlackboardVars?: BlackboardVariables
): void => { ): void => {
const now = performance.now(); // 更新执行日志
// 节流日志更新最多每100ms更新一次
if (now - lastLogUpdateRef.current > 100) {
setExecutionLogs([...logs]); setExecutionLogs([...logs]);
lastLogUpdateRef.current = now;
}
// 同步运行时黑板变量到 store无论运行还是暂停都同步 // 同步运行时黑板变量到 store无论运行还是暂停都同步
if (runtimeBlackboardVars) { if (runtimeBlackboardVars) {
@@ -1442,7 +1318,6 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
executionModeRef.current = 'running'; executionModeRef.current = 'running';
setExecutionMode('running'); setExecutionMode('running');
setExecutionHistory(['使用ECS系统执行行为树...']);
setTickCount(0); setTickCount(0);
lastTickTimeRef.current = 0; lastTickTimeRef.current = 0;
@@ -1468,7 +1343,6 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
if (executionModeRef.current === 'running') { if (executionModeRef.current === 'running') {
executionModeRef.current = 'paused'; executionModeRef.current = 'paused';
setExecutionMode('paused'); setExecutionMode('paused');
setExecutionHistory((prev) => [...prev, '执行已暂停']);
if (executorRef.current) { if (executorRef.current) {
executorRef.current.pause(); executorRef.current.pause();
@@ -1481,7 +1355,6 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
} else if (executionModeRef.current === 'paused') { } else if (executionModeRef.current === 'paused') {
executionModeRef.current = 'running'; executionModeRef.current = 'running';
setExecutionMode('running'); setExecutionMode('running');
setExecutionHistory((prev) => [...prev, '执行已恢复']);
lastTickTimeRef.current = 0; lastTickTimeRef.current = 0;
if (executorRef.current) { if (executorRef.current) {
@@ -1495,7 +1368,6 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
const handleStop = () => { const handleStop = () => {
executionModeRef.current = 'idle'; executionModeRef.current = 'idle';
setExecutionMode('idle'); setExecutionMode('idle');
setExecutionHistory([]);
setTickCount(0); setTickCount(0);
lastTickTimeRef.current = 0; lastTickTimeRef.current = 0;
@@ -1547,8 +1419,6 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
if (executorRef.current) { if (executorRef.current) {
executorRef.current.cleanup(); executorRef.current.cleanup();
} }
setExecutionHistory(['重置到初始状态']);
}; };
useEffect(() => { useEffect(() => {
@@ -1845,7 +1715,7 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
> >
{isBlackboardVariable ? ( {isBlackboardVariable ? (
(() => { (() => {
const varName = node.data.variableName; const varName = node.data.variableName as string;
const currentValue = blackboardVariables[varName]; const currentValue = blackboardVariables[varName];
const initialValue = initialBlackboardVariables[varName]; const initialValue = initialBlackboardVariables[varName];
const isModified = isExecuting && JSON.stringify(currentValue) !== JSON.stringify(initialValue); const isModified = isExecuting && JSON.stringify(currentValue) !== JSON.stringify(initialValue);