事件
事件系统是 NeoForge 的主要功能之一。游戏中发生各种行为时,都会触发相应事件,例如玩家右键点击、玩家或其他实体跳跃、方块渲染,以及游戏加载等。Mod 开发者可以为这些事件订阅处理方法,并在方法中实现所需行为。
事件会在各自对应的事件总线上触发。最重要的是 NeoForge.EVENT_BUS,也称游戏事件总线。此外,在启动过程中,每个已加载的 Mod 都会创建自己的 Mod 事件总线,并通过构造函数传给 Mod。许多 Mod 总线事件会并行触发,大幅提高启动速度;主事件总线上的事件则始终在同一线程上运行。详情见下文。
注册事件处理方法
注册事件处理方法有多种方式。它们的共同要求是:每个处理方法只接受一个事件参数,且不返回结果,即返回类型为 void。
IEventBus#addListener
最简单的方式是直接注册方法引用:
@Mod("yourmodid")
public class YourMod {
public YourMod(IEventBus modBus) {
NeoForge.EVENT_BUS.addListener(YourMod::onLivingJump);
}
// Heals an entity by half a heart every time they jump.
private static void onLivingJump(LivingJumpEvent event) {
Entity entity = event.getEntity();
// Only heal on the server side
if (!entity.level().isClientSide()) {
entity.heal(1);
}
}
}
@SubscribeEvent
也可以通过注解注册:创建事件处理方法并为其添加 @SubscribeEvent,然后向事件总线传入所属类的实例。这样会注册这个实例中所有带 @SubscribeEvent 注解的处理方法:
public class EventHandler {
@SubscribeEvent
public void onLivingJump(LivingJumpEvent event) {
Entity entity = event.getEntity();
if (!entity.level().isClientSide()) {
entity.heal(1);
}
}
}
@Mod("yourmodid")
public class YourMod {
public YourMod(IEventBus modBus) {
NeoForge.EVENT_BUS.register(new EventHandler());
}
}
也可以使用静态方法。将所有事件处理方法声明为静态,并传入类本身,而非类实例:
public class EventHandler {
@SubscribeEvent
public static void onLivingJump(LivingJumpEvent event) {
Entity entity = event.getEntity();
if (!entity.level().isClientSide()) {
entity.heal(1);
}
}
}
@Mod("yourmodid")
public class YourMod {
public YourMod(IEventBus modBus) {
NeoForge.EVENT_BUS.register(EventHandler.class);
}
}
@EventBusSubscriber
最新版本
还可以进一步为事件处理类添加 @EventBusSubscriber。NeoForge 会自动发现这个注解,从而无需在 Mod 构造函数中编写事件注册代码。其效果相当于在构造函数末尾调用 NeoForge.EVENT_BUS.register(EventHandler.class) 和 modBus.register(EventHandler.class)。因此,所有处理方法也必须是静态方法。
[21.1.0,21.1.180]
还可以进一步为事件处理类添加 @EventBusSubscriber。NeoForge 会自动发现这个注解,从而无需在 Mod 构造函数中编写事件注册代码。其效果相当于在构造函数末尾调用 NeoForge.EVENT_BUS.register(EventHandler.class)。因此,所有处理方法也必须是静态方法。
虽然不是必填项,但强烈建议在注解中指定 modid,以便调试,尤其是在排查 Mod 冲突时。
@EventBusSubscriber(modid = "yourmodid")
public class EventHandler {
@SubscribeEvent
public static void onLivingJump(LivingJumpEvent event) {
Entity entity = event.getEntity();
if (!entity.level().isClientSide()) {
entity.heal(1);
}
}
}
事件选项
字段与方法
字段和方法通常是事件中最直观的部分。大部分事件会提供上下文供处理方法使用,例如触发事件的实体,或事件发生的世界。
继承层次
为了利用继承,一些事件不会直接继承 Event,而是继承它的子类。例如,BlockEvent 提供方块相关事件的上下文,EntityEvent 提供实体上下文;后者又有 LivingEvent 和 PlayerEvent 子类,分别提供 LivingEntity 与 Player 相关的上下文。这些用于提供上下文的父事件是 abstract 的,不能直接监听。
重要警告 监听
abstract事件会导致游戏崩溃。应当监听具体的子事件。
可取消的事件
部分事件实现 ICancellableEvent 接口,可以通过 #setCanceled(boolean canceled) 取消,并通过 #isCanceled() 检查取消状态。事件被取消后,其他处理方法通常不会再接收它,并会执行与“取消”对应的行为。例如,取消 LivingJumpEvent 会阻止跳跃。
处理方法也可以明确选择接收已取消的事件。根据注册方式,将 IEventBus#addListener 或 @SubscribeEvent 中的布尔参数 receiveCanceled 设为 true 即可。
三态值与结果
有些事件有三种可能的结果状态,使用 TriState,或直接定义在事件类中的 Result 枚举表示。这些状态通常分别对应取消事件所处理的行为(TriState#FALSE)、强制执行该行为(TriState#TRUE),以及遵循原版默认行为(TriState#DEFAULT)。
这类事件会提供某个 set* 方法,用来设置期望结果。
// In some event handler class
@SubscribeEvent // on the game event bus
public static void renderNameTag(RenderNameTagEvent event) {
// Uses TriState to set the return state
event.setCanRender(TriState.FALSE);
}
@SubscribeEvent // on the game event bus
public static void mobDespawn(MobDespawnEvent event) {
// Uses a Result enum to set the return state
event.setResult(MobDespawnEvent.Result.DENY);
}
优先级
可以为事件处理方法指定优先级。EventPriority 枚举有五个值:HIGHEST、HIGH、NORMAL(默认)、LOW 和 LOWEST。处理方法按优先级从高到低执行。如果优先级相同,主总线上按注册顺序执行,这通常与 Mod 加载顺序大致相关;Mod 总线上则严格按 Mod 加载顺序执行,见下文。
根据注册方式,在 IEventBus#addListener 或 @SubscribeEvent 中设置 priority 参数即可。注意,并行触发的事件会忽略优先级。
仅在特定端触发的事件
有些事件只会在某一端触发,常见例子是只在客户端触发的各种渲染事件。这类事件通常需要访问 Minecraft 的其他客户端专用代码,因此注册时也必须区分运行环境。
使用 IEventBus#addListener() 时,应通过 FMLEnvironment.dist 或 Mod 构造函数中的 Dist 参数检查当前物理端,并在单独的客户端专用类中添加监听器,具体见客户端与服务端一文。
使用 @EventBusSubscriber 时,可以通过注解的 value 参数指定物理端,例如 @EventBusSubscriber(value = Dist.CLIENT, modid = "yourmodid")。
事件总线
大多数事件会发布到 NeoForge.EVENT_BUS,但也有一些会发布到 Mod 事件总线,通常称为 Mod 总线事件。它们实现的父接口 IModBusEvent 可用于区分这两类事件。
最新版本
Mod 事件总线通过 Mod 构造函数的参数传入,可以在其上订阅相应事件。使用 @EventBusSubscriber 时,事件会自动订阅到正确的总线。
[21.1.0,21.1.180]
Mod 事件总线通过 Mod 构造函数的参数传入,可以在其上订阅相应事件。使用 @EventBusSubscriber 时,可以通过注解参数设置总线,例如 @EventBusSubscriber(bus = Bus.MOD, modid = "yourmodid")。默认总线为 Bus.GAME。
Mod 生命周期
大部分 Mod 总线事件属于生命周期事件。它们在启动期间、每个 Mod 的生命周期中各执行一次。许多事件继承 ParallelDispatchEvent,以并行方式触发。如果需要从这类事件中将代码交给主线程执行,应使用 #enqueueWork(Runnable runnable) 将任务加入队列。
生命周期通常按以下顺序进行:
- 调用 Mod 构造函数。在这里注册事件处理方法,或在下一步完成注册。
- 处理所有
@EventBusSubscriber。 - 触发
FMLConstructModEvent。 - 触发注册表事件,包括
NewRegistryEvent、DataPackRegistryEvent.NewRegistry,以及每个注册表各自的RegisterEvent。 - 触发
FMLCommonSetupEvent,执行各种通用初始化工作。 - 触发特定端的初始化事件:物理客户端上是
FMLClientSetupEvent,物理服务端上是FMLDedicatedServerSetupEvent。 - 处理
InterModComms,见下文。 - 触发
FMLLoadCompleteEvent。
InterModComms
InterModComms 允许开发者向其他 Mod 发送消息,以实现兼容功能。这个类会保存发送给各个 Mod 的消息,所有方法都可以安全地从多个线程调用。系统主要由 InterModEnqueueEvent 和 InterModProcessEvent 两个事件驱动。
在 InterModEnqueueEvent 中,可通过 InterModComms#sendTo 向其他 Mod 发送消息。参数包括接收 Mod 的 ID、用于区分不同消息的数据键,以及保存消息数据的 Supplier。也可以选择显式指定发送者。
随后,在 InterModProcessEvent 中,可通过 InterModComms#getMessages 获取接收到的全部消息,返回由 IMCMessage 对象组成的流。每条消息包含发送者、预期接收者、数据键,以及提供实际数据的 supplier。
其他 Mod 总线事件
除了生命周期事件,还有少量其他事件会在 Mod 总线上触发,主要出于历史原因。它们通常用于注册、配置或初始化各种内容。与生命周期事件不同,其中大多数不会并行执行。例如:
RegisterColorHandlersEventModelEvent.BakingCompletedTextureAtlasStitchedEvent
警告 原文计划在未来版本中将这些事件中的大部分移至主事件总线。
来源: NeoForged/Documentation(1.21.1)。中文翻译依据提交
4a680cdd4d15。