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 枚举包含五个值:HIGHESTHIGHNORMAL(默认)、LOWLOWEST。事件处理程序按从高到低的优先级执行。如果它们具有相同的优先级,则它们在主总线上按注册顺序触发,这大致与模组加载顺序相关,并且在模组总线上按精确的模组加载顺序触发(见下文)。

可以通过在 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 是一个允许模组制作者为了兼容性而向其他模组发送消息的系统。这个类保存着给模组的消息, 类中的所有方法都是调用起来是线程安全的. 这个系统主要是通过两个事件驱动的: InterModEnqueueEventInterModProcessEvent.

InterModEnqueueEvent中, 你可以使用InterModComms#sendTo 来向其他模组发送消息.这些方法接受模组id作为参数表示消息发送的目标,键与消息数据相关联(为了区分不同的消息),还有一个Supplier 保存着消息数据.发送者也可以在某些情况下被识别.

之后,在InterModProcessEvent中,你可以使用InterModComms#getMessages来获取所有作为IMCMessage物体的消息的流.他们保存着数据的发送者,数据的目标接收者,数据键,以及实际数据的提供者.

其他模组总线事件

在生命周期事件之后,模组事件总线上会启动一些杂项事件,其中大部分是由于历史遗留因素.这些事件你可以用来注册,设置,或者初始化一些东西的通用事件.与生命周期事件相反,其中大部分的事件并不是平行运行的.就像这些例子:

  • RegisterColorHandlersEvent.Block, .ItemTintSources, .ColorResolvers

  • ModelEvent.BakingCompleted

  • TextureAtlasStitchedEvent

⚠️大部分这些事件被计划在之后的版本中移动到游戏事件总线里面

一个还在寻找自己的三流开发者