- 我的世界:Minecraft模组开发指南
- 土球球
- 2547字
- 2020-08-27 14:04:58
3.3 Forge的事件系统
3.3.1 Minecraft游戏事件
除了在之前提到的生命周期事件,Mod开发中经常遇到的就是Forge为Minecraft提供的游戏事件,比如说玩家进入了世界,玩家破坏了一个方块、捡起了一个物品等,如果Mod想要在其中做些什么,为了考虑兼容性,不想直接修改Minecraft的源代码,那么这些修改源代码的工作就由Forge进行,然后Forge便会在相应的代码中触发事件。
Minecraft游戏事件的触发机制和生命周期事件是类似的。不过,对于Minecraft游戏中的事件,为了与生命周期事件区分,使用的注解也不一样。对于Minecraft游戏事件的监听器,需要为代表监听器的类添加@EventBusSubscriber注解,从而指示这个类中包含若干事件监听器,而在这个类中,@SubscribeEvent注解取代了@EventHandler的位置,用于具体指明哪些方法是真正的事件监听器。在本书中,所有Minecraft游戏事件都可以使用@EventBusSubscriber和@SubscribeEvent两个注解的方式监听。也有一小部分事件不能使用这种方式监听,不过这部分事件已经超出了本书的讨论范围。
首先新建一个事件监听器类。右击zzzz.fmltutor这一包名,然后依次单击New→Java Class:
在弹出的对话框内输入event.EventHandler:
建立了一个名为zzzz.fmltutor.event的子包,并在其中建立了一个名为EventHandler的类。
这个类的完整名称应该是zzzz.fmltutor.event.EventHandler,然后写出下面的代码。在本书后面的章节中,作者将逐一介绍每一行代码的用途:
需要注意的是,package部分和import部分不需要去完成。比如当我们写下@EventBusSubscriber注解时:
无论是IntelliJ IDEA还是Eclipse,都会提示有一个@EventBusSubscriber类,然后这时直接按Enter键。
可以注意到,IntelliJ IDEA已经自动添加了import,并把输入一半的注解自动补全了。在添加方法时也会用到自动补全:
后面几乎所有类似的情况都是如此。在绝大多数情况下,import开头的代码都可以由IntelliJ IDEA或者Eclipse等工具自动生成。
通过阅读代码可以猜到,我们监听了net.minecraftforge.event.entity.EntityJoinWorldEvent事件,从它的名字来看,就是实体加入世界时触发的事件。
3.3.2 静态字段和静态方法
仔细看这个方法的声明(也就是不包含大括号里的部分),并把它和之前在Mod主类中的init方法比较:
除使用的注解和事件的类型不同之外,读者应该很容易发现:这次使用的方法多了一个static。static修饰符代表这个字段是静态(Static)的。静态是指这个字段归属于这个类,而不是归属于这个类的实例。把这句话应用到两个方法上:
● 为FMLTutor类定义了一个非静态(Non Static)的,名为init的方法。
● 为EventHandler类定义了一个静态的,名为onPlayerJoin的方法。
现在再把在Mod主类中定义的几个字段进行验证。
● 调用了FMLTutor类的MODID、NAME和VERSION字段,之所以调用的是类的字段而不是类的实例的字段,是因为这个字段是静态的。
● 调用了Mod主类的logger字段和Blocks类的DIRT字段,这两个字段都是静态的。
● 调用了logger字段对应的对象的info方法和DIRT字段对应的对象的getRegistryName方法,这两个方法都是非静态的。
通过查找这些方法的声明,就可以非常容易地验证它们是静态的还是非静态的。这里以DIRT字段为例,把鼠标指针放在DIRT上,按Ctrl键并单击,然后就可以得到下图的画面:
在net.minecraft.init.Blocks类的声明处我们可以看到static在DIRT之前。
然后再回到Mod主类,把鼠标指针放在“getRegistryName”上,按Ctrl键并单击:
我们可以看到,Block类声明了一个名为getRegistryName的,其返回值为ResourceLocation的实例的非静态方法。在前面章节,输出的正是ResourceLocation代表的内容。
3.3.3 方法和字段的命名惯例
在之前的代码中,已经接触了不同的方法和字段,在通常情况下,它们有两种命名风格:
● 小写驼峰式(Lower Camel Case),小写驼峰式指的是只有第一个单词全部小写,第二个开始的单词的所有字母中只有首字母大写,然后把它们直接拼合起来,比如getRegistryName方法等。
● 全大写加下画线(Upper Snake Case),即将所有字母大写,然后单词之间使用下画线分隔,比如我们在Blocks类看到的所有字段,在Mod主类添加的MODID和VERSION两个字段等。
在通常情况下,Java规定,只有同时被final和static两个修饰符修饰的字段,即不可变静态字段,才应该使用全大写加下画线的命名风格命名,而在其他情况下的方法和字段,都应该使用小写驼峰式命名。
包含Java官方代码在内的大量Java项目都遵循这套命名惯例,虽然这套命名惯例并非强制,但是自觉地遵守这一惯例会大大增加代码可读性。在本书的后续部分出现的代码中,所有方法和字段也都将遵循这样的惯例。
3.3.4 对象的继承关系
现在再回到EventHandler类,也就是它目前监听的唯一事件——EntityJoinWorldEvent。
上述代码省去了import和package声明及注释之后的EntityJoinWorld Event类。读者可以通过按Ctrl键并单击EntityJoinWorldEvent后看到这段代码。
@Cancelable注解的作用是表示这个事件是可以取消的,不过现在还用不到这一点。
现在来看这个类的声明,与之前对类的声明不同,这一声明在类名后又出现了extends和EntityEvent。
先把结论给出:EntityJoinWorldEvent的声明代表它继承(Inherit)了EntityEvent类,同时,还会把EntityJoinWorldEvent类称为EntityEvent类的子类(Subclass)或子类型(Subtype),而EntityEvent类又被称为EntityJoinWorldEvent的父类(Superclass)或父类型(Supertype),又可以说,一个EntityJoinWorldEvent类的实例,同时也是EntityEvent类的实例。
虽然严格上说,即使不考虑基本数据类型,子类和子类型,父类和父类型,也是在描述两类不同的概念,但是由于在Java中,在大部分情况下它们描述的事情是一致的,因此本书也不会探讨两者的区别,相关的内容也已经超出了本书的讨论范围。
点开EntityEvent类,还会注意到它还是net.minecraftforge.fml.common.eventhandler.Event类的子类。因此,EntityJoinWorldEvent类同时是EntityEvent类和Event两个类的子类,它的实例同时也是EntityEvent类和Event类的实例。实际上,所有被@SubscribeEvent注解修饰的方法,都只能监听Event类的实例,也就是其唯一的参数类型都只能是Event类的子类。
对于一个类本身,它只能继承一个类,不过它却可以被多个类继承,因此如果把所有的继承关系集合到一起,则可以形成一个继承树(Inheritance tree)。那么所有类是不是都是同一个类的子类(除了这个类本身)呢?答案是肯定的。对于所有声明中没有声明extends的类,Java规定,它们都将直接继承java.lang.Object类(除Object类本身),换言之,除Object类本身之外,所有类都是Object类的子类。Event的所有子类代表所有可以监听的事件。如果我们想看EntityEvent类都有哪些子类呢?在IntelliJ IDEA中,可以右击EntityEvent这个字符串,然后选择“Find Usages”(默认组合键是Alt+F7):
在出现的字样中找到Usage in extends/implements clause,就可以看到它的所有子类了:
这个功能用于查找一个类分别在什么地方被引用过,是一个开发过程中十分常用的功能。对于Eclipse,不同的用途查找的方式各不相同,对于查找继承类,Eclipse中的组合键是Ctrl+Shift+H,至于其他引用如何在Eclipse中查找,感兴趣的读者可以自行去网上搜索相对应的资料。
实际上,在主类中所有监听生命周期事件的方法,其方法参数类型都必须是net.minecraftforge.fml.common.event.FMLEvent类的子类,比如我们的Mod主类中监听的FMLInitializationEvent类。感兴趣的读者可以自己分析继承关系。
继承是作为一个面向对象的语言的至关重要的特性之一,至于继承会带来什么编写代码上的优势,方法体内部的代码又是什么意思,如何转换一个类的实例和其不同的父类的关系,将在后续章节中具体讲解。