事件系统
文章翻译自官方文档:
事件
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);
}
// 每次实体跳跃时为其恢复半颗心。
private static void onLivingJump(LivingJumpEvent event) {
Entity entity = event.getEntity();
// 仅在服务器端恢复
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 构造函数中删除所有与事件相关的代码。本质上,它等同于在 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
(用于 LivingEntity
特定上下文)和 PlayerEvent
(用于 Player
特定上下文)。这些提供上下文的超事件是抽象的,无法被监听。
警告:如果您监听抽象事件,您的游戏将崩溃,因为这绝不是您想要的。您应该始终监听其中一个子事件。
可取消事件
某些事件实现了 ICancellableEvent
接口。这些事件可以使用 #setCanceled(boolean canceled)
取消,并且可以使用 #isCanceled()
检查取消状态。如果事件被取消,其他事件处理器将不会运行,并且与“取消”相关的某种行为将被启用。例如,取消 LivingJumpEvent
将阻止跳跃。
事件处理器可以选择显式接收已取消的事件。这是通过在 IEventBus#addListener
(或 @SubscribeEvent
,取决于您附加事件处理器的方式)中将 receiveCanceled
布尔参数设置为 true
来完成的。
TriState
和 Result
某些事件具有三个潜在的返回状态,由 TriState
表示,或直接在事件类上的 Result
枚举表示。返回状态通常可以取消事件处理的操作(TriState#FALSE
),强制操作运行(TriState#TRUE
),或执行默认的 Vanilla 行为(TriState#DEFAULT
)。
具有三个潜在返回状态的事件具有某种 set*
方法来设置所需的结果。
// 在某个类中,监听器订阅了游戏事件总线
@SubscribeEvent
public void renderNameTag(RenderNameTagEvent event) {
// 使用 TriState 设置返回状态
event.setCanRender(TriState.FALSE);
}
@SubscribeEvent
public void mobDespawn(MobDespawnEvent event) {
// 使用 Result 枚举设置返回状态
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 总线事件。Mod 总线事件可以通过它们的超接口 IModBusEvent
来区分。
Mod 事件总线作为参数传递到 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 发送消息以实现兼容性功能的系统。该类保存了 Mod 的消息,所有方法都是线程安全的。该系统主要由两个事件驱动:InterModEnqueueEvent
和 InterModProcessEvent
。
在 InterModEnqueueEvent
期间,您可以使用 InterModComms#sendTo
向其他 Mod 发送消息。这些方法接受要发送消息的 Mod ID、与消息数据关联的键(以区分不同的消息)以及保存消息数据的 Supplier
。还可以选择指定发送者。
然后,在 InterModProcessEvent
期间,您可以使用 InterModComms#getMessages
获取所有接收到的消息的流作为 IMCMessage
对象。这些对象包含数据的发送者、数据的预期接收者、数据键以及实际数据的 Supplier
。
其他 Mod 总线事件
除了生命周期事件外,还有一些杂项事件在 Mod 事件总线上触发,主要是出于历史原因。这些通常是您可以注册、设置或初始化各种内容的事件。与生命周期事件不同,大多数这些事件不是并行运行的。一些例子包括:
RegisterColorHandlersEvent
ModelEvent.BakingCompleted
TextureAtlasStitchedEvent
警告:大多数这些事件计划在未来的版本中移动到主事件总线。