脚本
defineBehavior 是游戏循环的编排糖。一次调用同时给你两样东西:
- 一个组件,持有这个行为的每实体
state——可挂到任意实体,并在编辑器 Details 面板里调。 - 一个自动注册的系统,为每个携带该组件的实体驱动
start/update/destroy生命周期。
这里没有第二套脚本运行时:一个 behavior 完全降解到 Estella 现有的 ECS 之上——还是你已经在用的那套组件、系统和调度——所以它 既快又完全可检视。
你的第一个 behavior
Section titled “你的第一个 behavior”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 |
帧计时:delta、elapsed、frameCount、fixedDelta。 |
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; },});可编辑状态与元数据
Section titled “可编辑状态与元数据”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 拿到固定步长 },});可用的相位包括 Startup、PreUpdate、Update、PostUpdate,以及固定三件套
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 执行;maxRepeat 为 0 = 无限。 |
句柄 pause() / resume() / cancel() / reset() |
控制单个计时器(可链式)。 |
句柄 isActive / elapsed / repeatCount |
查看状态。 |
cancelAll() · activeCount |
管理全体。 |
timeScale |
一次性放慢 / 加速所有计时器(0 冻结)。 |
计时器只在播放模式推进——编辑器编辑态下与其余游戏时间一样保持不动。
匀速运动:Velocity 组件
Section titled “匀速运动:Velocity 组件”对“就是一直动”的运动——子弹、漂移的碎片、旋转体——直接挂内置的 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;当你要一次处理整个查询、或在实体
间协调时,用一个普通系统。
钩子签名——entity、world、commands,外加 self——也和游戏 AI 层
的动作与行为树叶子是同一套编程模型,所以在 behavior 和 AI 动作之间搬逻辑很顺手。
编辑器播放期间保存代码时,只要可行就会触发保状态热替换:若组件形状未变、 也没有系统被增、删或改名,存活的世界会被保留,只替换你的函数体。否则编辑器退回 整体重载。这对你的代码的要求:
- **组件与 behavior 的名字就是身份。**改名属于结构性变更——预期整体重载,该 数据的状态归零。
- 任何一种重载后,存活实体的
start都会再跑一次——契合 Estella 的快速重启 语义,所以把start当作幂等的初始化来写。
其机制(探针上下文、模式指纹、App.hotSwapSystems)见
应用设置与生命周期。