上一章我们讲了Forge注册表的使用。本章我们讲一个Forge中非常重要的系统:事件系统。
1. 事件和事件总线
事件(Event)在Minecraft运行到特定位置时由事件总线(Event Bus)触发。目前可以暂时将事件总线理解为集中处理事件的处理器。
Forge在原版游戏代码的经常需要修改的位置插入了一些事件触发点(Hook)。当游戏运行到这些触发点时,事件总线触发相应事件,并且通过事件处理的结果(见第3节)调整相应游戏逻辑。
事件总线一共有两个:Forge和Mod。Forge总线用于发出绝大多数游戏运行中的事件;Mod总线用于在模组加载阶段执行一些初始化操作。要判断一个事件是Forge还是Mod总线触发,只需要看这个事件是否实现IModBusEvent
即可。这个接口仅用于标记,实现该接口的是由Mod总线发出,否则为Forge总线。
2. 监听事件
事件的监听(Subscribe)即定义一些代码,并通知事件总线在特定事件发出时执行这些代码。将一个静态方法标记为事件监听器(即需要在某个事件时执行)需要通过以下方法:
1)事件的方法签名必须是:
public static void someSubscriber(SomeEvent event)
即静态方法,输入参数为需要监听的事件,返回void
。
2)用注解标记该方法为事件监听器。一种方法是在相应方法加上@SubscribeEvent
注解。另一种方法是在方法所在类加上@Mod.EventBusSubscriber
注解,这个注解表示将类中所有具有上述签名的方法视为事件监听器。实际操作时建议两个注解同时加上。即:
@Mod.EventBusSubscriber(modid = ForgeTutorial.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE)
public class EventSubscribers {
@SubscribeEvent
public static void someSubscriber(SomeEvent event) {
someAction();
}
}
此时,当SomeEvent
每次被触发时,someSubscriber
就会被执行一次,输入参数为正在被触发的事件实例。
@Mod.EventBusSubscriber
的两个参数分别为模组ID和想要监听的事件总线,建议都填上,不要省略,以防出现难以排查的故障。
当游戏加载时,Forge会扫描并收集所有事件监听器,并在当事件触发时执行对应事件监听器的代码。
注意:尽可能不要监听有子类的事件类。如果监听器监听的是父事件类,则任一子事件类被触发时都会执行该监听器代码,这很可能会造成监听器被错误地多次调用,产生难以发现的bug。有些事件类的子类定义在父类内部,在监听事件前应当仔细阅读事件的类定义。
注意:个别事件会在某段代码执行前后各触发一次,其中一般有一个phase
变量确认目前在哪个阶段,例如LevelTickEvent
。监听事件(尤其是和世界tick相关的事件)时应当先确认是否有这个问题,否则有可能因为监听器错误地被执行两次而产生难以发现的bug。
3. 事件的结果和取消
有时,事件会有一些执行结果,这些结果会影响事件触发点之后的游戏逻辑。事件的执行结果(若有)可以在事件监听器中通过对参数传入的事件实例进行操作来修改。事件的执行结果主要有2种:取消(Cancel)和结果(Result)。
*事件的执行结果并非必须具有某种特定意义。*每个事件的执行结果所造成的影响都可能不同,要确定事件的取消或结果会带来什么影响,需要找到源码中的事件触发点(通常可以在事件处进行Alt+F7查找使用位置以跳转)并分析其代码。事件的类声明上方通常也有Javadoc注释,但有时表述并不是很明确。
3.1 事件的取消
带有@Cancelable
的事件可以取消。在事件监听器中执行setCanceled(true)
可以设置使事件取消。事件取消时,通常会导致触发点之后的一些代码被跳过。
例如:事件LivingHurtEvent
在有生实体(LivingEntity
,包括玩家、生物、盔甲架)受到伤害时触发,当被取消时,整个受伤害处理过程被跳过,实体不会受到该伤害。
注意:对不带有@Cancelable
的事件执行setCanceled
会导致抛出异常。
3.2 事件的结果
带有@HasResult
的事件有结果。结果可以通过setResult
和getResult
设置和获取。结果共有三种:ALLOW
, DEFAULT
, DENY
. 结果并不必然地具有某种特定含义,其含义在每个事件和触发点处可能不同。通常在涉及到某种判断时,事件的结果ALLOW
表示判断恒定为“是”,DENY
表示恒定为“否”,DEFAULT
表示为使用原逻辑判断。(但并非必然如此)
需要说明的是,事件的执行结果并不局限于取消和结果两种,也可以自定义。例如一些和右键互动相关的事件会另外定义一个InteractionResult
作为结果。
4. 自定义事件及触发
要自定义事件,首先创建一个新的事件类:
@Cancelable
public class MyEvent extends Event {
...
}
如果是Mod加载事件,加上implements IModBusEvent
。
随后触发该事件:
public void foo() {
doSomething();
MyEvent event = new MyEvent();
MinecraftForge.EVENT_BUS.post(new MyEvent()); // 如果事件可以取消,post方法在事件被取消时返回true,未被取消时返回false
if (event.isCanceled()) return;
doSomethingElse();
}
MinecraftForge.EVENT_BUS.post
方法中将该事件的所有监听器各执行一次,在这个过程中事件可能会被修改(例如取消、设置结果等)。在调用post
后再查询上面的事件实例即可获得其是否被取消、结果等信息。
在上述代码中,如果事件被取消,则直接中断foo()
的执行,其后的doSomethingElse
被跳过。
如果是Mod总线事件,使用如下代码触发事件:
ModLoader.get().postEvent(event); // 这个方法没有返回值
自定义事件的监听和第2节所述完全相同。