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:
@@ -0,0 +1,136 @@
|
||||
import { BehaviorTreeData } from './BehaviorTreeData';
|
||||
import { createLogger, IService } from '@esengine/ecs-framework';
|
||||
import { EditorToBehaviorTreeDataConverter } from '../Serialization/EditorToBehaviorTreeDataConverter';
|
||||
|
||||
const logger = createLogger('BehaviorTreeAssetManager');
|
||||
|
||||
/**
|
||||
* 行为树资产管理器(服务)
|
||||
*
|
||||
* 管理所有共享的BehaviorTreeData
|
||||
* 多个实例可以引用同一份数据
|
||||
*
|
||||
* 使用方式:
|
||||
* ```typescript
|
||||
* // 注册服务
|
||||
* Core.services.registerSingleton(BehaviorTreeAssetManager);
|
||||
*
|
||||
* // 使用服务
|
||||
* const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
* ```
|
||||
*/
|
||||
export class BehaviorTreeAssetManager implements IService {
|
||||
/**
|
||||
* 已加载的行为树资产
|
||||
*/
|
||||
private assets: Map<string, BehaviorTreeData> = new Map();
|
||||
|
||||
/**
|
||||
* 加载行为树资产
|
||||
*/
|
||||
loadAsset(asset: BehaviorTreeData): void {
|
||||
if (this.assets.has(asset.id)) {
|
||||
logger.warn(`行为树资产已存在,将被覆盖: ${asset.id}`);
|
||||
}
|
||||
this.assets.set(asset.id, asset);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取行为树资产
|
||||
*/
|
||||
getAsset(assetId: string): BehaviorTreeData | undefined {
|
||||
return this.assets.get(assetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查资产是否存在
|
||||
*/
|
||||
hasAsset(assetId: string): boolean {
|
||||
return this.assets.has(assetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载行为树资产
|
||||
*/
|
||||
unloadAsset(assetId: string): boolean {
|
||||
const result = this.assets.delete(assetId);
|
||||
if (result) {
|
||||
logger.info(`行为树资产已卸载: ${assetId}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有资产
|
||||
*/
|
||||
clearAll(): void {
|
||||
this.assets.clear();
|
||||
logger.info('所有行为树资产已清空');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已加载资产数量
|
||||
*/
|
||||
getAssetCount(): number {
|
||||
return this.assets.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有资产ID
|
||||
*/
|
||||
getAllAssetIds(): string[] {
|
||||
return Array.from(this.assets.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源(实现IService接口)
|
||||
*/
|
||||
dispose(): void {
|
||||
this.clearAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { TaskStatus, NodeType, AbortType } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 行为树节点定义(纯数据结构)
|
||||
*
|
||||
* 不依赖Entity,可以被多个实例共享
|
||||
*/
|
||||
export interface BehaviorNodeData {
|
||||
/** 节点唯一ID */
|
||||
id: string;
|
||||
|
||||
/** 节点名称(用于调试) */
|
||||
name: string;
|
||||
|
||||
/** 节点类型 */
|
||||
nodeType: NodeType;
|
||||
|
||||
/** 节点实现类型(对应Component类名) */
|
||||
implementationType: string;
|
||||
|
||||
/** 子节点ID列表 */
|
||||
children?: string[];
|
||||
|
||||
/** 节点特定配置数据 */
|
||||
config: Record<string, any>;
|
||||
|
||||
/** 属性到黑板变量的绑定映射 */
|
||||
bindings?: Record<string, string>;
|
||||
|
||||
/** 中止类型(条件装饰器使用) */
|
||||
abortType?: AbortType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树定义(可共享的Asset)
|
||||
*/
|
||||
export interface BehaviorTreeData {
|
||||
/** 树ID */
|
||||
id: string;
|
||||
|
||||
/** 树名称 */
|
||||
name: string;
|
||||
|
||||
/** 根节点ID */
|
||||
rootNodeId: string;
|
||||
|
||||
/** 所有节点(扁平化存储) */
|
||||
nodes: Map<string, BehaviorNodeData>;
|
||||
|
||||
/** 黑板变量定义 */
|
||||
blackboardVariables?: Map<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点运行时状态
|
||||
*
|
||||
* 每个BehaviorTreeRuntimeComponent实例独立维护
|
||||
*/
|
||||
export interface NodeRuntimeState {
|
||||
/** 当前执行状态 */
|
||||
status: TaskStatus;
|
||||
|
||||
/** 当前执行的子节点索引(复合节点使用) */
|
||||
currentChildIndex: number;
|
||||
|
||||
/** 执行顺序号(用于调试和可视化) */
|
||||
executionOrder?: number;
|
||||
|
||||
/** 开始执行时间(某些节点需要) */
|
||||
startTime?: number;
|
||||
|
||||
/** 上次执行时间(冷却节点使用) */
|
||||
lastExecutionTime?: number;
|
||||
|
||||
/** 当前重复次数(重复节点使用) */
|
||||
repeatCount?: number;
|
||||
|
||||
/** 缓存的结果(某些条件节点使用) */
|
||||
cachedResult?: any;
|
||||
|
||||
/** 洗牌后的索引(随机节点使用) */
|
||||
shuffledIndices?: number[];
|
||||
|
||||
/** 是否被中止 */
|
||||
isAborted?: boolean;
|
||||
|
||||
/** 上次条件评估结果(条件装饰器使用) */
|
||||
lastConditionResult?: boolean;
|
||||
|
||||
/** 正在观察的黑板键(条件装饰器使用) */
|
||||
observedKeys?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建默认的运行时状态
|
||||
*/
|
||||
export function createDefaultRuntimeState(): NodeRuntimeState {
|
||||
return {
|
||||
status: TaskStatus.Invalid,
|
||||
currentChildIndex: 0
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
import { EntitySystem, Matcher, Entity, Time, Core, ECSSystem, ServiceContainer } from '@esengine/ecs-framework';
|
||||
import type { IBTAssetManager, IBehaviorTreeAssetContent } from '../Types/AssetManagerInterface';
|
||||
import { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
|
||||
import { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager';
|
||||
import { NodeExecutorRegistry, NodeExecutionContext } from './NodeExecutor';
|
||||
import { BehaviorTreeData, BehaviorNodeData } from './BehaviorTreeData';
|
||||
import { TaskStatus } from '../Types/TaskStatus';
|
||||
import { NodeMetadataRegistry } from './NodeMetadata';
|
||||
import './Executors';
|
||||
|
||||
/**
|
||||
* 行为树执行系统
|
||||
*
|
||||
* 统一处理所有行为树的执行
|
||||
*/
|
||||
@ECSSystem('BehaviorTreeExecution')
|
||||
export class BehaviorTreeExecutionSystem extends EntitySystem {
|
||||
private btAssetManager: BehaviorTreeAssetManager | null = null;
|
||||
private executorRegistry: NodeExecutorRegistry;
|
||||
private _services: ServiceContainer | null = null;
|
||||
|
||||
/** 引用外部资产管理器(可选,由外部模块设置) */
|
||||
private _assetManager: IBTAssetManager | null = null;
|
||||
|
||||
/** 已警告过的缺失资产,避免重复警告 */
|
||||
private _warnedMissingAssets: Set<string> = new Set();
|
||||
|
||||
constructor(services?: ServiceContainer) {
|
||||
super(Matcher.empty().all(BehaviorTreeRuntimeComponent));
|
||||
this._services = services || null;
|
||||
this.executorRegistry = new NodeExecutorRegistry();
|
||||
this.registerBuiltInExecutors();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置外部资产管理器引用(可选)
|
||||
* @en Set external asset manager reference (optional)
|
||||
*
|
||||
* @zh 当与 ESEngine 集成时,由 BehaviorTreeRuntimeModule 调用。
|
||||
* 不使用 ESEngine 时,可以不调用此方法,
|
||||
* 直接使用 BehaviorTreeAssetManager.loadFromEditorJSON() 加载资产。
|
||||
*
|
||||
* @en Called by BehaviorTreeRuntimeModule when integrating with ESEngine.
|
||||
* When not using ESEngine, you can skip this and use
|
||||
* BehaviorTreeAssetManager.loadFromEditorJSON() to load assets directly.
|
||||
*/
|
||||
setAssetManager(assetManager: IBTAssetManager | 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;
|
||||
}
|
||||
|
||||
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(assetGuid: string): Promise<void> {
|
||||
const btAssetManager = this.getBTAssetManager();
|
||||
|
||||
// 如果资产已存在,直接返回
|
||||
if (btAssetManager.hasAsset(assetGuid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 AssetManager 加载(必须通过 setAssetManager 设置)
|
||||
// Use AssetManager (must be set via setAssetManager)
|
||||
if (!this._assetManager) {
|
||||
this.logger.warn(`AssetManager not set, cannot load: ${assetGuid}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用 loadAsset 通过 GUID 加载,而不是 loadAssetByPath
|
||||
// Use loadAsset with GUID instead of loadAssetByPath
|
||||
const result = await this._assetManager.loadAsset(assetGuid);
|
||||
if (result && result.asset) {
|
||||
this.logger.debug(`Behavior tree loaded via AssetManager: ${assetGuid}`);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.warn(`Failed to load via AssetManager: ${assetGuid}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
private getBTAssetManager(): BehaviorTreeAssetManager {
|
||||
if (!this.btAssetManager) {
|
||||
// 优先使用传入的 services,否则回退到全局 Core.services
|
||||
// Prefer passed services, fallback to global Core.services
|
||||
const services = this._services || Core.services;
|
||||
if (!services) {
|
||||
throw new Error('ServiceContainer is not available. Ensure Core.create() was called.');
|
||||
}
|
||||
this.btAssetManager = services.resolve(BehaviorTreeAssetManager);
|
||||
}
|
||||
return this.btAssetManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取行为树数据
|
||||
* Get behavior tree data from AssetManager or BehaviorTreeAssetManager
|
||||
*
|
||||
* 优先从 AssetManager 获取(新方式),如果没有再从 BehaviorTreeAssetManager 获取(兼容旧方式)
|
||||
*/
|
||||
private getTreeData(assetGuid: string): BehaviorTreeData | undefined {
|
||||
// 1. 优先从 AssetManager 获取(如果已加载)
|
||||
// First try AssetManager (preferred way)
|
||||
if (this._assetManager) {
|
||||
// 使用 getAsset 通过 GUID 获取,而不是 getAssetByPath
|
||||
// Use getAsset with GUID instead of getAssetByPath
|
||||
const cachedAsset = this._assetManager.getAsset<IBehaviorTreeAssetContent>(assetGuid);
|
||||
if (cachedAsset?.data) {
|
||||
return cachedAsset.data;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 回退到 BehaviorTreeAssetManager(兼容旧方式)
|
||||
// Fallback to BehaviorTreeAssetManager (legacy support)
|
||||
return this.getBTAssetManager().getAsset(assetGuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册所有执行器(包括内置和插件提供的)
|
||||
*/
|
||||
private registerBuiltInExecutors(): void {
|
||||
const constructors = NodeMetadataRegistry.getAllExecutorConstructors();
|
||||
|
||||
for (const [implementationType, ExecutorClass] of constructors) {
|
||||
try {
|
||||
const instance = new ExecutorClass();
|
||||
this.executorRegistry.register(implementationType, instance);
|
||||
} catch (error) {
|
||||
this.logger.error(`注册执行器失败: ${implementationType}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取执行器注册表
|
||||
*/
|
||||
getExecutorRegistry(): NodeExecutorRegistry {
|
||||
return this.executorRegistry;
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent)!;
|
||||
|
||||
if (!runtime.isRunning) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const treeData = this.getTreeData(runtime.treeAssetId);
|
||||
if (!treeData) {
|
||||
// 只警告一次,避免每帧重复输出
|
||||
// Only warn once to avoid repeated output every frame
|
||||
if (!this._warnedMissingAssets.has(runtime.treeAssetId)) {
|
||||
this._warnedMissingAssets.add(runtime.treeAssetId);
|
||||
this.logger.warn(`未找到行为树资产: ${runtime.treeAssetId}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果标记了需要重置,先重置状态
|
||||
if (runtime.needsReset) {
|
||||
runtime.resetAllStates();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行整个行为树
|
||||
*/
|
||||
private executeTree(
|
||||
entity: Entity,
|
||||
runtime: BehaviorTreeRuntimeComponent,
|
||||
treeData: BehaviorTreeData
|
||||
): void {
|
||||
const rootNode = treeData.nodes.get(treeData.rootNodeId);
|
||||
if (!rootNode) {
|
||||
this.logger.error(`未找到根节点: ${treeData.rootNodeId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const status = this.executeNode(entity, runtime, rootNode, treeData);
|
||||
|
||||
// 如果树完成了,标记在下一个tick时重置状态
|
||||
// 这样UI可以看到节点的最终状态
|
||||
if (status !== TaskStatus.Running) {
|
||||
runtime.needsReset = true;
|
||||
} else {
|
||||
runtime.needsReset = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个节点
|
||||
*/
|
||||
private executeNode(
|
||||
entity: Entity,
|
||||
runtime: BehaviorTreeRuntimeComponent,
|
||||
nodeData: BehaviorNodeData,
|
||||
treeData: BehaviorTreeData
|
||||
): TaskStatus {
|
||||
const state = runtime.getNodeState(nodeData.id);
|
||||
|
||||
if (runtime.shouldAbort(nodeData.id)) {
|
||||
runtime.clearAbortRequest(nodeData.id);
|
||||
state.isAborted = true;
|
||||
|
||||
const executor = this.executorRegistry.get(nodeData.implementationType);
|
||||
if (executor && executor.reset) {
|
||||
const context = this.createContext(entity, runtime, nodeData, treeData);
|
||||
executor.reset(context);
|
||||
}
|
||||
|
||||
runtime.activeNodeIds.delete(nodeData.id);
|
||||
state.status = TaskStatus.Failure;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
runtime.activeNodeIds.add(nodeData.id);
|
||||
state.isAborted = false;
|
||||
|
||||
if (state.executionOrder === undefined) {
|
||||
runtime.executionOrderCounter++;
|
||||
state.executionOrder = runtime.executionOrderCounter;
|
||||
}
|
||||
|
||||
const executor = this.executorRegistry.get(nodeData.implementationType);
|
||||
if (!executor) {
|
||||
this.logger.error(`未找到执行器: ${nodeData.implementationType}`);
|
||||
state.status = TaskStatus.Failure;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const context = this.createContext(entity, runtime, nodeData, treeData);
|
||||
|
||||
try {
|
||||
const status = executor.execute(context);
|
||||
state.status = status;
|
||||
|
||||
if (status !== TaskStatus.Running) {
|
||||
runtime.activeNodeIds.delete(nodeData.id);
|
||||
|
||||
if (executor.reset) {
|
||||
executor.reset(context);
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
} catch (error) {
|
||||
this.logger.error(`执行节点时发生错误: ${nodeData.name}`, error);
|
||||
state.status = TaskStatus.Failure;
|
||||
runtime.activeNodeIds.delete(nodeData.id);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建执行上下文
|
||||
*/
|
||||
private createContext(
|
||||
entity: Entity,
|
||||
runtime: BehaviorTreeRuntimeComponent,
|
||||
nodeData: BehaviorNodeData,
|
||||
treeData: BehaviorTreeData
|
||||
): NodeExecutionContext {
|
||||
return {
|
||||
entity,
|
||||
nodeData,
|
||||
state: runtime.getNodeState(nodeData.id),
|
||||
runtime,
|
||||
treeData,
|
||||
deltaTime: Time.deltaTime,
|
||||
totalTime: Time.totalTime,
|
||||
executeChild: (childId: string) => {
|
||||
const childData = treeData.nodes.get(childId);
|
||||
if (!childData) {
|
||||
this.logger.warn(`未找到子节点: ${childId}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
return this.executeNode(entity, runtime, childData, treeData);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行子节点列表
|
||||
*/
|
||||
executeChildren(
|
||||
context: NodeExecutionContext,
|
||||
childIndices?: number[]
|
||||
): TaskStatus[] {
|
||||
const { nodeData, treeData, entity, runtime } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results: TaskStatus[] = [];
|
||||
const indicesToExecute = childIndices ||
|
||||
Array.from({ length: nodeData.children.length }, (_, i) => i);
|
||||
|
||||
for (const index of indicesToExecute) {
|
||||
if (index >= nodeData.children.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[index]!;
|
||||
const childData = treeData.nodes.get(childId);
|
||||
|
||||
if (!childData) {
|
||||
this.logger.warn(`未找到子节点: ${childId}`);
|
||||
results.push(TaskStatus.Failure);
|
||||
continue;
|
||||
}
|
||||
|
||||
const status = this.executeNode(entity, runtime, childData, treeData);
|
||||
results.push(status);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import { Component, ECSComponent, Property } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
|
||||
import { NodeRuntimeState, createDefaultRuntimeState } from './BehaviorTreeData';
|
||||
import { TaskStatus } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 黑板变化监听器
|
||||
*/
|
||||
export type BlackboardChangeListener = (key: string, newValue: any, oldValue: any) => void;
|
||||
|
||||
/**
|
||||
* 黑板观察者信息
|
||||
*/
|
||||
interface BlackboardObserver {
|
||||
nodeId: string;
|
||||
keys: Set<string>;
|
||||
callback: BlackboardChangeListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树运行时组件
|
||||
*
|
||||
* 挂载到游戏Entity上,引用共享的BehaviorTreeData
|
||||
* 维护该Entity独立的运行时状态
|
||||
*/
|
||||
@ECSComponent('BehaviorTreeRuntime')
|
||||
@Serializable({ version: 1 })
|
||||
export class BehaviorTreeRuntimeComponent extends Component {
|
||||
/**
|
||||
* 引用的行为树资产ID(可序列化)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Behavior Tree', extensions: ['.btree'] })
|
||||
treeAssetId: string = '';
|
||||
|
||||
/**
|
||||
* 是否自动启动
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Auto Start' })
|
||||
autoStart: boolean = true;
|
||||
|
||||
/**
|
||||
* 是否正在运行
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
isRunning: boolean = false;
|
||||
|
||||
/**
|
||||
* 节点运行时状态(每个节点独立)
|
||||
* 不序列化,每次加载时重新初始化
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
private nodeStates: Map<string, NodeRuntimeState> = new Map();
|
||||
|
||||
/**
|
||||
* 黑板数据(该Entity独立的数据)
|
||||
* 不序列化,通过初始化设置
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
private blackboard: Map<string, any> = new Map();
|
||||
|
||||
/**
|
||||
* 黑板观察者列表
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
private blackboardObservers: Map<string, BlackboardObserver[]> = new Map();
|
||||
|
||||
/**
|
||||
* 当前激活的节点ID列表(用于调试)
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
activeNodeIds: Set<string> = new Set();
|
||||
|
||||
/**
|
||||
* 标记是否需要在下一个tick重置状态
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
needsReset: boolean = false;
|
||||
|
||||
/**
|
||||
* 需要中止的节点ID列表
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
nodesToAbort: Set<string> = new Set();
|
||||
|
||||
/**
|
||||
* 执行顺序计数器(用于调试和可视化)
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
executionOrderCounter: number = 0;
|
||||
|
||||
/**
|
||||
* 获取节点运行时状态
|
||||
*/
|
||||
getNodeState(nodeId: string): NodeRuntimeState {
|
||||
if (!this.nodeStates.has(nodeId)) {
|
||||
this.nodeStates.set(nodeId, createDefaultRuntimeState());
|
||||
}
|
||||
return this.nodeStates.get(nodeId)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置节点状态
|
||||
*/
|
||||
resetNodeState(nodeId: string): void {
|
||||
const state = this.getNodeState(nodeId);
|
||||
state.status = TaskStatus.Invalid;
|
||||
state.currentChildIndex = 0;
|
||||
delete state.startTime;
|
||||
delete state.lastExecutionTime;
|
||||
delete state.repeatCount;
|
||||
delete state.cachedResult;
|
||||
delete state.shuffledIndices;
|
||||
delete state.isAborted;
|
||||
delete state.lastConditionResult;
|
||||
delete state.observedKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置所有节点状态
|
||||
*/
|
||||
resetAllStates(): void {
|
||||
this.nodeStates.clear();
|
||||
this.activeNodeIds.clear();
|
||||
this.executionOrderCounter = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取黑板值
|
||||
*/
|
||||
getBlackboardValue<T = any>(key: string): T | undefined {
|
||||
return this.blackboard.get(key) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置黑板值
|
||||
*/
|
||||
setBlackboardValue(key: string, value: any): void {
|
||||
const oldValue = this.blackboard.get(key);
|
||||
this.blackboard.set(key, value);
|
||||
|
||||
if (oldValue !== value) {
|
||||
this.notifyBlackboardChange(key, value, oldValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查黑板是否有某个键
|
||||
*/
|
||||
hasBlackboardKey(key: string): boolean {
|
||||
return this.blackboard.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化黑板(从树定义的默认值)
|
||||
*/
|
||||
initializeBlackboard(variables?: Map<string, any>): void {
|
||||
if (variables) {
|
||||
variables.forEach((value, key) => {
|
||||
if (!this.blackboard.has(key)) {
|
||||
this.blackboard.set(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空黑板
|
||||
*/
|
||||
clearBlackboard(): void {
|
||||
this.blackboard.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动行为树
|
||||
*/
|
||||
start(): void {
|
||||
this.isRunning = true;
|
||||
this.resetAllStates();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止行为树
|
||||
*/
|
||||
stop(): void {
|
||||
this.isRunning = false;
|
||||
this.activeNodeIds.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停行为树
|
||||
*/
|
||||
pause(): void {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复行为树
|
||||
*/
|
||||
resume(): void {
|
||||
this.isRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册黑板观察者
|
||||
*/
|
||||
observeBlackboard(nodeId: string, keys: string[], callback: BlackboardChangeListener): void {
|
||||
const observer: BlackboardObserver = {
|
||||
nodeId,
|
||||
keys: new Set(keys),
|
||||
callback
|
||||
};
|
||||
|
||||
for (const key of keys) {
|
||||
if (!this.blackboardObservers.has(key)) {
|
||||
this.blackboardObservers.set(key, []);
|
||||
}
|
||||
this.blackboardObservers.get(key)!.push(observer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册黑板观察者
|
||||
*/
|
||||
unobserveBlackboard(nodeId: string): void {
|
||||
for (const observers of this.blackboardObservers.values()) {
|
||||
const index = observers.findIndex((o) => o.nodeId === nodeId);
|
||||
if (index !== -1) {
|
||||
observers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知黑板变化
|
||||
*/
|
||||
private notifyBlackboardChange(key: string, newValue: any, oldValue: any): void {
|
||||
const observers = this.blackboardObservers.get(key);
|
||||
if (!observers) return;
|
||||
|
||||
for (const observer of observers) {
|
||||
try {
|
||||
observer.callback(key, newValue, oldValue);
|
||||
} catch (error) {
|
||||
console.error(`黑板观察者回调错误 (节点: ${observer.nodeId}):`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求中止节点
|
||||
*/
|
||||
requestAbort(nodeId: string): void {
|
||||
this.nodesToAbort.add(nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查节点是否需要中止
|
||||
*/
|
||||
shouldAbort(nodeId: string): boolean {
|
||||
return this.nodesToAbort.has(nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除中止请求
|
||||
*/
|
||||
clearAbortRequest(nodeId: string): void {
|
||||
this.nodesToAbort.delete(nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有中止请求
|
||||
*/
|
||||
clearAllAbortRequests(): void {
|
||||
this.nodesToAbort.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 总是失败装饰器执行器
|
||||
*
|
||||
* 无论子节点结果如何都返回失败
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'AlwaysFail',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '总是失败',
|
||||
description: '无论子节点结果如何都返回失败',
|
||||
category: 'Decorator'
|
||||
})
|
||||
export class AlwaysFailExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 总是成功装饰器执行器
|
||||
*
|
||||
* 无论子节点结果如何都返回成功
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'AlwaysSucceed',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '总是成功',
|
||||
description: '无论子节点结果如何都返回成功',
|
||||
category: 'Decorator'
|
||||
})
|
||||
export class AlwaysSucceedExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 黑板比较条件执行器
|
||||
*
|
||||
* 比较黑板中的值
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'BlackboardCompare',
|
||||
nodeType: NodeType.Condition,
|
||||
displayName: '黑板比较',
|
||||
description: '比较黑板中的值',
|
||||
category: 'Condition',
|
||||
configSchema: {
|
||||
key: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '黑板变量名'
|
||||
},
|
||||
compareValue: {
|
||||
type: 'object',
|
||||
description: '比较值',
|
||||
supportBinding: true
|
||||
},
|
||||
operator: {
|
||||
type: 'string',
|
||||
default: 'equals',
|
||||
description: '比较运算符',
|
||||
options: ['equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterOrEqual', 'lessOrEqual']
|
||||
}
|
||||
}
|
||||
})
|
||||
export class BlackboardCompare implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime } = context;
|
||||
const key = BindingHelper.getValue<string>(context, 'key', '');
|
||||
const compareValue = BindingHelper.getValue(context, 'compareValue');
|
||||
const operator = BindingHelper.getValue<string>(context, 'operator', 'equals');
|
||||
|
||||
if (!key) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const actualValue = runtime.getBlackboardValue(key);
|
||||
|
||||
if (this.compare(actualValue, compareValue, operator)) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
private compare(actualValue: any, compareValue: any, operator: string): boolean {
|
||||
switch (operator) {
|
||||
case 'equals':
|
||||
return actualValue === compareValue;
|
||||
case 'notEquals':
|
||||
return actualValue !== compareValue;
|
||||
case 'greaterThan':
|
||||
return actualValue > compareValue;
|
||||
case 'lessThan':
|
||||
return actualValue < compareValue;
|
||||
case 'greaterOrEqual':
|
||||
return actualValue >= compareValue;
|
||||
case 'lessOrEqual':
|
||||
return actualValue <= compareValue;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 黑板存在检查条件执行器
|
||||
*
|
||||
* 检查黑板中是否存在指定的键
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'BlackboardExists',
|
||||
nodeType: NodeType.Condition,
|
||||
displayName: '黑板存在',
|
||||
description: '检查黑板中是否存在指定的键',
|
||||
category: 'Condition',
|
||||
configSchema: {
|
||||
key: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '黑板变量名'
|
||||
},
|
||||
checkNull: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: '检查是否为null'
|
||||
}
|
||||
}
|
||||
})
|
||||
export class BlackboardExists implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime } = context;
|
||||
const key = BindingHelper.getValue<string>(context, 'key', '');
|
||||
const checkNull = BindingHelper.getValue<boolean>(context, 'checkNull', false);
|
||||
|
||||
if (!key) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const value = runtime.getBlackboardValue(key);
|
||||
|
||||
if (value === undefined) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (checkNull && value === null) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import { TaskStatus, NodeType, AbortType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 条件装饰器执行器
|
||||
*
|
||||
* 根据条件决定是否执行子节点
|
||||
* 支持动态优先级和中止机制
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Conditional',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '条件',
|
||||
description: '根据条件决定是否执行子节点',
|
||||
category: 'Decorator',
|
||||
configSchema: {
|
||||
blackboardKey: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '黑板变量名'
|
||||
},
|
||||
expectedValue: {
|
||||
type: 'object',
|
||||
description: '期望值',
|
||||
supportBinding: true
|
||||
},
|
||||
operator: {
|
||||
type: 'string',
|
||||
default: 'equals',
|
||||
description: '比较运算符',
|
||||
options: ['equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterOrEqual', 'lessOrEqual']
|
||||
},
|
||||
abortType: {
|
||||
type: 'string',
|
||||
default: 'none',
|
||||
description: '中止类型',
|
||||
options: ['none', 'self', 'lower-priority', 'both']
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ConditionalExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, runtime, state } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const blackboardKey = BindingHelper.getValue<string>(context, 'blackboardKey', '');
|
||||
const expectedValue = BindingHelper.getValue(context, 'expectedValue');
|
||||
const operator = BindingHelper.getValue<string>(context, 'operator', 'equals');
|
||||
const abortType = (nodeData.abortType || AbortType.None) as AbortType;
|
||||
|
||||
if (!blackboardKey) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const actualValue = runtime.getBlackboardValue(blackboardKey);
|
||||
const conditionMet = this.evaluateCondition(actualValue, expectedValue, operator);
|
||||
|
||||
const wasRunning = state.status === TaskStatus.Running;
|
||||
|
||||
if (abortType !== AbortType.None) {
|
||||
if (!state.observedKeys || state.observedKeys.length === 0) {
|
||||
state.observedKeys = [blackboardKey];
|
||||
this.setupObserver(context, blackboardKey, expectedValue, operator, abortType);
|
||||
}
|
||||
|
||||
if (state.lastConditionResult !== undefined && state.lastConditionResult !== conditionMet) {
|
||||
if (conditionMet) {
|
||||
this.handleConditionBecameTrue(context, abortType);
|
||||
} else if (wasRunning) {
|
||||
this.handleConditionBecameFalse(context, abortType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.lastConditionResult = conditionMet;
|
||||
|
||||
if (!conditionMet) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
private evaluateCondition(actualValue: any, expectedValue: any, operator: string): boolean {
|
||||
switch (operator) {
|
||||
case 'equals':
|
||||
return actualValue === expectedValue;
|
||||
case 'notEquals':
|
||||
return actualValue !== expectedValue;
|
||||
case 'greaterThan':
|
||||
return actualValue > expectedValue;
|
||||
case 'lessThan':
|
||||
return actualValue < expectedValue;
|
||||
case 'greaterOrEqual':
|
||||
return actualValue >= expectedValue;
|
||||
case 'lessOrEqual':
|
||||
return actualValue <= expectedValue;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置黑板观察者
|
||||
*/
|
||||
private setupObserver(
|
||||
context: NodeExecutionContext,
|
||||
blackboardKey: string,
|
||||
expectedValue: any,
|
||||
operator: string,
|
||||
abortType: AbortType
|
||||
): void {
|
||||
const { nodeData, runtime } = context;
|
||||
|
||||
runtime.observeBlackboard(nodeData.id, [blackboardKey], (_key, newValue) => {
|
||||
const conditionMet = this.evaluateCondition(newValue, expectedValue, operator);
|
||||
const lastResult = context.state.lastConditionResult;
|
||||
|
||||
if (lastResult !== undefined && lastResult !== conditionMet) {
|
||||
if (conditionMet) {
|
||||
this.handleConditionBecameTrue(context, abortType);
|
||||
} else {
|
||||
this.handleConditionBecameFalse(context, abortType);
|
||||
}
|
||||
}
|
||||
|
||||
context.state.lastConditionResult = conditionMet;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理条件变为true
|
||||
*/
|
||||
private handleConditionBecameTrue(context: NodeExecutionContext, abortType: AbortType): void {
|
||||
if (abortType === AbortType.LowerPriority || abortType === AbortType.Both) {
|
||||
this.requestAbortLowerPriority(context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理条件变为false
|
||||
*/
|
||||
private handleConditionBecameFalse(context: NodeExecutionContext, abortType: AbortType): void {
|
||||
const { nodeData, runtime } = context;
|
||||
|
||||
if (abortType === AbortType.Self || abortType === AbortType.Both) {
|
||||
if (nodeData.children && nodeData.children.length > 0) {
|
||||
runtime.requestAbort(nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求中止低优先级节点
|
||||
*/
|
||||
private requestAbortLowerPriority(context: NodeExecutionContext): void {
|
||||
const { runtime } = context;
|
||||
runtime.requestAbort('__lower_priority__');
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime, state } = context;
|
||||
|
||||
if (state.observedKeys && state.observedKeys.length > 0) {
|
||||
runtime.unobserveBlackboard(nodeData.id);
|
||||
delete state.observedKeys;
|
||||
}
|
||||
|
||||
delete state.lastConditionResult;
|
||||
|
||||
if (nodeData.children && nodeData.children.length > 0) {
|
||||
runtime.resetNodeState(nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 冷却装饰器执行器
|
||||
*
|
||||
* 子节点执行成功后进入冷却时间
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Cooldown',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '冷却',
|
||||
description: '子节点执行成功后进入冷却时间',
|
||||
category: 'Decorator',
|
||||
configSchema: {
|
||||
cooldownTime: {
|
||||
type: 'number',
|
||||
default: 1.0,
|
||||
description: '冷却时间(秒)',
|
||||
min: 0,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class CooldownExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state, totalTime } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const cooldownTime = BindingHelper.getValue<number>(context, 'cooldownTime', 1.0);
|
||||
|
||||
if (state.lastExecutionTime !== undefined) {
|
||||
const timeSinceLastExecution = totalTime - state.lastExecutionTime;
|
||||
if (timeSinceLastExecution < cooldownTime) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Success) {
|
||||
state.lastExecutionTime = totalTime;
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
delete context.state.lastExecutionTime;
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 执行动作执行器
|
||||
*
|
||||
* 执行自定义动作逻辑
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'ExecuteAction',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '执行动作',
|
||||
description: '执行自定义动作逻辑',
|
||||
category: 'Action',
|
||||
configSchema: {
|
||||
actionName: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '动作名称(黑板中action_前缀的函数)'
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ExecuteAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime, entity } = context;
|
||||
const actionName = BindingHelper.getValue<string>(context, 'actionName', '');
|
||||
|
||||
if (!actionName) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const actionFunction = runtime.getBlackboardValue<(entity: NodeExecutionContext['entity']) => TaskStatus>(`action_${actionName}`);
|
||||
|
||||
if (!actionFunction || typeof actionFunction !== 'function') {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
try {
|
||||
return actionFunction(entity);
|
||||
} catch (error) {
|
||||
console.error(`ExecuteAction failed: ${error}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 执行条件执行器
|
||||
*
|
||||
* 执行自定义条件逻辑
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'ExecuteCondition',
|
||||
nodeType: NodeType.Condition,
|
||||
displayName: '执行条件',
|
||||
description: '执行自定义条件逻辑',
|
||||
category: 'Condition',
|
||||
configSchema: {
|
||||
conditionName: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '条件名称(黑板中condition_前缀的函数)'
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ExecuteCondition implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime, entity } = context;
|
||||
const conditionName = BindingHelper.getValue<string>(context, 'conditionName', '');
|
||||
|
||||
if (!conditionName) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const conditionFunction = runtime.getBlackboardValue<(entity: NodeExecutionContext['entity']) => boolean>(`condition_${conditionName}`);
|
||||
|
||||
if (!conditionFunction || typeof conditionFunction !== 'function') {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
try {
|
||||
return conditionFunction(entity) ? TaskStatus.Success : TaskStatus.Failure;
|
||||
} catch (error) {
|
||||
console.error(`ExecuteCondition failed: ${error}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 反转装饰器执行器
|
||||
*
|
||||
* 反转子节点的执行结果
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Inverter',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '反转',
|
||||
description: '反转子节点的执行结果',
|
||||
category: 'Decorator',
|
||||
childrenConstraints: {
|
||||
min: 1,
|
||||
max: 1
|
||||
}
|
||||
})
|
||||
export class InverterExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Success) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Failure) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 日志动作执行器
|
||||
*
|
||||
* 输出日志信息
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Log',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '日志',
|
||||
description: '输出日志信息',
|
||||
category: 'Action',
|
||||
configSchema: {
|
||||
message: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '日志消息,支持{key}占位符引用黑板变量',
|
||||
supportBinding: true
|
||||
},
|
||||
logLevel: {
|
||||
type: 'string',
|
||||
default: 'info',
|
||||
description: '日志级别',
|
||||
options: ['info', 'warn', 'error']
|
||||
}
|
||||
}
|
||||
})
|
||||
export class LogAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime } = context;
|
||||
const message = BindingHelper.getValue<string>(context, 'message', '');
|
||||
const logLevel = BindingHelper.getValue<string>(context, 'logLevel', 'info');
|
||||
|
||||
const finalMessage = this.replaceBlackboardVariables(message, runtime);
|
||||
|
||||
this.log(finalMessage, logLevel);
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
private replaceBlackboardVariables(message: string, runtime: NodeExecutionContext['runtime']): string {
|
||||
if (!message.includes('{') || !message.includes('}')) {
|
||||
return message;
|
||||
}
|
||||
|
||||
// 使用限制长度的正则表达式避免 ReDoS 攻击
|
||||
// 限制占位符名称最多100个字符,只允许字母、数字、下划线和点号
|
||||
return message.replace(/\{([\w.]{1,100})\}/g, (_, key) => {
|
||||
const value = runtime.getBlackboardValue(key.trim());
|
||||
return value !== undefined ? String(value) : `{${key}}`;
|
||||
});
|
||||
}
|
||||
|
||||
private log(message: string, level: string): void {
|
||||
switch (level) {
|
||||
case 'error':
|
||||
console.error(message);
|
||||
break;
|
||||
case 'warn':
|
||||
console.warn(message);
|
||||
break;
|
||||
case 'info':
|
||||
default:
|
||||
console.log(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 修改黑板值动作执行器
|
||||
*
|
||||
* 对黑板中的数值进行运算
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'ModifyBlackboardValue',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '修改黑板值',
|
||||
description: '对黑板中的数值进行运算',
|
||||
category: 'Action',
|
||||
configSchema: {
|
||||
key: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '黑板变量名'
|
||||
},
|
||||
operation: {
|
||||
type: 'string',
|
||||
default: 'add',
|
||||
description: '运算类型',
|
||||
options: ['add', 'subtract', 'multiply', 'divide', 'set']
|
||||
},
|
||||
value: {
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: '操作数',
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ModifyBlackboardValue implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime } = context;
|
||||
const key = BindingHelper.getValue<string>(context, 'key', '');
|
||||
const operation = BindingHelper.getValue<string>(context, 'operation', 'add');
|
||||
const value = BindingHelper.getValue<number>(context, 'value', 0);
|
||||
|
||||
if (!key) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const currentValue = runtime.getBlackboardValue<number>(key) || 0;
|
||||
let newValue: number;
|
||||
|
||||
switch (operation) {
|
||||
case 'add':
|
||||
newValue = currentValue + value;
|
||||
break;
|
||||
case 'subtract':
|
||||
newValue = currentValue - value;
|
||||
break;
|
||||
case 'multiply':
|
||||
newValue = currentValue * value;
|
||||
break;
|
||||
case 'divide':
|
||||
newValue = value !== 0 ? currentValue / value : currentValue;
|
||||
break;
|
||||
case 'set':
|
||||
newValue = value;
|
||||
break;
|
||||
default:
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
runtime.setBlackboardValue(key, newValue);
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 并行节点执行器
|
||||
*
|
||||
* 同时执行所有子节点
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Parallel',
|
||||
nodeType: NodeType.Composite,
|
||||
displayName: '并行',
|
||||
description: '同时执行所有子节点',
|
||||
category: 'Composite',
|
||||
configSchema: {
|
||||
successPolicy: {
|
||||
type: 'string',
|
||||
default: 'all',
|
||||
description: '成功策略',
|
||||
options: ['all', 'one']
|
||||
},
|
||||
failurePolicy: {
|
||||
type: 'string',
|
||||
default: 'one',
|
||||
description: '失败策略',
|
||||
options: ['all', 'one']
|
||||
}
|
||||
},
|
||||
childrenConstraints: {
|
||||
min: 2
|
||||
}
|
||||
})
|
||||
export class ParallelExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData } = context;
|
||||
const successPolicy = BindingHelper.getValue<string>(context, 'successPolicy', 'all');
|
||||
const failurePolicy = BindingHelper.getValue<string>(context, 'failurePolicy', 'one');
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
let hasRunning = false;
|
||||
let successCount = 0;
|
||||
let failureCount = 0;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
hasRunning = true;
|
||||
} else if (status === TaskStatus.Success) {
|
||||
successCount++;
|
||||
} else if (status === TaskStatus.Failure) {
|
||||
failureCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (successPolicy === 'one' && successCount > 0) {
|
||||
this.stopAllChildren(context);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
if (successPolicy === 'all' && successCount === nodeData.children.length) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
if (failurePolicy === 'one' && failureCount > 0) {
|
||||
this.stopAllChildren(context);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (failurePolicy === 'all' && failureCount === nodeData.children.length) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
return hasRunning ? TaskStatus.Running : TaskStatus.Success;
|
||||
}
|
||||
|
||||
private stopAllChildren(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime } = context;
|
||||
if (!nodeData.children) return;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
runtime.activeNodeIds.delete(childId);
|
||||
runtime.resetNodeState(childId);
|
||||
}
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime } = context;
|
||||
if (!nodeData.children) return;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
runtime.resetNodeState(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 并行选择器执行器
|
||||
*
|
||||
* 并行执行子节点,任一成功则成功
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'ParallelSelector',
|
||||
nodeType: NodeType.Composite,
|
||||
displayName: '并行选择器',
|
||||
description: '并行执行子节点,任一成功则成功',
|
||||
category: 'Composite',
|
||||
configSchema: {
|
||||
failurePolicy: {
|
||||
type: 'string',
|
||||
default: 'all',
|
||||
description: '失败策略',
|
||||
options: ['all', 'one']
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ParallelSelectorExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData } = context;
|
||||
const failurePolicy = BindingHelper.getValue<string>(context, 'failurePolicy', 'all');
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
let hasRunning = false;
|
||||
let successCount = 0;
|
||||
let failureCount = 0;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
hasRunning = true;
|
||||
} else if (status === TaskStatus.Success) {
|
||||
successCount++;
|
||||
} else if (status === TaskStatus.Failure) {
|
||||
failureCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
this.stopAllChildren(context);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
if (failurePolicy === 'one' && failureCount > 0) {
|
||||
this.stopAllChildren(context);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (failurePolicy === 'all' && failureCount === nodeData.children.length) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
return hasRunning ? TaskStatus.Running : TaskStatus.Failure;
|
||||
}
|
||||
|
||||
private stopAllChildren(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime } = context;
|
||||
if (!nodeData.children) return;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
runtime.activeNodeIds.delete(childId);
|
||||
runtime.resetNodeState(childId);
|
||||
}
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime } = context;
|
||||
if (!nodeData.children) return;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
runtime.resetNodeState(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 随机概率条件执行器
|
||||
*
|
||||
* 根据概率返回成功或失败
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'RandomProbability',
|
||||
nodeType: NodeType.Condition,
|
||||
displayName: '随机概率',
|
||||
description: '根据概率返回成功或失败',
|
||||
category: 'Condition',
|
||||
configSchema: {
|
||||
probability: {
|
||||
type: 'number',
|
||||
default: 0.5,
|
||||
description: '成功概率(0-1)',
|
||||
min: 0,
|
||||
max: 1,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class RandomProbability implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const probability = BindingHelper.getValue<number>(context, 'probability', 0.5);
|
||||
|
||||
const clampedProbability = Math.max(0, Math.min(1, probability));
|
||||
|
||||
if (Math.random() < clampedProbability) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 随机选择器执行器
|
||||
*
|
||||
* 随机顺序执行子节点,任一成功则成功
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'RandomSelector',
|
||||
nodeType: NodeType.Composite,
|
||||
displayName: '随机选择器',
|
||||
description: '随机顺序执行子节点,任一成功则成功',
|
||||
category: 'Composite'
|
||||
})
|
||||
export class RandomSelectorExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (!state.shuffledIndices || state.shuffledIndices.length === 0) {
|
||||
state.shuffledIndices = this.shuffleIndices(nodeData.children.length);
|
||||
}
|
||||
|
||||
while (state.currentChildIndex < state.shuffledIndices.length) {
|
||||
const shuffledIndex = state.shuffledIndices[state.currentChildIndex]!;
|
||||
const childId = nodeData.children[shuffledIndex]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Success) {
|
||||
state.currentChildIndex = 0;
|
||||
delete state.shuffledIndices;
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
state.currentChildIndex++;
|
||||
}
|
||||
|
||||
state.currentChildIndex = 0;
|
||||
delete state.shuffledIndices;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
private shuffleIndices(length: number): number[] {
|
||||
const indices = Array.from({ length }, (_, i) => i);
|
||||
for (let i = indices.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
const temp = indices[i]!;
|
||||
indices[i] = indices[j]!;
|
||||
indices[j] = temp;
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
context.state.currentChildIndex = 0;
|
||||
delete context.state.shuffledIndices;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 随机序列执行器
|
||||
*
|
||||
* 随机顺序执行子节点序列,全部成功才成功
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'RandomSequence',
|
||||
nodeType: NodeType.Composite,
|
||||
displayName: '随机序列',
|
||||
description: '随机顺序执行子节点,全部成功才成功',
|
||||
category: 'Composite',
|
||||
childrenConstraints: {
|
||||
min: 1
|
||||
}
|
||||
})
|
||||
export class RandomSequenceExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
if (!state.shuffledIndices || state.shuffledIndices.length === 0) {
|
||||
state.shuffledIndices = this.shuffleIndices(nodeData.children.length);
|
||||
}
|
||||
|
||||
while (state.currentChildIndex < state.shuffledIndices.length) {
|
||||
const shuffledIndex = state.shuffledIndices[state.currentChildIndex]!;
|
||||
const childId = nodeData.children[shuffledIndex]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Failure) {
|
||||
state.currentChildIndex = 0;
|
||||
delete state.shuffledIndices;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
state.currentChildIndex++;
|
||||
}
|
||||
|
||||
state.currentChildIndex = 0;
|
||||
delete state.shuffledIndices;
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
private shuffleIndices(length: number): number[] {
|
||||
const indices = Array.from({ length }, (_, i) => i);
|
||||
for (let i = indices.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
const temp = indices[i]!;
|
||||
indices[i] = indices[j]!;
|
||||
indices[j] = temp;
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
context.state.currentChildIndex = 0;
|
||||
delete context.state.shuffledIndices;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 重复装饰器执行器
|
||||
*
|
||||
* 重复执行子节点指定次数
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Repeater',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '重复',
|
||||
description: '重复执行子节点指定次数',
|
||||
category: 'Decorator',
|
||||
configSchema: {
|
||||
repeatCount: {
|
||||
type: 'number',
|
||||
default: 1,
|
||||
description: '重复次数(-1表示无限循环)',
|
||||
supportBinding: true
|
||||
},
|
||||
endOnFailure: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: '子节点失败时是否结束'
|
||||
}
|
||||
},
|
||||
childrenConstraints: {
|
||||
min: 1,
|
||||
max: 1
|
||||
}
|
||||
})
|
||||
export class RepeaterExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state, runtime } = context;
|
||||
const repeatCount = BindingHelper.getValue<number>(context, 'repeatCount', 1);
|
||||
const endOnFailure = BindingHelper.getValue<boolean>(context, 'endOnFailure', false);
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
|
||||
if (!state.repeatCount) {
|
||||
state.repeatCount = 0;
|
||||
}
|
||||
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Failure && endOnFailure) {
|
||||
state.repeatCount = 0;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
state.repeatCount++;
|
||||
runtime.resetNodeState(childId);
|
||||
|
||||
const shouldContinue = (repeatCount === -1) || (state.repeatCount < repeatCount);
|
||||
|
||||
if (shouldContinue) {
|
||||
return TaskStatus.Running;
|
||||
} else {
|
||||
state.repeatCount = 0;
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
delete context.state.repeatCount;
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
// 根节点没有需要重置的状态
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 选择器节点执行器
|
||||
*
|
||||
* 按顺序执行子节点,任一成功则成功,全部失败才失败
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Selector',
|
||||
nodeType: NodeType.Composite,
|
||||
displayName: '选择器',
|
||||
description: '按顺序执行子节点,任一成功则成功',
|
||||
category: 'Composite',
|
||||
childrenConstraints: {
|
||||
min: 1
|
||||
}
|
||||
})
|
||||
export class SelectorExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
while (state.currentChildIndex < nodeData.children.length) {
|
||||
const childId = nodeData.children[state.currentChildIndex]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Success) {
|
||||
state.currentChildIndex = 0;
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
state.currentChildIndex++;
|
||||
}
|
||||
|
||||
state.currentChildIndex = 0;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
context.state.currentChildIndex = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 序列节点执行器
|
||||
*
|
||||
* 按顺序执行子节点,全部成功才成功,任一失败则失败
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Sequence',
|
||||
nodeType: NodeType.Composite,
|
||||
displayName: '序列',
|
||||
description: '按顺序执行子节点,全部成功才成功',
|
||||
category: 'Composite',
|
||||
childrenConstraints: {
|
||||
min: 1
|
||||
}
|
||||
})
|
||||
export class SequenceExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
while (state.currentChildIndex < nodeData.children.length) {
|
||||
const childId = nodeData.children[state.currentChildIndex]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Failure) {
|
||||
state.currentChildIndex = 0;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
state.currentChildIndex++;
|
||||
}
|
||||
|
||||
state.currentChildIndex = 0;
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
context.state.currentChildIndex = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* Service执行接口
|
||||
*/
|
||||
export interface IServiceExecutor {
|
||||
/**
|
||||
* Service开始执行
|
||||
*/
|
||||
onServiceStart?(context: NodeExecutionContext): void;
|
||||
|
||||
/**
|
||||
* Service每帧更新
|
||||
*/
|
||||
onServiceTick(context: NodeExecutionContext): void;
|
||||
|
||||
/**
|
||||
* Service结束执行
|
||||
*/
|
||||
onServiceEnd?(context: NodeExecutionContext): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service注册表
|
||||
*/
|
||||
class ServiceRegistry {
|
||||
private static services: Map<string, IServiceExecutor> = new Map();
|
||||
|
||||
static register(name: string, service: IServiceExecutor): void {
|
||||
this.services.set(name, service);
|
||||
}
|
||||
|
||||
static get(name: string): IServiceExecutor | undefined {
|
||||
return this.services.get(name);
|
||||
}
|
||||
|
||||
static has(name: string): boolean {
|
||||
return this.services.has(name);
|
||||
}
|
||||
|
||||
static unregister(name: string): boolean {
|
||||
return this.services.delete(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Service装饰器执行器
|
||||
*
|
||||
* 在子节点执行期间持续运行后台逻辑
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Service',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: 'Service',
|
||||
description: '在子节点执行期间持续运行后台逻辑',
|
||||
category: 'Decorator',
|
||||
configSchema: {
|
||||
serviceName: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Service名称'
|
||||
},
|
||||
tickInterval: {
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'Service更新间隔(秒,0表示每帧更新)',
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ServiceDecorator implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state, totalTime } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const serviceName = BindingHelper.getValue<string>(context, 'serviceName', '');
|
||||
const tickInterval = BindingHelper.getValue<number>(context, 'tickInterval', 0);
|
||||
|
||||
if (!serviceName) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const service = ServiceRegistry.get(serviceName);
|
||||
if (!service) {
|
||||
console.warn(`未找到Service: ${serviceName}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (state.status !== TaskStatus.Running) {
|
||||
state.startTime = totalTime;
|
||||
state.lastExecutionTime = totalTime;
|
||||
|
||||
if (service.onServiceStart) {
|
||||
service.onServiceStart(context);
|
||||
}
|
||||
}
|
||||
|
||||
const shouldTick = tickInterval === 0 ||
|
||||
(state.lastExecutionTime !== undefined &&
|
||||
(totalTime - state.lastExecutionTime) >= tickInterval);
|
||||
|
||||
if (shouldTick) {
|
||||
service.onServiceTick(context);
|
||||
state.lastExecutionTime = totalTime;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const childStatus = context.executeChild(childId);
|
||||
|
||||
if (childStatus !== TaskStatus.Running) {
|
||||
if (service.onServiceEnd) {
|
||||
service.onServiceEnd(context);
|
||||
}
|
||||
}
|
||||
|
||||
return childStatus;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime, state } = context;
|
||||
|
||||
const serviceName = BindingHelper.getValue<string>(context, 'serviceName', '');
|
||||
if (serviceName) {
|
||||
const service = ServiceRegistry.get(serviceName);
|
||||
if (service && service.onServiceEnd) {
|
||||
service.onServiceEnd(context);
|
||||
}
|
||||
}
|
||||
|
||||
delete state.startTime;
|
||||
delete state.lastExecutionTime;
|
||||
|
||||
if (nodeData.children && nodeData.children.length > 0) {
|
||||
runtime.resetNodeState(nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { ServiceRegistry };
|
||||
@@ -0,0 +1,43 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 设置黑板值动作执行器
|
||||
*
|
||||
* 设置黑板中的变量值
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'SetBlackboardValue',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '设置黑板值',
|
||||
description: '设置黑板中的变量值',
|
||||
category: 'Action',
|
||||
configSchema: {
|
||||
key: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '黑板变量名'
|
||||
},
|
||||
value: {
|
||||
type: 'object',
|
||||
description: '要设置的值',
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class SetBlackboardValue implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime } = context;
|
||||
const key = BindingHelper.getValue<string>(context, 'key', '');
|
||||
const value = BindingHelper.getValue(context, 'value');
|
||||
|
||||
if (!key) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
runtime.setBlackboardValue(key, value);
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
import { BehaviorTreeAssetManager } from '../BehaviorTreeAssetManager';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* SubTree执行器
|
||||
*
|
||||
* 引用并执行其他行为树,实现模块化和复用
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'SubTree',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '子树',
|
||||
description: '引用并执行其他行为树',
|
||||
category: 'Special',
|
||||
configSchema: {
|
||||
treeAssetId: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '要执行的行为树资产ID',
|
||||
supportBinding: true
|
||||
},
|
||||
shareBlackboard: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: '是否共享黑板数据'
|
||||
}
|
||||
}
|
||||
})
|
||||
export class SubTreeExecutor implements INodeExecutor {
|
||||
private assetManager: BehaviorTreeAssetManager | null = null;
|
||||
|
||||
private getAssetManager(): BehaviorTreeAssetManager {
|
||||
if (!this.assetManager) {
|
||||
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
}
|
||||
return this.assetManager;
|
||||
}
|
||||
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime, state, entity } = context;
|
||||
|
||||
const treeAssetId = BindingHelper.getValue<string>(context, 'treeAssetId', '');
|
||||
const shareBlackboard = BindingHelper.getValue<boolean>(context, 'shareBlackboard', true);
|
||||
|
||||
if (!treeAssetId) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const assetManager = this.getAssetManager();
|
||||
const subTreeData = assetManager.getAsset(treeAssetId);
|
||||
|
||||
if (!subTreeData) {
|
||||
console.warn(`未找到子树资产: ${treeAssetId}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const rootNode = subTreeData.nodes.get(subTreeData.rootNodeId);
|
||||
if (!rootNode) {
|
||||
console.warn(`子树根节点未找到: ${subTreeData.rootNodeId}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (!shareBlackboard && state.status !== TaskStatus.Running) {
|
||||
if (subTreeData.blackboardVariables) {
|
||||
for (const [key, value] of subTreeData.blackboardVariables.entries()) {
|
||||
if (!runtime.hasBlackboardKey(key)) {
|
||||
runtime.setBlackboardValue(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const subTreeContext: NodeExecutionContext = {
|
||||
entity,
|
||||
nodeData: rootNode,
|
||||
state: runtime.getNodeState(rootNode.id),
|
||||
runtime,
|
||||
treeData: subTreeData,
|
||||
deltaTime: context.deltaTime,
|
||||
totalTime: context.totalTime,
|
||||
executeChild: (childId: string) => {
|
||||
const childData = subTreeData.nodes.get(childId);
|
||||
if (!childData) {
|
||||
console.warn(`子树节点未找到: ${childId}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const childContext: NodeExecutionContext = {
|
||||
entity,
|
||||
nodeData: childData,
|
||||
state: runtime.getNodeState(childId),
|
||||
runtime,
|
||||
treeData: subTreeData,
|
||||
deltaTime: context.deltaTime,
|
||||
totalTime: context.totalTime,
|
||||
executeChild: subTreeContext.executeChild
|
||||
};
|
||||
|
||||
return this.executeSubTreeNode(childContext);
|
||||
}
|
||||
};
|
||||
|
||||
return this.executeSubTreeNode(subTreeContext);
|
||||
}
|
||||
|
||||
private executeSubTreeNode(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, runtime } = context;
|
||||
|
||||
const state = runtime.getNodeState(nodeData.id);
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[state.currentChildIndex]!;
|
||||
const childStatus = context.executeChild(childId);
|
||||
|
||||
if (childStatus === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (childStatus === TaskStatus.Failure) {
|
||||
state.currentChildIndex = 0;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
state.currentChildIndex++;
|
||||
|
||||
if (state.currentChildIndex >= nodeData.children.length) {
|
||||
state.currentChildIndex = 0;
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
const treeAssetId = BindingHelper.getValue<string>(context, 'treeAssetId', '');
|
||||
|
||||
if (treeAssetId) {
|
||||
const assetManager = this.getAssetManager();
|
||||
const subTreeData = assetManager.getAsset(treeAssetId);
|
||||
|
||||
if (subTreeData) {
|
||||
const rootNode = subTreeData.nodes.get(subTreeData.rootNodeId);
|
||||
if (rootNode) {
|
||||
context.runtime.resetNodeState(rootNode.id);
|
||||
|
||||
if (rootNode.children) {
|
||||
for (const childId of rootNode.children) {
|
||||
context.runtime.resetNodeState(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 超时装饰器执行器
|
||||
*
|
||||
* 限制子节点的执行时间
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Timeout',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '超时',
|
||||
description: '限制子节点的执行时间',
|
||||
category: 'Decorator',
|
||||
configSchema: {
|
||||
timeout: {
|
||||
type: 'number',
|
||||
default: 1.0,
|
||||
description: '超时时间(秒)',
|
||||
min: 0,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class TimeoutExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state, totalTime } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const timeout = BindingHelper.getValue<number>(context, 'timeout', 1.0);
|
||||
|
||||
if (state.startTime === undefined) {
|
||||
state.startTime = totalTime;
|
||||
}
|
||||
|
||||
const elapsedTime = totalTime - state.startTime;
|
||||
if (elapsedTime >= timeout) {
|
||||
delete state.startTime;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
delete state.startTime;
|
||||
return status;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
delete context.state.startTime;
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 直到失败装饰器执行器
|
||||
*
|
||||
* 重复执行子节点直到失败
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'UntilFail',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '直到失败',
|
||||
description: '重复执行子节点直到失败',
|
||||
category: 'Decorator'
|
||||
})
|
||||
export class UntilFailExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, runtime } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Failure) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
runtime.resetNodeState(childId);
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 直到成功装饰器执行器
|
||||
*
|
||||
* 重复执行子节点直到成功
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'UntilSuccess',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '直到成功',
|
||||
description: '重复执行子节点直到成功',
|
||||
category: 'Decorator'
|
||||
})
|
||||
export class UntilSuccessExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, runtime } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Success) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
runtime.resetNodeState(childId);
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 等待动作执行器
|
||||
*
|
||||
* 等待指定时间后返回成功
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Wait',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '等待',
|
||||
description: '等待指定时间后返回成功',
|
||||
category: 'Action',
|
||||
configSchema: {
|
||||
duration: {
|
||||
type: 'number',
|
||||
default: 1.0,
|
||||
description: '等待时长(秒)',
|
||||
min: 0,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class WaitAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { state, totalTime } = context;
|
||||
const duration = BindingHelper.getValue<number>(context, 'duration', 1.0);
|
||||
|
||||
if (!state.startTime) {
|
||||
state.startTime = totalTime;
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (totalTime - state.startTime >= duration) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
delete context.state.startTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { TaskStatus } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
|
||||
/**
|
||||
* 等待动作执行器
|
||||
*
|
||||
* 等待指定时间后返回成功
|
||||
*/
|
||||
export class WaitActionExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { state, nodeData, totalTime } = context;
|
||||
const duration = nodeData.config['duration'] as number || 1.0;
|
||||
|
||||
if (!state.startTime) {
|
||||
state.startTime = totalTime;
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (totalTime - state.startTime >= duration) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
delete context.state.startTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
export { RootExecutor } from './RootExecutor';
|
||||
export { SequenceExecutor } from './SequenceExecutor';
|
||||
export { SelectorExecutor } from './SelectorExecutor';
|
||||
export { ParallelExecutor } from './ParallelExecutor';
|
||||
export { ParallelSelectorExecutor } from './ParallelSelectorExecutor';
|
||||
export { RandomSequenceExecutor } from './RandomSequenceExecutor';
|
||||
export { RandomSelectorExecutor } from './RandomSelectorExecutor';
|
||||
|
||||
export { InverterExecutor } from './InverterExecutor';
|
||||
export { RepeaterExecutor } from './RepeaterExecutor';
|
||||
export { AlwaysSucceedExecutor } from './AlwaysSucceedExecutor';
|
||||
export { AlwaysFailExecutor } from './AlwaysFailExecutor';
|
||||
export { UntilSuccessExecutor } from './UntilSuccessExecutor';
|
||||
export { UntilFailExecutor } from './UntilFailExecutor';
|
||||
export { ConditionalExecutor } from './ConditionalExecutor';
|
||||
export { CooldownExecutor } from './CooldownExecutor';
|
||||
export { TimeoutExecutor } from './TimeoutExecutor';
|
||||
export { ServiceDecorator, ServiceRegistry } from './ServiceDecorator';
|
||||
export type { IServiceExecutor } from './ServiceDecorator';
|
||||
|
||||
export { WaitAction } from './WaitAction';
|
||||
export { LogAction } from './LogAction';
|
||||
export { SetBlackboardValue } from './SetBlackboardValue';
|
||||
export { ModifyBlackboardValue } from './ModifyBlackboardValue';
|
||||
export { ExecuteAction } from './ExecuteAction';
|
||||
export { SubTreeExecutor } from './SubTreeExecutor';
|
||||
|
||||
export { BlackboardCompare } from './BlackboardCompare';
|
||||
export { BlackboardExists } from './BlackboardExists';
|
||||
export { RandomProbability } from './RandomProbability';
|
||||
export { ExecuteCondition } from './ExecuteCondition';
|
||||
181
packages/framework/behavior-tree/src/execution/NodeExecutor.ts
Normal file
181
packages/framework/behavior-tree/src/execution/NodeExecutor.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { Entity } from '@esengine/ecs-framework';
|
||||
import { TaskStatus } from '../Types/TaskStatus';
|
||||
import { BehaviorNodeData, BehaviorTreeData, NodeRuntimeState } from './BehaviorTreeData';
|
||||
import { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
|
||||
|
||||
/**
|
||||
* 节点执行上下文
|
||||
*
|
||||
* 包含执行节点所需的所有信息
|
||||
*/
|
||||
export interface NodeExecutionContext {
|
||||
/** 游戏Entity(行为树宿主) */
|
||||
readonly entity: Entity;
|
||||
|
||||
/** 节点数据 */
|
||||
readonly nodeData: BehaviorNodeData;
|
||||
|
||||
/** 节点运行时状态 */
|
||||
readonly state: NodeRuntimeState;
|
||||
|
||||
/** 运行时组件(访问黑板等) */
|
||||
readonly runtime: BehaviorTreeRuntimeComponent;
|
||||
|
||||
/** 行为树数据(访问子节点等) */
|
||||
readonly treeData: BehaviorTreeData;
|
||||
|
||||
/** 当前帧增量时间 */
|
||||
readonly deltaTime: number;
|
||||
|
||||
/** 总时间 */
|
||||
readonly totalTime: number;
|
||||
|
||||
/** 执行子节点 */
|
||||
executeChild(childId: string): TaskStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点执行器接口
|
||||
*
|
||||
* 所有节点类型都需要实现对应的执行器
|
||||
* 执行器是无状态的,状态存储在NodeRuntimeState中
|
||||
*/
|
||||
export interface INodeExecutor {
|
||||
/**
|
||||
* 执行节点逻辑
|
||||
*
|
||||
* @param context 执行上下文
|
||||
* @returns 执行结果状态
|
||||
*/
|
||||
execute(context: NodeExecutionContext): TaskStatus;
|
||||
|
||||
/**
|
||||
* 重置节点状态(可选)
|
||||
*
|
||||
* 当节点完成或被中断时调用
|
||||
*/
|
||||
reset?(context: NodeExecutionContext): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复合节点执行结果
|
||||
*/
|
||||
export interface CompositeExecutionResult {
|
||||
/** 节点状态 */
|
||||
status: TaskStatus;
|
||||
|
||||
/** 要激活的子节点索引列表(undefined表示激活所有) */
|
||||
activateChildren?: number[];
|
||||
|
||||
/** 是否停止所有子节点 */
|
||||
stopAllChildren?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复合节点执行器接口
|
||||
*/
|
||||
export interface ICompositeExecutor extends INodeExecutor {
|
||||
/**
|
||||
* 执行复合节点逻辑
|
||||
*
|
||||
* @param context 执行上下文
|
||||
* @returns 复合节点执行结果
|
||||
*/
|
||||
executeComposite(context: NodeExecutionContext): CompositeExecutionResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定辅助工具
|
||||
*
|
||||
* 处理配置属性的黑板绑定
|
||||
*/
|
||||
export class BindingHelper {
|
||||
/**
|
||||
* 获取配置值(考虑黑板绑定)
|
||||
*
|
||||
* @param context 执行上下文
|
||||
* @param configKey 配置键名
|
||||
* @param defaultValue 默认值
|
||||
* @returns 解析后的值
|
||||
*/
|
||||
static getValue<T = any>(
|
||||
context: NodeExecutionContext,
|
||||
configKey: string,
|
||||
defaultValue?: T
|
||||
): T {
|
||||
const { nodeData, runtime } = context;
|
||||
|
||||
if (nodeData.bindings && nodeData.bindings[configKey]) {
|
||||
const blackboardKey = nodeData.bindings[configKey];
|
||||
const boundValue = runtime.getBlackboardValue<T>(blackboardKey);
|
||||
return boundValue !== undefined ? boundValue : (defaultValue as T);
|
||||
}
|
||||
|
||||
const configValue = nodeData.config[configKey];
|
||||
return configValue !== undefined ? configValue : (defaultValue as T);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查配置是否绑定到黑板变量
|
||||
*/
|
||||
static hasBinding(context: NodeExecutionContext, configKey: string): boolean {
|
||||
return !!(context.nodeData.bindings && context.nodeData.bindings[configKey]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取绑定的黑板变量名
|
||||
*/
|
||||
static getBindingKey(context: NodeExecutionContext, configKey: string): string | undefined {
|
||||
return context.nodeData.bindings?.[configKey];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点执行器注册表
|
||||
*
|
||||
* 管理所有节点类型的执行器
|
||||
*/
|
||||
export class NodeExecutorRegistry {
|
||||
private executors: Map<string, INodeExecutor> = new Map();
|
||||
|
||||
/**
|
||||
* 注册执行器
|
||||
*
|
||||
* @param implementationType 节点实现类型(对应BehaviorNodeData.implementationType)
|
||||
* @param executor 执行器实例
|
||||
*/
|
||||
register(implementationType: string, executor: INodeExecutor): void {
|
||||
if (this.executors.has(implementationType)) {
|
||||
console.warn(`执行器已存在,将被覆盖: ${implementationType}`);
|
||||
}
|
||||
this.executors.set(implementationType, executor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取执行器
|
||||
*/
|
||||
get(implementationType: string): INodeExecutor | undefined {
|
||||
return this.executors.get(implementationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有执行器
|
||||
*/
|
||||
has(implementationType: string): boolean {
|
||||
return this.executors.has(implementationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销执行器
|
||||
*/
|
||||
unregister(implementationType: string): boolean {
|
||||
return this.executors.delete(implementationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有执行器
|
||||
*/
|
||||
clear(): void {
|
||||
this.executors.clear();
|
||||
}
|
||||
}
|
||||
108
packages/framework/behavior-tree/src/execution/NodeMetadata.ts
Normal file
108
packages/framework/behavior-tree/src/execution/NodeMetadata.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { NodeType } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 配置参数定义
|
||||
*/
|
||||
export interface ConfigFieldDefinition {
|
||||
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
|
||||
default?: any;
|
||||
description?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
options?: string[];
|
||||
supportBinding?: boolean;
|
||||
allowMultipleConnections?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 子节点约束配置
|
||||
*/
|
||||
export interface ChildrenConstraints {
|
||||
min?: number;
|
||||
max?: number;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点元数据
|
||||
*/
|
||||
export interface NodeMetadata {
|
||||
implementationType: string;
|
||||
nodeType: NodeType;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
configSchema?: Record<string, ConfigFieldDefinition>;
|
||||
childrenConstraints?: ChildrenConstraints;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点元数据默认值
|
||||
*/
|
||||
export class NodeMetadataDefaults {
|
||||
static getDefaultConstraints(nodeType: NodeType): ChildrenConstraints | 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点元数据注册表
|
||||
*/
|
||||
export class NodeMetadataRegistry {
|
||||
private static metadataMap: Map<string, NodeMetadata> = new Map();
|
||||
private static executorClassMap: Map<Function, string> = new Map();
|
||||
private static executorConstructors: Map<string, new () => any> = new Map();
|
||||
|
||||
static register(target: Function, metadata: NodeMetadata): void {
|
||||
this.metadataMap.set(metadata.implementationType, metadata);
|
||||
this.executorClassMap.set(target, metadata.implementationType);
|
||||
this.executorConstructors.set(metadata.implementationType, target as new () => any);
|
||||
}
|
||||
|
||||
static getMetadata(implementationType: string): NodeMetadata | undefined {
|
||||
return this.metadataMap.get(implementationType);
|
||||
}
|
||||
|
||||
static getAllMetadata(): NodeMetadata[] {
|
||||
return Array.from(this.metadataMap.values());
|
||||
}
|
||||
|
||||
static getByCategory(category: string): NodeMetadata[] {
|
||||
return this.getAllMetadata().filter((m) => m.category === category);
|
||||
}
|
||||
|
||||
static getByNodeType(nodeType: NodeType): NodeMetadata[] {
|
||||
return this.getAllMetadata().filter((m) => m.nodeType === nodeType);
|
||||
}
|
||||
|
||||
static getImplementationType(executorClass: Function): string | undefined {
|
||||
return this.executorClassMap.get(executorClass);
|
||||
}
|
||||
|
||||
static getExecutorConstructor(implementationType: string): (new () => any) | undefined {
|
||||
return this.executorConstructors.get(implementationType);
|
||||
}
|
||||
|
||||
static getAllExecutorConstructors(): Map<string, new () => any> {
|
||||
return new Map(this.executorConstructors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点执行器元数据装饰器
|
||||
*/
|
||||
export function NodeExecutorMetadata(metadata: NodeMetadata) {
|
||||
return function (target: Function) {
|
||||
NodeMetadataRegistry.register(target, metadata);
|
||||
};
|
||||
}
|
||||
11
packages/framework/behavior-tree/src/execution/index.ts
Normal file
11
packages/framework/behavior-tree/src/execution/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type { BehaviorTreeData, BehaviorNodeData, NodeRuntimeState } from './BehaviorTreeData';
|
||||
export { createDefaultRuntimeState } from './BehaviorTreeData';
|
||||
export { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
|
||||
export { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager';
|
||||
export type { INodeExecutor, NodeExecutionContext } from './NodeExecutor';
|
||||
export { NodeExecutorRegistry, BindingHelper } from './NodeExecutor';
|
||||
export { BehaviorTreeExecutionSystem } from './BehaviorTreeExecutionSystem';
|
||||
export type { NodeMetadata, ConfigFieldDefinition, NodeExecutorMetadata } from './NodeMetadata';
|
||||
export { NodeMetadataRegistry } from './NodeMetadata';
|
||||
|
||||
export * from './Executors';
|
||||
Reference in New Issue
Block a user