Skip to main content
Version: 1.21.4

事件(Events)

NeoForge 的主要特性之一就是事件系统(event system)。事件会在游戏中各种行为发生时被触发。例如,玩家右键点击、玩家或其他实体跳跃、方块渲染、游戏加载等都会有对应的事件。模组开发者可以为这些事件注册事件处理器(event handler),并在事件处理器中实现自己想要的功能。

事件会在它们对应的事件总线(event bus)上被触发。最重要的事件总线是 NeoForge.EVENT_BUS,也叫做游戏总线(game bus)。除此之外,在启动过程中,每个已加载的模组都会生成一个模组总线(mod bus),并将其传递给模组的构造函数。许多模组总线事件会并行触发(而主总线事件始终在同一线程上执行),这大大提升了启动速度。更多信息请参见下文

注册事件处理器(Registering an Event 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

另外一种方式是通过注解驱动(annotation-driven)的方式。你可以创建一个事件处理方法,并使用 @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 会自动发现该注解,你可以将所有与事件相关的代码从模组(mod)构造函数中移除。本质上,这等同于在模组构造函数末尾调用 NeoForge.EVENT_BUS.register(EventHandler.class)。这意味着所有的事件处理器方法也必须是静态的(static)。

虽然不是强制要求,但强烈建议在注解中指定 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);
}
}
}

事件选项(Event Options)

字段与方法(Fields and Methods)

字段(fields)和方法(methods)大概是事件最直观的部分。大多数事件都包含了供事件处理器使用的上下文信息,比如触发该事件的实体(entity)或事件发生的世界(level)。

继承层级(Hierarchy)

为了利用继承的优势,有些事件不会直接继承自 Event,而是继承自其子类,比如 BlockEvent(为方块相关事件提供方块上下文)、EntityEvent(同样为实体相关事件提供上下文)以及其子类 LivingEvent(用于 LivingEntity 相关的上下文)和 PlayerEvent(用于 Player 相关的上下文)。这些提供上下文的父事件类是 abstract(抽象)的,无法被直接监听。

danger

如果你监听了一个 abstract(抽象)事件,游戏会崩溃,因为这绝对不是你想要的。你应该始终监听其子事件之一。

可取消事件(Cancellable Events)

有些事件实现了 ICancellableEvent 接口。这类事件可以通过 #setCanceled(boolean canceled) 方法被取消,并可通过 #isCanceled() 方法检查其取消状态。如果一个事件被取消,其他针对该事件的事件处理器将不会被执行,并且会启用某种与“取消”相关的特殊行为。例如,取消 LivingChangeTargetEvent 会阻止实体更改其目标实体。

事件处理器可以选择显式地接收已被取消的事件。实现方法是在 IEventBus#addListener(或根据你绑定事件处理器的方式,在 @SubscribeEvent)中,将 receiveCanceled 布尔参数设置为 true。

三态值与结果(TriStates and Results)

某些事件具有三种可能的返回状态,这些状态通过 TriState 或事件类上直接定义的 Result 枚举进行表示。通常,这三种返回状态分别为:取消事件所处理的动作(TriState#FALSE)、强制执行该动作(TriState#TRUE)、或者执行原版 Vanilla 的默认行为(TriState#DEFAULT)。

拥有三种返回状态的事件,通常会有一个 set* 方法用于设置期望的结果。

// 在某个类中,监听器被注册到游戏事件总线上

@SubscribeEvent
public void renderNameTag(RenderNameTagEvent.CanRender event) {
// 使用 TriState 设置返回状态
event.setCanRender(TriState.FALSE);
}

@SubscribeEvent
public void mobDespawn(MobDespawnEvent event) {
// 使用 Result 枚举设置返回状态
event.setResult(MobDespawnEvent.Result.DENY);
}

优先级(Priority)

事件处理器(event handler)可以选择性地指定优先级。EventPriority 枚举包含五个值:HIGHESTHIGHNORMAL(默认)、LOWLOWEST。事件处理器会按照从高到低的优先级依次执行。如果优先级相同,则在主事件总线(main bus)上按注册顺序执行,这与模组加载顺序大致相关;而在模组事件总线(mod bus)上,则严格按照模组加载顺序执行(详见下文)。

你可以通过在 IEventBus#addListener@SubscribeEvent 中设置 priority 参数来定义优先级,具体取决于你是如何附加事件处理器的。请注意,对于并行触发的事件,优先级设置会被忽略。

侧性事件(Sided Events)

有些事件只会在某一 侧(side) 被触发。常见的例子包括各种渲染事件,这些事件只会在客户端被触发。由于客户端专属事件通常需要访问 Minecraft 代码库中其他仅限客户端的部分,因此必须按照相应方式注册。

如果你使用 IEventBus#addListener() 注册事件处理器,应该通过 FMLEnvironment.dist 或在你的模组构造函数中使用 Dist 参数检查当前的物理侧,并在专门的客户端类中添加监听器,具体做法可参考 侧性(sides) 文章的说明。

如果你使用 @EventBusSubscriber 注解注册事件处理器,可以通过注解的 value 参数指定侧性,例如:@EventBusSubscriber(value = Dist.CLIENT, modid = "yourmodid")

事件总线(Event Buses)

虽然大多数事件都是在 NeoForge.EVENT_BUS 上发布的,但有些事件则是在模组事件总线(mod event bus)上发布。这些事件通常被称为模组总线事件(mod bus events)。你可以通过它们的超接口 IModBusEvent 将其与普通事件区分开来。

模组事件总线会作为参数传递给你的模组构造函数,你可以在其中订阅模组总线事件。如果你使用 @EventBusSubscriber,还可以通过注解参数设置总线类型,例如:@EventBusSubscriber(bus = Bus.MOD, modid = "yourmodid")。默认的总线是 Bus.GAME

模组生命周期(The Mod Lifecycle)

大多数模组总线事件(mod bus events)属于生命周期事件(lifecycle events)。生命周期事件会在每个模组的启动过程中仅运行一次。许多生命周期事件通过继承 ParallelDispatchEvent 实现并行触发;如果你希望在主线程中运行这些事件中的代码,可以使用 #enqueueWork(Runnable runnable) 方法将其加入队列。

生命周期的一般顺序如下:

  • 调用模组构造函数。在这里或下一步注册你的事件处理器。
  • 所有 @EventBusSubscriber 被调用。
  • 触发 FMLConstructModEvent
  • 触发注册表事件(registry events),包括 NewRegistryEventDataPackRegistryEvent.NewRegistry,以及针对每个注册表的 RegisterEvent
  • 触发 FMLCommonSetupEvent。通常在这里进行各种杂项初始化。
  • 触发侧向设置事件:如果在物理客户端则触发 FMLClientSetupEvent,如果在物理服务端则触发 FMLDedicatedServerSetupEvent
  • 处理 InterModComms(见下文)。
  • 触发 FMLLoadCompleteEvent

InterModComms

InterModComms 是一个允许模组开发者向其他模组发送消息以实现兼容性功能的系统。该类用于存储各模组的消息,所有方法都支持线程安全调用。整个系统主要由两个事件驱动:InterModEnqueueEventInterModProcessEvent

InterModEnqueueEvent 阶段,你可以使用 InterModComms#sendTo 方法向其他模组发送消息。该方法需要指定目标模组的 id、消息数据关联的 key(用于区分不同消息),以及一个包含消息数据的 Supplier。你也可以选择性地指定发送者。

随后,在 InterModProcessEvent 阶段,你可以使用 InterModComms#getMessages 方法获取所有收到的消息流(stream),每条消息以 IMCMessage 对象的形式存在。该对象包含数据发送者、目标接收者、数据 key 以及实际数据的 supplier。

其他模组总线事件(Other Mod Bus Events)

除了生命周期事件之外,模组事件总线上还会触发一些杂项事件,这些事件主要是为了兼容旧版而保留。通常,这些事件用于注册、设置或初始化各种内容。与生命周期事件不同,这些杂项事件大多数不会并行运行。以下是一些示例:

  • RegisterColorHandlersEvent.Block.ItemTintSources.ColorResolvers
  • ModelEvent.BakingCompleted
  • TextureAtlasStitchedEvent
warning

这些事件中的大多数计划在未来版本迁移到游戏事件总线(game event bus)。