NeoForge 的主要特性之一是事件系统。事件会在游戏中发生的各种事情时触发。例如,有玩家右键单击、玩家或其他实体跳跃、方块渲染、游戏加载等事件。模组开发者可以将事件处理程序订阅到这些事件中的每一个,然后在这些事件处理程序内部执行他们想要的行为。
事件在其各自的事件总线上触发。最重要的总线是 NeoForge.EVENT_BUS
,也称为游戏总线。除此之外,在启动期间,会为每个加载的模组生成一个模组总线,并将其传递到模组的构造函数中。许多模组总线事件是并行触发的(与始终在同一线程上运行的主总线事件相反),从而大大提高了启动速度。有关更多信息,请参见下文。
注册事件Handler
有多种注册事件处理程序的方法。所有这些方法的共同点是,每个事件处理程序都是一个具有单个事件参数且没有结果(即返回类型为 void
)的方法。
IEventBus#addListener
注册方法处理程序的最简单方法是注册它们的方法引用,如下所示:
@Mod("yourmodid")
public class YourMod {
public YourMod(IEventBus modBus) {
NeoForge.EVENT_BUS.addListener(YourMod::onLivingJump);
}
// 每次实体跳跃时为其恢复半颗心。
private static void onLivingJump(LivingEvent.LivingJumpEvent event) {
LivingEntity entity = event.getEntity();
// 仅在服务器端恢复
if (!entity.level().isClientSide()) {
entity.heal(1);
}
}
}
@SubscribeEvent
或者,可以通过创建事件处理程序方法并使用 @SubscribeEvent
注解它来以注解驱动的方式处理事件。然后,你可以将包含类的实例传递给事件总线,注册该实例的所有 @SubscribeEvent
注解的事件处理程序:
public class EventHandler {
@SubscribeEvent
public void onLivingJump(LivingEvent.LivingJumpEvent event) {
LivingEntity 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(LivingEvent.LivingJumpEvent event) {
LivingEntity 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 自动发现,允许你从模组构造函数中删除所有与事件相关的代码。本质上,它等效于在模组构造函数的末尾调用 NeoForge.EVENT_BUS.register(EventHandler.class)
。这意味着所有处理程序也必须是静态的。
虽然不是必需的,但强烈建议在注解中指定 modid
参数,以便更轻松地进行调试(尤其是在涉及模组冲突时)。
@EventBusSubscriber(modid = "yourmodid")
public class EventHandler {
@SubscribeEvent
public static void onLivingJump(LivingEvent.LivingJumpEvent event) {
LivingEntity entity = event.getEntity();
if (!entity.level().isClientSide()) {
entity.heal(1);
}
}
}
事件选项
字段和方法
字段和方法可能是事件最明显的部分。大多数事件包含供事件处理程序使用的上下文,例如导致事件的实体或事件发生的 Level
。
继承
为了利用继承的优势,一些事件不直接扩展 Event
,而是扩展其子类之一,例如 BlockEvent
(包含与方块相关事件的方块上下文)或 EntityEvent
(类似地包含实体上下文)及其子类 LivingEvent
(用于 LivingEntity
特定的上下文)和 PlayerEvent
(用于 Player
特定的上下文)。这些提供上下文的父事件是抽象的,无法被监听。
📛如果你监听一个抽象事件,你的游戏将会崩溃,因为这绝对不是你想要的。你总是应该监听其中一个子事件。
可取消的事件
一些事件实现了 ICancellableEvent
接口。这些事件可以使用 #setCanceled(boolean canceled)
取消,并且可以使用 #isCanceled()
检查取消状态。如果一个事件被取消,则此事件的其他事件处理程序将不会运行,并且会启用与“取消”相关的某种行为。例如,取消 LivingChangeTargetEvent
将阻止实体的目标实体发生更改。
事件处理程序可以选择显式接收已取消的事件。这可以通过在 IEventBus#addListener
(或 @SubscribeEvent
,取决于你附加事件处理程序的方式)中将 receiveCanceled
布尔参数设置为 true
来完成。
TriStates与结果
一些事件有三种可能的返回状态,由 TriState
或事件类上的 Result
枚举直接表示。返回状态通常可以取消事件正在处理的操作 (TriState#FALSE
),强制操作运行 (TriState#TRUE
),或执行默认的原版行为 (TriState#DEFAULT
)。
具有三种可能返回状态的事件具有一些 set*
方法来设置所需的结果。
// In some class where the listeners are subscribed to the game event bus
@SubscribeEvent
public void renderNameTag(RenderNameTagEvent.CanRender event) {
// Uses TriState to set the return state
event.setCanRender(TriState.FALSE);
}
@SubscribeEvent
public void mobDespawn(MobDespawnEvent event) {
// Uses a Result enum to set the return state
event.setResult(MobDespawnEvent.Result.DENY);
}
优先级
事件处理程序可以选择分配一个优先级。EventPriority
枚举包含五个值:HIGHEST
、HIGH
、NORMAL
(默认)、LOW
和 LOWEST
。事件处理程序按从高到低的优先级执行。如果它们具有相同的优先级,则它们在主总线上按注册顺序触发,这大致与模组加载顺序相关,并且在模组总线上按精确的模组加载顺序触发(见下文)。
可以通过在 IEventBus#addListener
或 @SubscribeEvent
中设置 priority
参数来定义优先级,具体取决于你如何附加事件处理程序。请注意,对于并行触发的事件,优先级将被忽略。
侧事件
一些事件仅在一侧触发。常见的例子包括各种渲染事件,它们仅在客户端触发。由于仅客户端事件通常需要访问 Minecraft 代码库的其他仅客户端部分,因此需要相应地注册它们。
使用 IEventBus#addListener()
的事件处理程序应通过 FMLEnvironment.dist
或你创建的模组构造函数中的 Dist
参数检查当前的物理侧,并在单独的仅客户端类中添加侦听器,如有关侧面的文章中所述。
使用 @EventBusSubscriber
的事件处理程序可以将侧面指定为注解的 value
参数,例如 @EventBusSubscriber(value = Dist.CLIENT, modid = "yourmodid")
。
事件总线
虽然大多数事件都发布于NeoForge.EVENT_BUS
,一些其他的事件反而会发布于模组的事件总线。这些事件通常叫做模组总线事件。模组总线事件因为他们的超接口IModBusEvent
而区别于普通的事件
在模组的构造函数中,模组总线是通过作为参数来传递给你的,你可以把模组总线事件订阅到上面。如果你使用@EventBusSubscriber
注解,你也可以将总线设置为注解参数,就像这样:@EventBusSubscriber(bus = Bus.MOD, modid = "yourmodid")
. 默认的bus是 Bus.GAME
.
模组生命周期
大部分模组总线事件都是被称作生命周期事件的东西。生命周期事件在启动期间每个模组的生命周期中都会执行一次。其中许多都是通过子类化ParallelDispatchEvent
平行启动的;如果你想在主线程上的这些事件中运行代码,通过#enqueueWork(Runnable runnable)
来将其入队
生命周期通常遵循以下顺序:
调用了模组的构造函数. 在这一步或者下一步中注册你的事件处理器.
调用所有的
@EventBusSubscriber
FMLConstructModEvent
启动了.注册事件启动,这些事件包括
NewRegistryEvent
,DataPackRegistryEvent.NewRegistry
以及对于每个注册表的RegisterEvent
.FMLCommonSetupEvent
启动. 这一步是各种各样设置生效的地方.侧设置事件启动:在物理客户端上是
FMLClientSetupEvent
, 如果在物理服务端则是FMLDedicatedServerSetupEvent
.InterModComms
被处理(见下文).FMLLoadCompleteEvent
启动.
InterModComms
InterModComms
是一个允许模组制作者为了兼容性而向其他模组发送消息的系统。这个类保存着给模组的消息, 类中的所有方法都是调用起来是线程安全的. 这个系统主要是通过两个事件驱动的: InterModEnqueueEvent
和 InterModProcessEvent
.
在InterModEnqueueEvent
中, 你可以使用InterModComms#sendTo
来向其他模组发送消息.这些方法接受模组id作为参数表示消息发送的目标,键与消息数据相关联(为了区分不同的消息),还有一个Supplier
保存着消息数据.发送者也可以在某些情况下被识别.
之后,在InterModProcessEvent
中,你可以使用InterModComms#getMessages
来获取所有作为IMCMessage
物体的消息的流.他们保存着数据的发送者,数据的目标接收者,数据键,以及实际数据的提供者.
其他模组总线事件
在生命周期事件之后,模组事件总线上会启动一些杂项事件,其中大部分是由于历史遗留因素.这些事件你可以用来注册,设置,或者初始化一些东西的通用事件.与生命周期事件相反,其中大部分的事件并不是平行运行的.就像这些例子:
RegisterColorHandlersEvent.Block
,.ItemTintSources
,.ColorResolvers
ModelEvent.BakingCompleted
TextureAtlasStitchedEvent
⚠️大部分这些事件被计划在之后的版本中移动到游戏事件总线里面