refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 (#216)
* refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 * feat(editor): 添加插件市场功能 * feat(editor): 重构插件市场以支持版本管理和ZIP打包 * feat(editor): 重构插件发布流程并修复React渲染警告 * fix(plugin): 修复插件发布和市场的路径不一致问题 * feat: 重构插件发布流程并添加插件删除功能 * fix(editor): 完善插件删除功能并修复多个关键问题 * fix(auth): 修复自动登录与手动登录的竞态条件问题 * feat(editor): 重构插件管理流程 * feat(editor): 支持 ZIP 文件直接发布插件 - 新增 PluginSourceParser 解析插件源 - 重构发布流程支持文件夹和 ZIP 两种方式 - 优化发布向导 UI * feat(editor): 插件市场支持多版本安装 - 插件解压到项目 plugins 目录 - 新增 Tauri 后端安装/卸载命令 - 支持选择任意版本安装 - 修复打包逻辑,保留完整 dist 目录结构 * feat(editor): 个人中心支持多版本管理 - 合并同一插件的不同版本 - 添加版本历史展开/折叠功能 - 禁止有待审核 PR 时更新插件 * fix(editor): 修复 InspectorRegistry 服务注册 - InspectorRegistry 实现 IService 接口 - 注册到 Core.services 供插件使用 * feat(behavior-tree-editor): 完善插件注册和文件操作 - 添加文件创建模板和操作处理器 - 实现右键菜单创建行为树功能 - 修复文件读取权限问题(使用 Tauri 命令) - 添加 BehaviorTreeEditorPanel 组件 - 修复 rollup 配置支持动态导入 * feat(plugin): 完善插件构建和发布流程 * fix(behavior-tree-editor): 完整恢复编辑器并修复 Toast 集成 * fix(behavior-tree-editor): 修复节点选中、连线跟随和文件加载问题并优化性能 * fix(behavior-tree-editor): 修复端口连接失败问题并优化连线样式 * refactor(behavior-tree-editor): 移除调试面板功能简化代码结构 * refactor(behavior-tree-editor): 清理冗余代码合并重复逻辑 * feat(behavior-tree-editor): 完善编辑器核心功能增强扩展性 * fix(lint): 修复ESLint错误确保CI通过 * refactor(behavior-tree-editor): 优化编辑器工具栏和编译器功能 * refactor(behavior-tree-editor): 清理技术债务,优化代码质量 * fix(editor-app): 修复字符串替换安全问题
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { BehaviorTreeData } from './BehaviorTreeData';
|
||||
import { createLogger, IService } from '@esengine/ecs-framework';
|
||||
import { EditorToBehaviorTreeDataConverter } from '../Serialization/EditorToBehaviorTreeDataConverter';
|
||||
|
||||
const logger = createLogger('BehaviorTreeAssetManager');
|
||||
|
||||
@@ -35,6 +36,50 @@ export class BehaviorTreeAssetManager implements IService {
|
||||
logger.info(`行为树资产已加载: ${asset.name} (${asset.nodes.size}个节点)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从编辑器 JSON 格式加载行为树资产
|
||||
*
|
||||
* @param json 编辑器导出的 JSON 字符串
|
||||
* @returns 加载的行为树数据
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
* const jsonContent = await readFile('path/to/tree.btree');
|
||||
* const treeData = assetManager.loadFromEditorJSON(jsonContent);
|
||||
* ```
|
||||
*/
|
||||
loadFromEditorJSON(json: string): BehaviorTreeData {
|
||||
try {
|
||||
const treeData = EditorToBehaviorTreeDataConverter.fromEditorJSON(json);
|
||||
this.loadAsset(treeData);
|
||||
return treeData;
|
||||
} catch (error) {
|
||||
logger.error('从编辑器JSON加载失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量加载多个行为树资产(从编辑器JSON)
|
||||
*
|
||||
* @param jsonDataList JSON字符串列表
|
||||
* @returns 成功加载的资产数量
|
||||
*/
|
||||
loadMultipleFromEditorJSON(jsonDataList: string[]): number {
|
||||
let successCount = 0;
|
||||
for (const json of jsonDataList) {
|
||||
try {
|
||||
this.loadFromEditorJSON(json);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
logger.error('批量加载时出错:', error);
|
||||
}
|
||||
}
|
||||
logger.info(`批量加载完成: ${successCount}/${jsonDataList.length} 个资产`);
|
||||
return successCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取行为树资产
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
import { BehaviorTreeData, BehaviorNodeData } from '../Runtime/BehaviorTreeData';
|
||||
import { NodeType, AbortType } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 编辑器节点数据接口
|
||||
*/
|
||||
interface EditorNode {
|
||||
id: string;
|
||||
template: {
|
||||
type: string;
|
||||
className: string;
|
||||
displayName?: string;
|
||||
};
|
||||
data: Record<string, any>;
|
||||
children?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器行为树数据接口
|
||||
*/
|
||||
interface EditorBehaviorTreeData {
|
||||
version?: string;
|
||||
metadata?: {
|
||||
name: string;
|
||||
description?: string;
|
||||
createdAt?: string;
|
||||
modifiedAt?: string;
|
||||
};
|
||||
nodes: EditorNode[];
|
||||
blackboard?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器格式到运行时格式的转换器
|
||||
*
|
||||
* 负责将编辑器的 JSON 格式(包含UI信息)转换为运行时的 BehaviorTreeData 格式
|
||||
*/
|
||||
export class EditorToBehaviorTreeDataConverter {
|
||||
/**
|
||||
* 将编辑器 JSON 字符串转换为运行时 BehaviorTreeData
|
||||
*/
|
||||
static fromEditorJSON(json: string): BehaviorTreeData {
|
||||
const editorData: EditorBehaviorTreeData = JSON.parse(json);
|
||||
return this.convert(editorData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将编辑器数据对象转换为运行时 BehaviorTreeData
|
||||
*/
|
||||
static convert(editorData: EditorBehaviorTreeData): BehaviorTreeData {
|
||||
// 查找根节点
|
||||
const rootNode = editorData.nodes.find(n =>
|
||||
n.template.type === 'root' || n.data['nodeType'] === 'root'
|
||||
);
|
||||
|
||||
if (!rootNode) {
|
||||
throw new Error('Behavior tree must have a root node');
|
||||
}
|
||||
|
||||
// 转换所有节点
|
||||
const nodesMap = new Map<string, BehaviorNodeData>();
|
||||
for (const editorNode of editorData.nodes) {
|
||||
const behaviorNodeData = this.convertNode(editorNode);
|
||||
nodesMap.set(behaviorNodeData.id, behaviorNodeData);
|
||||
}
|
||||
|
||||
// 转换黑板变量
|
||||
const blackboardVariables = editorData.blackboard
|
||||
? new Map(Object.entries(editorData.blackboard))
|
||||
: new Map();
|
||||
|
||||
return {
|
||||
id: this.generateTreeId(editorData),
|
||||
name: editorData.metadata?.name || 'Untitled',
|
||||
rootNodeId: rootNode.id,
|
||||
nodes: nodesMap,
|
||||
blackboardVariables
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换单个节点
|
||||
*/
|
||||
private static convertNode(editorNode: EditorNode): BehaviorNodeData {
|
||||
const nodeType = this.mapNodeType(editorNode.template.type);
|
||||
const config = this.extractConfig(editorNode.data);
|
||||
const bindings = this.extractBindings(editorNode.data);
|
||||
const abortType = this.extractAbortType(editorNode.data);
|
||||
|
||||
return {
|
||||
id: editorNode.id,
|
||||
name: editorNode.template.displayName || editorNode.template.className,
|
||||
nodeType,
|
||||
implementationType: editorNode.template.className,
|
||||
children: editorNode.children || [],
|
||||
config,
|
||||
...(Object.keys(bindings).length > 0 && { bindings }),
|
||||
...(abortType && { abortType })
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射节点类型
|
||||
*/
|
||||
private static mapNodeType(type: string): NodeType {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'root':
|
||||
return NodeType.Root;
|
||||
case 'composite':
|
||||
return NodeType.Composite;
|
||||
case 'decorator':
|
||||
return NodeType.Decorator;
|
||||
case 'action':
|
||||
return NodeType.Action;
|
||||
case 'condition':
|
||||
return NodeType.Condition;
|
||||
default:
|
||||
throw new Error(`Unknown node type: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取节点配置(过滤掉内部字段和绑定字段)
|
||||
*/
|
||||
private static extractConfig(data: Record<string, any>): Record<string, any> {
|
||||
const config: Record<string, any> = {};
|
||||
const internalFields = new Set(['nodeType', 'abortType']);
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
// 跳过内部字段
|
||||
if (internalFields.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 跳过黑板绑定字段(它们会被提取到 bindings 中)
|
||||
if (this.isBinding(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
config[key] = value;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取黑板变量绑定
|
||||
*/
|
||||
private static extractBindings(data: Record<string, any>): Record<string, string> {
|
||||
const bindings: Record<string, string> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (this.isBinding(value)) {
|
||||
bindings[key] = this.extractBindingKey(value);
|
||||
}
|
||||
}
|
||||
|
||||
return bindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为黑板绑定
|
||||
*/
|
||||
private static isBinding(value: any): boolean {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return value._isBlackboardBinding === true ||
|
||||
value.type === 'blackboard' ||
|
||||
(value.blackboardKey !== undefined);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取黑板绑定的键名
|
||||
*/
|
||||
private static extractBindingKey(binding: any): string {
|
||||
return binding.blackboardKey || binding.key || binding.value || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取中止类型(条件装饰器使用)
|
||||
*/
|
||||
private static extractAbortType(data: Record<string, any>): AbortType | undefined {
|
||||
if (!data['abortType']) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const abortTypeStr = String(data['abortType']).toLowerCase();
|
||||
switch (abortTypeStr) {
|
||||
case 'none':
|
||||
return AbortType.None;
|
||||
case 'self':
|
||||
return AbortType.Self;
|
||||
case 'lowerpriority':
|
||||
case 'lower_priority':
|
||||
return AbortType.LowerPriority;
|
||||
case 'both':
|
||||
return AbortType.Both;
|
||||
default:
|
||||
return AbortType.None;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成行为树ID
|
||||
*/
|
||||
private static generateTreeId(editorData: EditorBehaviorTreeData): string {
|
||||
if (editorData.metadata?.name) {
|
||||
// 将名称转换为合法ID(移除特殊字符)
|
||||
return editorData.metadata.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
}
|
||||
return `tree_${Date.now()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将运行时格式转换回编辑器格式(用于双向转换)
|
||||
*/
|
||||
static toEditorJSON(treeData: BehaviorTreeData): string {
|
||||
const editorData = this.convertToEditor(treeData);
|
||||
return JSON.stringify(editorData, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将运行时 BehaviorTreeData 转换为编辑器格式
|
||||
*/
|
||||
static convertToEditor(treeData: BehaviorTreeData): EditorBehaviorTreeData {
|
||||
const nodes: EditorNode[] = [];
|
||||
|
||||
for (const [_id, nodeData] of treeData.nodes) {
|
||||
nodes.push(this.convertNodeToEditor(nodeData));
|
||||
}
|
||||
|
||||
const blackboard = treeData.blackboardVariables
|
||||
? Object.fromEntries(treeData.blackboardVariables)
|
||||
: {};
|
||||
|
||||
return {
|
||||
version: '1.0.0',
|
||||
metadata: {
|
||||
name: treeData.name,
|
||||
description: '',
|
||||
modifiedAt: new Date().toISOString()
|
||||
},
|
||||
nodes,
|
||||
blackboard
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将运行时节点转换为编辑器节点
|
||||
*/
|
||||
private static convertNodeToEditor(nodeData: BehaviorNodeData): EditorNode {
|
||||
const data: Record<string, any> = { ...nodeData.config };
|
||||
|
||||
// 添加绑定回数据对象
|
||||
if (nodeData.bindings) {
|
||||
for (const [key, blackboardKey] of Object.entries(nodeData.bindings)) {
|
||||
data[key] = {
|
||||
_isBlackboardBinding: true,
|
||||
blackboardKey
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 添加中止类型
|
||||
if (nodeData.abortType !== undefined) {
|
||||
data['abortType'] = nodeData.abortType;
|
||||
}
|
||||
|
||||
// 获取节点类型字符串
|
||||
let typeStr: string;
|
||||
if (typeof nodeData.nodeType === 'string') {
|
||||
typeStr = nodeData.nodeType;
|
||||
} else {
|
||||
typeStr = 'action'; // 默认值
|
||||
}
|
||||
|
||||
const result: EditorNode = {
|
||||
id: nodeData.id,
|
||||
template: {
|
||||
type: typeStr,
|
||||
className: nodeData.implementationType,
|
||||
displayName: nodeData.name
|
||||
},
|
||||
data
|
||||
};
|
||||
|
||||
if (nodeData.children && nodeData.children.length > 0) {
|
||||
result.children = nodeData.children;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ export enum TaskStatus {
|
||||
* 内置节点类型常量
|
||||
*/
|
||||
export const NodeType = {
|
||||
/** 根节点 - 行为树的起始节点 */
|
||||
Root: 'root',
|
||||
/** 复合节点 - 有多个子节点 */
|
||||
Composite: 'composite',
|
||||
/** 装饰器节点 - 有一个子节点 */
|
||||
|
||||
@@ -21,6 +21,7 @@ export * from './Serialization/NodeTemplates';
|
||||
export * from './Serialization/BehaviorTreeAsset';
|
||||
export * from './Serialization/EditorFormatConverter';
|
||||
export * from './Serialization/BehaviorTreeAssetSerializer';
|
||||
export * from './Serialization/EditorToBehaviorTreeDataConverter';
|
||||
|
||||
// 服务
|
||||
export * from './Services/GlobalBlackboardService';
|
||||
|
||||
Reference in New Issue
Block a user