跳转到内容

脚本

defineBehavior 是游戏循环的编排糖。一次调用同时给你两样东西:

  1. 一个组件,持有这个行为的每实体 state——可挂到任意实体,并在编辑器 Details 面板里调。
  2. 一个自动注册的系统,为每个携带该组件的实体驱动 start / update / destroy 生命周期。

这里没有第二套脚本运行时:一个 behavior 完全降解到 Estella 现有的 ECS 之上——还是你已经在用的那套组件、系统和调度——所以它 既快又完全可检视。

defineBehavior(name, def) 返回其背后的组件,所以你能像任何组件一样 spawn 或 insert 它。 state 对象成为该组件的每实体数据:

import { defineBehavior, Transform } from 'esengine';
export const Patrol = defineBehavior('Patrol', {
state: { speed: 60 }, // 每实体数据,可在 Details 面板里编辑
update(ctx, dt) {
ctx.get(Transform).position.x += ctx.self.speed * dt;
},
});

这一次调用就注册了更新系统。要把行为放到实体上,insert 它的组件——还能顺便覆盖起始 state:

import { defineSystem, Commands, Transform } from 'esengine';
const spawn = defineSystem([Commands()], (cmds) => {
cmds.spawn()
.insert(Transform, { position: { x: 0, y: 0, z: 0 } })
.insert(Patrol, { speed: 120 }); // 每个实例单独覆盖默认值
});

每个钩子都是可选的;只定义你需要的那些。

钩子 何时运行
start(ctx) 一次,在该行为的组件首次出现在某实体上的那一帧。
update(ctx, dt) 每帧,对每个携带该行为的实体。dt 是帧间隔(秒)(等同 ctx.time.delta)。
destroy(ctx) 一次,当组件被移除实体被 despawn 时。
export const Enemy = defineBehavior('Enemy', {
state: { hp: 100 },
start(ctx) {
// 一次性初始化:播种状态、缓存查找、生成血条……
},
update(ctx, dt) {
if (ctx.self.hp <= 0) ctx.commands.despawn(ctx.entity);
},
destroy(ctx) {
// 收尾:不管实体是被杀死还是组件被卸下,都会运行
},
});

每个钩子都会收到一个 BehaviorContext——通往它所运行实体、以及周围世界的句柄:

成员 类型 说明
ctx.self S 该行为自己的 state随意改——修改会持久化。
ctx.entity Entity 该实例所挂的实体。
ctx.time TimeData 帧计时:deltaelapsedframeCountfixedDelta
ctx.input InputState 键盘 / 鼠标 / 触摸 / 手柄(见 输入)。
ctx.commands Commands 延迟的 spawn / despawn / insert——update 中调用安全。
ctx.world World 完整世界访问,用于跨实体读写。
ctx.get(Comp) ComponentData 读取实体上的另一个组件。
ctx.set(Comp, data) void 写入实体上的一个组件。
ctx.has(Comp) boolean 本实体是否拥有 Comp
import { defineBehavior, Transform } from 'esengine';
export const PlayerController = defineBehavior('PlayerController', {
state: { speed: 200, facing: 1 },
update(ctx, dt) {
const move = (ctx.input.isKeyDown('KeyD') ? 1 : 0) - (ctx.input.isKeyDown('KeyA') ? 1 : 0);
if (move !== 0) ctx.self.facing = move; // 改 self 会持久化
ctx.get(Transform).position.x += move * ctx.self.speed * dt;
},
});

state 对象定义了组件的字段和默认值。传 metadata 来控制这些字段在编辑器里的呈现——范围、 步进、单位、枚举——和任何组件用的字段元数据是同一套:

export const Turret = defineBehavior('Turret', {
state: { range: 300, fireRate: 2, target: 0 },
metadata: {
fields: {
range: { min: 0, unit: 'px', category: 'Targeting' },
fireRate: { min: 0, unit: '/s', category: 'Targeting', tooltip: 'Shots per second.' },
},
},
update(ctx, dt) { /* … */ },
});

因为状态是一个真正的、可序列化的组件,每实体的覆盖值会随场景保存,并在 Details 面板里实时编辑。

默认情况下生命周期系统在 Schedule.Update 里运行,每帧一次。传一个不同的 schedule 可以在帧 里的别处运行——例如用 Schedule.FixedUpdate 与物理同步推进:

import { defineBehavior, Schedule } from 'esengine';
export const Thruster = defineBehavior('Thruster', {
schedule: Schedule.FixedUpdate,
state: { force: 500 },
update(ctx) {
// 固定节奏——读 ctx.time.fixedDelta 拿到固定步长
},
});

可用的相位包括 StartupPreUpdateUpdatePostUpdate,以及固定三件套 FixedPreUpdate / FixedUpdate / FixedPostUpdate

“2 秒后…” / “每 0.5 秒…“这类逻辑,用内置的计时器资源比手搓累计时间干净。 它随引擎循环走(所以和 setTimeout 不同,会跟游戏一起暂停):

import { defineSystem, Res, TimerRes } from 'esengine';
const arm = defineSystem([Res(TimerRes)], (timers) => {
timers.delay(2, () => explode()); // 2 秒后执行一次
const h = timers.interval(0.5, (t) => spawnWave()); // 重复执行
timers.interval(1, (t) => tick(), 3); // 恰好 3 次
h.pause(); h.resume(); h.cancel(); h.reset(); // 句柄控制
});
成员 说明
delay(seconds, cb) seconds 后执行一次;返回 TimerHandle
interval(seconds, cb, maxRepeat?) seconds 执行;maxRepeat0 = 无限。
句柄 pause() / resume() / cancel() / reset() 控制单个计时器(可链式)。
句柄 isActive / elapsed / repeatCount 查看状态。
cancelAll() · activeCount 管理全体。
timeScale 一次性放慢 / 加速所有计时器(0 冻结)。

计时器只在播放模式推进——编辑器编辑态下与其余游戏时间一样保持不动。

对“就是一直动”的运动——子弹、漂移的碎片、旋转体——直接挂内置的 Velocity 组件, 不必手写移动系统。引擎在播放模式下每次 update 把它积分进 Transform:

import { Velocity } from 'esengine';
world.spawn()
.insert(Transform, { position: { x: 0, y: 0, z: 0 } })
.insert(Sprite, { texture: bulletTex })
.insert(Velocity, {
linear: { x: 240, y: 0, z: 0 }, // 单位/秒
angular: { x: 0, y: 0, z: Math.PI }, // 弧度/秒(z = 2D 自转)
});

RigidBody 的实体会被跳过——物理求解器拥有它们的变换,两者永远不会争抢同一个实体。 Velocity 也是复制感知的:它的字段参与网络复制,联网客户端可以在快照之间做航位推算。

一个 behavior 并不是特例——它编译成恰好一个 defineComponent 加一个 defineSystem。当你的逻辑 天然是每实体的(巡逻、抛射物、拾取物)时,用 defineBehavior;当你要一次处理整个查询、或在实体 间协调时,用一个普通系统

钩子签名——entityworldcommands,外加 self——也和游戏 AI 层 的动作与行为树叶子是同一套编程模型,所以在 behavior 和 AI 动作之间搬逻辑很顺手。

编辑器播放期间保存代码时,只要可行就会触发保状态热替换:若组件形状未变、 也没有系统被增、删或改名,存活的世界会被保留,只替换你的函数体。否则编辑器退回 整体重载。这对你的代码的要求:

  • **组件与 behavior 的名字就是身份。**改名属于结构性变更——预期整体重载,该 数据的状态归零。
  • 任何一种重载后,存活实体的 start 都会再跑一次——契合 Estella 的快速重启 语义,所以把 start 当作幂等的初始化来写。

其机制(探针上下文、模式指纹、App.hotSwapSystems)见 应用设置与生命周期

  • 系统 —— 完整的查询词汇、过滤器与调度。
  • ECS 架构 —— 实体、组件,以及系统如何遍历它们。
  • 输入 —— 在系统里读取键盘、指针与手势。