MCBBS Wiki欢迎您共同参与编辑!在参与编辑之前请先阅读Wiki方针。
如果在编辑的过程中遇到了什么问题,可以去讨论板提问。
为了您能够无阻碍地参与编辑 未验证/绑定过邮箱的用户,请尽快绑定/验证。
MCBBS Wiki GitHub群组已上线!
您可以在回声洞中发表吐槽!
服务器状态监控。点击进入
本站由MCBBS用户自行搭建,与MCBBS及东银河系漫游指南(北京)科技有限公司没有从属关系。点此了解 MCBBS Wiki 不是什么>>
用户:MashKJo/1.12.2模组开发教程/4.Forge事件系统
事件系统,是Forge的一大特色——没错,Minecraft原版并没有事件这一概念。
在上一节,我们已经看到了事件系统的应用——就是那三个被@EventHandler
注解的监听器方法。读者可能已经注意到了:这三个方法的形参都是唯一的,实际上,这是必须的,且它们各自的形参类型就代表了监听器监听的事件类型。
FMLPreInitializationEvent、FMLInitializationEvent和FMLPostInitializationEvent这些事件类型,都和FML加载Forge Mod的行为密切相关,它们通常被归为一类,被称为Forge事件;而事件还有另一类:就是玩家在实际进行Minecraft游戏时,发挥作用的事件,如:实体受到伤害时发布的事件,玩家钓鱼钓到物品时发布的事件,等等,通常被称为Minecraft事件。Forge事件不能胡乱监听,在绝大多数情况下都是要放在Mod主类里监听才行。而本节主要讲述Minecraft事件的应用。
事件系统存在的意义
作为Modder,肯定有修改原版的游戏逻辑的需求,但是原版的很多东西都是写死的,如果不借助黑魔法之类的东西,根本没法直接修改原版的源码。这里的黑魔法指的是什么?如果是高级一点的,那就用Mixin——一种能让Modder在Minecraft源码中插入自己的代码的工具,被Fabric尤为推崇;如果再原始一点,那就是手搓ASM……至于反射,不提也罢。
上面提到的这些黑魔法,比较难用,而且很容易把游戏搞崩。于是Forge出手了,通过监听Minecraft事件,Modder们可以轻松地改变原版的很多游戏逻辑。原理是什么?其实就是Forge帮Modder们把黑魔法的步骤做完了,它在Minecraft原版代码中插入了很多代码,一般流程是:在Minecraft相关游戏逻辑执行前后,Forge发布某个特定类型的Minecraft事件,再根据这个事件的监听情况决定原版逻辑到底会不会被执行,如果不会,到底要怎么改变。很明显,Forge帮模组开发者们统一执行黑魔法步骤,而非让模组开发者们各自使用黑魔法,大大提高了Forge Mod之间的兼容性,保证了良好的跨Mod交互。这就是Minecraft事件存在的意义:让Modder们能轻松干涉原版机制,且同时保证Mod之间良好的交互。
至于Forge事件?这个也有保证良好的跨Mod交互的功能,此外还有为Forge的一些机制服务的功能,这些用的很少,因此不是本节的重点内容。
事件监听方法的格式
对于Forge事件的监听方法格式,上一节已有示例。
而对于Minecraft事件的监听方法,有以下几点要求:
- 访问修饰符必须为public
- 返回值类型必须为void
- 形参必须唯一,且形参类型必须为Event类的子类——Event类实际上是所有Minecraft事件的共同父类
- 必须打上@SubscribeEvent注解
比如,举个例子:
@SubscribeEvent public void exampleEventHandler(ExampleEvent event) { ... }
通过这个event,你通常会有一堆getter和setter能用,由此你就能干涉原版的机制。
另外注意到,这个方法是非静态的,但其实,是静态的也无妨,只是注册的时候会略有点小区别,读者往下看即可。
Minecraft事件的注册、取消和优先级
事件总线
这是一个解释起来非常抽象的概念,读者可以简单地把它理解为一个“中转站”——所有事件被发布后,会被传到事件总线中处理,而事件总线就会尝试去获取所有事件监听方法,并产生实际效果。
Forge内建了三条事件总线(EventBus):一般事件总线(EVENT_BUS)、矿物生成总线(ORE_GEN_BUS)和地形生成总线(TERRAIN_GEN_BUS),它们在net.minecraftforge.common.MinecraftForge
类中以静态字段的形式存在。看看它们的名字,你就知道它们各自的用途都是什么。你当然也可以new一个自己的事件总线,但是没必要。
事件的注册
很简单,用EventBus#register
这一方法就好了。注意它要求传入一个Object——这有点让人摸不着头脑了,不是吗?
事情是这样的:传入什么东西,取决于你的事件监听方法是不是静态的。如果是静态的,那么传入该方法所在的类对应的Class<?>对象;如果是非静态的,则直接new一个该类的对象,传入进去。
不过,对于static的情况,实际上,还有一种便捷的注册方法:直接给监听方法所在的类打上@Mod.EventBusSubscriber
这个注解,这个注解可以传入你的modid,以及应该发挥作用的物理端。
实例
src/main/java/net/tutorial_mod/event/EventHandler.java:
package net.tutorial_mod.event; import net.minecraft.entity.player.EntityPlayer; import net.minecraftforge.event.entity.EntityJoinWorldEvent; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.tutorial_mod.TutorialMod; @Mod.EventBusSubscriber(modid = TutorialMod.MODID) public class EventHandler { @SubscribeEvent public static void testEventHandler(EntityJoinWorldEvent event){ if(event.getEntity() instanceof EntityPlayer) System.out.println("My event handler works!"); } }
这里我们新建了一个EventHandler类来存放事件监听器。这里我们监听了EntityJoinWorldEvent——顾名思义,实体在世界上生成时,这个事件会被发布。这个事件有着若干getter,我们通过getEntity这一getter获取了生成在世界中的实体对象,并通过instanceof运算符判断其是否为玩家——没错,玩家加入世界的实质是生成了EntityPlayer对象(注意这里笔者的用词,笔者并没有说“生成了一个EntityPlayer对象”,想想看这是为什么)。然后有鉴于这个方法是static的,我们用了自动注册的注解。
现在运行游戏客户端,随便进入一个存档,你应该能在命令行中看到输出的字符串了。但是注意:这个字符串输出了两次,这实际上是因为两个逻辑端各调用了一次这个监听方法。那么,我们有什么办法能判断当前所处的逻辑端呢?这个会在后面讲到,读者现在留个心眼就行。
如何知道自己要用什么事件?
所有Minecraft事件都是Event类的子类,因此理论上你只需要去看看Event的继承树就好。但实际上这么做并不妥,因为Forge的事件类型非常多,你这么搞,效率低下且不说,而且Forge的有些事件的名字起得并不好,你不一定看到名字就知道某个事件是在什么时候发布的、是用来干什么的。因此,最好的方法是:先思考你要干涉什么原版机制,再找到原版相关的类,看看其中有没有Forge插入进去的代码,如果有,看看Forge在这里面发布了什么事件即可。另外说一句,Forge插入原版类的代码中,Forge的类一般都会写全包名而非在源文件开头import,因此会显得很突兀;且Forge的代码一般都有详尽的注释。所以,有没有Forge插进去的代码,一看即知。
那么,如果真的找不到能用的事件,怎么办?尽管Forge的事件非常多,有的甚至做到了仅仅针对某个单一的游戏元素(如RenderItemInFrameEvent,这个事件影响物品在展示框中的渲染情况),但任何API都做不到完全满足使用者的需求。Forge官方早在多年前就停止了对1.12 Forge的维护,所以指望着去Pull Request是不行了。这个时候,你就只能考虑使用一些黑魔法了。