# 钩子 游戏里的某个动作发生时——勇士属性变了、战斗开始了、门被推开了——你希望「顺手做点什么」,但又不想去改引擎本身的代码。钩子(hook)就是为此准备的:它是本引擎提供的「事件订阅」机制。动作发生,主对象会依次通知挂在它上面的钩子;你把想做的事写进钩子方法,就能在正确的时机介入。 ## 钩子是什么 在数据端,勇士、地图、战斗流程等主对象在发生关键动作时,会依次调用自己身上每个钩子的对应方法。于是你只需要写一个钩子类、把它注册到关心的主对象上,就能在这些动作发生时收到通知。 一个钩子就是一个**类**:类里实现某个钩子族接口的方法,**「什么时候」由方法名决定,「做什么」由方法体决定**。例如想在勇士属性变化时做点什么,就实现 `IHeroAttributeHooks` 的 `onUpdateAttribute`: ```ts // [模板] 钩子类:implements 后面写钩子族接口 export class MyHeroAttributeHooks implements IHeroAttributeHooks { // [模板] 构造函数里保存 state,方法体里才能取到数据端主对象 constructor(readonly state: ICoreState) {} // [需改] 方法体换成你要做的事 onUpdateAttribute( name: K, value: IHeroAttr[K] ): void { // 当勇士的某个属性变化时执行 } } ``` 引擎里的内置钩子也是这样的类,例如 `src/data/hook/hero.ts` 的 `MainHeroAttributeHooks`;它们的实例在 `src/data/hook/initialize.ts` 里统一注册。日常改内置钩子时,不需要自己写上面这个类,改内置类的方法体即可(见下文「改内置钩子」)。 ## 常用钩子族一览 「钩子族」是同一类钩子方法的集合(一个 `interface`)。常用的几族如下,方法名以源码为准: | 钩子族 | 所属主对象 | 主要方法 | | ------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `IDataCommonHooks` | 数据端公共对象 | `onTip` | | `IHeroAttributeHooks` | 勇士属性 | `onUpdateAttribute` | | `IHeroLocationHooks` | 勇士位置 | `onSetPos`、`onSetFloor` | | `IHeroStateHooks` | 勇士状态 | `onBeforeChangeFloor`、`onAfterChangeFloor` | | `IEquipmentStateHooks` | 装备 | `onChangeModifier` | | `IHeroFollowersControllerHooks` | 勇士跟随者 | `onAddFollower`、`onRemoveFollower`、`onGatherFollowers` | | `IObjectMoverHooks` | 移动器 | `onMoveStart`、`onMoveEnd`、`onStepStart`、`onStepEnd`、`onSetPos`、`onSetFaceDir`、`onSetMoveDir` | | `ICombatFlowHooks` | 战斗流程 | `onBeforeCombat`、`onAfterCombat` | | `IGameEventExecutorHooks` | 事件执行器 | `onBeforeExecuteEvent`、`onAfterExecuteEvent` | | `IPathfindingSystemHooks` | 寻路系统 | `onBeforeTeleportTo`、`onAfterTeleportTo` | | `IMapLayerHooks` | 地图图层 | `onResize`、`onUpdateArea`、`onUpdateBlock`、`onOpenDoor`、`onCloseDoor`、`onCreateDynamic`、`onDeleteDynamic`、`onUpdateDynamicPosition` | | `IGameMapHooks` | 地图 | `onChangeBackground`、`onUpdateLayer`、`onResizeLayer` | | `IReplaySystemHooks` | 录像系统 | `onCreateSandbox`、`onRecordCommand` | | `IReplaySandboxHooks` | 录像沙盒 | `onStep`、`onSpeedSet`、`onStartReplay`、`onStopReplay`、`onPauseReplay`、`onResumeReplay` | ## 需求 → 钩子对照表 真要动手时,多半是先有一个需求、再去找它对应哪个钩子。下表把常见需求和对应的钩子方法列在一起,照着找即可: | 需求 | 对应钩子 | | ---------------------------- | --------------------------------------------------------------------------- | | 勇士属性变化时做点事 | `IHeroAttributeHooks.onUpdateAttribute` | | 勇士被设置坐标或切换楼层 | `IHeroLocationHooks.onSetPos` / `onSetFloor` | | 勇士移动开始、结束或每走一步 | `IObjectMoverHooks.onMoveStart` / `onMoveEnd` / `onStepStart` / `onStepEnd` | | 切换楼层前后做过渡处理 | `IHeroStateHooks.onBeforeChangeFloor` / `onAfterChangeFloor` | | 战斗前或战斗后介入 | `ICombatFlowHooks.onBeforeCombat` / `onAfterCombat` | | 事件执行前后介入 | `IGameEventExecutorHooks.onBeforeExecuteEvent` / `onAfterExecuteEvent` | | 地图图块被修改、区域被更新 | `IMapLayerHooks.onUpdateBlock` / `onUpdateArea` | | 开门或关门时做点什么 | `IMapLayerHooks.onOpenDoor` / `onCloseDoor` | | 动态图块被创建或删除 | `IMapLayerHooks.onCreateDynamic` / `onDeleteDynamic` | | 装备的修饰器发生变化 | `IEquipmentStateHooks.onChangeModifier` | | 瞬移寻路前后介入 | `IPathfindingSystemHooks.onBeforeTeleportTo` / `onAfterTeleportTo` | | 录像播放到某一步 | `IReplaySandboxHooks.onStep` | | 录像记录新指令 | `IReplaySystemHooks.onRecordCommand` | | 通知渲染端进行一次提示 | `IDataCommonHooks.onTip` | 注意区分「切换楼层」相关的两族钩子,它们挂在不同的主对象上:`onSetFloor` 属于 `IHeroLocationHooks`,挂在 `state.hero.location` 上;`onBeforeChangeFloor` / `onAfterChangeFloor` 属于 `IHeroStateHooks`,挂在 `state.hero` 上。 若某类需求暂未在表中找到,可以在对应主对象的 `IHookable` 接口处查看它支持的全部钩子方法(例如 `hero-attribute`、`enemy-status-hooks` 等篇会进一步说明相关钩子的用法)。 ## 改内置钩子(推荐) 日常需求优先改内置钩子文件(`src/data/hook/*`):内置钩子已经在 `initializeHooks`(`src/data/hook/initialize.ts`)里注册好,你只要改对应类的方法体,不需要自己写类,也不需要再调用 `addHook`。 内置钩子类与所在文件: | 钩子族 | 内置类 | 文件 | | ------------------------- | ---------------------------- | ------------------------- | | `ICombatFlowHooks` | `MainCombatFlowHooks` | `src/data/hook/combat.ts` | | `IGameEventExecutorHooks` | `MainGameEventExecutorHooks` | `src/data/hook/event.ts` | | `IHeroStateHooks` | `MainHeroStateHooks` | `src/data/hook/hero.ts` | | `IHeroAttributeHooks` | `MainHeroAttributeHooks` | `src/data/hook/hero.ts` | | `IHeroLocationHooks` | `MainHeroLocationHooks` | `src/data/hook/hero.ts` | | `IObjectMoverHooks` | `MainHeroMoverHooks` | `src/data/hook/hero.ts` | | `IMotaDataLoaderHooks` | `MainMotaDataLoaderHooks` | `src/data/hook/loader.ts` | | `IPathfindingSystemHooks` | `MainPathfindingSystemHooks` | `src/data/hook/path.ts` | **目标**:在勇士某个属性变化时,立刻执行你写好的处理。 **改哪里**:`src/data/hook/hero.ts` 的 `MainHeroAttributeHooks.onUpdateAttribute` 方法体。 **复制什么**:下面的类本来就在文件里,复制时保留类与构造函数,只把方法体里的占位行换成你的处理: ```ts export class MainHeroAttributeHooks implements IHeroAttributeHooks { constructor(readonly state: ICoreState) {} onUpdateAttribute( name: K, value: IHeroAttr[K] ): void { // [需改] 删掉下面两行占位,换成你的处理 void name; void value; // 示例:攻击变化时给出提示 if (name === 'atk') { this.state.emitTip(`攻击变为 ${value}`); } } } ``` > 复制说明:这段就是内置类的现有代码,不需要新增文件;若编辑器提示缺少某个名字,再在文件顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。 **改什么**:把 `void name;` / `void value;` 换成你的处理;示例只是提示,也可以改成加属性、记 flag、触发事件等。 **怎么验证**:手动改一次勇士攻击力(例如使用道具或调试命令),看提示有没有出现。 **注意**:一个钩子方法会对该主对象的**所有**同类动作触发——例如 `onUpdateAttribute` 对每个属性变化都触发,需要时要在方法体里判断 `name`,别误以为只对某一个属性生效。内置类已注册,改完方法体就会生效,不要重复 `addHook`。 ## 异步钩子 部分钩子返回 `Promise`,是**异步钩子**,会在对应的动画或流程真正结束后才兑现;等待它们完成可以保证时序正确。例如: - `IObjectMoverHooks` 的 `onMoveStart`、`onMoveEnd`、`onStepStart`、`onStepEnd`; - `ICombatFlowHooks` 的 `onBeforeCombat`、`onAfterCombat`; - `IHeroStateHooks` 的 `onBeforeChangeFloor`、`onAfterChangeFloor`; - `IMapLayerHooks` 的 `onOpenDoor`、`onCloseDoor`、`onDeleteDynamic`; - `IReplaySandboxHooks` 的 `onStep`。 其余钩子均为同步钩子,触发时立即执行。 ## 一个提醒:钩子可能被非本对象触发 钩子由所属主对象触发,但同一个主对象可能对应多个游戏对象(例如场上有多只怪物)。因此钩子被触发时,不一定是针对你关心的那一只;当场上存在多个同类主对象时,需要在回调里根据传入的参数判断是否为你的目标对象。一般情况下不会串。 钩子建立在数据端的主对象之上,两端分工见[数据端与渲染端](./data-vs-render.md);编写钩子所需的语法见[代码基础](./code-basics.md)。 ## 自己写钩子类 如果不想改内置文件,或需要给同一个主对象挂多个互不影响的钩子,可以自己写一个钩子类,再用 `addHook(new ...(state))` 注册: ```ts // [需改] 类名与处理逻辑 export class MyHeroAttributeHooks implements IHeroAttributeHooks { constructor(readonly state: ICoreState) {} onUpdateAttribute( name: K, value: IHeroAttr[K] ): void { // 属性变化时要做的事 } } // [模板] 注册:实例先存进变量,之后移除时要用 const hook = new MyHeroAttributeHooks(state); const controller = state.hero.attribute.addHook(hook); ``` > 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。 **改什么**:类名、`implements` 后面的钩子族接口、方法名(对照上两张表),以及 `addHook` 前面的目标主对象。 **注意**:一个主对象可以挂多个钩子,它们会依次被调用。需要移除时,保留注册时的实例或控制器:把**原来的实例**传给 `removeHook`,或调用控制器的 `unload` 方法。 ```ts // 方式一:传回原来的钩子实例 state.hero.attribute.removeHook(hook); // 方式二:用注册时返回的控制器卸载 controller.unload(); ``` **如果你不怎么懂代码,建议直接改内置钩子(`src/data/hook/*` 里已有的类),而不是自己写新的类。** 改内置只需要替换方法体,注册也已经完成;自己写类还要处理接口签名、构造函数与注册/移除时机。