thumbnail
论一种模块化的 Minecraft Minigame 游戏架构模型
本文最后更新于 717 天前,其中的信息可能已经有所发展或是发生改变。

论一种模块化的 Minecraft Minigame 游戏架构模型

TL;DR: 本文章试图说明一种可用于开发 Minecraft Minigame 或其他相似内容的,模块化的架构模型,作者基于 “分而治之” 的理念设计了它们。基本上,这些架构由 Flow, PhaseModule 共同组成。文章也试图说明一种基于上述架构模型的,由事件驱动的玩家加入游戏管理方法。最后,该文章给出了一个运行在 Bukkit 平台上的,使用上述架构开发的框架。

前言

近一年来,我都在负责一款 Minecraft Minigame 的开发,籍此机会,我总结了一套灵活的,可拓展的,模块化的架构,可以高效的处理游戏主循环的运行。简而言之,这些架构由一些被称之为 Flow, PhaseModule 的东西共同组成。要想了解它们,我们需要先从游戏主循环开始...

游戏主循环

大多数游戏都具有游戏主循环,Minecraft 也不例外。游戏主循环在每一个单位时间下进行一次,处理用户输入,更新游戏内容的一些状态信息。在一个 Minigame 中,自然也应该存在游戏主循环。但是实际上,一个 Minigame 可能拥有多个线性阶段(例如等待阶段,游戏阶段等),这些阶段可能会在不同的情况下被触发,并层层递进,直到游戏结束。

img

为了更方便的在游戏主循环上进行开发,我们引入 FlowPhase 的概念。

Phase —— 各自独立又彼此相连

简而言之,Phase 用于代表一个“单元游戏阶段”,Flow 用于代表一个真正的游戏阶段,多个 Phase 被一个 Flow 组织在一起,而多个 Flow 共同组成了一个游戏主循环。

让我们先看看 Phase 是如何组成的:它由 onStart, onTick, onEnd 三个函数组成,分别代表"阶段开始"、"阶段运行"、"阶段结束",其中,onTick 函数还拥有一个布尔值返回值,代表阶段是否应当结束。如果使用 Java 代码来表示,那么 Phase 应该大致是这样的:

public class Phase {

    void onStart(){}

    boolean onTick{}

    void onEnd(){}

}

这三个函数会以这样的方式每刻调用:首先检查有没有执行过 onStart 函数,如果没有执行过,则执行该函数,并在执行完成后返回;如果执行过,那么执行 onTick 函数;当执行 onTick 函数时,检查 onTick 函数的返回值是否为 true,如果不是,那么下一刻将会继续执行 onTick 函数,并重复这一步骤;如果是,那么下一刻将执行 onEnd 函数;如果 onEnd 函数均已执行,则下一刻不再进行任何行为。用流程图表示大概是这样(简化起见,每一个箭头都代表进入下一 tick 执行):

img

用 Java 代码表示如下:

private boolean isStartFinish = false;
private boolean isTickFinish = false;
private boolean isEndFinish = false;

public boolean tick() {
    if (!isStartFinish) {
        onStart();
        isStartFinish = true;
        return false;
    }

    if (!isTickFinish) {
        isTickFinish = onTick();
        return false;
    }

    if (!isEndFinish) {
        onEnd();
        isEndFinish = true;
        return false;
    }
    return true;
}

你可能会注意到上述实现的 tick 方法包含一个额外的布尔返回值,这个返回值的作用接下来会说到。

多个 Phase 组合起来,就是一个 Flow,即一个实际上的游戏阶段。在同一个 Flow 中的多个 Phase 是并发运行的,它们之间的状态并不会互相影响。

img

当我们将多个 Flow 串在一起,便是一个完整的游戏流程了。

同一时间只能有一个 Flow 在运行,那么问题来了,何时从一个 Flow 进入下一个 Flow 呢?这就要由 Flow 中的每一个 Phase 共同决定了 —— 只有一个 Flow 中的所有 Phase 均被执行完成(也即其 tick 函数返回 true)时,才视为这个 Flow 完成,可以进入下一个 Flow

img

这样做的好处是,每一个 Phase 在设计过程中不必考虑其他 Phase 的生命周期,当自己的工作完成后,即会停止运作,不会干扰其他 Phase 的运行;而所有 Phase 组合在一起便可以共同决定一个 Flow 是否结束。

线性流程?那么循环呢?(1/4/2023 更新)

经过与一些热心群友的讨论,该模型其实存在一个问题:他没有办法高效的表示一个循环流程,考虑一种采用如下流程的游戏:

img

如果您玩过《太空狼人杀 Among Us》,应该熟悉这种流程:游戏开始后,会在做任务/杀人阶段和讨论阶段之间不断循环,直到一方达成游戏目的,游戏结束。由于我们先前的模型是不可变且线性的,因此这种游戏流程无法被成功实现。

为了解决这个问题,我们引入了一个新的事件(如果您不了解事件总线,可以先参看下方 “事件驱动的玩家加入游戏设计” 一节然后再回来看这里)FlowPointerTransferEvent,该事件包含一个默认值为-1intpointer。此事件会在进入正常下一个流程前被 post,并等待订阅者处理,任一事件订阅者可以通过修改 pointer 的值来决定接下来要进入的流程;如果这个值依然保持默认值,则游戏将会正常进入下一个 Flow,否则,进入指定 pointer 所代表的 Flow 中。

一个示例的实现大致如下:

public class FlowPointerTransferEvent {

    @Getter
    private int pointer = -1;

    public void setPointer(int pointer) {
        if (pointer < 0) throw new IllegalArgumentException("Priority should start from 0.");
        this.pointer = pointer;
    }
}
// In the flow manager
private boolean next() {
        val nextPointer = game.postEvent(new FlowPointerTransferEvent(game)).getPointer();
        if (nextPointer >= 0) pointer = nextPointer - 1;

        // If the maximum number of phases is reached, stop going to the next flow.
        if (pointer + 1 > flows.keySet().stream().max(Comparator.comparingInt(Integer::intValue)).orElse(0))
            return false;

        // If next priority is not exist, continue to next flow.
        if (!flows.containsKey(++pointer))
            return next();

        // enter next flow.
        return true;
    }

通过这种方式,我们可以一定程度上的解决上述循环游戏流程的问题。

如此一来,我们便成功设计了一套高效的游戏主循环模型,但是,是不是还差了点什么?

Module —— 全局状态管理

上述模型中,一个 Flow 和另一个 Flow 之间并无联系,这就会引发一些问题,想象一下,如果我们需要一个贯穿全局的监听器,那么就必须在每一个 Flow 中做一样的事!在复杂一些,如果这个监听器又需要存储一些玩家状态,那么我们还需要进行跨 Flow 的状态转移,这就把简单的事情搞复杂了。

要想解决这个问题也十分简单,引入一个可以全局存在的 "Phase" 即可,这就是 Module

Module 拥有 onInstall, onTickonUninstall 三种生命周期,可以跨 Flow 存在,可以在任意时刻被加入到游戏中,亦或者从游戏中卸载。

public class Module {

    void onInstall(){}

    void onTick(){}

    void onUninstall(){}

}

当我们试图安装一个模块时,该模块的 onInstall 方法便会被调用,然后,该模块的 onTick 方法便会在每一次游戏主循环调用一次,最后,当希望卸载该模块时,该模块的 onUninstall 方法会被调用,此后对 onTick 方法的调用也会停止。

通过 Module,我们可以实现对 Flow 的辅助,这也可以同时弥补 Flow 只能以线性方式运行的缺陷 —— Module 可以在任何时刻随时被安装和卸载,十分灵活。

事件驱动的玩家加入游戏设计

在 Minigame 中,当一个玩家尝试加入一个游戏,可能会产生非常多的情况 —— 游戏未开始,可以加入;游戏未开始,但是等待大厅人数已满,不能加入;游戏已开始,不能加入;游戏已开始,可以作为观察者玩家加入。如此多的情况带来的结果就是难以管理的各种状态。但是,通过事件驱动+Module的方式,我们可以优雅的解决这些问题。

首先,让我们引入一个事件总线(EventBus),其包含一个 post 方法,接受一个 Object 形参,可以将 Object 对象实例发布给所有订阅该对象(事件)的订阅者;包含一个 Object 返回值,代表经过所有事件订阅者处理(修改)过后得到的事件对象;包含一个 register 方法,可以用来注册事件订阅者;包含一个 unregister 方法,可以用来反注册事件订阅者。该事件总线被一个游戏实例所拥有。

然后,我们引入三个事件:PlayerAttemptToJoinGameEvent, PlayerPreJoinGameEventPlayerPostJoinGameEvent ,分别代表"玩家尝试加入游戏事件","玩家预加入游戏事件"和"玩家已加入游戏事件"。

这些事件会按这样的方式工作:

  1. 当一个玩家试图加入一场游戏时,该玩家会向希望加入游戏的事件总线 post 一个 PlayerAttemptToJoinGameEvent,代表玩家尝试加入该游戏。
  2. 该事件存在一个 isCancelled 属性,默认值为 true,代表是否拒绝该玩家加入游戏。默认情况下,当没有订阅者处理该事件时,玩家即被拒绝进入游戏 —— 此时玩家也可通过查询 isCancelled 属性是否为 true得知自己是否被拒绝加入游戏。
  3. 如果该游戏实例有条件允许玩家加入该游戏(例如等待大厅开放,或是允许观战),则可以通过安装一个订阅该事件的 Module,修改 PlayerAttemptToJoinGameEvent 事件的 isCancelled 属性,告知玩家可以加入游戏。
  4. PlayerAttemptToJoinGameEvent 事件的 isCancelled 属性为 false 时,立即 post 一个 PlayerPreJoinGameEvent,此时 PlayerPreJoinGameEvent 事件的订阅者可以在此事件中对玩家信息进行初始化,并将玩家加入到游戏中。
  5. 最后,post 一个 PlayerPostJoinGameEvent,代表玩家顺利的加入了游戏。

通过这种方式,我们将玩家加入游戏这一件事分解成了三件事情,并可以允许来自三个不同位置的处理方(Module)按顺序处理它们。而三个处理方在不同的游戏阶段也可以被其他处理方替换,达到不同的效果。

举个例子,当游戏在等待阶段时,等待大厅 Module 可以处理 PlayerAttemptToJoinGameEventPlayerPreJoinGameEvent ,允许玩家加入游戏并将其传送到等待大厅;一个公告 Module 通过订阅PlayerPostJoinGameEvent 事件为在游戏中的玩家提示有玩家加入了游戏。一旦游戏开始,等待大厅 Module 被卸载,观察者玩家加入 Module 被加载,那么后者便可以正确的接收希望观战的玩家并将他们传送到正确的位置。

对于玩家退出游戏的情况,我们也可以如法炮制,引入 PlayerPreQuitGameEventPlayerPostQuitameEvent(没有 PlayerAttemptToQuitGameEvent 的原因是玩家退出游戏往往是一个强制性的情况,并没有"试图退出"的说法,因此也不需要这样的事件),此处便不再赘述。

最后:GameSenseLib

将以上种种组合起来,便是我最近正在积极开发的 GameSenseLib 插件了,这是一个基于 Apache 2.0 协议开源的项目,你可以在其中看到我对 Phase, Flow 以及 Module 的封装,以及将他们统合在一起的 AbstractGame 基类。除此之外,这个项目还提供了一些内置的 Module, Phase 实现,并通过 GameTemplate 允许你用一种简单的方式快速创建一个 Minigame:

public class ExampleGame {
    public static AbstractGame simpleGame() {
        GameTemplate.of(plugin)
                .withWaitingRoom(
                        1, // min player
                        2, // max player
                        Duration.ofSeconds(10), // waiting duration
                        new Location(Bukkit.getWorld("game_world"), 0, 64, 0) // waiting room spawn location
                )
                .withBroadcastMessagePlayerJoinAndQuit(
                        player -> "Player Join the game: " + player.getName(),
                        player -> "Player Quit the game: " + player.getName()
                )
                .setRemovePlayerOnQuit(true)
                .setTeleportPlayerOnQuit(true)
                .setQuitCommand("quit")
                .setJoinCommand("join")
                .addPhase(
                        0, // priority
                        () -> Phase.builder()
                                .onStart((it) -> Bukkit.broadcastMessage("Game Start!"))
                                .build()
                )
                .world(Bukkit.getWorld("game_world"))
                .build();
    }
}

当然,这都只是 GameSenseLib 插件的冰山一角。最后,也期望读者可以对这套模型提出建议,并参与贡献。

本文所有的流程图均使用 draw.io 网站生成。

扫码关注 HikariLan's Blog 微信公众号,及时获取最新博文!


微信公众号图片

评论

  1. ggboy
    Windows Edge 108.0.1462.54
    2 年前
    2023-1-03 1:14:02

    啊嘞

    • 博主
      ggboy
      Windows Edge 108.0.1462.54
      2 年前
      2023-1-03 1:17:11

      图片用 base64 直接嵌入结果好像内容太长了导致内容没显示出来,在换了(

  2. Chuanwise
    Android Chrome 98.0.4758.102
    2 年前
    2023-1-03 14:29:41

    设计的好优雅!

    • Hanamizu
      Chuanwise
      Macintosh Safari 14.0
      2 年前
      2023-1-08 23:06:29

      大师球

  3. 木芒果
    Windows Chrome 94.0.4606.71
    2 年前
    2023-1-17 16:18:23

    博客好看

  4. MaxnessAWA
    Windows Chrome 92.0.4515.131
    2 年前
    2023-1-19 22:03:55

    nb

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇