Feature/ecs behavior tree (#188)

* feat(behavior-tree): 完全 ECS 化的行为树系统

* feat(editor-app): 添加行为树可视化编辑器

* chore: 移除 Cocos Creator 扩展目录

* feat(editor-app): 行为树编辑器功能增强

* fix(editor-app): 修复 TypeScript 类型错误

* feat(editor-app): 使用 FlexLayout 重构面板系统并优化资产浏览器

* feat(editor-app): 改进编辑器UI样式并修复行为树执行顺序

* feat(behavior-tree,editor-app): 添加装饰器系统并优化编辑器性能

* feat(behavior-tree,editor-app): 添加属性绑定系统

* feat(editor-app,behavior-tree): 优化编辑器UI并改进行为树功能

* feat(editor-app,behavior-tree): 添加全局黑板系统并增强资产浏览器功能

* feat(behavior-tree,editor-app): 添加运行时资产导出系统

* feat(behavior-tree,editor-app): 添加SubTree系统和资产选择器

* feat(behavior-tree,editor-app): 优化系统架构并改进编辑器文件管理

* fix(behavior-tree,editor-app): 修复SubTree节点错误显示空节点警告

* fix(editor-app): 修复局部黑板类型定义文件扩展名错误
This commit is contained in:
YHH
2025-10-27 09:29:11 +08:00
committed by GitHub
parent 0cd99209c4
commit 009f8af4e1
234 changed files with 21824 additions and 15295 deletions

View File

@@ -0,0 +1,87 @@
import { Component, ECSComponent, Entity } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { BlackboardComponent } from '../BlackboardComponent';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* 自定义动作函数类型
*/
export type CustomActionFunction = (
entity: Entity,
blackboard?: BlackboardComponent,
deltaTime?: number
) => TaskStatus;
/**
* 执行自定义函数动作组件
*
* 允许用户提供自定义的动作执行函数
*/
@BehaviorNode({
displayName: '自定义动作',
category: '动作',
type: NodeType.Action,
icon: 'Code',
description: '执行自定义代码',
color: '#FFC107'
})
@ECSComponent('ExecuteAction')
@Serializable({ version: 1 })
export class ExecuteAction extends Component {
@BehaviorProperty({
label: '动作代码',
type: 'code',
description: 'JavaScript 代码,返回 TaskStatus',
required: true
})
@Serialize()
actionCode?: string = 'return TaskStatus.Success;';
@Serialize()
parameters: Record<string, any> = {};
/** 编译后的函数(不序列化) */
@IgnoreSerialization()
private compiledFunction?: CustomActionFunction;
/**
* 获取或编译执行函数
*/
getFunction(): CustomActionFunction | undefined {
if (!this.compiledFunction && this.actionCode) {
try {
const func = new Function(
'entity',
'blackboard',
'deltaTime',
'parameters',
'TaskStatus',
`
const { Success, Failure, Running, Invalid } = TaskStatus;
try {
${this.actionCode}
} catch (error) {
return TaskStatus.Failure;
}
`
);
this.compiledFunction = (entity, blackboard, deltaTime) => {
return func(entity, blackboard, deltaTime, this.parameters, TaskStatus) || TaskStatus.Success;
};
} catch (error) {
return undefined;
}
}
return this.compiledFunction;
}
/**
* 设置自定义函数(运行时使用)
*/
setFunction(func: CustomActionFunction): void {
this.compiledFunction = func;
}
}

View File

@@ -0,0 +1,49 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* 日志动作组件
*
* 输出日志信息
*/
@BehaviorNode({
displayName: '日志',
category: '动作',
type: NodeType.Action,
icon: 'FileText',
description: '输出日志消息',
color: '#673AB7'
})
@ECSComponent('LogAction')
@Serializable({ version: 1 })
export class LogAction extends Component {
@BehaviorProperty({
label: '消息',
type: 'string',
required: true
})
@Serialize()
message: string = 'Hello';
@BehaviorProperty({
label: '级别',
type: 'select',
options: [
{ label: 'Log', value: 'log' },
{ label: 'Info', value: 'info' },
{ label: 'Warn', value: 'warn' },
{ label: 'Error', value: 'error' }
]
})
@Serialize()
level: 'log' | 'info' | 'warn' | 'error' = 'log';
@BehaviorProperty({
label: '包含实体信息',
type: 'boolean'
})
@Serialize()
includeEntityInfo: boolean = false;
}

View File

@@ -0,0 +1,76 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* 修改操作类型
*/
export enum ModifyOperation {
/** 加法 */
Add = 'add',
/** 减法 */
Subtract = 'subtract',
/** 乘法 */
Multiply = 'multiply',
/** 除法 */
Divide = 'divide',
/** 取模 */
Modulo = 'modulo',
/** 追加(数组/字符串) */
Append = 'append',
/** 移除(数组) */
Remove = 'remove'
}
/**
* 修改黑板变量值动作组件
*
* 对黑板变量执行数学或逻辑操作
*/
@BehaviorNode({
displayName: '修改变量',
category: '动作',
type: NodeType.Action,
icon: 'Calculator',
description: '对黑板变量执行数学或逻辑操作',
color: '#FF9800'
})
@ECSComponent('ModifyBlackboardValueAction')
@Serializable({ version: 1 })
export class ModifyBlackboardValueAction extends Component {
@BehaviorProperty({
label: '变量名',
type: 'variable',
required: true
})
@Serialize()
variableName: string = '';
@BehaviorProperty({
label: '操作类型',
type: 'select',
options: [
{ label: '加法', value: 'add' },
{ label: '减法', value: 'subtract' },
{ label: '乘法', value: 'multiply' },
{ label: '除法', value: 'divide' },
{ label: '取模', value: 'modulo' },
{ label: '追加', value: 'append' },
{ label: '移除', value: 'remove' }
]
})
@Serialize()
operation: ModifyOperation = ModifyOperation.Add;
@BehaviorProperty({
label: '操作数',
type: 'string',
description: '可以是固定值或变量引用 {{varName}}'
})
@Serialize()
operand: any = 0;
@Serialize()
force: boolean = false;
}

View File

@@ -0,0 +1,43 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* 设置黑板变量值动作组件
*
* 将指定值或另一个黑板变量的值设置到目标变量
*/
@BehaviorNode({
displayName: '设置变量',
category: '动作',
type: NodeType.Action,
icon: 'Edit',
description: '设置黑板变量的值',
color: '#3F51B5'
})
@ECSComponent('SetBlackboardValueAction')
@Serializable({ version: 1 })
export class SetBlackboardValueAction extends Component {
@BehaviorProperty({
label: '变量名',
type: 'variable',
required: true
})
@Serialize()
variableName: string = '';
@BehaviorProperty({
label: '值',
type: 'string',
description: '可以使用 {{varName}} 引用其他变量'
})
@Serialize()
value: any = '';
@Serialize()
sourceVariable?: string;
@Serialize()
force: boolean = false;
}

View File

@@ -0,0 +1,43 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* 等待动作组件
*
* 等待指定时间后返回成功
*/
@BehaviorNode({
displayName: '等待',
category: '动作',
type: NodeType.Action,
icon: 'Clock',
description: '等待指定时间',
color: '#9E9E9E'
})
@ECSComponent('WaitAction')
@Serializable({ version: 1 })
export class WaitAction extends Component {
@BehaviorProperty({
label: '等待时间',
type: 'number',
min: 0,
step: 0.1,
description: '等待时间(秒)',
required: true
})
@Serialize()
waitTime: number = 1.0;
/** 已等待时间(秒) */
@IgnoreSerialization()
elapsedTime: number = 0;
/**
* 重置等待状态
*/
reset(): void {
this.elapsedTime = 0;
}
}

View File

@@ -0,0 +1,20 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
/**
* 活跃节点标记组件
*
* 标记当前应该被执行的节点。
* 只有带有此组件的节点才会被各个执行系统处理。
*
* 这是一个标记组件Tag Component不包含数据只用于标识。
*
* 执行流程:
* 1. 初始时只有根节点带有 ActiveNode
* 2. 父节点决定激活哪个子节点时,为子节点添加 ActiveNode
* 3. 节点执行完成后移除 ActiveNode
* 4. 通过这种方式实现按需执行,避免每帧遍历整棵树
*/
@ECSComponent('ActiveNode')
export class ActiveNode extends Component {
// 标记组件,无需数据字段
}

View File

@@ -0,0 +1,61 @@
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
/**
* 资产元数据组件
*
* 附加到从资产实例化的行为树根节点上,
* 用于标记资产ID和版本信息便于循环引用检测和调试。
*
* @example
* ```typescript
* const rootEntity = BehaviorTreeAssetLoader.instantiate(asset, scene);
*
* // 添加元数据
* const metadata = rootEntity.addComponent(new BehaviorTreeAssetMetadata());
* metadata.assetId = 'patrol';
* metadata.assetVersion = '1.0.0';
* ```
*/
@ECSComponent('BehaviorTreeAssetMetadata')
@Serializable({ version: 1 })
export class BehaviorTreeAssetMetadata extends Component {
/**
* 资产ID
*/
@Serialize()
assetId: string = '';
/**
* 资产版本
*/
@Serialize()
assetVersion: string = '';
/**
* 资产名称
*/
@Serialize()
assetName: string = '';
/**
* 加载时间
*/
@Serialize()
loadedAt: number = 0;
/**
* 资产描述
*/
@Serialize()
description: string = '';
/**
* 初始化
*/
initialize(assetId: string, assetVersion: string, assetName?: string): void {
this.assetId = assetId;
this.assetVersion = assetVersion;
this.assetName = assetName || assetId;
this.loadedAt = Date.now();
}
}

View File

@@ -0,0 +1,44 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { TaskStatus, NodeType } from '../Types/TaskStatus';
/**
* 行为树节点基础组件
*
* 所有行为树节点都必须包含此组件
*/
@ECSComponent('BehaviorTreeNode')
@Serializable({ version: 1 })
export class BehaviorTreeNode extends Component {
/** 节点类型 */
@Serialize()
nodeType: NodeType = NodeType.Action;
/** 节点名称(用于调试) */
@Serialize()
nodeName: string = 'Node';
/** 当前执行状态 */
@IgnoreSerialization()
status: TaskStatus = TaskStatus.Invalid;
/** 当前执行的子节点索引(用于复合节点) */
@IgnoreSerialization()
currentChildIndex: number = 0;
/**
* 重置节点状态
*/
reset(): void {
this.status = TaskStatus.Invalid;
this.currentChildIndex = 0;
}
/**
* 标记节点为失效(递归重置子节点)
* 注意:此方法只重置当前节点,子节点需要在 System 中处理
*/
invalidate(): void {
this.reset();
}
}

View File

@@ -0,0 +1,201 @@
import { Component, ECSComponent, Core } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { BlackboardValueType } from '../Types/TaskStatus';
import { GlobalBlackboardService } from '../Services/GlobalBlackboardService';
/**
* 黑板变量定义
*/
export interface BlackboardVariable {
name: string;
type: BlackboardValueType;
value: any;
readonly?: boolean;
description?: string;
}
/**
* 黑板组件 - 用于节点间共享数据
*
* 支持分层查找:
* 1. 先查找本地变量
* 2. 如果找不到,自动查找全局 Blackboard
*
* 通常附加到行为树的根节点上
*/
@ECSComponent('Blackboard')
@Serializable({ version: 1 })
export class BlackboardComponent extends Component {
/** 存储的本地变量 */
@Serialize()
private variables: Map<string, BlackboardVariable> = new Map();
/** 是否启用全局 Blackboard 查找 */
private useGlobalBlackboard: boolean = true;
/**
* 定义一个新变量
*/
defineVariable(
name: string,
type: BlackboardValueType,
initialValue: any,
options?: {
readonly?: boolean;
description?: string;
}
): void {
this.variables.set(name, {
name,
type,
value: initialValue,
readonly: options?.readonly ?? false,
description: options?.description
});
}
/**
* 获取变量值
* 先查找本地变量,找不到则查找全局变量
*/
getValue<T = any>(name: string): T | undefined {
const variable = this.variables.get(name);
if (variable !== undefined) {
return variable.value as T;
}
if (this.useGlobalBlackboard) {
return Core.services.resolve(GlobalBlackboardService).getValue<T>(name);
}
return undefined;
}
/**
* 获取本地变量值(不查找全局)
*/
getLocalValue<T = any>(name: string): T | undefined {
const variable = this.variables.get(name);
return variable?.value as T;
}
/**
* 设置变量值
* 优先设置本地变量,如果本地不存在且全局存在,则设置全局变量
*/
setValue(name: string, value: any, force: boolean = false): boolean {
const variable = this.variables.get(name);
if (variable) {
if (variable.readonly && !force) {
return false;
}
variable.value = value;
return true;
}
if (this.useGlobalBlackboard) {
return Core.services.resolve(GlobalBlackboardService).setValue(name, value, force);
}
return false;
}
/**
* 设置本地变量值(不影响全局)
*/
setLocalValue(name: string, value: any, force: boolean = false): boolean {
const variable = this.variables.get(name);
if (!variable) {
return false;
}
if (variable.readonly && !force) {
return false;
}
variable.value = value;
return true;
}
/**
* 检查变量是否存在(包括本地和全局)
*/
hasVariable(name: string): boolean {
if (this.variables.has(name)) {
return true;
}
if (this.useGlobalBlackboard) {
return Core.services.resolve(GlobalBlackboardService).hasVariable(name);
}
return false;
}
/**
* 检查本地变量是否存在
*/
hasLocalVariable(name: string): boolean {
return this.variables.has(name);
}
/**
* 删除变量
*/
removeVariable(name: string): boolean {
return this.variables.delete(name);
}
/**
* 获取所有变量名
*/
getVariableNames(): string[] {
return Array.from(this.variables.keys());
}
/**
* 清空所有本地变量
*/
clear(): void {
this.variables.clear();
}
/**
* 启用/禁用全局 Blackboard 查找
*/
setUseGlobalBlackboard(enabled: boolean): void {
this.useGlobalBlackboard = enabled;
}
/**
* 是否启用全局 Blackboard 查找
*/
isUsingGlobalBlackboard(): boolean {
return this.useGlobalBlackboard;
}
/**
* 获取所有变量(包括本地和全局)
*/
getAllVariables(): BlackboardVariable[] {
const locals = Array.from(this.variables.values());
if (this.useGlobalBlackboard) {
const globals = Core.services.resolve(GlobalBlackboardService).getAllVariables();
const localNames = new Set(this.variables.keys());
const filteredGlobals = globals.filter(v => !localNames.has(v.name));
return [...locals, ...filteredGlobals];
}
return locals;
}
/**
* 获取全局 Blackboard 服务的引用
*/
static getGlobalBlackboard(): GlobalBlackboardService {
return Core.services.resolve(GlobalBlackboardService);
}
}

View File

@@ -0,0 +1,66 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { CompositeType } from '../Types/TaskStatus';
/**
* 复合节点组件
*
* 用于标识复合节点类型Sequence, Selector, Parallel等
*/
@ECSComponent('CompositeNode')
@Serializable({ version: 1 })
export class CompositeNodeComponent extends Component {
/** 复合节点类型 */
@Serialize()
compositeType: CompositeType = CompositeType.Sequence;
/** 随机化的子节点索引顺序 */
protected shuffledIndices: number[] = [];
/** 是否在重启时重新洗牌(子类可选) */
protected reshuffleOnRestart: boolean = true;
/**
* 获取下一个子节点索引
*/
getNextChildIndex(currentIndex: number, totalChildren: number): number {
// 对于随机类型,使用洗牌后的索引
if (this.compositeType === CompositeType.RandomSequence ||
this.compositeType === CompositeType.RandomSelector) {
// 首次执行或需要重新洗牌
if (this.shuffledIndices.length === 0 || currentIndex === 0 && this.reshuffleOnRestart) {
this.shuffleIndices(totalChildren);
}
if (currentIndex < this.shuffledIndices.length) {
return this.shuffledIndices[currentIndex];
}
return totalChildren; // 结束
}
// 普通顺序执行
return currentIndex;
}
/**
* 洗牌子节点索引
*/
private shuffleIndices(count: number): void {
this.shuffledIndices = Array.from({ length: count }, (_, i) => i);
// Fisher-Yates 洗牌算法
for (let i = this.shuffledIndices.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this.shuffledIndices[i], this.shuffledIndices[j]] =
[this.shuffledIndices[j], this.shuffledIndices[i]];
}
}
/**
* 重置洗牌状态
*/
resetShuffle(): void {
this.shuffledIndices = [];
}
}

View File

@@ -0,0 +1,51 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType, CompositeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { CompositeNodeComponent } from '../CompositeNodeComponent';
/**
* 并行节点
*
* 同时执行所有子节点
*/
@BehaviorNode({
displayName: '并行',
category: '组合',
type: NodeType.Composite,
icon: 'Layers',
description: '同时执行所有子节点',
color: '#CDDC39'
})
@ECSComponent('ParallelNode')
@Serializable({ version: 1 })
export class ParallelNode extends CompositeNodeComponent {
@BehaviorProperty({
label: '成功策略',
type: 'select',
description: '多少个子节点成功时整体成功',
options: [
{ label: '全部成功', value: 'all' },
{ label: '任意一个成功', value: 'one' }
]
})
@Serialize()
successPolicy: 'all' | 'one' = 'all';
@BehaviorProperty({
label: '失败策略',
type: 'select',
description: '多少个子节点失败时整体失败',
options: [
{ label: '任意一个失败', value: 'one' },
{ label: '全部失败', value: 'all' }
]
})
@Serialize()
failurePolicy: 'one' | 'all' = 'one';
constructor() {
super();
this.compositeType = CompositeType.Parallel;
}
}

View File

@@ -0,0 +1,39 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType, CompositeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { CompositeNodeComponent } from '../CompositeNodeComponent';
/**
* 并行选择节点
*
* 并行执行子节点,任一成功则成功
*/
@BehaviorNode({
displayName: '并行选择',
category: '组合',
type: NodeType.Composite,
icon: 'Sparkles',
description: '并行执行子节点,任一成功则成功',
color: '#FFC107'
})
@ECSComponent('ParallelSelectorNode')
@Serializable({ version: 1 })
export class ParallelSelectorNode extends CompositeNodeComponent {
@BehaviorProperty({
label: '失败策略',
type: 'select',
description: '多少个子节点失败时整体失败',
options: [
{ label: '任意一个失败', value: 'one' },
{ label: '全部失败', value: 'all' }
]
})
@Serialize()
failurePolicy: 'one' | 'all' = 'all';
constructor() {
super();
this.compositeType = CompositeType.ParallelSelector;
}
}

View File

@@ -0,0 +1,35 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType, CompositeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { CompositeNodeComponent } from '../CompositeNodeComponent';
/**
* 随机选择节点
*
* 随机顺序执行子节点选择
*/
@BehaviorNode({
displayName: '随机选择',
category: '组合',
type: NodeType.Composite,
icon: 'Dices',
description: '随机顺序执行子节点选择',
color: '#F44336'
})
@ECSComponent('RandomSelectorNode')
@Serializable({ version: 1 })
export class RandomSelectorNode extends CompositeNodeComponent {
@BehaviorProperty({
label: '重启时重新洗牌',
type: 'boolean',
description: '每次重启时是否重新随机子节点顺序'
})
@Serialize()
override reshuffleOnRestart: boolean = true;
constructor() {
super();
this.compositeType = CompositeType.RandomSelector;
}
}

View File

@@ -0,0 +1,35 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType, CompositeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { CompositeNodeComponent } from '../CompositeNodeComponent';
/**
* 随机序列节点
*
* 随机顺序执行子节点序列
*/
@BehaviorNode({
displayName: '随机序列',
category: '组合',
type: NodeType.Composite,
icon: 'Shuffle',
description: '随机顺序执行子节点序列',
color: '#FF5722'
})
@ECSComponent('RandomSequenceNode')
@Serializable({ version: 1 })
export class RandomSequenceNode extends CompositeNodeComponent {
@BehaviorProperty({
label: '重启时重新洗牌',
type: 'boolean',
description: '每次重启时是否重新随机子节点顺序'
})
@Serialize()
override reshuffleOnRestart: boolean = true;
constructor() {
super();
this.compositeType = CompositeType.RandomSequence;
}
}

View File

@@ -0,0 +1,27 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable } from '@esengine/ecs-framework';
import { NodeType, CompositeType } from '../../Types/TaskStatus';
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
import { CompositeNodeComponent } from '../CompositeNodeComponent';
/**
* 根节点
*
* 行为树的根节点,简单地激活第一个子节点
*/
@BehaviorNode({
displayName: '根节点',
category: '根节点',
type: NodeType.Composite,
icon: 'TreePine',
description: '行为树的根节点',
color: '#FFD700'
})
@ECSComponent('RootNode')
@Serializable({ version: 1 })
export class RootNode extends CompositeNodeComponent {
constructor() {
super();
this.compositeType = CompositeType.Sequence;
}
}

View File

@@ -0,0 +1,41 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType, CompositeType, AbortType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { CompositeNodeComponent } from '../CompositeNodeComponent';
/**
* 选择节点
*
* 按顺序执行子节点,任一成功则成功
*/
@BehaviorNode({
displayName: '选择',
category: '组合',
type: NodeType.Composite,
icon: 'GitBranch',
description: '按顺序执行子节点,任一成功则成功',
color: '#8BC34A'
})
@ECSComponent('SelectorNode')
@Serializable({ version: 1 })
export class SelectorNode extends CompositeNodeComponent {
@BehaviorProperty({
label: '中止类型',
type: 'select',
description: '条件变化时的中止行为',
options: [
{ label: '无', value: 'none' },
{ label: '自身', value: 'self' },
{ label: '低优先级', value: 'lower-priority' },
{ label: '两者', value: 'both' }
]
})
@Serialize()
abortType: AbortType = AbortType.None;
constructor() {
super();
this.compositeType = CompositeType.Selector;
}
}

View File

@@ -0,0 +1,41 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType, CompositeType, AbortType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { CompositeNodeComponent } from '../CompositeNodeComponent';
/**
* 序列节点
*
* 按顺序执行所有子节点,全部成功才成功
*/
@BehaviorNode({
displayName: '序列',
category: '组合',
type: NodeType.Composite,
icon: 'List',
description: '按顺序执行子节点,全部成功才成功',
color: '#4CAF50'
})
@ECSComponent('SequenceNode')
@Serializable({ version: 1 })
export class SequenceNode extends CompositeNodeComponent {
@BehaviorProperty({
label: '中止类型',
type: 'select',
description: '条件变化时的中止行为',
options: [
{ label: '无', value: 'none' },
{ label: '自身', value: 'self' },
{ label: '低优先级', value: 'lower-priority' },
{ label: '两者', value: 'both' }
]
})
@Serialize()
abortType: AbortType = AbortType.None;
constructor() {
super();
this.compositeType = CompositeType.Sequence;
}
}

View File

@@ -0,0 +1,172 @@
import { ECSComponent, Serializable, Serialize, Entity } from '@esengine/ecs-framework';
import { CompositeNodeComponent } from '../CompositeNodeComponent';
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* SubTree 节点 - 引用其他行为树作为子树
*
* 允许将其他行为树嵌入到当前树中,实现行为树的复用和模块化。
*
* 注意SubTreeNode 是一个特殊的叶子节点,它不会执行编辑器中静态连接的子节点,
* 只会执行从 assetId 动态加载的外部行为树文件。
*
* @example
* ```typescript
* const subTree = entity.addComponent(SubTreeNode);
* subTree.assetId = 'patrol';
* subTree.inheritParentBlackboard = true;
* ```
*/
@BehaviorNode({
displayName: '子树',
category: '组合',
type: NodeType.Composite,
icon: 'GitBranch',
description: '引用并执行外部行为树文件(不支持静态子节点)',
color: '#FF9800',
requiresChildren: false
})
@ECSComponent('SubTreeNode')
@Serializable({ version: 1 })
export class SubTreeNode extends CompositeNodeComponent {
/**
* 引用的子树资产ID
* 逻辑标识符,例如 'patrol' 或 'ai/patrol'
* 实际的文件路径由 AssetLoader 决定
*/
@BehaviorProperty({
label: '资产ID',
type: 'asset',
description: '要引用的行为树资产ID'
})
@Serialize()
assetId: string = '';
/**
* 是否将父黑板传递给子树
*
* - true: 子树可以访问和修改父树的黑板变量
* - false: 子树使用独立的黑板实例
*/
@BehaviorProperty({
label: '继承父黑板',
type: 'boolean',
description: '子树是否可以访问父树的黑板变量'
})
@Serialize()
inheritParentBlackboard: boolean = true;
/**
* 子树执行失败时是否传播失败状态
*
* - true: 子树失败时SubTree 节点返回 Failure
* - false: 子树失败时SubTree 节点返回 Success忽略失败
*/
@BehaviorProperty({
label: '传播失败',
type: 'boolean',
description: '子树失败时是否传播失败状态'
})
@Serialize()
propagateFailure: boolean = true;
/**
* 是否在行为树启动时预加载子树
*
* - true: 在根节点开始执行前预加载此子树,确保执行时子树已就绪
* - false: 运行时异步加载,执行到此节点时才开始加载(可能会有延迟)
*/
@BehaviorProperty({
label: '预加载',
type: 'boolean',
description: '在行为树启动时预加载子树,避免运行时加载延迟'
})
@Serialize()
preload: boolean = true;
/**
* 子树的根实体(运行时)
* 在执行时动态创建,执行结束后销毁
*/
private subTreeRoot?: Entity;
/**
* 子树是否已完成
*/
private subTreeCompleted: boolean = false;
/**
* 子树的最终状态
*/
private subTreeResult: TaskStatus = TaskStatus.Invalid;
/**
* 获取子树根实体
*/
getSubTreeRoot(): Entity | undefined {
return this.subTreeRoot;
}
/**
* 设置子树根实体(由执行系统调用)
*/
setSubTreeRoot(root: Entity | undefined): void {
this.subTreeRoot = root;
this.subTreeCompleted = false;
this.subTreeResult = TaskStatus.Invalid;
}
/**
* 标记子树完成(由执行系统调用)
*/
markSubTreeCompleted(result: TaskStatus): void {
this.subTreeCompleted = true;
this.subTreeResult = result;
}
/**
* 检查子树是否已完成
*/
isSubTreeCompleted(): boolean {
return this.subTreeCompleted;
}
/**
* 获取子树执行结果
*/
getSubTreeResult(): TaskStatus {
return this.subTreeResult;
}
/**
* 重置子树状态
*/
reset(): void {
this.subTreeRoot = undefined;
this.subTreeCompleted = false;
this.subTreeResult = TaskStatus.Invalid;
}
/**
* 重置完成状态(用于复用预加载的子树)
* 保留子树根引用,只重置完成标记
*/
resetCompletionState(): void {
this.subTreeCompleted = false;
this.subTreeResult = TaskStatus.Invalid;
}
/**
* 验证配置
*/
validate(): string[] {
const errors: string[] = [];
if (!this.assetId || this.assetId.trim() === '') {
errors.push('SubTree 节点必须指定资产ID');
}
return errors;
}
}

View File

@@ -0,0 +1,83 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* 比较运算符
*/
export enum CompareOperator {
/** 等于 */
Equal = 'equal',
/** 不等于 */
NotEqual = 'notEqual',
/** 大于 */
Greater = 'greater',
/** 大于等于 */
GreaterOrEqual = 'greaterOrEqual',
/** 小于 */
Less = 'less',
/** 小于等于 */
LessOrEqual = 'lessOrEqual',
/** 包含(字符串/数组) */
Contains = 'contains',
/** 正则匹配 */
Matches = 'matches'
}
/**
* 黑板变量比较条件组件
*
* 比较黑板变量与指定值或另一个变量
*/
@BehaviorNode({
displayName: '比较变量',
category: '条件',
type: NodeType.Condition,
icon: 'Scale',
description: '比较黑板变量与指定值',
color: '#2196F3'
})
@ECSComponent('BlackboardCompareCondition')
@Serializable({ version: 1 })
export class BlackboardCompareCondition extends Component {
@BehaviorProperty({
label: '变量名',
type: 'variable',
required: true
})
@Serialize()
variableName: string = '';
@BehaviorProperty({
label: '运算符',
type: 'select',
options: [
{ label: '等于', value: 'equal' },
{ label: '不等于', value: 'notEqual' },
{ label: '大于', value: 'greater' },
{ label: '大于等于', value: 'greaterOrEqual' },
{ label: '小于', value: 'less' },
{ label: '小于等于', value: 'lessOrEqual' },
{ label: '包含', value: 'contains' },
{ label: '正则匹配', value: 'matches' }
]
})
@Serialize()
operator: CompareOperator = CompareOperator.Equal;
@BehaviorProperty({
label: '比较值',
type: 'string',
description: '可以是固定值或变量引用 {{varName}}'
})
@Serialize()
compareValue: any = null;
@BehaviorProperty({
label: '反转结果',
type: 'boolean'
})
@Serialize()
invertResult: boolean = false;
}

View File

@@ -0,0 +1,45 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* 黑板变量存在性检查条件组件
*
* 检查黑板变量是否存在
*/
@BehaviorNode({
displayName: '检查变量存在',
category: '条件',
type: NodeType.Condition,
icon: 'Search',
description: '检查黑板变量是否存在',
color: '#00BCD4'
})
@ECSComponent('BlackboardExistsCondition')
@Serializable({ version: 1 })
export class BlackboardExistsCondition extends Component {
@BehaviorProperty({
label: '变量名',
type: 'variable',
required: true
})
@Serialize()
variableName: string = '';
@BehaviorProperty({
label: '检查非空',
type: 'boolean',
description: '检查值不为 null/undefined'
})
@Serialize()
checkNotNull: boolean = false;
@BehaviorProperty({
label: '反转结果',
type: 'boolean',
description: '检查不存在'
})
@Serialize()
invertResult: boolean = false;
}

View File

@@ -0,0 +1,92 @@
import { Component, ECSComponent, Entity } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { BlackboardComponent } from '../BlackboardComponent';
/**
* 自定义条件函数类型
*/
export type CustomConditionFunction = (
entity: Entity,
blackboard?: BlackboardComponent,
deltaTime?: number
) => boolean;
/**
* 执行自定义条件组件
*
* 允许用户提供自定义的条件检查函数
*/
@BehaviorNode({
displayName: '自定义条件',
category: '条件',
type: NodeType.Condition,
icon: 'Code',
description: '执行自定义条件代码',
color: '#9C27B0'
})
@ECSComponent('ExecuteCondition')
@Serializable({ version: 1 })
export class ExecuteCondition extends Component {
@BehaviorProperty({
label: '条件代码',
type: 'code',
description: 'JavaScript 代码,返回 boolean',
required: true
})
@Serialize()
conditionCode?: string;
@Serialize()
parameters: Record<string, any> = {};
@BehaviorProperty({
label: '反转结果',
type: 'boolean'
})
@Serialize()
invertResult: boolean = false;
/** 编译后的函数(不序列化) */
@IgnoreSerialization()
private compiledFunction?: CustomConditionFunction;
/**
* 获取或编译条件函数
*/
getFunction(): CustomConditionFunction | undefined {
if (!this.compiledFunction && this.conditionCode) {
try {
const func = new Function(
'entity',
'blackboard',
'deltaTime',
'parameters',
`
try {
${this.conditionCode}
} catch (error) {
return false;
}
`
);
this.compiledFunction = (entity, blackboard, deltaTime) => {
return Boolean(func(entity, blackboard, deltaTime, this.parameters));
};
} catch (error) {
return undefined;
}
}
return this.compiledFunction;
}
/**
* 设置自定义函数(运行时使用)
*/
setFunction(func: CustomConditionFunction): void {
this.compiledFunction = func;
}
}

View File

@@ -0,0 +1,61 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* 随机概率条件组件
*
* 根据概率返回成功或失败
*/
@BehaviorNode({
displayName: '随机概率',
category: '条件',
type: NodeType.Condition,
icon: 'Dice',
description: '根据概率返回成功或失败',
color: '#E91E63'
})
@ECSComponent('RandomProbabilityCondition')
@Serializable({ version: 1 })
export class RandomProbabilityCondition extends Component {
@BehaviorProperty({
label: '成功概率',
type: 'number',
min: 0,
max: 1,
step: 0.1,
description: '0.0 - 1.0',
required: true
})
@Serialize()
probability: number = 0.5;
@BehaviorProperty({
label: '总是重新随机',
type: 'boolean',
description: 'false则第一次随机后固定结果'
})
@Serialize()
alwaysRandomize: boolean = true;
/** 缓存的随机结果(不序列化) */
private cachedResult?: boolean;
/**
* 评估随机概率
*/
evaluate(): boolean {
if (this.alwaysRandomize || this.cachedResult === undefined) {
this.cachedResult = Math.random() < this.probability;
}
return this.cachedResult;
}
/**
* 重置缓存
*/
reset(): void {
this.cachedResult = undefined;
}
}

View File

@@ -0,0 +1,18 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { DecoratorType } from '../Types/TaskStatus';
/**
* 装饰器节点组件基类
*
* 只包含通用的装饰器类型标识
* 具体的属性由各个子类自己定义
*/
@ECSComponent('DecoratorNode')
@Serializable({ version: 1 })
export class DecoratorNodeComponent extends Component {
/** 装饰器类型 */
@Serialize()
decoratorType: DecoratorType = DecoratorType.Inverter;
}

View File

@@ -0,0 +1,27 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
/**
* 总是失败节点
*
* 无论子节点结果如何都返回失败
*/
@BehaviorNode({
displayName: '总是失败',
category: '装饰器',
type: NodeType.Decorator,
icon: 'ThumbsDown',
description: '无论子节点结果如何都返回失败',
color: '#FF5722'
})
@ECSComponent('AlwaysFailNode')
@Serializable({ version: 1 })
export class AlwaysFailNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.AlwaysFail;
}
}

View File

@@ -0,0 +1,27 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
/**
* 总是成功节点
*
* 无论子节点结果如何都返回成功
*/
@BehaviorNode({
displayName: '总是成功',
category: '装饰器',
type: NodeType.Decorator,
icon: 'ThumbsUp',
description: '无论子节点结果如何都返回成功',
color: '#8BC34A'
})
@ECSComponent('AlwaysSucceedNode')
@Serializable({ version: 1 })
export class AlwaysSucceedNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.AlwaysSucceed;
}
}

View File

@@ -0,0 +1,89 @@
import { ECSComponent, Entity } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
import { BlackboardComponent } from '../BlackboardComponent';
/**
* 条件装饰器节点
*
* 基于条件判断是否执行子节点
*/
@BehaviorNode({
displayName: '条件装饰器',
category: '装饰器',
type: NodeType.Decorator,
icon: 'Filter',
description: '基于条件判断是否执行子节点',
color: '#3F51B5'
})
@ECSComponent('ConditionalNode')
@Serializable({ version: 1 })
export class ConditionalNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.Conditional;
}
@BehaviorProperty({
label: '条件代码',
type: 'code',
description: 'JavaScript 代码,返回 boolean',
required: true
})
@Serialize()
conditionCode?: string;
@BehaviorProperty({
label: '重新评估条件',
type: 'boolean',
description: '每次执行时是否重新评估条件'
})
@Serialize()
shouldReevaluate: boolean = true;
/** 编译后的条件函数(不序列化) */
@IgnoreSerialization()
private compiledCondition?: (entity: Entity, blackboard?: BlackboardComponent) => boolean;
/**
* 评估条件
*/
evaluateCondition(entity: Entity, blackboard?: BlackboardComponent): boolean {
if (!this.conditionCode) {
return false;
}
if (!this.compiledCondition) {
try {
const func = new Function(
'entity',
'blackboard',
`
try {
return Boolean(${this.conditionCode});
} catch (error) {
return false;
}
`
);
this.compiledCondition = (entity, blackboard) => {
return Boolean(func(entity, blackboard));
};
} catch (error) {
return false;
}
}
return this.compiledCondition(entity, blackboard);
}
/**
* 设置条件函数(运行时使用)
*/
setConditionFunction(func: (entity: Entity, blackboard?: BlackboardComponent) => boolean): void {
this.compiledCondition = func;
}
}

View File

@@ -0,0 +1,67 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
/**
* 冷却节点
*
* 在冷却时间内阻止子节点执行
*/
@BehaviorNode({
displayName: '冷却',
category: '装饰器',
type: NodeType.Decorator,
icon: 'Timer',
description: '在冷却时间内阻止子节点执行',
color: '#00BCD4'
})
@ECSComponent('CooldownNode')
@Serializable({ version: 1 })
export class CooldownNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.Cooldown;
}
@BehaviorProperty({
label: '冷却时间',
type: 'number',
min: 0,
step: 0.1,
description: '冷却时间(秒)',
required: true
})
@Serialize()
cooldownTime: number = 1.0;
/** 上次执行时间 */
@IgnoreSerialization()
lastExecutionTime: number = 0;
/**
* 检查是否可以执行
*/
canExecute(currentTime: number): boolean {
// 如果从未执行过,允许执行
if (this.lastExecutionTime === 0) {
return true;
}
return currentTime - this.lastExecutionTime >= this.cooldownTime;
}
/**
* 记录执行时间
*/
recordExecution(currentTime: number): void {
this.lastExecutionTime = currentTime;
}
/**
* 重置状态
*/
reset(): void {
this.lastExecutionTime = 0;
}
}

View File

@@ -0,0 +1,27 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
/**
* 反转节点
*
* 反转子节点的执行结果
*/
@BehaviorNode({
displayName: '反转',
category: '装饰器',
type: NodeType.Decorator,
icon: 'RotateCcw',
description: '反转子节点的执行结果',
color: '#607D8B'
})
@ECSComponent('InverterNode')
@Serializable({ version: 1 })
export class InverterNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.Inverter;
}
}

View File

@@ -0,0 +1,74 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
/**
* 重复节点
*
* 重复执行子节点指定次数
*/
@BehaviorNode({
displayName: '重复',
category: '装饰器',
type: NodeType.Decorator,
icon: 'Repeat',
description: '重复执行子节点指定次数',
color: '#9E9E9E'
})
@ECSComponent('RepeaterNode')
@Serializable({ version: 1 })
export class RepeaterNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.Repeater;
}
@BehaviorProperty({
label: '重复次数',
type: 'number',
min: -1,
step: 1,
description: '-1表示无限重复',
required: true
})
@Serialize()
repeatCount: number = 1;
@BehaviorProperty({
label: '失败时停止',
type: 'boolean',
description: '子节点失败时是否停止重复'
})
@Serialize()
endOnFailure: boolean = false;
/** 当前已重复次数 */
@IgnoreSerialization()
currentRepeatCount: number = 0;
/**
* 增加重复计数
*/
incrementRepeat(): void {
this.currentRepeatCount++;
}
/**
* 检查是否应该继续重复
*/
shouldContinueRepeat(): boolean {
if (this.repeatCount === -1) {
return true;
}
return this.currentRepeatCount < this.repeatCount;
}
/**
* 重置状态
*/
reset(): void {
this.currentRepeatCount = 0;
}
}

View File

@@ -0,0 +1,68 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
/**
* 超时节点
*
* 子节点执行超时则返回失败
*/
@BehaviorNode({
displayName: '超时',
category: '装饰器',
type: NodeType.Decorator,
icon: 'Clock',
description: '子节点执行超时则返回失败',
color: '#FF9800'
})
@ECSComponent('TimeoutNode')
@Serializable({ version: 1 })
export class TimeoutNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.Timeout;
}
@BehaviorProperty({
label: '超时时间',
type: 'number',
min: 0,
step: 0.1,
description: '超时时间(秒)',
required: true
})
@Serialize()
timeoutDuration: number = 5.0;
/** 开始执行时间 */
@IgnoreSerialization()
startTime: number = 0;
/**
* 记录开始时间
*/
recordStartTime(currentTime: number): void {
if (this.startTime === 0) {
this.startTime = currentTime;
}
}
/**
* 检查是否超时
*/
isTimeout(currentTime: number): boolean {
if (this.startTime === 0) {
return false;
}
return currentTime - this.startTime >= this.timeoutDuration;
}
/**
* 重置状态
*/
reset(): void {
this.startTime = 0;
}
}

View File

@@ -0,0 +1,27 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
/**
* 直到失败节点
*
* 重复执行子节点直到失败
*/
@BehaviorNode({
displayName: '直到失败',
category: '装饰器',
type: NodeType.Decorator,
icon: 'XCircle',
description: '重复执行子节点直到失败',
color: '#F44336'
})
@ECSComponent('UntilFailNode')
@Serializable({ version: 1 })
export class UntilFailNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.UntilFail;
}
}

View File

@@ -0,0 +1,27 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
/**
* 直到成功节点
*
* 重复执行子节点直到成功
*/
@BehaviorNode({
displayName: '直到成功',
category: '装饰器',
type: NodeType.Decorator,
icon: 'CheckCircle',
description: '重复执行子节点直到成功',
color: '#4CAF50'
})
@ECSComponent('UntilSuccessNode')
@Serializable({ version: 1 })
export class UntilSuccessNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.UntilSuccess;
}
}

View File

@@ -0,0 +1,36 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
/**
* 日志输出组件
*
* 存储运行时输出的日志信息用于在UI中显示
*/
@ECSComponent('LogOutput')
export class LogOutput extends Component {
/**
* 日志消息列表
*/
messages: Array<{
timestamp: number;
message: string;
level: 'log' | 'info' | 'warn' | 'error';
}> = [];
/**
* 添加日志消息
*/
addMessage(message: string, level: 'log' | 'info' | 'warn' | 'error' = 'log'): void {
this.messages.push({
timestamp: Date.now(),
message,
level
});
}
/**
* 清空日志
*/
clear(): void {
this.messages = [];
}
}

View File

@@ -0,0 +1,42 @@
import { Component } from '@esengine/ecs-framework';
/**
* 属性绑定组件
* 记录节点属性到黑板变量的绑定关系
*/
export class PropertyBindings extends Component {
/**
* 属性绑定映射
* key: 属性名称 (如 'message')
* value: 黑板变量名 (如 'test1')
*/
bindings: Map<string, string> = new Map();
/**
* 添加属性绑定
*/
addBinding(propertyName: string, blackboardKey: string): void {
this.bindings.set(propertyName, blackboardKey);
}
/**
* 获取属性绑定的黑板变量名
*/
getBinding(propertyName: string): string | undefined {
return this.bindings.get(propertyName);
}
/**
* 检查属性是否绑定到黑板变量
*/
hasBinding(propertyName: string): boolean {
return this.bindings.has(propertyName);
}
/**
* 清除所有绑定
*/
clearBindings(): void {
this.bindings.clear();
}
}