refactor: reorganize package structure and decouple framework packages (#338)

* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View File

@@ -0,0 +1,303 @@
import { NodeType, BlackboardValueType } from '../Types/TaskStatus';
/**
* 行为树资产元数据
*/
export interface AssetMetadata {
name: string;
description?: string;
version: string;
createdAt?: string;
modifiedAt?: string;
}
/**
* 黑板变量定义
*/
export interface BlackboardVariableDefinition {
name: string;
type: BlackboardValueType;
defaultValue: any;
readonly?: boolean;
description?: string;
}
/**
* 行为树节点配置数据
*/
export interface BehaviorNodeConfigData {
className?: string;
[key: string]: any;
}
/**
* 行为树节点数据(运行时格式)
*/
export interface BehaviorTreeNodeData {
id: string;
name: string;
nodeType: NodeType;
// 节点类型特定数据
data: BehaviorNodeConfigData;
// 子节点ID列表
children: string[];
}
/**
* 属性绑定定义
*/
export interface PropertyBinding {
nodeId: string;
propertyName: string;
variableName: string;
}
/**
* 行为树资产(运行时格式)
*
* 这是用于游戏运行时的优化格式不包含编辑器UI信息
*/
export interface BehaviorTreeAsset {
/**
* 资产格式版本
*/
version: string;
/**
* 元数据
*/
metadata: AssetMetadata;
/**
* 根节点ID
*/
rootNodeId: string;
/**
* 所有节点数据扁平化存储通过children建立层级
*/
nodes: BehaviorTreeNodeData[];
/**
* 黑板变量定义
*/
blackboard: BlackboardVariableDefinition[];
/**
* 属性绑定
*/
propertyBindings?: PropertyBinding[];
}
/**
* 资产验证结果
*/
export interface AssetValidationResult {
valid: boolean;
errors?: string[];
warnings?: string[];
}
/**
* 资产验证器
*/
export class BehaviorTreeAssetValidator {
/**
* 验证资产数据的完整性和正确性
*/
static validate(asset: BehaviorTreeAsset): AssetValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
// 检查版本
if (!asset.version) {
errors.push('Missing version field');
}
// 检查元数据
if (!asset.metadata || !asset.metadata.name) {
errors.push('Missing or invalid metadata');
}
// 检查根节点
if (!asset.rootNodeId) {
errors.push('Missing rootNodeId');
}
// 检查节点列表
if (!asset.nodes || !Array.isArray(asset.nodes)) {
errors.push('Missing or invalid nodes array');
} else {
const nodeIds = new Set<string>();
const rootNode = asset.nodes.find((n) => n.id === asset.rootNodeId);
if (!rootNode) {
errors.push(`Root node '${asset.rootNodeId}' not found in nodes array`);
}
// 检查节点ID唯一性
for (const node of asset.nodes) {
if (!node.id) {
errors.push('Node missing id field');
continue;
}
if (nodeIds.has(node.id)) {
errors.push(`Duplicate node id: ${node.id}`);
}
nodeIds.add(node.id);
// 检查节点类型
if (!node.nodeType) {
errors.push(`Node ${node.id} missing nodeType`);
}
// 检查子节点引用
if (node.children) {
for (const childId of node.children) {
if (!asset.nodes.find((n) => n.id === childId)) {
errors.push(`Node ${node.id} references non-existent child: ${childId}`);
}
}
}
}
// 检查是否有孤立节点
const referencedNodes = new Set<string>([asset.rootNodeId]);
const collectReferencedNodes = (nodeId: string) => {
const node = asset.nodes.find((n) => n.id === nodeId);
if (node && node.children) {
for (const childId of node.children) {
referencedNodes.add(childId);
collectReferencedNodes(childId);
}
}
};
collectReferencedNodes(asset.rootNodeId);
for (const node of asset.nodes) {
if (!referencedNodes.has(node.id)) {
warnings.push(`Orphaned node detected: ${node.id} (${node.name})`);
}
}
}
// 检查黑板定义
if (asset.blackboard && Array.isArray(asset.blackboard)) {
const varNames = new Set<string>();
for (const variable of asset.blackboard) {
if (!variable.name) {
errors.push('Blackboard variable missing name');
continue;
}
if (varNames.has(variable.name)) {
errors.push(`Duplicate blackboard variable: ${variable.name}`);
}
varNames.add(variable.name);
if (!variable.type) {
errors.push(`Blackboard variable ${variable.name} missing type`);
}
}
}
// 检查属性绑定
if (asset.propertyBindings && Array.isArray(asset.propertyBindings)) {
const nodeIds = new Set(asset.nodes.map((n) => n.id));
const varNames = new Set(asset.blackboard?.map((v) => v.name) || []);
for (const binding of asset.propertyBindings) {
if (!nodeIds.has(binding.nodeId)) {
errors.push(`Property binding references non-existent node: ${binding.nodeId}`);
}
if (!varNames.has(binding.variableName)) {
errors.push(`Property binding references non-existent variable: ${binding.variableName}`);
}
if (!binding.propertyName) {
errors.push('Property binding missing propertyName');
}
}
}
const result: AssetValidationResult = {
valid: errors.length === 0
};
if (errors.length > 0) {
result.errors = errors;
}
if (warnings.length > 0) {
result.warnings = warnings;
}
return result;
}
/**
* 获取资产统计信息
*/
static getStats(asset: BehaviorTreeAsset): {
nodeCount: number;
actionCount: number;
conditionCount: number;
compositeCount: number;
decoratorCount: number;
blackboardVariableCount: number;
propertyBindingCount: number;
maxDepth: number;
} {
let actionCount = 0;
let conditionCount = 0;
let compositeCount = 0;
let decoratorCount = 0;
for (const node of asset.nodes) {
switch (node.nodeType) {
case NodeType.Action:
actionCount++;
break;
case NodeType.Condition:
conditionCount++;
break;
case NodeType.Composite:
compositeCount++;
break;
case NodeType.Decorator:
decoratorCount++;
break;
}
}
// 计算最大深度
const getDepth = (nodeId: string, currentDepth: number = 0): number => {
const node = asset.nodes.find((n) => n.id === nodeId);
if (!node || !node.children || node.children.length === 0) {
return currentDepth;
}
let maxChildDepth = currentDepth;
for (const childId of node.children) {
const childDepth = getDepth(childId, currentDepth + 1);
maxChildDepth = Math.max(maxChildDepth, childDepth);
}
return maxChildDepth;
};
return {
nodeCount: asset.nodes.length,
actionCount,
conditionCount,
compositeCount,
decoratorCount,
blackboardVariableCount: asset.blackboard?.length || 0,
propertyBindingCount: asset.propertyBindings?.length || 0,
maxDepth: getDepth(asset.rootNodeId)
};
}
}

View File

@@ -0,0 +1,329 @@
import { createLogger, BinarySerializer } from '@esengine/ecs-framework';
import type { BehaviorTreeAsset } from './BehaviorTreeAsset';
import { BehaviorTreeAssetValidator } from './BehaviorTreeAsset';
import { EditorFormatConverter, type EditorFormat } from './EditorFormatConverter';
const logger = createLogger('BehaviorTreeAssetSerializer');
/**
* 行为树序列化格式
* Behavior tree serialization format
*/
export type BehaviorTreeSerializationFormat = 'json' | 'binary';
/**
* 序列化选项
*/
export interface SerializationOptions {
/**
* 序列化格式
*/
format: BehaviorTreeSerializationFormat;
/**
* 是否美化JSON输出仅format='json'时有效)
*/
pretty?: boolean;
/**
* 是否在序列化前验证资产
*/
validate?: boolean;
}
/**
* 反序列化选项
*/
export interface DeserializationOptions {
/**
* 是否在反序列化后验证资产
*/
validate?: boolean;
/**
* 是否严格模式(验证失败抛出异常)
*/
strict?: boolean;
}
/**
* 行为树资产序列化器
*
* 支持JSON和二进制两种格式
*/
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}`);
}
}
/**
* 序列化为二进制格式
*/
private static serializeToBinary(asset: BehaviorTreeAsset): Uint8Array {
try {
const binary = BinarySerializer.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 = BinarySerializer.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): BehaviorTreeSerializationFormat {
if (typeof data === 'string') {
return 'json';
} else {
return 'binary';
}
}
/**
* 获取序列化数据的信息(不完全反序列化)
*
* @param data 序列化的数据
* @returns 资产元信息
*/
static getInfo(data: string | Uint8Array): {
format: BehaviorTreeSerializationFormat;
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 = BinarySerializer.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: BehaviorTreeSerializationFormat,
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
};
}
}

View File

@@ -0,0 +1,397 @@
import { createLogger } from '@esengine/ecs-framework';
import type { BehaviorTreeAsset, AssetMetadata, BehaviorTreeNodeData, BlackboardVariableDefinition, PropertyBinding } from './BehaviorTreeAsset';
import { NodeType, BlackboardValueType } from '../Types/TaskStatus';
const logger = createLogger('EditorFormatConverter');
/**
* 编辑器节点格式
*/
export interface EditorNodeTemplate {
displayName: string;
category: string;
type: NodeType;
className?: string;
[key: string]: any;
}
export interface EditorNodeData {
nodeType?: string;
className?: string;
variableName?: string;
name?: string;
[key: string]: any;
}
export interface EditorNode {
id: string;
template: EditorNodeTemplate;
data: EditorNodeData;
position: { x: number; y: number };
children: string[];
}
/**
* 编辑器连接格式
*/
export interface EditorConnection {
from: string;
to: string;
fromProperty?: string;
toProperty?: string;
connectionType: 'node' | 'property';
}
/**
* 编辑器格式
*/
export interface EditorFormat {
version?: string;
metadata?: {
name: string;
description?: string;
createdAt?: string;
modifiedAt?: string;
};
nodes: EditorNode[];
connections: EditorConnection[];
blackboard: Record<string, any>;
canvasState?: {
offset: { x: number; y: number };
scale: number;
};
}
/**
* 编辑器格式转换器
*
* 将编辑器格式转换为运行时资产格式
*/
export class EditorFormatConverter {
/**
* 转换编辑器格式为资产格式
*
* @param editorData 编辑器数据
* @param metadata 可选的元数据覆盖
* @returns 行为树资产
*/
static toAsset(editorData: EditorFormat, metadata?: Partial<AssetMetadata>): BehaviorTreeAsset {
logger.info('开始转换编辑器格式到资产格式');
const rootNode = this.findRootNode(editorData.nodes);
if (!rootNode) {
throw new Error('未找到根节点');
}
const assetMetadata: AssetMetadata = {
name: metadata?.name || editorData.metadata?.name || 'Untitled Behavior Tree',
version: metadata?.version || editorData.version || '1.0.0'
};
const description = metadata?.description || editorData.metadata?.description;
if (description) {
assetMetadata.description = description;
}
const createdAt = metadata?.createdAt || editorData.metadata?.createdAt;
if (createdAt) {
assetMetadata.createdAt = createdAt;
}
const modifiedAt = metadata?.modifiedAt || new Date().toISOString();
if (modifiedAt) {
assetMetadata.modifiedAt = modifiedAt;
}
const nodes = this.convertNodes(editorData.nodes);
const blackboard = this.convertBlackboard(editorData.blackboard);
const propertyBindings = this.convertPropertyBindings(
editorData.connections,
editorData.nodes,
blackboard
);
const asset: BehaviorTreeAsset = {
version: '1.0.0',
metadata: assetMetadata,
rootNodeId: rootNode.id,
nodes,
blackboard
};
if (propertyBindings.length > 0) {
asset.propertyBindings = propertyBindings;
}
logger.info(`转换完成: ${nodes.length}个节点, ${blackboard.length}个黑板变量, ${propertyBindings.length}个属性绑定`);
return asset;
}
/**
* 查找根节点
*/
private static findRootNode(nodes: EditorNode[]): EditorNode | null {
return nodes.find((node) =>
node.template.category === '根节点' ||
node.data.nodeType === 'root'
) || null;
}
/**
* 转换节点列表
*/
private static convertNodes(editorNodes: EditorNode[]): BehaviorTreeNodeData[] {
return editorNodes.map((node) => this.convertNode(node));
}
/**
* 转换单个节点
*/
private static convertNode(editorNode: EditorNode): BehaviorTreeNodeData {
const data = { ...editorNode.data };
delete data.nodeType;
if (editorNode.template.className) {
data.className = editorNode.template.className;
}
return {
id: editorNode.id,
name: editorNode.template.displayName || editorNode.data.name || 'Node',
nodeType: editorNode.template.type,
data,
children: editorNode.children || []
};
}
/**
* 转换黑板变量
*/
private static convertBlackboard(blackboard: Record<string, any>): BlackboardVariableDefinition[] {
const variables: BlackboardVariableDefinition[] = [];
for (const [name, value] of Object.entries(blackboard)) {
const type = this.inferBlackboardType(value);
variables.push({
name,
type,
defaultValue: value
});
}
return variables;
}
/**
* 推断黑板变量类型
*/
private static inferBlackboardType(value: any): BlackboardValueType {
if (typeof value === 'number') {
return BlackboardValueType.Number;
} else if (typeof value === 'string') {
return BlackboardValueType.String;
} else if (typeof value === 'boolean') {
return BlackboardValueType.Boolean;
} else {
return BlackboardValueType.Object;
}
}
/**
* 转换属性绑定
*/
private static convertPropertyBindings(
connections: EditorConnection[],
nodes: EditorNode[],
blackboard: BlackboardVariableDefinition[]
): PropertyBinding[] {
const bindings: PropertyBinding[] = [];
const blackboardVarNames = new Set(blackboard.map((v) => v.name));
const propertyConnections = connections.filter((conn) => conn.connectionType === 'property');
for (const conn of propertyConnections) {
const fromNode = nodes.find((n) => n.id === conn.from);
const toNode = nodes.find((n) => n.id === conn.to);
if (!fromNode || !toNode || !conn.toProperty) {
logger.warn(`跳过无效的属性连接: from=${conn.from}, to=${conn.to}`);
continue;
}
let variableName: string | undefined;
if (fromNode.data.nodeType === 'blackboard-variable') {
variableName = fromNode.data.variableName;
} else if (conn.fromProperty) {
variableName = conn.fromProperty;
}
if (!variableName) {
logger.warn(`无法确定变量名: from节点=${fromNode.template.displayName}`);
continue;
}
if (!blackboardVarNames.has(variableName)) {
logger.warn(`属性绑定引用了不存在的黑板变量: ${variableName}`);
continue;
}
bindings.push({
nodeId: toNode.id,
propertyName: conn.toProperty,
variableName
});
}
return bindings;
}
/**
* 从资产格式转换回编辑器格式(用于加载)
*
* @param asset 行为树资产
* @returns 编辑器格式数据
*/
static fromAsset(asset: BehaviorTreeAsset): EditorFormat {
logger.info('开始转换资产格式到编辑器格式');
const nodes = this.convertNodesFromAsset(asset.nodes);
const blackboard: Record<string, any> = {};
for (const variable of asset.blackboard) {
blackboard[variable.name] = variable.defaultValue;
}
const connections = this.convertPropertyBindingsToConnections(
asset.propertyBindings || []
);
const nodeConnections = this.buildNodeConnections(asset.nodes);
connections.push(...nodeConnections);
const metadata: { name: string; description?: string; createdAt?: string; modifiedAt?: string } = {
name: asset.metadata.name
};
if (asset.metadata.description) {
metadata.description = asset.metadata.description;
}
if (asset.metadata.createdAt) {
metadata.createdAt = asset.metadata.createdAt;
}
if (asset.metadata.modifiedAt) {
metadata.modifiedAt = asset.metadata.modifiedAt;
}
const editorData: EditorFormat = {
version: asset.metadata.version,
metadata,
nodes,
connections,
blackboard,
canvasState: {
offset: { x: 0, y: 0 },
scale: 1
}
};
logger.info(`转换完成: ${nodes.length}个节点, ${connections.length}个连接`);
return editorData;
}
/**
* 从资产格式转换节点
*/
private static convertNodesFromAsset(assetNodes: BehaviorTreeNodeData[]): EditorNode[] {
return assetNodes.map((node, index) => {
const position = {
x: 100 + (index % 5) * 250,
y: 100 + Math.floor(index / 5) * 150
};
const template: any = {
displayName: node.name,
category: this.inferCategory(node.nodeType),
type: node.nodeType
};
if (node.data.className) {
template.className = node.data.className;
}
return {
id: node.id,
template,
data: { ...node.data },
position,
children: node.children
};
});
}
/**
* 推断节点分类
*/
private static inferCategory(nodeType: NodeType): string {
switch (nodeType) {
case NodeType.Action:
return '动作';
case NodeType.Condition:
return '条件';
case NodeType.Composite:
return '组合';
case NodeType.Decorator:
return '装饰器';
default:
return '其他';
}
}
/**
* 将属性绑定转换为连接
*/
private static convertPropertyBindingsToConnections(
bindings: PropertyBinding[]
): EditorConnection[] {
const connections: EditorConnection[] = [];
for (const binding of bindings) {
connections.push({
from: 'blackboard',
to: binding.nodeId,
toProperty: binding.propertyName,
connectionType: 'property'
});
}
return connections;
}
/**
* 根据children关系构建节点连接
*/
private static buildNodeConnections(nodes: BehaviorTreeNodeData[]): EditorConnection[] {
const connections: EditorConnection[] = [];
for (const node of nodes) {
for (const childId of node.children) {
connections.push({
from: node.id,
to: childId,
connectionType: 'node'
});
}
}
return connections;
}
}

View File

@@ -0,0 +1,434 @@
import { BehaviorTreeData, BehaviorNodeData } from '../execution/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 EditorConnection {
from: string;
to: string;
connectionType: 'node' | 'property';
fromProperty?: string;
toProperty?: string;
}
/**
* 编辑器行为树数据接口
*/
interface EditorBehaviorTreeData {
version?: string;
metadata?: {
name: string;
description?: string;
createdAt?: string;
modifiedAt?: string;
};
nodes: EditorNode[];
connections?: EditorConnection[];
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');
}
// 构建属性绑定映射nodeId -> { propertyName -> blackboardKey }
const propertyBindingsMap = this.buildPropertyBindingsMap(editorData);
// 转换所有节点(过滤掉不可执行的节点,如黑板变量节点)
const nodesMap = new Map<string, BehaviorNodeData>();
for (const editorNode of editorData.nodes) {
// 跳过黑板变量节点,它们只用于编辑器的可视化绑定
if (this.isNonExecutableNode(editorNode)) {
continue;
}
const propertyBindings = propertyBindingsMap.get(editorNode.id);
const behaviorNodeData = this.convertNode(editorNode, propertyBindings);
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
};
}
/**
* 从连接数据构建属性绑定映射
* 处理 connectionType === 'property' 的连接,将黑板变量节点连接到目标节点的属性
*/
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 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 || implementationType,
nodeType,
implementationType,
children: editorNode.children || [],
config,
...(Object.keys(bindings).length > 0 && { bindings }),
...(abortType && { abortType })
};
}
/**
* 检查是否为不可执行的节点(如黑板变量节点)
* 这些节点只在编辑器中使用,不参与运行时执行
*/
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';
}
/**
* 映射节点类型
*/
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;
}
}

View File

@@ -0,0 +1,420 @@
import { NodeType } from '../Types/TaskStatus';
import { NodeMetadataRegistry, ConfigFieldDefinition, NodeMetadata } from '../execution/NodeMetadata';
/**
* 节点数据JSON格式
*/
export interface NodeDataJSON {
nodeType: string;
compositeType?: string;
decoratorType?: string;
actionType?: string;
conditionType?: string;
[key: string]: any;
}
/**
* 行为树节点属性类型常量
* Behavior tree node property type constants
*/
export const NodePropertyType = {
/** 字符串 */
String: 'string',
/** 数值 */
Number: 'number',
/** 布尔值 */
Boolean: 'boolean',
/** 选择框 */
Select: 'select',
/** 黑板变量引用 */
Blackboard: 'blackboard',
/** 代码编辑器 */
Code: 'code',
/** 变量引用 */
Variable: 'variable',
/** 资产引用 */
Asset: 'asset'
} as const;
/**
* 节点属性类型(支持自定义扩展)
* Node property type (supports custom extensions)
*
* @example
* ```typescript
* // 使用内置类型
* type: NodePropertyType.String
*
* // 使用自定义类型
* type: 'color-picker'
* type: 'curve-editor'
* ```
*/
export type NodePropertyType = (typeof NodePropertyType)[keyof typeof NodePropertyType] | string;
/**
* 属性定义(用于编辑器)
*/
export interface PropertyDefinition {
name: string;
type: NodePropertyType;
label: string;
description?: string;
defaultValue?: any;
options?: Array<{ label: string; value: any }>;
min?: number;
max?: number;
step?: number;
required?: boolean;
/**
* 字段编辑器配置
*
* 指定使用哪个字段编辑器以及相关选项
*
* @example
* ```typescript
* fieldEditor: {
* type: 'asset',
* options: { fileExtension: '.btree' }
* }
* ```
*/
fieldEditor?: {
type: string;
options?: Record<string, any>;
};
/**
* 自定义渲染配置
*
* 用于指定编辑器如何渲染此属性
*
* @example
* ```typescript
* renderConfig: {
* component: 'ColorPicker', // 渲染器组件名称
* props: { // 传递给组件的属性
* showAlpha: true,
* presets: ['#FF0000', '#00FF00']
* }
* }
* ```
*/
renderConfig?: {
/** 渲染器组件名称或路径 */
component?: string;
/** 传递给渲染器的属性配置 */
props?: Record<string, any>;
/** 渲染器的样式类名 */
className?: string;
/** 渲染器的内联样式 */
style?: Record<string, any>;
/** 其他自定义配置 */
[key: string]: any;
};
/**
* 验证规则
*
* 用于在编辑器中验证输入
*
* @example
* ```typescript
* validation: {
* pattern: /^\d+$/,
* message: '只能输入数字',
* validator: (value) => value > 0
* }
* ```
*/
validation?: {
/** 正则表达式验证 */
pattern?: RegExp | string;
/** 验证失败的提示信息 */
message?: string;
/** 自定义验证函数 */
validator?: string; // 函数字符串,编辑器会解析
/** 最小长度(字符串) */
minLength?: number;
/** 最大长度(字符串) */
maxLength?: number;
};
/**
* 是否允许多个连接
* 默认 false只允许一个黑板变量连接
*/
allowMultipleConnections?: boolean;
}
/**
* 节点模板(用于编辑器)
*/
export interface NodeTemplate {
type: NodeType;
displayName: string;
category: string;
icon?: string;
description: string;
color?: string;
className?: string;
componentClass?: Function;
requiresChildren?: boolean;
minChildren?: number;
maxChildren?: number;
defaultConfig: Partial<NodeDataJSON>;
properties: PropertyDefinition[];
}
/**
* 节点模板库
*/
export class NodeTemplates {
/**
* 获取所有节点模板
*/
static getAllTemplates(): NodeTemplate[] {
const allMetadata = NodeMetadataRegistry.getAllMetadata();
return allMetadata.map((metadata) => this.convertMetadataToTemplate(metadata));
}
/**
* 根据类型和子类型获取模板
*/
static getTemplate(type: NodeType, subType: string): NodeTemplate | undefined {
return this.getAllTemplates().find((t) => {
if (t.type !== type) return false;
const config: any = t.defaultConfig;
switch (type) {
case NodeType.Composite:
return config.compositeType === subType;
case NodeType.Decorator:
return config.decoratorType === subType;
case NodeType.Action:
return config.actionType === subType;
case NodeType.Condition:
return config.conditionType === subType;
default:
return false;
}
});
}
/**
* 将NodeMetadata转换为NodeTemplate
*/
private static convertMetadataToTemplate(metadata: NodeMetadata): NodeTemplate {
const properties = this.convertConfigSchemaToProperties(metadata.configSchema || {});
const defaultConfig: Partial<NodeDataJSON> = {
nodeType: this.nodeTypeToString(metadata.nodeType)
};
switch (metadata.nodeType) {
case NodeType.Composite:
defaultConfig.compositeType = metadata.implementationType;
break;
case NodeType.Decorator:
defaultConfig.decoratorType = metadata.implementationType;
break;
case NodeType.Action:
defaultConfig.actionType = metadata.implementationType;
break;
case NodeType.Condition:
defaultConfig.conditionType = metadata.implementationType;
break;
}
if (metadata.configSchema) {
for (const [key, field] of Object.entries(metadata.configSchema)) {
const fieldDef = field as ConfigFieldDefinition;
if (fieldDef.default !== undefined) {
defaultConfig[key] = fieldDef.default;
}
}
}
// 根据节点类型生成默认颜色和图标
const { icon, color } = this.getIconAndColorByType(metadata.nodeType, metadata.category || '');
// 应用子节点约束
const constraints = metadata.childrenConstraints || this.getDefaultConstraintsByNodeType(metadata.nodeType);
const template: NodeTemplate = {
type: metadata.nodeType,
displayName: metadata.displayName,
category: metadata.category || this.getCategoryByNodeType(metadata.nodeType),
description: metadata.description || '',
className: metadata.implementationType,
icon,
color,
defaultConfig,
properties
};
if (constraints) {
if (constraints.min !== undefined) {
template.minChildren = constraints.min;
template.requiresChildren = constraints.min > 0;
}
if (constraints.max !== undefined) {
template.maxChildren = constraints.max;
}
}
return template;
}
/**
* 获取节点类型的默认约束
*/
private static getDefaultConstraintsByNodeType(nodeType: NodeType): { min?: number; max?: number } | undefined {
switch (nodeType) {
case NodeType.Composite:
return { min: 1 };
case NodeType.Decorator:
return { min: 1, max: 1 };
case NodeType.Action:
case NodeType.Condition:
return { max: 0 };
default:
return undefined;
}
}
/**
* 将ConfigSchema转换为PropertyDefinition数组
*/
private static convertConfigSchemaToProperties(
configSchema: Record<string, ConfigFieldDefinition>
): PropertyDefinition[] {
const properties: PropertyDefinition[] = [];
for (const [name, field] of Object.entries(configSchema)) {
const property: PropertyDefinition = {
name,
type: this.mapFieldTypeToPropertyType(field),
label: name
};
if (field.description !== undefined) {
property.description = field.description;
}
if (field.default !== undefined) {
property.defaultValue = field.default;
}
if (field.min !== undefined) {
property.min = field.min;
}
if (field.max !== undefined) {
property.max = field.max;
}
if (field.allowMultipleConnections !== undefined) {
property.allowMultipleConnections = field.allowMultipleConnections;
}
if (field.options) {
property.options = field.options.map((opt) => ({
label: opt,
value: opt
}));
}
if (field.supportBinding) {
property.renderConfig = {
component: 'BindableInput',
props: {
supportBinding: true
}
};
}
properties.push(property);
}
return properties;
}
/**
* 映射字段类型到属性类型
*/
private static mapFieldTypeToPropertyType(field: ConfigFieldDefinition): NodePropertyType {
if (field.options && field.options.length > 0) {
return NodePropertyType.Select;
}
switch (field.type) {
case 'string':
return NodePropertyType.String;
case 'number':
return NodePropertyType.Number;
case 'boolean':
return NodePropertyType.Boolean;
case 'array':
case 'object':
default:
return NodePropertyType.String;
}
}
/**
* NodeType转字符串
*/
private static nodeTypeToString(nodeType: NodeType): string {
switch (nodeType) {
case NodeType.Composite:
return 'composite';
case NodeType.Decorator:
return 'decorator';
case NodeType.Action:
return 'action';
case NodeType.Condition:
return 'condition';
default:
return 'unknown';
}
}
/**
* 根据NodeType获取默认分类
*/
private static getCategoryByNodeType(nodeType: NodeType): string {
switch (nodeType) {
case NodeType.Composite:
return '组合';
case NodeType.Decorator:
return '装饰器';
case NodeType.Action:
return '动作';
case NodeType.Condition:
return '条件';
default:
return '其他';
}
}
/**
* 根据节点类型获取默认图标和颜色
*/
private static getIconAndColorByType(nodeType: NodeType, _category: string): { icon: string; color: string } {
// 根据节点类型设置默认值
switch (nodeType) {
case NodeType.Composite:
return { icon: 'GitBranch', color: '#1976d2' }; // 蓝色
case NodeType.Decorator:
return { icon: 'Settings', color: '#fb8c00' }; // 橙色
case NodeType.Action:
return { icon: 'Play', color: '#388e3c' }; // 绿色
case NodeType.Condition:
return { icon: 'HelpCircle', color: '#d32f2f' }; // 红色
default:
return { icon: 'Circle', color: '#757575' }; // 灰色
}
}
}