* feat(behavior-tree): 完全 ECS 化的行为树系统 * feat(editor-app): 添加行为树可视化编辑器 * chore: 移除 Cocos Creator 扩展目录 * feat(editor-app): 行为树编辑器功能增强 * fix(editor-app): 修复 TypeScript 类型错误 * feat(editor-app): 使用 FlexLayout 重构面板系统并优化资产浏览器 * feat(editor-app): 改进编辑器UI样式并修复行为树执行顺序 * feat(behavior-tree,editor-app): 添加装饰器系统并优化编辑器性能 * feat(behavior-tree,editor-app): 添加属性绑定系统 * feat(editor-app,behavior-tree): 优化编辑器UI并改进行为树功能 * feat(editor-app,behavior-tree): 添加全局黑板系统并增强资产浏览器功能 * feat(behavior-tree,editor-app): 添加运行时资产导出系统 * feat(behavior-tree,editor-app): 添加SubTree系统和资产选择器 * feat(behavior-tree,editor-app): 优化系统架构并改进编辑器文件管理 * fix(behavior-tree,editor-app): 修复SubTree节点错误显示空节点警告 * fix(editor-app): 修复局部黑板类型定义文件扩展名错误
330 lines
9.7 KiB
TypeScript
330 lines
9.7 KiB
TypeScript
import { encode, decode } from '@msgpack/msgpack';
|
||
import { createLogger } from '@esengine/ecs-framework';
|
||
import type { BehaviorTreeAsset } from './BehaviorTreeAsset';
|
||
import { BehaviorTreeAssetValidator } from './BehaviorTreeAsset';
|
||
import { EditorFormatConverter, type EditorFormat } from './EditorFormatConverter';
|
||
|
||
const logger = createLogger('BehaviorTreeAssetSerializer');
|
||
|
||
/**
|
||
* 序列化格式
|
||
*/
|
||
export type SerializationFormat = 'json' | 'binary';
|
||
|
||
/**
|
||
* 序列化选项
|
||
*/
|
||
export interface SerializationOptions {
|
||
/**
|
||
* 序列化格式
|
||
*/
|
||
format: SerializationFormat;
|
||
|
||
/**
|
||
* 是否美化JSON输出(仅format='json'时有效)
|
||
*/
|
||
pretty?: boolean;
|
||
|
||
/**
|
||
* 是否在序列化前验证资产
|
||
*/
|
||
validate?: boolean;
|
||
}
|
||
|
||
/**
|
||
* 反序列化选项
|
||
*/
|
||
export interface DeserializationOptions {
|
||
/**
|
||
* 是否在反序列化后验证资产
|
||
*/
|
||
validate?: boolean;
|
||
|
||
/**
|
||
* 是否严格模式(验证失败抛出异常)
|
||
*/
|
||
strict?: boolean;
|
||
}
|
||
|
||
/**
|
||
* 行为树资产序列化器
|
||
*
|
||
* 支持JSON和二进制(MessagePack)两种格式
|
||
*/
|
||
export class BehaviorTreeAssetSerializer {
|
||
/**
|
||
* 序列化资产
|
||
*
|
||
* @param asset 行为树资产
|
||
* @param options 序列化选项
|
||
* @returns 序列化后的数据(字符串或Uint8Array)
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* // JSON格式
|
||
* const jsonData = BehaviorTreeAssetSerializer.serialize(asset, { format: 'json', pretty: true });
|
||
*
|
||
* // 二进制格式
|
||
* const binaryData = BehaviorTreeAssetSerializer.serialize(asset, { format: 'binary' });
|
||
* ```
|
||
*/
|
||
static serialize(
|
||
asset: BehaviorTreeAsset,
|
||
options: SerializationOptions = { format: 'json', pretty: true }
|
||
): string | Uint8Array {
|
||
// 验证资产(如果需要)
|
||
if (options.validate !== false) {
|
||
const validation = BehaviorTreeAssetValidator.validate(asset);
|
||
if (!validation.valid) {
|
||
const errors = validation.errors?.join(', ') || 'Unknown error';
|
||
throw new Error(`资产验证失败: ${errors}`);
|
||
}
|
||
|
||
if (validation.warnings && validation.warnings.length > 0) {
|
||
logger.warn(`资产验证警告: ${validation.warnings.join(', ')}`);
|
||
}
|
||
}
|
||
|
||
// 根据格式序列化
|
||
if (options.format === 'json') {
|
||
return this.serializeToJSON(asset, options.pretty);
|
||
} else {
|
||
return this.serializeToBinary(asset);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 序列化为JSON格式
|
||
*/
|
||
private static serializeToJSON(asset: BehaviorTreeAsset, pretty: boolean = true): string {
|
||
try {
|
||
const json = pretty
|
||
? JSON.stringify(asset, null, 2)
|
||
: JSON.stringify(asset);
|
||
|
||
logger.info(`已序列化为JSON: ${json.length} 字符`);
|
||
return json;
|
||
} catch (error) {
|
||
throw new Error(`JSON序列化失败: ${error}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 序列化为二进制格式(MessagePack)
|
||
*/
|
||
private static serializeToBinary(asset: BehaviorTreeAsset): Uint8Array {
|
||
try {
|
||
const binary = encode(asset);
|
||
logger.info(`已序列化为二进制: ${binary.length} 字节`);
|
||
return binary;
|
||
} catch (error) {
|
||
throw new Error(`二进制序列化失败: ${error}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 反序列化资产
|
||
*
|
||
* @param data 序列化的数据(字符串或Uint8Array)
|
||
* @param options 反序列化选项
|
||
* @returns 行为树资产
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* // 从JSON加载
|
||
* const asset = BehaviorTreeAssetSerializer.deserialize(jsonString);
|
||
*
|
||
* // 从二进制加载
|
||
* const asset = BehaviorTreeAssetSerializer.deserialize(binaryData);
|
||
* ```
|
||
*/
|
||
static deserialize(
|
||
data: string | Uint8Array,
|
||
options: DeserializationOptions = { validate: true, strict: true }
|
||
): BehaviorTreeAsset {
|
||
let asset: BehaviorTreeAsset;
|
||
|
||
try {
|
||
if (typeof data === 'string') {
|
||
asset = this.deserializeFromJSON(data);
|
||
} else {
|
||
asset = this.deserializeFromBinary(data);
|
||
}
|
||
} catch (error) {
|
||
throw new Error(`反序列化失败: ${error}`);
|
||
}
|
||
|
||
// 验证资产(如果需要)
|
||
if (options.validate !== false) {
|
||
const validation = BehaviorTreeAssetValidator.validate(asset);
|
||
|
||
if (!validation.valid) {
|
||
const errors = validation.errors?.join(', ') || 'Unknown error';
|
||
if (options.strict) {
|
||
throw new Error(`资产验证失败: ${errors}`);
|
||
} else {
|
||
logger.error(`资产验证失败: ${errors}`);
|
||
}
|
||
}
|
||
|
||
if (validation.warnings && validation.warnings.length > 0) {
|
||
logger.warn(`资产验证警告: ${validation.warnings.join(', ')}`);
|
||
}
|
||
}
|
||
|
||
return asset;
|
||
}
|
||
|
||
/**
|
||
* 从JSON反序列化
|
||
*/
|
||
private static deserializeFromJSON(json: string): BehaviorTreeAsset {
|
||
try {
|
||
const data = JSON.parse(json);
|
||
|
||
// 检测是否是编辑器格式(EditorFormat)
|
||
// 编辑器格式有 nodes/connections/blackboard,但没有 rootNodeId
|
||
// 运行时资产格式有 rootNodeId
|
||
const isEditorFormat = !data.rootNodeId && data.nodes && data.connections;
|
||
|
||
if (isEditorFormat) {
|
||
logger.info('检测到编辑器格式,正在转换为运行时资产格式...');
|
||
const editorData = data as EditorFormat;
|
||
const asset = EditorFormatConverter.toAsset(editorData);
|
||
logger.info(`已从编辑器格式转换: ${asset.nodes.length} 个节点`);
|
||
return asset;
|
||
} else {
|
||
const asset = data as BehaviorTreeAsset;
|
||
logger.info(`已从运行时资产格式反序列化: ${asset.nodes.length} 个节点`);
|
||
return asset;
|
||
}
|
||
} catch (error) {
|
||
throw new Error(`JSON解析失败: ${error}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从二进制反序列化
|
||
*/
|
||
private static deserializeFromBinary(binary: Uint8Array): BehaviorTreeAsset {
|
||
try {
|
||
const asset = decode(binary) as BehaviorTreeAsset;
|
||
logger.info(`已从二进制反序列化: ${asset.nodes.length} 个节点`);
|
||
return asset;
|
||
} catch (error) {
|
||
throw new Error(`二进制解码失败: ${error}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检测数据格式
|
||
*
|
||
* @param data 序列化的数据
|
||
* @returns 格式类型
|
||
*/
|
||
static detectFormat(data: string | Uint8Array): SerializationFormat {
|
||
if (typeof data === 'string') {
|
||
return 'json';
|
||
} else {
|
||
return 'binary';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取序列化数据的信息(不完全反序列化)
|
||
*
|
||
* @param data 序列化的数据
|
||
* @returns 资产元信息
|
||
*/
|
||
static getInfo(data: string | Uint8Array): {
|
||
format: SerializationFormat;
|
||
name: string;
|
||
version: string;
|
||
nodeCount: number;
|
||
blackboardVariableCount: number;
|
||
size: number;
|
||
} | null {
|
||
try {
|
||
const format = this.detectFormat(data);
|
||
let asset: BehaviorTreeAsset;
|
||
|
||
if (format === 'json') {
|
||
asset = JSON.parse(data as string);
|
||
} else {
|
||
asset = decode(data as Uint8Array) as BehaviorTreeAsset;
|
||
}
|
||
|
||
const size = typeof data === 'string' ? data.length : data.length;
|
||
|
||
return {
|
||
format,
|
||
name: asset.metadata.name,
|
||
version: asset.version,
|
||
nodeCount: asset.nodes.length,
|
||
blackboardVariableCount: asset.blackboard.length,
|
||
size
|
||
};
|
||
} catch (error) {
|
||
logger.error(`获取资产信息失败: ${error}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 转换格式
|
||
*
|
||
* @param data 源数据
|
||
* @param targetFormat 目标格式
|
||
* @param pretty 是否美化JSON(仅当目标格式为json时有效)
|
||
* @returns 转换后的数据
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* // JSON转二进制
|
||
* const binary = BehaviorTreeAssetSerializer.convert(jsonString, 'binary');
|
||
*
|
||
* // 二进制转JSON
|
||
* const json = BehaviorTreeAssetSerializer.convert(binaryData, 'json', true);
|
||
* ```
|
||
*/
|
||
static convert(
|
||
data: string | Uint8Array,
|
||
targetFormat: SerializationFormat,
|
||
pretty: boolean = true
|
||
): string | Uint8Array {
|
||
const asset = this.deserialize(data, { validate: false });
|
||
|
||
return this.serialize(asset, {
|
||
format: targetFormat,
|
||
pretty,
|
||
validate: false
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 比较两个资产数据的大小
|
||
*
|
||
* @param jsonData JSON格式数据
|
||
* @param binaryData 二进制格式数据
|
||
* @returns 压缩率(百分比)
|
||
*/
|
||
static compareSize(jsonData: string, binaryData: Uint8Array): {
|
||
jsonSize: number;
|
||
binarySize: number;
|
||
compressionRatio: number;
|
||
savedBytes: number;
|
||
} {
|
||
const jsonSize = jsonData.length;
|
||
const binarySize = binaryData.length;
|
||
const savedBytes = jsonSize - binarySize;
|
||
const compressionRatio = (savedBytes / jsonSize) * 100;
|
||
|
||
return {
|
||
jsonSize,
|
||
binarySize,
|
||
compressionRatio,
|
||
savedBytes
|
||
};
|
||
}
|
||
}
|