Skip to content

事件系统

字数
2424 字
阅读时间
10 分钟

文章翻译自官方文档:

事件

NeoForge 的主要功能之一是事件系统。游戏中发生的各种事情都会触发事件。例如,当玩家右键点击时、当玩家或其他实体跳跃时、当方块被渲染时、当游戏加载时等,都会触发事件。Mod 开发者可以为这些事件订阅事件处理器,然后在这些事件处理器中执行他们想要的行为。

事件在其各自的事件总线上触发。最重要的总线是 NeoForge.EVENT_BUS,也称为游戏总线。除此之外,在启动期间,会为每个加载的 Mod 生成一个 Mod 总线,并将其传递到 Mod 的构造函数中。许多 Mod 总线事件是并行触发的(与始终在同一线程上运行的主总线事件不同),从而显著提高了启动速度。有关更多信息,请参见下文。

注册事件处理器

有多种方法可以注册事件处理器。所有这些方法的共同点是,每个事件处理器都是一个具有单个事件参数且无返回值(即返回类型为 void)的方法。

IEventBus#addListener

注册方法处理器的最简单方法是注册它们的方法引用,如下所示:

java
@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 注解的事件处理器:

java
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());
    }
}

您也可以静态地执行此操作。只需使所有事件处理器变为静态的,然后传递类本身而不是类实例:

java
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 冲突时)。

java
@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 来完成的。

TriStateResult

某些事件具有三个潜在的返回状态,由 TriState 表示,或直接在事件类上的 Result 枚举表示。返回状态通常可以取消事件处理的操作(TriState#FALSE),强制操作运行(TriState#TRUE),或执行默认的 Vanilla 行为(TriState#DEFAULT)。

具有三个潜在返回状态的事件具有某种 set* 方法来设置所需的结果。

java
// 在某个类中,监听器订阅了游戏事件总线

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

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

优先级

事件处理器可以选择分配优先级。EventPriority 枚举包含五个值:HIGHESTHIGHNORMAL(默认)、LOWLOWEST。事件处理器按优先级从高到低执行。如果它们具有相同的优先级,则它们在主总线上按注册顺序触发(大致与 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) 将它们加入队列。

生命周期通常遵循以下顺序:

  1. 调用 Mod 构造函数。在此处或下一步中注册您的事件处理器。
  2. 调用所有 @EventBusSubscriber
  3. 触发 FMLConstructModEvent
  4. 触发注册事件,包括 NewRegistryEventDataPackRegistryEvent.NewRegistry 以及每个注册表的 RegisterEvent
  5. 触发 FMLCommonSetupEvent。这是各种杂项设置发生的地方。
  6. 触发端特定设置:如果在物理客户端上,则触发 FMLClientSetupEvent;如果在物理服务器上,则触发 FMLDedicatedServerSetupEvent
  7. 处理 InterModComms(见下文)。
  8. 触发 FMLLoadCompleteEvent

InterModComms

InterModComms 是一个允许 Mod 开发者向其他 Mod 发送消息以实现兼容性功能的系统。该类保存了 Mod 的消息,所有方法都是线程安全的。该系统主要由两个事件驱动:InterModEnqueueEventInterModProcessEvent

InterModEnqueueEvent 期间,您可以使用 InterModComms#sendTo 向其他 Mod 发送消息。这些方法接受要发送消息的 Mod ID、与消息数据关联的键(以区分不同的消息)以及保存消息数据的 Supplier。还可以选择指定发送者。

然后,在 InterModProcessEvent 期间,您可以使用 InterModComms#getMessages 获取所有接收到的消息的流作为 IMCMessage 对象。这些对象包含数据的发送者、数据的预期接收者、数据键以及实际数据的 Supplier

其他 Mod 总线事件

除了生命周期事件外,还有一些杂项事件在 Mod 事件总线上触发,主要是出于历史原因。这些通常是您可以注册、设置或初始化各种内容的事件。与生命周期事件不同,大多数这些事件不是并行运行的。一些例子包括:

  • RegisterColorHandlersEvent
  • ModelEvent.BakingCompleted
  • TextureAtlasStitchedEvent

警告:大多数这些事件计划在未来的版本中移动到主事件总线。

贡献者

The avatar of contributor named as 小飘 小飘

文件历史