Feature/physics and tilemap enhancement (#247)
* feat(behavior-tree,tilemap): 修复编辑器连线缩放问题并增强插件系统 * feat(node-editor,blueprint): 新增通用节点编辑器和蓝图可视化脚本系统 * feat(editor,tilemap): 优化编辑器UI样式和Tilemap编辑器功能 * fix: 修复CodeQL安全警告和CI类型检查错误 * fix: 修复CodeQL安全警告和CI类型检查错误 * fix: 修复CodeQL安全警告和CI类型检查错误
This commit is contained in:
@@ -6,17 +6,22 @@
|
||||
import type { IScene, ServiceContainer } from '@esengine/ecs-framework';
|
||||
import { ComponentRegistry, Core } from '@esengine/ecs-framework';
|
||||
import type { IRuntimeModuleLoader, SystemContext } from '@esengine/ecs-components';
|
||||
import type { AssetManager } from '@esengine/asset-system';
|
||||
|
||||
import { BehaviorTreeRuntimeComponent } from './execution/BehaviorTreeRuntimeComponent';
|
||||
import { BehaviorTreeExecutionSystem } from './execution/BehaviorTreeExecutionSystem';
|
||||
import { BehaviorTreeAssetManager } from './execution/BehaviorTreeAssetManager';
|
||||
import { GlobalBlackboardService } from './Services/GlobalBlackboardService';
|
||||
import { BehaviorTreeLoader } from './loaders/BehaviorTreeLoader';
|
||||
import { BehaviorTreeAssetType } from './index';
|
||||
|
||||
/**
|
||||
* Behavior Tree Runtime Module
|
||||
* 行为树运行时模块
|
||||
*/
|
||||
export class BehaviorTreeRuntimeModule implements IRuntimeModuleLoader {
|
||||
private _loaderRegistered = false;
|
||||
|
||||
registerComponents(registry: typeof ComponentRegistry): void {
|
||||
registry.register(BehaviorTreeRuntimeComponent);
|
||||
}
|
||||
@@ -31,8 +36,28 @@ export class BehaviorTreeRuntimeModule implements IRuntimeModuleLoader {
|
||||
}
|
||||
|
||||
createSystems(scene: IScene, context: SystemContext): void {
|
||||
// 注册行为树加载器到 AssetManager
|
||||
// Register behavior tree loader to AssetManager
|
||||
const assetManager = context.assetManager as AssetManager | undefined;
|
||||
console.log('[BehaviorTreeRuntimeModule] createSystems called, assetManager:', assetManager ? 'exists' : 'null');
|
||||
|
||||
if (!this._loaderRegistered && assetManager) {
|
||||
assetManager.registerLoader(BehaviorTreeAssetType, new BehaviorTreeLoader());
|
||||
this._loaderRegistered = true;
|
||||
console.log('[BehaviorTreeRuntimeModule] Registered BehaviorTreeLoader for type:', BehaviorTreeAssetType);
|
||||
}
|
||||
|
||||
const behaviorTreeSystem = new BehaviorTreeExecutionSystem(Core);
|
||||
|
||||
// 设置 AssetManager 引用
|
||||
// Set AssetManager reference
|
||||
if (assetManager) {
|
||||
behaviorTreeSystem.setAssetManager(assetManager);
|
||||
console.log('[BehaviorTreeRuntimeModule] Set assetManager on behaviorTreeSystem');
|
||||
} else {
|
||||
console.warn('[BehaviorTreeRuntimeModule] assetManager is null, cannot set on behaviorTreeSystem');
|
||||
}
|
||||
|
||||
if (context.isEditor) {
|
||||
behaviorTreeSystem.enabled = false;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,17 @@ interface EditorNode {
|
||||
children?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器连接数据接口
|
||||
*/
|
||||
interface EditorConnection {
|
||||
from: string;
|
||||
to: string;
|
||||
connectionType: 'node' | 'property';
|
||||
fromProperty?: string;
|
||||
toProperty?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器行为树数据接口
|
||||
*/
|
||||
@@ -27,6 +38,7 @@ interface EditorBehaviorTreeData {
|
||||
modifiedAt?: string;
|
||||
};
|
||||
nodes: EditorNode[];
|
||||
connections?: EditorConnection[];
|
||||
blackboard?: Record<string, any>;
|
||||
}
|
||||
|
||||
@@ -57,10 +69,18 @@ export class EditorToBehaviorTreeDataConverter {
|
||||
throw new Error('Behavior tree must have a root node');
|
||||
}
|
||||
|
||||
// 转换所有节点
|
||||
// 构建属性绑定映射:nodeId -> { propertyName -> blackboardKey }
|
||||
const propertyBindingsMap = this.buildPropertyBindingsMap(editorData);
|
||||
|
||||
// 转换所有节点(过滤掉不可执行的节点,如黑板变量节点)
|
||||
const nodesMap = new Map<string, BehaviorNodeData>();
|
||||
for (const editorNode of editorData.nodes) {
|
||||
const behaviorNodeData = this.convertNode(editorNode);
|
||||
// 跳过黑板变量节点,它们只用于编辑器的可视化绑定
|
||||
if (this.isNonExecutableNode(editorNode)) {
|
||||
continue;
|
||||
}
|
||||
const propertyBindings = propertyBindingsMap.get(editorNode.id);
|
||||
const behaviorNodeData = this.convertNode(editorNode, propertyBindings);
|
||||
nodesMap.set(behaviorNodeData.id, behaviorNodeData);
|
||||
}
|
||||
|
||||
@@ -79,19 +99,81 @@ export class EditorToBehaviorTreeDataConverter {
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换单个节点
|
||||
* 从连接数据构建属性绑定映射
|
||||
* 处理 connectionType === 'property' 的连接,将黑板变量节点连接到目标节点的属性
|
||||
*/
|
||||
private static convertNode(editorNode: EditorNode): BehaviorNodeData {
|
||||
private static buildPropertyBindingsMap(
|
||||
editorData: EditorBehaviorTreeData
|
||||
): Map<string, Record<string, string>> {
|
||||
const bindingsMap = new Map<string, Record<string, string>>();
|
||||
|
||||
if (!editorData.connections) {
|
||||
return bindingsMap;
|
||||
}
|
||||
|
||||
// 构建节点 ID 到变量名的映射(用于黑板变量节点)
|
||||
const nodeToVariableMap = new Map<string, string>();
|
||||
for (const node of editorData.nodes) {
|
||||
if (node.data['nodeType'] === 'blackboard-variable' && node.data['variableName']) {
|
||||
nodeToVariableMap.set(node.id, node.data['variableName']);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理属性连接
|
||||
for (const conn of editorData.connections) {
|
||||
if (conn.connectionType === 'property' && conn.toProperty) {
|
||||
const variableName = nodeToVariableMap.get(conn.from);
|
||||
if (variableName) {
|
||||
// 获取或创建目标节点的绑定记录
|
||||
let bindings = bindingsMap.get(conn.to);
|
||||
if (!bindings) {
|
||||
bindings = {};
|
||||
bindingsMap.set(conn.to, bindings);
|
||||
}
|
||||
// 将属性绑定到黑板变量
|
||||
bindings[conn.toProperty] = variableName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bindingsMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换单个节点
|
||||
* @param editorNode 编辑器节点数据
|
||||
* @param propertyBindings 从连接中提取的属性绑定(可选)
|
||||
*/
|
||||
private static convertNode(
|
||||
editorNode: EditorNode,
|
||||
propertyBindings?: Record<string, string>
|
||||
): BehaviorNodeData {
|
||||
const nodeType = this.mapNodeType(editorNode.template.type);
|
||||
const config = this.extractConfig(editorNode.data);
|
||||
const bindings = this.extractBindings(editorNode.data);
|
||||
// 从节点数据中提取绑定
|
||||
const dataBindings = this.extractBindings(editorNode.data);
|
||||
// 合并连接绑定和数据绑定(连接绑定优先)
|
||||
const bindings = { ...dataBindings, ...propertyBindings };
|
||||
const abortType = this.extractAbortType(editorNode.data);
|
||||
|
||||
// 获取 implementationType:优先从 template.className,其次从 data 中的类型字段
|
||||
let implementationType: string | undefined = editorNode.template.className;
|
||||
if (!implementationType) {
|
||||
// 尝试从 data 中提取类型
|
||||
implementationType = this.extractImplementationType(editorNode.data, nodeType);
|
||||
}
|
||||
|
||||
if (!implementationType) {
|
||||
console.warn(`[EditorToBehaviorTreeDataConverter] Node ${editorNode.id} has no implementationType, using fallback`);
|
||||
// 根据节点类型使用默认实现
|
||||
implementationType = this.getDefaultImplementationType(nodeType);
|
||||
}
|
||||
|
||||
return {
|
||||
id: editorNode.id,
|
||||
name: editorNode.template.displayName || editorNode.template.className,
|
||||
name: editorNode.template.displayName || editorNode.template.className || implementationType,
|
||||
nodeType,
|
||||
implementationType: editorNode.template.className,
|
||||
implementationType,
|
||||
children: editorNode.children || [],
|
||||
config,
|
||||
...(Object.keys(bindings).length > 0 && { bindings }),
|
||||
@@ -99,6 +181,64 @@ export class EditorToBehaviorTreeDataConverter {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为不可执行的节点(如黑板变量节点)
|
||||
* 这些节点只在编辑器中使用,不参与运行时执行
|
||||
*/
|
||||
private static isNonExecutableNode(editorNode: EditorNode): boolean {
|
||||
const nodeType = editorNode.data['nodeType'];
|
||||
// 黑板变量节点不需要执行,只用于可视化绑定
|
||||
return nodeType === 'blackboard-variable';
|
||||
}
|
||||
|
||||
/**
|
||||
* 从节点数据中提取实现类型
|
||||
*
|
||||
* 优先级:
|
||||
* 1. template.className(标准方式)
|
||||
* 2. data 中的类型字段(compositeType, actionType 等)
|
||||
* 3. 特殊节点类型的默认值(如 Root)
|
||||
*/
|
||||
private static extractImplementationType(data: Record<string, any>, nodeType: NodeType): string | undefined {
|
||||
// 节点类型到数据字段的映射
|
||||
const typeFieldMap: Record<NodeType, string> = {
|
||||
[NodeType.Composite]: 'compositeType',
|
||||
[NodeType.Decorator]: 'decoratorType',
|
||||
[NodeType.Action]: 'actionType',
|
||||
[NodeType.Condition]: 'conditionType',
|
||||
[NodeType.Root]: '', // Root 没有对应的数据字段
|
||||
};
|
||||
|
||||
const field = typeFieldMap[nodeType];
|
||||
if (field && data[field]) {
|
||||
return data[field];
|
||||
}
|
||||
|
||||
// Root 节点的特殊处理
|
||||
if (nodeType === NodeType.Root) {
|
||||
return 'Root';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点类型的默认实现
|
||||
* 当无法确定具体实现类型时使用
|
||||
*/
|
||||
private static getDefaultImplementationType(nodeType: NodeType): string {
|
||||
// 节点类型到默认实现的映射
|
||||
const defaultImplementations: Record<NodeType, string> = {
|
||||
[NodeType.Root]: 'Root',
|
||||
[NodeType.Composite]: 'Sequence',
|
||||
[NodeType.Decorator]: 'Inverter',
|
||||
[NodeType.Action]: 'Wait',
|
||||
[NodeType.Condition]: 'AlwaysTrue',
|
||||
};
|
||||
|
||||
return defaultImplementations[nodeType] || 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射节点类型
|
||||
*/
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
/**
|
||||
* Behavior Tree Unified Plugin
|
||||
* 行为树统一插件
|
||||
* Behavior Tree Plugin Descriptor
|
||||
* 行为树插件描述符
|
||||
*/
|
||||
|
||||
import type { IScene, ServiceContainer } from '@esengine/ecs-framework';
|
||||
import { ComponentRegistry, Core } from '@esengine/ecs-framework';
|
||||
import type {
|
||||
IPluginLoader,
|
||||
IRuntimeModuleLoader,
|
||||
PluginDescriptor,
|
||||
SystemContext
|
||||
} from '@esengine/editor-runtime';
|
||||
|
||||
// Runtime imports
|
||||
import { BehaviorTreeRuntimeComponent } from '../execution/BehaviorTreeRuntimeComponent';
|
||||
import { BehaviorTreeExecutionSystem } from '../execution/BehaviorTreeExecutionSystem';
|
||||
import { BehaviorTreeAssetManager } from '../execution/BehaviorTreeAssetManager';
|
||||
import { GlobalBlackboardService } from '../Services/GlobalBlackboardService';
|
||||
import type { PluginDescriptor } from '@esengine/editor-runtime';
|
||||
|
||||
/**
|
||||
* 插件描述符
|
||||
@@ -49,50 +36,3 @@ export const descriptor: PluginDescriptor = {
|
||||
],
|
||||
icon: 'GitBranch'
|
||||
};
|
||||
|
||||
/**
|
||||
* Behavior Tree Runtime Module
|
||||
* 行为树运行时模块
|
||||
*/
|
||||
export class BehaviorTreeRuntimeModule implements IRuntimeModuleLoader {
|
||||
registerComponents(registry: typeof ComponentRegistry): void {
|
||||
registry.register(BehaviorTreeRuntimeComponent);
|
||||
}
|
||||
|
||||
registerServices(services: ServiceContainer): void {
|
||||
if (!services.isRegistered(GlobalBlackboardService)) {
|
||||
services.registerSingleton(GlobalBlackboardService);
|
||||
}
|
||||
if (!services.isRegistered(BehaviorTreeAssetManager)) {
|
||||
services.registerSingleton(BehaviorTreeAssetManager);
|
||||
}
|
||||
}
|
||||
|
||||
createSystems(scene: IScene, context: SystemContext): void {
|
||||
const behaviorTreeSystem = new BehaviorTreeExecutionSystem(Core);
|
||||
|
||||
// 编辑器模式下默认禁用
|
||||
if (context.isEditor) {
|
||||
behaviorTreeSystem.enabled = false;
|
||||
}
|
||||
|
||||
scene.addSystem(behaviorTreeSystem);
|
||||
|
||||
// 保存引用
|
||||
context.behaviorTreeSystem = behaviorTreeSystem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Behavior Tree Plugin Loader
|
||||
* 行为树插件加载器
|
||||
*
|
||||
* 注意:editorModule 在 ./index.ts 中通过 createBehaviorTreePlugin() 设置
|
||||
*/
|
||||
export const BehaviorTreePlugin: IPluginLoader = {
|
||||
descriptor,
|
||||
runtimeModule: new BehaviorTreeRuntimeModule(),
|
||||
// editorModule 将在 index.ts 中设置
|
||||
};
|
||||
|
||||
export default BehaviorTreePlugin;
|
||||
|
||||
@@ -462,8 +462,13 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
handleNodeMouseUp();
|
||||
};
|
||||
|
||||
const getPortPosition = (nodeId: string, propertyName?: string, portType: 'input' | 'output' = 'output') =>
|
||||
getPortPositionUtil(canvasRef, canvasOffset, canvasScale, nodes, nodeId, propertyName, portType, draggingNodeId, dragDelta, selectedNodeIds);
|
||||
// 使用 useCallback 包装 getPortPosition,确保在 canvasScale/canvasOffset 变化时更新
|
||||
// Use useCallback to wrap getPortPosition to ensure updates when canvasScale/canvasOffset changes
|
||||
const getPortPosition = useCallback(
|
||||
(nodeId: string, propertyName?: string, portType: 'input' | 'output' = 'output') =>
|
||||
getPortPositionUtil(canvasRef, canvasOffset, canvasScale, nodes, nodeId, propertyName, portType, draggingNodeId, dragDelta, selectedNodeIds),
|
||||
[canvasOffset, canvasScale, nodes, draggingNodeId, dragDelta, selectedNodeIds]
|
||||
);
|
||||
|
||||
stopExecutionRef.current = handleStop;
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ interface ConnectionLayerProps {
|
||||
getPortPosition: (nodeId: string, propertyName?: string, portType?: 'input' | 'output') => { x: number; y: number } | null;
|
||||
onConnectionClick?: (e: React.MouseEvent, fromId: string, toId: string) => void;
|
||||
onConnectionContextMenu?: (e: React.MouseEvent, fromId: string, toId: string) => void;
|
||||
/** 用于强制刷新连线(当 canvasScale 等变化时) */
|
||||
/** Used to force refresh connections (when canvasScale etc. changes) */
|
||||
refreshKey?: number;
|
||||
}
|
||||
export const ConnectionLayer: React.FC<ConnectionLayerProps> = ({
|
||||
connections,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { React, useMemo } from '@esengine/editor-runtime';
|
||||
import { React } from '@esengine/editor-runtime';
|
||||
import { ConnectionViewData } from '../../types';
|
||||
import { Node } from '../../domain/models/Node';
|
||||
|
||||
@@ -20,45 +20,40 @@ const ConnectionRendererComponent: React.FC<ConnectionRendererProps> = ({
|
||||
}) => {
|
||||
const { connection, isSelected } = connectionData;
|
||||
|
||||
const pathData = useMemo(() => {
|
||||
let fromPos, toPos;
|
||||
// 直接计算路径数据,不使用 useMemo
|
||||
// getPortPosition 使用节点数据直接计算,不依赖缩放状态
|
||||
let fromPos, toPos;
|
||||
|
||||
if (connection.connectionType === 'property') {
|
||||
// 属性连接:使用 fromProperty 和 toProperty
|
||||
fromPos = getPortPosition(connection.from, connection.fromProperty);
|
||||
toPos = getPortPosition(connection.to, connection.toProperty);
|
||||
} else {
|
||||
// 节点连接:使用输出和输入端口
|
||||
fromPos = getPortPosition(connection.from, undefined, 'output');
|
||||
toPos = getPortPosition(connection.to, undefined, 'input');
|
||||
}
|
||||
if (connection.connectionType === 'property') {
|
||||
fromPos = getPortPosition(connection.from, connection.fromProperty);
|
||||
toPos = getPortPosition(connection.to, connection.toProperty);
|
||||
} else {
|
||||
fromPos = getPortPosition(connection.from, undefined, 'output');
|
||||
toPos = getPortPosition(connection.to, undefined, 'input');
|
||||
}
|
||||
|
||||
if (!fromPos || !toPos) {
|
||||
return null;
|
||||
}
|
||||
if (!fromPos || !toPos) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const x1 = fromPos.x;
|
||||
const y1 = fromPos.y;
|
||||
const x2 = toPos.x;
|
||||
const y2 = toPos.y;
|
||||
const x1 = fromPos.x;
|
||||
const y1 = fromPos.y;
|
||||
const x2 = toPos.x;
|
||||
const y2 = toPos.y;
|
||||
|
||||
let pathD: string;
|
||||
let pathD: string;
|
||||
|
||||
if (connection.connectionType === 'property') {
|
||||
const controlX1 = x1 + (x2 - x1) * 0.5;
|
||||
const controlX2 = x1 + (x2 - x1) * 0.5;
|
||||
pathD = `M ${x1} ${y1} C ${controlX1} ${y1}, ${controlX2} ${y2}, ${x2} ${y2}`;
|
||||
} else {
|
||||
const controlY = y1 + (y2 - y1) * 0.5;
|
||||
pathD = `M ${x1} ${y1} C ${x1} ${controlY}, ${x2} ${controlY}, ${x2} ${y2}`;
|
||||
}
|
||||
if (connection.connectionType === 'property') {
|
||||
const controlX1 = x1 + (x2 - x1) * 0.5;
|
||||
const controlX2 = x1 + (x2 - x1) * 0.5;
|
||||
pathD = `M ${x1} ${y1} C ${controlX1} ${y1}, ${controlX2} ${y2}, ${x2} ${y2}`;
|
||||
} else {
|
||||
const controlY = y1 + (y2 - y1) * 0.5;
|
||||
pathD = `M ${x1} ${y1} C ${x1} ${controlY}, ${x2} ${controlY}, ${x2} ${y2}`;
|
||||
}
|
||||
|
||||
return {
|
||||
path: pathD,
|
||||
midX: (x1 + x2) / 2,
|
||||
midY: (y1 + y2) / 2
|
||||
};
|
||||
}, [connection, fromNode, toNode, getPortPosition]);
|
||||
const midX = (x1 + x2) / 2;
|
||||
const midY = (y1 + y2) / 2;
|
||||
|
||||
const isPropertyConnection = connection.connectionType === 'property';
|
||||
|
||||
@@ -69,11 +64,6 @@ const ConnectionRendererComponent: React.FC<ConnectionRendererProps> = ({
|
||||
|
||||
const gradientId = `gradient-${connection.from}-${connection.to}`;
|
||||
|
||||
if (!pathData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pathD = pathData.path;
|
||||
const endPosMatch = pathD.match(/C [0-9.-]+ [0-9.-]+, [0-9.-]+ [0-9.-]+, ([0-9.-]+) ([0-9.-]+)/);
|
||||
const endX = endPosMatch ? parseFloat(endPosMatch[1]) : 0;
|
||||
const endY = endPosMatch ? parseFloat(endPosMatch[2]) : 0;
|
||||
@@ -106,14 +96,14 @@ const ConnectionRendererComponent: React.FC<ConnectionRendererProps> = ({
|
||||
</defs>
|
||||
|
||||
<path
|
||||
d={pathData.path}
|
||||
d={pathD}
|
||||
fill="none"
|
||||
stroke="transparent"
|
||||
strokeWidth={24}
|
||||
/>
|
||||
|
||||
<path
|
||||
d={pathData.path}
|
||||
d={pathD}
|
||||
fill="none"
|
||||
stroke={glowColor}
|
||||
strokeWidth={strokeWidth + 2}
|
||||
@@ -122,7 +112,7 @@ const ConnectionRendererComponent: React.FC<ConnectionRendererProps> = ({
|
||||
/>
|
||||
|
||||
<path
|
||||
d={pathData.path}
|
||||
d={pathD}
|
||||
fill="none"
|
||||
stroke={`url(#${gradientId})`}
|
||||
strokeWidth={strokeWidth}
|
||||
@@ -141,15 +131,15 @@ const ConnectionRendererComponent: React.FC<ConnectionRendererProps> = ({
|
||||
{isSelected && (
|
||||
<>
|
||||
<circle
|
||||
cx={pathData.midX}
|
||||
cy={pathData.midY}
|
||||
cx={midX}
|
||||
cy={midY}
|
||||
r="8"
|
||||
fill={strokeColor}
|
||||
opacity="0.3"
|
||||
/>
|
||||
<circle
|
||||
cx={pathData.midX}
|
||||
cy={pathData.midY}
|
||||
cx={midX}
|
||||
cy={midY}
|
||||
r="5"
|
||||
fill={strokeColor}
|
||||
stroke="rgba(0, 0, 0, 0.5)"
|
||||
|
||||
@@ -19,6 +19,11 @@ import {
|
||||
IInspectorRegistry,
|
||||
MessageHub,
|
||||
IMessageHub,
|
||||
FileActionRegistry,
|
||||
IDialogService,
|
||||
IFileSystemService,
|
||||
type IDialog,
|
||||
type IFileSystem,
|
||||
createLogger,
|
||||
PluginAPI,
|
||||
} from '@esengine/editor-runtime';
|
||||
@@ -36,14 +41,14 @@ import { useBehaviorTreeDataStore } from './stores';
|
||||
import { createRootNode } from './domain/constants/RootNode';
|
||||
import { PluginContext } from './PluginContext';
|
||||
|
||||
// Import runtime module and descriptor
|
||||
import { BehaviorTreeRuntimeModule, descriptor } from './BehaviorTreePlugin';
|
||||
// Import descriptor from local file, runtime module from main module
|
||||
import { descriptor } from './BehaviorTreePlugin';
|
||||
import { BehaviorTreeRuntimeModule } from '../BehaviorTreeRuntimeModule';
|
||||
|
||||
// 导入编辑器 CSS 样式
|
||||
// 导入编辑器 CSS 样式(会被 vite 自动处理并注入到 DOM)
|
||||
// Import editor CSS styles (automatically handled and injected by vite)
|
||||
import './styles/BehaviorTreeNode.css';
|
||||
import './styles/Toast.css';
|
||||
import './components/panels/BehaviorTreeEditorPanel.css';
|
||||
import './components/panels/BehaviorTreePropertiesPanel.css';
|
||||
|
||||
const logger = createLogger('BehaviorTreeEditorModule');
|
||||
|
||||
@@ -53,6 +58,7 @@ const logger = createLogger('BehaviorTreeEditorModule');
|
||||
*/
|
||||
export class BehaviorTreeEditorModule implements IEditorModuleLoader {
|
||||
private services?: ServiceContainer;
|
||||
private unsubscribers: Array<() => void> = [];
|
||||
|
||||
async install(services: ServiceContainer): Promise<void> {
|
||||
this.services = services;
|
||||
@@ -69,10 +75,102 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
|
||||
// 注册节点检视器
|
||||
this.registerInspectorProviders(services);
|
||||
|
||||
// 注册资产创建消息映射
|
||||
this.registerAssetCreationMappings(services);
|
||||
|
||||
// 订阅创建资产消息
|
||||
this.subscribeToMessages(services);
|
||||
|
||||
logger.info('BehaviorTree editor module installed');
|
||||
}
|
||||
|
||||
private registerAssetCreationMappings(services: ServiceContainer): void {
|
||||
try {
|
||||
const fileActionRegistry = services.resolve(FileActionRegistry);
|
||||
if (fileActionRegistry) {
|
||||
fileActionRegistry.registerAssetCreationMapping({
|
||||
extension: '.btree',
|
||||
createMessage: 'behavior-tree:create-asset'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('FileActionRegistry not available:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private subscribeToMessages(services: ServiceContainer): void {
|
||||
try {
|
||||
const messageHub = services.resolve<MessageHub>(IMessageHub);
|
||||
if (messageHub) {
|
||||
const unsubscribe = messageHub.subscribe('behavior-tree:create-asset', async (payload: {
|
||||
entityId?: string;
|
||||
onChange?: (value: string | null) => void;
|
||||
}) => {
|
||||
await this.handleCreateBehaviorTreeAsset(services, payload);
|
||||
});
|
||||
this.unsubscribers.push(unsubscribe);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('MessageHub not available:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleCreateBehaviorTreeAsset(
|
||||
services: ServiceContainer,
|
||||
payload: { entityId?: string; onChange?: (value: string | null) => void }
|
||||
): Promise<void> {
|
||||
try {
|
||||
const dialog = services.resolve<IDialog>(IDialogService);
|
||||
const fileSystem = services.resolve<IFileSystem>(IFileSystemService);
|
||||
const messageHub = services.resolve<MessageHub>(IMessageHub);
|
||||
|
||||
if (!dialog || !fileSystem) {
|
||||
logger.error('Dialog or FileSystem service not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = await dialog.saveDialog({
|
||||
title: 'Create Behavior Tree Asset',
|
||||
filters: [{ name: 'Behavior Tree', extensions: ['btree'] }],
|
||||
defaultPath: 'new-behavior-tree.btree'
|
||||
});
|
||||
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取默认行为树内容
|
||||
const templates = this.getFileCreationTemplates();
|
||||
const btreeTemplate = templates.find(t => t.extension === 'btree');
|
||||
const content = btreeTemplate
|
||||
? await btreeTemplate.getContent(filePath.split(/[\\/]/).pop() || 'new-behavior-tree.btree')
|
||||
: '{}';
|
||||
|
||||
await fileSystem.writeFile(filePath, content);
|
||||
|
||||
if (payload.onChange) {
|
||||
payload.onChange(filePath);
|
||||
}
|
||||
|
||||
// 打开行为树编辑器
|
||||
if (messageHub) {
|
||||
messageHub.publish('dynamic-panel:open', {
|
||||
panelId: 'behavior-tree-editor',
|
||||
title: `Behavior Tree - ${filePath.split(/[\\/]/).pop()}`
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('Created behavior tree asset:', filePath);
|
||||
} catch (error) {
|
||||
logger.error('Failed to create behavior tree asset:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
// 清理订阅
|
||||
this.unsubscribers.forEach(unsub => unsub());
|
||||
this.unsubscribers = [];
|
||||
|
||||
if (this.services) {
|
||||
this.services.unregister(FileSystemService);
|
||||
this.services.unregister(BehaviorTreeService);
|
||||
@@ -155,7 +253,7 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
|
||||
label: 'Behavior Tree',
|
||||
extension: 'btree',
|
||||
icon: 'GitBranch',
|
||||
create: async (filePath: string) => {
|
||||
getContent: (fileName: string) => {
|
||||
const rootNode = createRootNode();
|
||||
const rootNodeData = {
|
||||
id: rootNode.id,
|
||||
@@ -170,16 +268,13 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
|
||||
};
|
||||
|
||||
const emptyTree = {
|
||||
name: filePath.replace(/.*[/\\]/, '').replace('.btree', ''),
|
||||
name: fileName.replace('.btree', ''),
|
||||
nodes: [rootNodeData],
|
||||
connections: [],
|
||||
variables: {}
|
||||
};
|
||||
|
||||
const content = JSON.stringify(emptyTree, null, 2);
|
||||
// Write using Tauri FS API
|
||||
const { writeTextFile } = await import('@tauri-apps/plugin-fs');
|
||||
await writeTextFile(filePath, content);
|
||||
return JSON.stringify(emptyTree, null, 2);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -3,9 +3,14 @@ import { Node as BehaviorTreeNode } from '../domain/models/Node';
|
||||
|
||||
const logger = createLogger('portUtils');
|
||||
|
||||
// 端口偏移常量(与 CSS 保持一致)
|
||||
const NODE_PORT_OFFSET = 8; // top: -8px / bottom: -8px
|
||||
|
||||
/**
|
||||
* 获取端口在画布世界坐标系中的位置
|
||||
* 直接从 DOM 元素获取实际渲染位置,避免硬编码和手动计算
|
||||
*
|
||||
* 由于 SVG 和节点都在同一个 transform 容器内,直接使用节点的世界坐标计算。
|
||||
* 这种方式不受缩放影响,因为不依赖 getBoundingClientRect。
|
||||
*/
|
||||
export function getPortPosition(
|
||||
canvasRef: RefObject<HTMLDivElement>,
|
||||
@@ -20,41 +25,85 @@ export function getPortPosition(
|
||||
selectedNodeIds?: string[]
|
||||
): { x: number; y: number } | null {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return null;
|
||||
|
||||
const node = nodes.find((n: BehaviorTreeNode) => n.id === nodeId);
|
||||
if (!node) return null;
|
||||
|
||||
// 构造端口选择器
|
||||
let portSelector: string;
|
||||
|
||||
if (propertyName) {
|
||||
// 属性端口:使用 data-property 属性定位
|
||||
portSelector = `[data-node-id="${nodeId}"][data-property="${propertyName}"]`;
|
||||
} else {
|
||||
// 节点端口:使用 data-port-type 属性定位
|
||||
const portTypeAttr = portType === 'input' ? 'node-input' : 'node-output';
|
||||
portSelector = `[data-node-id="${nodeId}"][data-port-type="${portTypeAttr}"]`;
|
||||
}
|
||||
|
||||
const portElement = canvas.querySelector(portSelector) as HTMLElement;
|
||||
if (!portElement) {
|
||||
logger.warn(`Port not found: ${portSelector}`);
|
||||
if (!canvas) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取端口和画布的屏幕矩形
|
||||
const portRect = portElement.getBoundingClientRect();
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
const node = nodes.find((n: BehaviorTreeNode) => n.id === nodeId);
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 计算端口中心相对于画布的屏幕坐标
|
||||
const screenX = portRect.left + portRect.width / 2 - canvasRect.left;
|
||||
const screenY = portRect.top + portRect.height / 2 - canvasRect.top;
|
||||
// 获取节点 DOM 元素来获取尺寸
|
||||
const nodeElement = canvas.querySelector(`[data-node-id="${nodeId}"].bt-node`) as HTMLElement;
|
||||
if (!nodeElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 转换为世界坐标
|
||||
// 屏幕坐标到世界坐标的转换:world = (screen - offset) / scale
|
||||
const worldX = (screenX - canvasOffset.x) / canvasScale;
|
||||
const worldY = (screenY - canvasOffset.y) / canvasScale;
|
||||
// 使用 offsetWidth/offsetHeight 获取未缩放的原始尺寸
|
||||
const nodeWidth = nodeElement.offsetWidth;
|
||||
const nodeHeight = nodeElement.offsetHeight;
|
||||
|
||||
return { x: worldX, y: worldY };
|
||||
// 节点世界坐标(考虑拖拽偏移)
|
||||
let nodeX = node.position.x;
|
||||
let nodeY = node.position.y;
|
||||
|
||||
if (draggingNodeId && dragDelta) {
|
||||
const isBeingDragged = draggingNodeId === nodeId ||
|
||||
(selectedNodeIds && selectedNodeIds.includes(nodeId) && selectedNodeIds.includes(draggingNodeId));
|
||||
if (isBeingDragged) {
|
||||
nodeX += dragDelta.dx;
|
||||
nodeY += dragDelta.dy;
|
||||
}
|
||||
}
|
||||
|
||||
// 节点使用 transform: translate(-50%, -50%) 居中,所以 (nodeX, nodeY) 是视觉中心
|
||||
|
||||
if (propertyName) {
|
||||
// 属性端口:需要找到端口在节点内的相对位置
|
||||
const portElement = nodeElement.querySelector(`[data-property="${propertyName}"]`) as HTMLElement;
|
||||
if (!portElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 使用 offsetLeft/offsetTop 获取相对于 offsetParent 的位置
|
||||
// 需要累加到节点元素
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
let el: HTMLElement | null = portElement;
|
||||
|
||||
while (el && el !== nodeElement) {
|
||||
offsetX += el.offsetLeft;
|
||||
offsetY += el.offsetTop;
|
||||
el = el.offsetParent as HTMLElement | null;
|
||||
}
|
||||
|
||||
// 端口中心相对于节点左上角的偏移
|
||||
const portCenterX = offsetX + portElement.offsetWidth / 2;
|
||||
const portCenterY = offsetY + portElement.offsetHeight / 2;
|
||||
|
||||
// 节点左上角世界坐标
|
||||
const nodeLeft = nodeX - nodeWidth / 2;
|
||||
const nodeTop = nodeY - nodeHeight / 2;
|
||||
|
||||
return {
|
||||
x: nodeLeft + portCenterX,
|
||||
y: nodeTop + portCenterY
|
||||
};
|
||||
} else {
|
||||
// 节点端口(输入/输出)
|
||||
if (portType === 'input') {
|
||||
// 输入端口在顶部中央
|
||||
return {
|
||||
x: nodeX,
|
||||
y: nodeY - nodeHeight / 2 - NODE_PORT_OFFSET
|
||||
};
|
||||
} else {
|
||||
// 输出端口在底部中央
|
||||
return {
|
||||
x: nodeX,
|
||||
y: nodeY + nodeHeight / 2 + NODE_PORT_OFFSET
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EntitySystem, Matcher, Entity, Time, Core, ECSSystem } from '@esengine/ecs-framework';
|
||||
import type { AssetManager } from '@esengine/asset-system';
|
||||
import { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
|
||||
import { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager';
|
||||
import { NodeExecutorRegistry, NodeExecutionContext } from './NodeExecutor';
|
||||
@@ -14,10 +15,13 @@ import './Executors';
|
||||
*/
|
||||
@ECSSystem('BehaviorTreeExecution')
|
||||
export class BehaviorTreeExecutionSystem extends EntitySystem {
|
||||
private assetManager: BehaviorTreeAssetManager | null = null;
|
||||
private btAssetManager: BehaviorTreeAssetManager | null = null;
|
||||
private executorRegistry: NodeExecutorRegistry;
|
||||
private coreInstance: typeof Core | null = null;
|
||||
|
||||
/** 引用 asset-system 的 AssetManager(由 BehaviorTreeRuntimeModule 设置) */
|
||||
private _assetManager: AssetManager | null = null;
|
||||
|
||||
constructor(coreInstance?: typeof Core) {
|
||||
super(Matcher.empty().all(BehaviorTreeRuntimeComponent));
|
||||
this.coreInstance = coreInstance || null;
|
||||
@@ -25,12 +29,102 @@ export class BehaviorTreeExecutionSystem extends EntitySystem {
|
||||
this.registerBuiltInExecutors();
|
||||
}
|
||||
|
||||
private getAssetManager(): BehaviorTreeAssetManager {
|
||||
if (!this.assetManager) {
|
||||
const core = this.coreInstance || Core;
|
||||
this.assetManager = core.services.resolve(BehaviorTreeAssetManager);
|
||||
/**
|
||||
* 设置 AssetManager 引用
|
||||
* Set AssetManager reference
|
||||
*/
|
||||
setAssetManager(assetManager: AssetManager | null): void {
|
||||
this._assetManager = assetManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动所有 autoStart 的行为树(用于预览模式)
|
||||
* Start all autoStart behavior trees (for preview mode)
|
||||
*
|
||||
* 由于编辑器模式下系统默认禁用,实体添加时 onAdded 不会处理自动启动。
|
||||
* 预览开始时需要手动调用此方法来启动所有需要自动启动的行为树。
|
||||
*/
|
||||
startAllAutoStartTrees(): void {
|
||||
if (!this.scene) {
|
||||
this.logger.warn('Scene not available, cannot start auto-start trees');
|
||||
return;
|
||||
}
|
||||
return this.assetManager;
|
||||
|
||||
const entities = this.scene.entities.findEntitiesWithComponent(BehaviorTreeRuntimeComponent);
|
||||
for (const entity of entities) {
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
if (runtime && runtime.autoStart && runtime.treeAssetId && !runtime.isRunning) {
|
||||
this.ensureAssetLoaded(runtime.treeAssetId).then(() => {
|
||||
if (runtime && runtime.autoStart && !runtime.isRunning) {
|
||||
runtime.start();
|
||||
this.logger.debug(`Auto-started behavior tree for entity: ${entity.name}`);
|
||||
}
|
||||
}).catch(e => {
|
||||
this.logger.error(`Failed to load behavior tree for entity ${entity.name}:`, e);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当实体添加到系统时,处理自动启动
|
||||
* Handle auto-start when entity is added to system
|
||||
*/
|
||||
protected override onAdded(entity: Entity): void {
|
||||
// 只有在系统启用时才自动启动
|
||||
// Only auto-start when system is enabled
|
||||
if (!this.enabled) return;
|
||||
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
if (runtime && runtime.autoStart && runtime.treeAssetId && !runtime.isRunning) {
|
||||
// 先尝试加载资产(如果是文件路径)
|
||||
this.ensureAssetLoaded(runtime.treeAssetId).then(() => {
|
||||
// 检查实体是否仍然有效
|
||||
if (runtime && runtime.autoStart && !runtime.isRunning) {
|
||||
runtime.start();
|
||||
this.logger.debug(`Auto-started behavior tree for entity: ${entity.name}`);
|
||||
}
|
||||
}).catch(e => {
|
||||
this.logger.error(`Failed to load behavior tree for entity ${entity.name}:`, e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保行为树资产已加载
|
||||
* Ensure behavior tree asset is loaded
|
||||
*/
|
||||
private async ensureAssetLoaded(assetIdOrPath: string): Promise<void> {
|
||||
const btAssetManager = this.getBTAssetManager();
|
||||
|
||||
// 如果资产已存在,直接返回
|
||||
if (btAssetManager.hasAsset(assetIdOrPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 AssetManager 加载(必须通过 setAssetManager 设置)
|
||||
// Use AssetManager (must be set via setAssetManager)
|
||||
if (!this._assetManager) {
|
||||
this.logger.warn(`AssetManager not set, cannot load: ${assetIdOrPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this._assetManager.loadAssetByPath(assetIdOrPath);
|
||||
if (result && result.asset) {
|
||||
this.logger.debug(`Behavior tree loaded via AssetManager: ${assetIdOrPath}`);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.warn(`Failed to load via AssetManager: ${assetIdOrPath}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
private getBTAssetManager(): BehaviorTreeAssetManager {
|
||||
if (!this.btAssetManager) {
|
||||
const core = this.coreInstance || Core;
|
||||
this.btAssetManager = core.services.resolve(BehaviorTreeAssetManager);
|
||||
}
|
||||
return this.btAssetManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,7 +158,7 @@ export class BehaviorTreeExecutionSystem extends EntitySystem {
|
||||
continue;
|
||||
}
|
||||
|
||||
const treeData = this.getAssetManager().getAsset(runtime.treeAssetId);
|
||||
const treeData = this.getBTAssetManager().getAsset(runtime.treeAssetId);
|
||||
if (!treeData) {
|
||||
this.logger.warn(`未找到行为树资产: ${runtime.treeAssetId}`);
|
||||
continue;
|
||||
@@ -76,6 +170,12 @@ export class BehaviorTreeExecutionSystem extends EntitySystem {
|
||||
runtime.needsReset = false;
|
||||
}
|
||||
|
||||
// 初始化黑板变量(如果行为树定义了默认值)
|
||||
// Initialize blackboard variables from tree definition
|
||||
if (treeData.blackboardVariables && treeData.blackboardVariables.size > 0) {
|
||||
runtime.initializeBlackboard(treeData.blackboardVariables);
|
||||
}
|
||||
|
||||
this.executeTree(entity, runtime, treeData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 根节点执行器
|
||||
*
|
||||
* 行为树的入口节点,执行其唯一的子节点
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Root',
|
||||
nodeType: NodeType.Root,
|
||||
displayName: '根节点',
|
||||
description: '行为树的入口节点',
|
||||
category: 'Root',
|
||||
childrenConstraints: {
|
||||
min: 1,
|
||||
max: 1
|
||||
}
|
||||
})
|
||||
export class RootExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData } = context;
|
||||
|
||||
// 根节点必须有且仅有一个子节点
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
return context.executeChild(childId);
|
||||
}
|
||||
|
||||
reset(_context: NodeExecutionContext): void {
|
||||
// 根节点没有需要重置的状态
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export { RootExecutor } from './RootExecutor';
|
||||
export { SequenceExecutor } from './SequenceExecutor';
|
||||
export { SelectorExecutor } from './SelectorExecutor';
|
||||
export { ParallelExecutor } from './ParallelExecutor';
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
// Asset type constant for behavior tree
|
||||
// 行为树资产类型常量
|
||||
export const BehaviorTreeAssetType = 'behaviortree' as const;
|
||||
|
||||
// Types
|
||||
export * from './Types/TaskStatus';
|
||||
|
||||
|
||||
110
packages/behavior-tree/src/loaders/BehaviorTreeLoader.ts
Normal file
110
packages/behavior-tree/src/loaders/BehaviorTreeLoader.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Behavior Tree Asset Loader
|
||||
* 行为树资产加载器
|
||||
*
|
||||
* 实现 IAssetLoader 接口,用于通过 AssetManager 加载行为树文件
|
||||
*/
|
||||
|
||||
import type {
|
||||
IAssetLoader,
|
||||
IAssetMetadata,
|
||||
IAssetLoadOptions,
|
||||
IAssetLoadResult
|
||||
} from '@esengine/asset-system';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeData } from '../execution/BehaviorTreeData';
|
||||
import { BehaviorTreeAssetManager } from '../execution/BehaviorTreeAssetManager';
|
||||
import { EditorToBehaviorTreeDataConverter } from '../Serialization/EditorToBehaviorTreeDataConverter';
|
||||
import { BehaviorTreeAssetType } from '../index';
|
||||
|
||||
/**
|
||||
* 行为树资产接口
|
||||
*/
|
||||
export interface IBehaviorTreeAsset {
|
||||
/** 行为树数据 */
|
||||
data: BehaviorTreeData;
|
||||
/** 文件路径 */
|
||||
path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树加载器
|
||||
* Behavior tree loader implementing IAssetLoader interface
|
||||
*/
|
||||
export class BehaviorTreeLoader implements IAssetLoader<IBehaviorTreeAsset> {
|
||||
readonly supportedType = BehaviorTreeAssetType;
|
||||
readonly supportedExtensions = ['.btree'];
|
||||
|
||||
/**
|
||||
* 加载行为树资产
|
||||
* Load behavior tree asset
|
||||
*/
|
||||
async load(
|
||||
path: string,
|
||||
metadata: IAssetMetadata,
|
||||
_options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<IBehaviorTreeAsset>> {
|
||||
// 获取文件系统服务
|
||||
const IFileSystemServiceKey = Symbol.for('IFileSystemService');
|
||||
const fileSystem = Core.services.tryResolve(IFileSystemServiceKey) as IFileSystem | null;
|
||||
|
||||
if (!fileSystem) {
|
||||
throw new Error('FileSystem service not available');
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
const content = await fileSystem.readFile(path);
|
||||
|
||||
// 转换为运行时数据
|
||||
const treeData = EditorToBehaviorTreeDataConverter.fromEditorJSON(content);
|
||||
|
||||
// 使用文件路径作为 ID
|
||||
treeData.id = path;
|
||||
|
||||
// 注册到 BehaviorTreeAssetManager(保持兼容性)
|
||||
const btAssetManager = Core.services.tryResolve(BehaviorTreeAssetManager);
|
||||
if (btAssetManager) {
|
||||
btAssetManager.loadAsset(treeData);
|
||||
}
|
||||
|
||||
const asset: IBehaviorTreeAsset = {
|
||||
data: treeData,
|
||||
path
|
||||
};
|
||||
|
||||
return {
|
||||
asset,
|
||||
handle: 0, // 由 AssetManager 分配
|
||||
metadata,
|
||||
loadTime: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以加载
|
||||
* Check if can load this asset
|
||||
*/
|
||||
canLoad(path: string, _metadata: IAssetMetadata): boolean {
|
||||
return path.endsWith('.btree');
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资产
|
||||
* Dispose asset
|
||||
*/
|
||||
dispose(asset: IBehaviorTreeAsset): void {
|
||||
// 从 BehaviorTreeAssetManager 卸载
|
||||
const btAssetManager = Core.services.tryResolve(BehaviorTreeAssetManager);
|
||||
if (btAssetManager && asset.data) {
|
||||
btAssetManager.unloadAsset(asset.data.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件系统接口(简化版,仅用于类型)
|
||||
*/
|
||||
interface IFileSystem {
|
||||
readFile(path: string): Promise<string>;
|
||||
exists(path: string): Promise<boolean>;
|
||||
}
|
||||
Reference in New Issue
Block a user