感知 Perception
Perceiver 在视野锥内感知附近的 PerceptionTarget,把看到的东西写进 Perception 组件。
Estella 的 AI 层把一个实体变成会感知、会决策、会移动的角色。它由四个协作部件 组成,而这四者共享同一套设计:
感知 Perception
Perceiver 在视野锥内感知附近的 PerceptionTarget,把看到的东西写进 Perception 组件。
导航 Navigation
NavGrid + A* 寻路把 NavAgent 移动到任意世界坐标,并随目标移动而重新规划。
状态机 FSM
StateMachineAgent 运行一个 .esfsm 图——带进入/更新/退出钩子和带守卫的转移的状态。
行为树 BT
BehaviorTreeAgent 运行一个 .esbt 图——选择器、序列、装饰器与叶子。
统一的核心思想:状态机和行为树是同一套构件之上的两种编排范式。 你在代码里注册一次具名 的动作与条件,然后把它们连进 FSM 或 BT——图在编辑器里可视化编排,而不是写死在代码里。 感知喂给决策,决策驱动导航。
一个动作做某件事(追击、攻击、放个音效);一个条件回答一个是/否问题(我看得见玩家吗?)。 你按名字注册一次,FSM 和 BT 都会对同一份注册表按名字解析——于是一套叶子同时服务两种范式。
import { registerAction, registerCondition, Status, Perception, setNavDestination } from 'esengine';
registerCondition('seesPlayer', (ctx) => ctx.has(Perception) && ctx.get(Perception).visible);
registerAction('chase', (ctx) => { const per = ctx.get(Perception); setNavDestination(ctx.world, ctx.entity, { x: per.targetX, y: per.targetY });});每个动作和条件都会收到它所运行实体的 AiContext。它和一个
defineBehavior 的 update 是同一套编程模型——实体、世界、
命令的访问——外加一个每个 agent 独有的 blackboard(黑板):
| 成员 | 类型 | 说明 |
|---|---|---|
ctx.entity |
Entity |
该动作/条件运行所在的 agent 实体。 |
ctx.dt |
number |
帧间隔(秒)。 |
ctx.world |
World |
完整世界访问,用于跨实体读取。 |
ctx.commands |
Commands |
延迟的 spawn/despawn,tick 中调用安全。 |
ctx.blackboard |
Blackboard |
该 agent 的 AI 数据面(见黑板)。 |
ctx.get(Comp) |
ComponentData |
读取本实体上的另一个组件。 |
ctx.set(Comp, data) |
void |
写入本实体上的一个组件。 |
ctx.has(Comp) |
boolean |
本实体是否拥有 Comp。 |
跑你的状态机的那个系统,够到的东西就是它的叶子够到的东西——而这只有在图加载之后才 知道。在注册时声明出来,调度器就能把这个系统和「可能碰任何东西」的系统区分开:
registerAction('chase', { run: (ctx) => { /* ... */ }, touches: { reads: ['Perception'], writes: ['NavAgent'] },});
registerCondition('seesPlayer', { check: (ctx) => ctx.has(Perception) && ctx.get(Perception).visible, touches: { reads: ['Perception'] },});可选,但值得知道不写的代价:只要有一个叶子没声明,整个系统就退回「碰一切」——因为 一份悄悄丢掉了读不懂那部分的并集,是一个调度器会信、而帧会推翻的声明。参见 系统碰什么。
条件总是返回 boolean。动作可以返回一个 Status,也可以什么都不返回:
export enum Status { Success = 'success', Failure = 'failure', Running = 'running' }Status 以跨帧运行(Running)、成功或失败。什么都不返回视为
Success。写一个返回 Status 的动作,就能让同一个动作既当 BT 叶子又当 FSM 钩子。
引擎预注册了少量名字,常见的胶水逻辑因此完全不用写代码——它们会和你自己注册的 名字一起出现在编辑器调色板里。它们全部只操作代理实体自身的组件(与你的代码、 编辑器共用同一条通道):
| 名字 | 类别 | 效果 |
|---|---|---|
timeline.play |
动作 | 拉起该实体 TimelinePlayer 的播放旗标。对已播完的片段,从头重播。 |
timeline.pause |
动作 | 放下播放旗标(用 timeline.play 恢复)。 |
timeline.finished |
条件 | 片段已完成(once 片段播到末尾)且未在播放时为真。 |
spriteAnim.play |
动作 | 播放代理实体的精灵翻页动画;参数可切换到指定剪辑。 |
spriteAnim.restart |
动作 | 回卷到第 0 帧并播放(参数可切换剪辑)。 |
spriteAnim.stop |
动作 | 暂停翻页动画。 |
spriteAnim.finished |
条件 | 一次性精灵剪辑播完后为真。 |
动作可携带一个可选字符串参数——在 FSM 状态检查器的动作名旁(或 BT 动作节点上)填写。
内置动作用它承载组件无法按状态携带的数据,比如 spriteAnim.play 要切换到哪个剪辑;
你自己注册的动作在第三个参数收到它:(ctx, blackboard, arg) => …。
三者组合出一个零代码的过场状态:进入状态即播放片段,片段播完即驱动转移:
registerFsm('intro', { initial: 'Cutscene', states: [ { name: 'Cutscene', onEnter: 'timeline.play', transitions: [{ to: 'Gameplay', condition: 'timeline.finished' }], }, { name: 'Gameplay' }, ],});代理实体自己携带 TimelinePlayer(播哪个片段、速度、循环模式)——FSM 只翻它的
旗标。内建名字永远不会遮蔽你的注册:同名时你的注册获胜。游戏侧的名字请避开
timeline. 前缀,保持命名空间分离。
在 FSM / 行为树编辑器里,动作与条件输入框会随输入提示所有已注册的名字——按命名 空间分组,内建项带一行“它做什么”的说明。自由输入仍然合法,留给游戏运行时注册的 名字。
cutscene 示例
就是这个模式的完整落地:场景开始即播开场时间轴,播完解锁操作,按 R 重播——
全程零注册动作。
enemy-ai 示例
用两个敌人猎杀玩家,它们共享同一套感官和叶子——一个由状态机驱动,另一个由行为树驱动:
import { defineSystem, Res, Nav, NavGrid, registerAction, registerCondition, setNavDestination, Perception,} from 'esengine';
// 共享叶子——同一批名字同时服务 .esfsm 和 .esbt。registerCondition('seesPlayer', (ctx) => ctx.has(Perception) && ctx.get(Perception).visible);registerCondition('lostPlayer', (ctx) => !ctx.has(Perception) || !ctx.get(Perception).visible);
registerAction('chase', (ctx) => { if (!ctx.has(Perception)) return; const per = ctx.get(Perception); if (per.visible) setNavDestination(ctx.world, ctx.entity, { x: per.targetX, y: per.targetY });});registerAction('patrol', () => { /* 在看见玩家之前保持原地 */ });
// 一张覆盖竞技场的开阔导航网格。export const setupNavGrid = defineSystem([Res(Nav)], (nav) => { nav.setGrid(new NavGrid({ width: 60, height: 44, cellSize: 20, origin: { x: -600, y: -440 } }));}, { name: 'SetupNavGrid' });两个敌人的 StateMachineAgent.fsm 和 BehaviorTreeAgent.bt 分别指向 enemy.esfsm /
enemy.esbt——在编辑器里编排,由引擎加载。感知、FSM/BT 的 tick、以及跟随导航全是内置的,
所以唯一的游戏代码就是上面的共享叶子加上那张一次性的网格。