首页 / 命令模式专栏 / 案例一 · loadwp

案例一 · loadwp 的 cmd

一条命令,装下整条场景加载主链。 SceneLoadingWpMapCmd 是这条链的根命令,但它自己不干活: 它在构链期用 OnAddInitCommand 把二十多条子命令原地展开,执行期只 Succeed() 让位。 看懂它,就理解了「初始化型命令(InitCommand)」这个设计在整个框架里扮演的角色。

InitCommand 构链期展开 场景加载主链根 条件分支 3 处 OnExecute 只 Succeed

1. 它在哪、长什么样

文件在 Src/Game/World/SceneLoading/Commands/SceneLoadingCmds/SceneLoadingWpMapCmd.ts。 它继承场景加载自己的命令基类 SceneLoadingCommandBase<SceneLoadingCmdContext>:

export class SceneLoadingWpMapCmd extends SceneLoadingCommandBase<SceneLoadingCmdContext> {
    protected override OnAddInitCommand(context: SceneLoadingCmdContext): void { ... }  // 构链期
    protected override OnExecute(context: SceneLoadingCmdContext | undefined): void { this.Succeed(); }
    protected override GetCommandDesc(): string { return '初始化场景加载关卡命令'; }
    protected readonly Kind = ESceneLoadingCommandKind.Load;
}

三个要点:方法名是 OnAddInitCommand(不是 OnExecute);执行体只有一句 Succeed(); Kind 标成 Load。这三点合起来说明——它是一条「初始化型命令」。

2. 关键:子命令在「构链期」就展开,不是执行时才展开

框架里命令分两类。OnExecute 是普通命令的执行入口,被 Executor 逐条 PopCommandAndExecute 调用; 而 OnAddInitCommand 属于 InitCommand,它在整条链被构建的那一刻(AddInitCommand(rootCmd) 时) 就被调用,把自己要跑的子命令原地铺开进主链,并让 depth + 1。

// 构链阶段被调用,不是执行阶段
exec.AddInitCommand(new SceneLoadingWpMapCmd());
//   → Executor 调 rootCmd.OnAddInitCommand(context)
//   → 里面每条 this.AddInitCommand(sub) 都展开成主链上的一环
为什么「执行到它时才展开」是错的 因为展开发生在 AddInitCommand 那一刻,早于 Execute()。 所以这条根命令跑起来时,队列里早已是那二十多条子命令;它自身的 OnExecute 只是队列里的 一个「占位步骤」,跑完就 Succeed()。这也是排查时容易误判的地方:日志里看到根命令「秒过」,别以为它没生效—— 它的活在建链时就干完了。

3. 它展开出的主链

OnAddInitCommand 里一共 24 处 AddInitCommand,其中 3 处带条件。 真正的展开顺序如下(带 条件 标记的三步不是每次都会进队):

1
SceneLoadingSetLoadModeCmd
客户端进入加载模式
2
SceneLoadingDispatchEventCmd.StartClientLoading()
事件:开始客户端加载
3
SceneLoadingOpenUiCmd
打开加载界面
4
SceneLoadingMainMapCmd (条件 isWpMapChanged)
开始加载 WPMap;进队时顺带把 shouldRaiseMapChangeEvents 置 true
5
SceneLoadingResetDataLayerCmd
重置 DataLayer
6
SceneLoadingDispatchEventCmd.StartEnterMap() (条件 shouldRaiseMapChangeEvents)
事件:开始进入地图
7
SceneLoadingDispatchEventCmd.StartEnterLevel()
事件:开始进入关卡
8
SceneLoadLevelCmd
加载 load 场景
9
SceneLoadingUpdateLayerTasksCmd
更新子关卡
10
SceneLoadingNewSceneCmd
开始加载 WPLayer
11
ScenePreloadEntityAssetsCmd
预加载实体资源
12
SceneWaitWpStreamingCmd
等待场景流程完成(等待型命令,条件不满足时 Succeed() 跳过)
13
SceneLoadingMoveCmd
移动到新场景
14
SceneLoadingBattleUiAssetsCmd
预加载战斗 UI 资源
15
ScenePreloadCommonUiAssetsCmd
预加载通用 UI 资源
16
SceneLoadingAfterCmd
加载完成后处理
17
SceneLoadingClearUnrefAssetsCmd
清理预加载资源
18
SceneLoadingDispatchEventCmd.EndEnterLevel()
事件:结束进入关卡
19
SceneLoadingDispatchEventCmd.EndEnterMap() (条件 shouldRaiseMapChangeEvents)
事件:结束进入地图
20
SceneLoadingWaitTeleportCmdFinishedCmd
等待传送指令完成
21
SceneLoadingCloseUiCmd
关闭加载界面
22
SceneLoadingCompleteCmd
关卡加载完成
23
SceneLoadingAnimDownCmd
落地表现
24
SceneLoadingDispatchEventCmd.EndClientLoading()
事件:结束客户端加载
顺序即语义 子命令是「先进先出」串行执行的,所以上面这份顺序就是运行时的先后顺序。 起始三步(进加载模式 → 广播开始 → 开 Loading 界面)在前,收尾三步(关界面 → 完成 → 落地表现 → 广播结束)在后, 中间是「加载场景 → 流送 → 移动 → 预加载 → 清理」。把这条链和 场景加载命令链对照着看,能把每个节点的职责对上号。

4. 三处条件分支由什么决定

展开前先算两个上下文布尔值,它们决定了是否多进三条命令:

变量来源影响
isWpMapChangedcontext.IsWpMapChanged()决定是否进 SceneLoadingMainMapCmd(第 4 步),并把 shouldRaiseMapChangeEvents 置 true
isFromLoginScenecontext.CurLevelId === 0配合 PublicUtil.GetIsSilentLogin() 判断:初次登录 + 静默登录时也抬 shouldRaiseMapChangeEvents
shouldRaiseMapChangeEvents上面两者推导决定是否进 StartEnterMap(第 6 步)与 EndEnterMap(第 19 步)

也就是说:换图(IsWpMapChanged)一定会走地图进退事件;不换图但属于「静默登录的初次登录」时, 也会把这些事件补上,好让依赖「进入地图」事件的系统拿到通知。其余两种事件(StartEnterLevel / EndEnterLevel)是无条件进队的。

5. 基类给了它什么:Kind 与 FailCritical

SceneLoadingCommandBase 很薄,只加了两样东西:

export const enum ESceneLoadingCommandKind { Load, Unload, Destroy }

export abstract class SceneLoadingCommandBase<T extends CommandContextBase> extends CommandBase<T> {
    protected abstract get Kind(): ESceneLoadingCommandKind;

    // 关键节点出严重错误时,把错误升级成「需立即删除关卡」的统一包装
    protected FailCritical(reason: string): void {
        const wrappedReason = `关卡加载关键节点出现严重错误,客户端状态转为 ExceptionalDestroyed\n上级错误原因:${reason}`;
        this.Fail(wrappedReason);
    }
}

6. 这个案例的四个看点

根命令不干活,是一种什么设计设计
把「链的组装」和「链的执行」分开。根命令是装配清单: 它在构链期把整条业务链摊平成一张有序表,执行器只需按表逐条推进。 好处是链的结构集中在一处、一眼可读;代价是链的规模在建链时就固定,运行期想动态插命令不能靠它。
OnAddInitCommand 和 OnExecute 分别是什么时候被调易错
OnAddInitCommand 在 AddInitCommand(rootCmd) 那一刻(构链期)被调,用来展开子命令; OnExecute 在 Execute() 之后由 Executor 逐条驱动时(执行期)被调。 本案例里两者都存在:前者搭链,后者(只 Succeed())是链里的一个占位步骤。
为什么「跳过」用 Succeed 而不是 Bail易错
因为 Bail() 会终止整条 executor 并回滚(见专栏 §6 Bail 语义)。 链里那些「进来判断条件、不满足就跳过本步」的命令(如 SceneWaitWpStreamingCmd、SceneLoadingMoveCmd) 必须让链条继续往后走,所以调的是 Succeed()——结果是 Succeeded,不是 Bailed。
这条链卡住时从哪查细节
因为 executor 默认不因超时结算(见专栏 §5),卡住通常是某条等待型命令自己没设超时。 查法:先看进度上报停在第几步 → 定位那条命令 → 看它内部等待是否带超时兜底。 细节见 场景加载命令链。