Feature/ecs behavior tree (#188)
* 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): 修复局部黑板类型定义文件扩展名错误
This commit is contained in:
287
packages/behavior-tree/src/Serialization/BehaviorTreeAsset.ts
Normal file
287
packages/behavior-tree/src/Serialization/BehaviorTreeAsset.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
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 BehaviorTreeNodeData {
|
||||
id: string;
|
||||
name: string;
|
||||
nodeType: NodeType;
|
||||
|
||||
// 节点类型特定数据
|
||||
data: Record<string, any>;
|
||||
|
||||
// 子节点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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
warnings: warnings.length > 0 ? warnings : undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资产统计信息
|
||||
*/
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user