一种基于循环的游戏控制的设计思路

一种基于循环的游戏控制的设计思路

因为最近心血来潮在写小游戏插件,可以理解为彩虹六号:异种的MC复刻。 于是就要设计一套游戏控制流来引导玩家完成任务等操作。 然后我就写了一套挺烂的控制流( 主要部分 Game - 游戏实例 每个 Game Object 代表了一个游

因为最近心血来潮在写小游戏插件,可以理解为彩虹六号:异种的MC复刻。

于是就要设计一套游戏控制流来引导玩家完成任务等操作。

然后我就写了一套挺烂的控制流(

主要部分

Game - 游戏实例

每个 Game Object 代表了一个游戏实例,下面所讲的一切都发生在 Game Object 中。

虽然一个 Spigot 实例只跑一个 Game,但是这样设计如果以后要改一个 Spigot 实例跑多个 Game 的话就不用重构插件了。

Module - 实例模块

一个 Game 中可以存在多个 Module。

每个 Module 在 Game 开始之后遵从 Minecraft 的游戏世界循环(也称 tick)而tick。并自动注册监听器。

Stage - 游戏阶段

在上面我们说了 Module 是游戏开始后就一直持续运行,并且可以存在多个同时运行的。

而 Stage 则恰恰相反,同时只有 1 个 Stage 可以执行,但是一个 Game 实例可以注册多个 Stage —— 每个 Stage 会按照注册的顺序执行。

当一个 Stage 结束后,则立刻被丢弃,不再使用。

Quest - 游戏任务

每个 Stage 中会包含多个 Quest,每个 Quest 有着自己的监听器,并且随着 Stage 的 tick 而被一同 tick。

Stage 在 tick 其包含的所有游戏任务的时候会检查他们的返回值,如果返回了一个 true,则代表该任务已结束(不管是完成还是失败,反正就是结束了)。

当所有 Quest 都返回 true 的时候,Stage 也会返回一个 true,此时 Game 的 tickStages() 方法会调用 nextStage()方法切换到下一个 Stage 继续执行。

不过和 Stage 不一样的是,即使 Quest 返回了 true,它在 Stage 的生命周期中也会持续 tick,提供处理任务完成之后又触发了失败条件而失败的功能。

流控制

执行流

每个 Game 实例中都有个 Deque(双端队列),用以控制 Stage 的执行。

当 Game 的 start() 方法被调用时,游戏就进入 running 状态,此时 Game 实例会被注册到 Bukkit 调度器,周期化执行 onTick 方法。

同时,在 Deque 头部的 Stage 元素会被 peek 出来并跟随MC主循环开始 tick。

每个 Stage 都会先执行 startTick() 方法,然后在生命周期内持续执行自己和其包含的Quest的 tick() 方法。

当 Stage 返回 true 后,生命周期结束。使用 Deque#poll 取出这个 Stage,调用一次 endTick() 方法收尾,随后丢弃。

下一个新的 Stage 来到双端队列的头部,调用新的 Stage 的 startTick() 方法开始其生命周期,然后其余工作交给主循环。

    /**
     * 切换到下一个 Stage
     */
    public void nextStage() {
        Stage stage = stageSequences.poll();
        if (stage == null)
            return;
        stage.endTick();
        stage = stageSequences.peek();
        if (stage == null)
            return;
        if (!stage.isPaused())
            stage.startTick();
        else
            stage.resume();
     }
    /**
     * Tick stages
     *
     * @param firstTick 是否为首次tick
     * @param lastTick  是否为最后一次tick (最后一次tick可能失败,因为游戏一般退出是因为 stage 执行完毕,这种情况的lastTick由nextStage执行)
     */
    private void tickStages(boolean firstTick, boolean lastTick) {
        Stage stage = getTickingStage();
        if (stage != null) {
            if (firstTick) {
                stage.startTick();
                return;
            }
            if (lastTick) {
                stage.endTick();
                return;
            }
            if (stage.tick())
                nextStage();
        }
    }

总结一下:

开始:Game#start() -> Stage#startTick() -> Quest#startTick()

执行:Bukkit Scheduler -> Game#tick() -> Stage#tick() -> Quest#tick()

退出: Bukkit Scheduler -> Game#tick() -> Stage#tick() -> Quest#tick() -> return true => Stage#tick() - return true => Game#tick() -> Game#nextStage() -> Deque#poll() -> Stage#endTick() -> Quest#endTick() => Stage#endTick() => nextStage -> Deque#peek()#startTick() -> NewStage#startTick() -> NewQuest#startTick() ......

流插入

还是以彩虹六号:异种举例,我们知道当干员倒地后,REACT静滞泡沫就会激活,随后你就不能动了。

游戏会立刻插入一个最高优先级的任务 “不放弃任何人”,此时原有的任务会被打断,你的队友需要先把你安排好才能继续执行任务。

所以,我也参照为 Stage 增加了几个新方法 Stage#pause, Stage#resumeStage#isPaused()

当我们需要打断这个任务的时候,先调用 Stage#pause,这会注销掉此 Stage 和其下所有 Quest 的监听器。

然后我们直接将新的 Stage 在 Deque 的头部位置插入,并调用一次 Stage#startTick,随后交给主循环开始新的 Stage 的 ticking。

不过,因为插入机制的引入,我们还需要改造一下之前提到的 Game#nextStage 方法,使其检查判断 Stage#isPaused 的结果,如果为 true,则切换到这个 Stage 的时候不执行 Stage#startTick 而是转而执行 Stage#resume 将监听器注册回来。

    /**
     * 插入一个 Stage 并暂停当前 Stage,先执行新的 Stage
     *
     * @param stage Stage
     */
    public void insertSwitchStage(@NotNull Stage stage) {
        if (getTickingStage() != null)
            getTickingStage().pause();
        stageSequences.offerFirst(stage);
        stage.startTick();
    }

当然,也可以通过往 Deque 追加新 Stage 延长游戏流程。

流终止

当所有 Stage 被执行完毕后,我们就可以视为 “这场游戏的所有阶段都已结束”。

此时 Deque 内为空,Game#nextStage() 会取不到任何新的 Stage,此时调用 Game#stop() 方法进行游戏结算即可

最后的效果

大概你可以这么理解:

Game:
  Modules:
     - 禁用友伤 # 不同的Modules,在游戏期间持续运行,可同时运行多个
     - 锁定黑夜 
     - 队友高亮
  Stage:
     阶段-猎杀古菌: # 不同的阶段
         - 击杀超多低语者 # 每个阶段需要完成的任务
         - 击杀精英刺袭者
     # 上一阶段所有任务完成 Stage 生命周期结束,转入下个阶段
     阶段-追踪孵化囊:
         - 定位孵化囊 
     # 如果队友在这里趴窝了,阶段暂停,插入新的阶段,完成后在从此处继续执行
         - 安装追踪器
     阶段-前往下个任务区域:
         - 到达指定地点
     阶段-拯救大兵Jager:
         - 定位 Jager
         - 和一堆古菌战斗
         - 送 Jager 回家(大雾
   # 所有 Stage 结束,游戏也随即结束

结尾

最近在看的东西挺多的,等这套控制流完全施工完毕后大概就去做生物和方块刷新了。

到时候会涉及处理蔓生菌蔓延啊,古菌生成等等的东西,估计还可以在水一篇文章(笑。

用 Stage 的主意来自 @hikarilan,然而经过贺兰的讲解,我除了听懂了 Stage 只能用一次就扔掉之外什么也没听懂。

除特殊说明以外,本站原创内容采用 知识共享 署名-非商业性使用 4.0 (CC BY-NC 4.0) 许可。转载时请注明来源,以及原文链接。
Comment