1.2 Mod在游戏程序中的地位
既然Mod修改的是游戏原版意料之外的游戏行为,那么就一定会在代码的某些地方发生改变。作为示例,我们考虑玩家进入世界的过程。
游戏主循环:
……
第一步:创建一个代表玩家的游戏元素
第二步:设置玩家的游戏进度、位置和面朝方向
第三步:在世界上生成玩家
……
如果想要在世界上生成玩家前执行一些Mod自己的代码,我们会这样做。
游戏主循环:
……
第一步:创建一个代表玩家的游戏元素
第二步:设置玩家的游戏进度、位置和面朝方向
第三步:执行我们Mod自己的代码
第四步:在世界上生成玩家
……
在代码中上述行为被称作添加钩子(Hook)。这种方法很容易想到,但是就Minecraft而言,一个需要面对的问题是:如果很多个Mod希望同时在同一个地方下钩子呢?
游戏主循环:
……
第一步:创建一个代表玩家的游戏元素
第二步:设置玩家的游戏进度、位置和面朝方向
第三步:执行我们Mod自己的代码
第四步:执行ModAlpha的代码
第五步:执行ModBeta的代码
第六步:执行ModGamma的代码
……
第N步:在世界上生成玩家
……
这样的代码太过复杂,而突出的问题在于——它允许一个Mod直接修改Minecraft本体代码。如果每个Mod都私下直接修改Minecraft代码,那么每个Mod运行时所面对的代码,都将和编写Mod时期望的代码有一定差异。为了解决这个问题,世界上出现了各种各样的Mod框架,在通常情况下,Mod框架为ModLoader本身,相对应的,在本书中是FML。每个框架都会维护一个Mod的列表,在合适的时机加载Mod,在Minecraft本体代码的合适位置添加钩子,并在钩子触发时告知Mod应该做什么。由于添加钩子的工作由Mod框架统一完成,因此Mod框架的出现,在避免Mod直接修改Minecraft本体代码的同时,允许Mod为Minecraft添加各种丰富的功能。为具体解释Mod框架的一部分实现细节,在此引入事件系统的概念。
1.2.1 事件系统
就事件系统(Event System)而言,Mod框架会维护一个事件监听器(Event Listener)的列表,并在合适的时机发布事件(Post Event)。
首先定义事件监听器。如果listener是一个事件监听器,那么它被要求实现以下方法。
● 接收事件event:listener.accept(event)
然后基于事件和事件监听器模拟事件系统的全过程。
初始化:
……
第一步:定义事件监听器的列表listenerList
第二步:向列表添加事件监听器listener:listenerList.add(0,listener)
……
游戏主循环:
……
第一步:创建一个代表玩家的游戏元素
第二步:设置玩家的游戏进度、位置和面朝方向
第三步:引入事件event,代表玩家进入世界的事件
第四步:引入索引index,赋初值为0
第五步:index大于或等于listenerList.size()的返回值吗?如果大于或等于则跳到第十步,否则跳到第六步
第六步:引入listenerList的第index的元素listener:listener=listenerList.get(index)
第七步:向listener发布事件event:listener.accept(event)
第八步:将index赋值为旧值加1:index=index+1
第九步:跳到第五步
第十步:在世界上生成玩家
……
在上面的步骤中,只有初始化的第二步,也就是向listenerList添加listener这一步,是和特定的Mod相关联的,剩下的步骤全部可以由Mod框架完成。由于这一步是在初始化阶段完成的,因此Mod框架完全可以在加载Mod时完成这件事,事实上大部分Mod框架就是这么做的。我们注意到,对于任何一个钩子,事件监听和触发的机制都是类似的。将事件监听器的列表包装起来,在此引入事件总线的概念。
如果eventBus是一个事件总线(Event Bus),那么它被要求实现以下方法。
● 注册事件监听器listener:eventBus.register(listener)
● 发布事件event:eventBus.post(event)
我们把和玩家登录事件有关的全过程,使用事件总线简化如下。
初始化:
……
第一步:定义事件总线eventBus
第二步:向事件总线添加事件监听器listener:eventBus.register(listener)
……
游戏主循环:
……
第一步:创建一个代表玩家的游戏元素
第二步:设置玩家的游戏进度、位置和面朝方向
第三步:引入事件event,代表玩家进入世界的事件
第四步:向事件总线发布事件event:eventBus.post(event)
第五步:在世界上生成玩家
……
实际的事件总线实现和上述实现略有差别,但本质上是一样的。很多Mod框架都会在Minecraft代码中塞入大量的钩子,并提供大量的事件,其中,我们很容易想到的是游戏运行时,在游戏主循环中触发的事件,那么是否有必要在游戏初始化时触发事件呢?答案是肯定的。
1.2.2 注册系统
现在考虑Minecraft中的游戏元素,比如物品种类。在之前的部分我们得知,Minecraft使用一个字符串到物品类型的映射存储所有物品种类。如果想要添加新的物品种类,就要向该映射里添加新的键值对。
整理以下初始化过程。
初始化:
第一步:注册Minecraft的游戏元素
……
注册方块
注册状态效果
注册附魔
注册物品
- 定义字符串到物品类型对象的映射map
- 向map添加键为”minecraft:air”,值为空物品的键值对
- 向map添加键为”minecraft:stone”,值为石头的键值对
- ……
……
第二步:进行Minecraft引擎的初始化工作
现在考虑Mod框架存在的情况。可能会这样修改Minecraft的初始化流程。
初始化:
第一步:初始化Mod框架
……
定义事件总线eventBus
将所有Mod的事件监听器添加到eventBus
……
第二步:注册Minecraft的游戏元素
……
注册物品
- 定义字符串到物品类型对象的映射map
- 向map添加键为”minecraft:air”,值为空物品的键值对
- 向map添加键为”minecraft:stone”,值为石头的键值对
- ……
……
第三步:触发Mod游戏元素的注册事件
……
注册Mod物品
- 定义Mod物品注册事件event
- 向事件总线发布事件event:eventBus.post(event)
……
第四步:进行Minecraft引擎的初始化工作
Mod框架的做法是在Minecraft的游戏元素注册完成后,Minecraft引擎初始化开始前加入钩子,并触发不同游戏元素的注册事件,从而让Mod在事件监听器中注册物品等Mod提供的第三方游戏元素。
就Minecraft 1.12.2而言,FML为很多不同种类的游戏元素都提供了注册事件,包括但不限于方块类型、物品类型、状态效果、药水类型、附魔类型和村民类型等。不过,Minecraft体系庞杂,游戏元素种类繁多,FML不可能面面俱到,那么怎么办?
FML除注册事件外还提供了生命周期(Life Cycle)事件。生命周期事件有很多种,不过对Mod开发来说,常用的生命周期事件只有三种,分别被称为Pre-Initialization事件、Initialization事件和Post-Initialization事件。这三种生命周期事件都会在游戏初始化时触发,其中,Pre-Initialization事件安插在Minecraft引擎初始化前,而Initialization事件和Post-Initialization事件安插在Minecraft引擎初始化后。就Mod开发而言,何时使用什么生命周期事件往往有一些不成文的惯例,读者将会在本书的后续章节慢慢了解到一些这样的惯例。
现在再将添加了生命周期事件的初始化流程整理如下。
初始化:
第一步:初始化Mod框架
……
定义事件总线eventBus
将所有Mod的事件监听器添加到eventBus
定义生命周期事件总线lifeCycleEventBus
将所有Mod的生命周期事件监听器添加到lifeCycleEventBus
……
第二步:注册Minecraft的游戏元素
第三步:向生命周期事件总线发布Pre-Initialization事件
定义Pre-Initialization事件event
发布Pre-Initialization事件:lifeCycleEventBus.post(event)
第四步:触发Mod游戏元素的注册事件
第五步:进行Minecraft引擎的初始化工作
第六步:向生命周期事件总线发布Initialization事件
定义Initialization事件event
发布Initialization事件:lifeCycleEventBus.post(event)
第七步:向生命周期事件总线发布Post-Initialization事件
定义Post-Initialization事件event
发布Post-Initialization事件:lifeCycleEventBus.post(event)
需要注意的是,在FML中,生命周期事件所使用的事件总线和其他事件不同,因此在后续章节的代码中,读者将会注意到监听器的声明方式也有所不同。本书在这里提个醒,希望读者在阅读后续章节实现事件监听器时,务必注意生命周期事件和其他事件的差别。
读者读到这里,虽然一行代码都未曾编写,但也应能清楚地意识到一点——整个Mod开发都是围绕着事件监听器进行的。考虑到一个普通的Mod不能也不应直接修改Minecraft本体代码,包括但不限于FML等大量Mod框架都引入了事件系统和注册系统的概念,以方便Mod开发者基于Mod框架编写扩展Minecraft本体特性的代码。