事件

事件

事件系统是 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(LivingEvent.LivingJumpEvent event) {
        LivingEntity 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(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) 和 modBus.register(EventHandler.class),因此所有处理器也必须是静态方法。

虽然不是强制要求,但强烈建议指定注解的 modid 参数,便于调试,尤其是排查 Mod 冲突。

@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,而是继承其子类。例如,BlockEvent 为方块相关事件提供方块上下文;EntityEvent 为实体事件提供实体上下文。后者又有 LivingEvent(LivingEntity 专用上下文)和 PlayerEvent(Player 专用上下文)等子类。用于提供上下文的这些父事件是 abstract,不能直接监听。

重要警告 监听 abstract 事件会导致游戏崩溃。应始终监听具体的子事件。

graph TD;
    Event --> BlockEvent --> BlockDropsEvent;
    Event --> EntityEvent --> LivingEvent --> PlayerEvent --> CanPlayerSleepEvent;

    class Event,BlockEvent,EntityEvent,LivingEvent,PlayerEvent yellow;
    class BlockDropsEvent,CanPlayerSleepEvent blue;

黄色类是 abstract,蓝色类不是 abstract。

可取消的事件

一些事件实现了 ICancellableEvent 接口。可以用 #setCanceled(boolean canceled) 取消这些事件,用 #isCanceled() 检查取消状态。事件取消后,其他处理器将不再处理它,并会触发与“取消”对应的行为。例如,取消 LivingChangeTargetEvent 可以阻止实体改变目标。

处理器也可以明确要求接收已取消的事件:将 IEventBus#addListener 或 @SubscribeEvent 的 receiveCanceled 布尔参数设为 true,具体取决于注册方式。

TriState 与结果

有些事件有三种可能的结果状态,通过 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.CanRender 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 参数指定优先级。并行触发的事件会忽略优先级。

特定侧的事件

一些事件只在某一侧触发,例如各种渲染事件只在客户端触发。这些事件的处理器通常还需要访问其他客户端专用代码,因此必须按侧正确注册。

使用 IEventBus#addListener 时,应通过 FMLEnvironment#getDist 或主 Mod 构造函数的 Dist 参数检查当前物理侧,并按侧的说明在独立的客户端专用类中注册监听器。

使用 @EventBusSubscriber 时,可以通过注解的 value 参数指定侧,例如 @EventBusSubscriber(value = Dist.CLIENT, modid = "yourmodid")。

事件总线

多数事件发布到 NeoForge.EVENT_BUS,但也有一些发布到 Mod 事件总线,通常称为 Mod 总线事件。它们实现了 IModBusEvent 接口,可据此与普通事件区分。

Mod 事件总线通过 Mod 构造函数参数传入,随后就可以在其上订阅事件。使用 @EventBusSubscriber 时,事件会自动订阅到正确的总线。

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.BlockTintSources、.ItemTintSources、.ColorResolvers
  • ModelEvent.BakingCompleted
  • TextureAtlasStitchedEvent

警告 这些事件大多计划在未来版本迁移到游戏事件总线。


来源: NeoForged/Documentation(26.1)。中文翻译依据提交 4a680cdd4d15。