<基于1.8 Forge的Minecraft mod制作经验分享>
这一章其实才应该是第一章,矿物生成里面用到了Event的一些内容。如果你对之前矿物生成那一章的将算法插入ORE_GEN_BUS那块没看懂,那么相信这一章会给你解释清楚。
下面开始逐一分析MC与Forge的Event系统。
一、EventHandler。
很熟悉不是?其实从制作mod的第一步开始,我们已经在于Event打交道了。我们用一个@EventHandler注解,标注了mod主类中的几个带有唯一事件参数方法,从而使这几个方法取得了事件的控制权,从而插入一些操作到MC中去。确切的说,当MC运行开始,Forge就会把这几个方法插进去,使得你运行的MC不再是原来的MC,而是被部分偷梁换柱了的。下面简单回顾一下以前提到过的,几个@EventHandler所接受的Forge的生命周期Event:
-
FMLPreInitializationEvent,源码注释道:Run before anything else. Read your config, create blocks, items, etc, and register them with the {@link GameRegistry}.
也就是说,这个事件是第一个运行的,建议你在这个事件里完成读取配置、创建方块、物品等,以及在GameRegistry中注册它们。
-
FMLInitializationEvent,Do your mod setup. Build whatever data structures you care about. Register recipes, send {@link FMLInterModComms} messages to other mods.
意思是建立你的mod,构造你的数据结构,注册合成表,向其它mod发送FMLInterModComms消息。
-
FMLPostInitializationEvent,Handle interaction with other mods, complete your setup based on this.
与其它mod交互,并基于此完成最后的配置。
这三个事件很常用也很简单,不做过多解释。当然,你可以对着@EventHandler标签Ctrl + clickL,看看Forge还为你提供了哪些可用的事件。唯一需要注意的就是,这三个事件实际上都是在游戏开始前执行的,当你运行打了Forge的MC,看到一个小锤子在那敲的时候,这三个事件就以此发生了。
二、EventBus,本文的重点。
EventBus,事件公交,一般我们称它为事件主线,其实就是观察者模式。按照名称来理解,EventBus是一个交通载体,事件是它的乘客,EventBus会载着它的乘客们贯穿于MC中。但我更喜欢这么来理解:EventBus是一个电台,每一个事件是一则广播。我们用post();方法,向某个EventBus电台发布一则事件广播,然后可以在其它地方用register();方法来注册一个订阅者(观察者),来监听这个事件广播。一旦一个事件被post到一条事件主线上,那么所有这条线上的订阅者都可以监听到它,并对它做出响应。
首先我们需要掌握几个与EventBus交互的几个重要方法:
-
public boolean post(Event event);
发布一个事件广播参数是一个事件的实例,可以是预设的,也可以是你自己的。关键是返回值。这个返回值并不是你的事件发布成功或失败,事实上当你post事件后,事件第一时间就会发给各个订阅者。而这里的返回值,实际上是由这个事件是否被取消决定的(决定!=是,如果取消,返回的是true!如果没有取消,返回的反而是false!!!)。也就是说,如果订阅者对你发布的event,setCanceled(true);,那么post();执行完毕后就会返回true。
但要注意,setCanceled();仅仅决定post();的返回值,而不会改变post();的动作,post();一定会在逐一为每个订阅者发布完事件后返回,每个订阅者自然也必定会监听到事件广播,你无法组织!由此,又引发了另一个问题:最终cancel与否,是由最后一个执行setCanceled();方法的订阅者的选择来决定的!所以,请不要闲的没事随便setCanceled();,如果post();的返回值与你无关,你就不应该参与到这个竞争里面去,无论setCanceled(true);还是setCanceled(false)。
还要注意,根据源码来看,EventBus的订阅者目前是被一个ArrayList所记录的,所以它是有序的,但我仍然不建议你做任何依赖于订阅者订阅先后顺序的操作,因为这个ArrayList是完全的内部实现,不是对外约定的一部分,Forge可以随时轻易的换掉它,这会为你的mod造成很大的遗留问题隐患。
-
public void register(Object target);
注册一个订阅者订阅者可以是一个任何形式的类,它必须具备一个监听器。监听器是一个带有@SubscribeEvent注解的、含有唯一参数为一个Event的方法。你可以把这个监听方法写在你的主类或Proxy里,然后任性的evenBus.register(this);,稍好些的童鞋可能会传入一个匿名内部类。但我建议你新建几个类作为订阅者,最起码也要用几个内部类的实例域或静态内部类,理由是,你可能会需要订阅多个分属于不同EventBus主线上的事件,那么把主类作为订阅者,把所有监听器放在里面,注册this,就会一股脑的在每个主线上注册同一个订阅者,这个订阅者包含了你所有的监听器方法,而不管这个监听器要订阅的事件是否在这个主线上,以后每次有事件广播时,还可能会平白多出很多次没必要的响应。另一方面,也为接下来的注销订阅者带来了困难,因为这个订阅者会带走所有的监听器。与前面几次不同的是,订阅者注册时一次性的,而不是像生成算法那样频繁的调用,所以分别创建几个不同的订阅者,管理不同的事件监听并不是问题。
-
public void register(Object target);
注销一个订阅者。
一般情况下,我们如果hold住之前注册的订阅者,那么现在把它原样传回去就ok。如果不能呢?当我们之前的实例无法获取或者已经不在了,该怎么办?如果你听从了我之前的建议(什么建议?呵呵,往上翻,认真看),那么还有救。严谨的Java程序猿的应该知道:1、要实现从集合中删除对象这类操作,一定要先为这个对象的类重写
public boolean equals(Object obj);
方法。2、如果要重新equals();,那么最好也同时重写掉public int hashCode();
第一条还好,第二条却很容易被忽略,而很不幸的,据源码观察,Forge在这个方法内使用了ConcurrentHashMap的remove方法。当注销一个订阅者时,ConcurrentHashMap会使用hashCode来辨识你传入的object,所以如果你没有重写hashCode,很可能掉坑。如果你掉了,这次真不是Forge的错,是你自己zuo死。那么是不是意味着,咱就覆写hashCode();就行了呢?当然不是。因为ConcurrentHashMap同样是属于Forge的内部实现,不是对外约定的一部分,人家随时可能选择更好的方案。至于你,老老实实的按照规范,equals();、hashCode();一起复写吧,然后你就可以new一个新的对象,把它作为参数传入。
再接下来,我们还需掌握几个重要的Event的通用方法:
-
观察
@Cancelable ...... @hasResult ......
注解。是的,我把它作为一个方法,因为前者其实等价于
public boolean isCancelable();
,后者等价于public boolean hasResult();
。如果一个事件是可取消的,那么isCancelable();将返回true,并且按照约定,它会得到一个@Cancelable注解。@hasResult同理。当你拿不准一个event是否可以被取消,或者是否可以有一个返回值,其实更多的情况下二者只取其一,我们只是想要让某个event不在执行,弄不清是应该用setCanceled(true);还是setResult(Event.Result.Deny);来取消它,那么就可以Ctrl + clickL,到源码里去寻找上面两个注解,以及对应的方法,来确定该怎么做。鉴于你从上面的方法里得到的方便,也请你也遵守这个约定来给别人方便:如果你自定义的事件复写了isCanelable();或hasResult();,并返回了true,请打上相应的个注解。继承自Event且未手动重写的这两个方法默认返回值为false,但如果你继承了它的子类,并且不打算重写这两个方法,那么请一定要看看它的子类是否打了注解、返回true,如果是,这个方法的返回值对你是否重要,是,则请打上注解,否,建议你把它们重写回false。
-
public void setCanceled(boolean cancel); public void public void setResult(Result value);
前文已经讨论了setCanceled();的注意事项和建议,这里再次总结一下:setCanceled();可以设定event的isCanceled属性,但最终的值总是由最后一次执行的setCanceled();操作决定。如果是否取消对你不重要,那么就不要set,无论set个true还是false。如果当前event并没有@Cancelable注解、isCancelable();的返回值为false,请不要setCanceled();,无论true还是false。
这些注意事项和建议也同样适用于setResult();,不过特殊的是,Event提供了一个公开的Result枚举,它只有三个元素:DENY, DEFAULT, ALLOW,而不是像isCanceled那样的倒霉boolean。你可以通过setResult(Event.Result.DEFAULT);,来一定程度降低一些其它订阅者搞出的乱子,甚至你可以更精确的,通过getResult();方法,来看看是否由其他订阅者做了羞羞事。最后,除非你确定其它订阅者的确造成了麻烦,否则还是别参与到set的竞争里面去。
-
public Result getResult(); public ListenerList getListenerList();
getResult();方法可以获取到event的result域,这是个Result枚举的实例,通过它,你可以与事件的发布者,甚至其它订阅者交流。与事件的发布者交流,这很好理解,与其它订阅者交流是怎么回事呢?前文说过,Result是个public的枚举类,你可以为一个event对象替换掉它的Result,但你需要保证的新Result枚举中仍然有DENY, DEFAULT, ALLOW这三个元素,最好也不要添加新的元素名,以免引起不必要的麻烦。但你可以为元素增加新的属性,这样不就可以做到与其它订阅者的交流了。什么?我怎么想到的?仔细看看Event类吧,它有一个getListenerList();方法,这个方法显然是拿来获取监听者列表的,也就是我前面一直说的其它订阅者。为什么这里不叫getSubscriberList();,应该是因为,我们实际得到且需要的并不是其它订阅者,而是它们的监听方法吧(吐槽一下,Forge的各种命名系统真心太坑,还记得前面的Json那张不。。。)。那么,既然能获取到其他订阅者的监听器,自然也就可以与其它订阅者交流咯,怎么交流呢?联系Result那个奇怪的非静态非不可变枚举,一切水到渠成。
呼~终于掌握了足够的与EventBus、Event交互的方法,来到了最后一步:看看四条Forge为我们提供的EventBus。(童鞋挺住啊,斗罗大坑在前方等待着你呢)认识这四条主线很重要,如果事件在一条线上发布、广播了,你却跑到另一条线上去订阅、收听,怎么可能收到呢。什么?为什么会有这么多条“主线”?为了分工啊童鞋,不然所有的发布事件、注册订阅者都对同一条总线操作,会堵车的啊,则也再次印证了我上文的建议——建立多个订阅者,分管不同的事件:你看,Forge都这么做了。你虽然没办法得到一条真正的“主线”,但你可以直接在ListenerList层面上进行操作,它有大量公开的诸如
public void register(int id, EventPriority priority, IEventListener listener);之类的定制程度很高的方法,第一个参数id就是主线的id,一个事件的所有存在监听器的主线都被放在了一个ListenerListInst[]中,所以这个id其实应该是主线在[]里的index。ListenerList里面还有一个静态的allLists域,虽然是私有的,但也能看出它拥有很高的自定义度了。不过目前我还没有实际运用过,所以暂不讨论,有兴趣的童鞋自行探索。
-
FMLCommonHandler.eventBus:
这条主线是由FML提供的,主要负责最基本的事件的汇总。
FML,ForgeModLoader,顾名思义,是用来加载Mod的,比Forge更加底层。所以发布在这里面的事件,都是些最基本层面的东西,它们基本位于net.minecraftforge.fml.common及其子包中,比如InputEvent、PlayerEvent、TickEvent。现在不对这几个事件做过多解释,以后用到了再说。通常我们不应该往这么基础的主线里发布事件,事实上真正需要发布到这里的事件也就前面那三个。但我们可能经常需要从这里面订阅事件,来实现键盘按键、鼠标点击之类的事件的监听。
要取得这条主线,你需要先取得一个FMLCommonHandler的实例,然后调用它的bus();方法获取它的eventBus域:
FMLCommonHandler.instance().bus();
。接着你就可以在这里订阅你要监听的事件了。
-
MinecraftForge.ORE_GEN_BUS:
这条主线由Forge提供,上一章教程没看懂的童鞋要仔细了。
顾名思义(我怎么总爱用这个词?嗯,一定是Java的错),这条事件主线负责矿物(ore)的生成(generate),所以准确的说,它是一条矿物生成事件专线。矿物生成事件是位于net.minecraftforge.event.terraingen包下面的OreGenEvent类,它还有几个子类:Pre、Post、GenerateMinable,发布的时机分别是在矿物准备生成前、矿物已经生成后、可开采的矿物生成前,其中最常用的无疑是GenerateMinable,这里讲细一点,来弥补上一章的部分内容。OreGenEvent有三个公开不可变域(但不是常量域,非静态,你不能在类上调用它们),world、rand、pos,分别持有当前世界、当前的随机数发生器、区块位置三个对象。Pre、Post只是简单继承了OreGenEvent,同上。而GenerateMinable在OreGenEvent的基础上,扩展了两个自己的公开不可变域:type、generator,持有要生成的矿物类型的枚举实例与一个矿物生成器,以及一个可变的枚举静态域(但注意,枚举本身不可变,你只能选择暴力的替换它)。结合上一章,我们现在可以明白,“把矿物生成算法插入到游戏中”,实际做的是注册了一个矿物生成事件的监听器(MinecraftForge.ORE_GEN_BUS.register(new MinableOreGen()),订阅了矿物生成的事件(在监听器MinableOreGen类中用@SubscribeEvent注解一个以OreGenEventGenerateMinable evnt事件为唯一参数的方法,来订阅一个OreGenEventGenerateMinable event事件),并在矿物生成事件发布时响应它(在这个方法里,生成自己的矿石),最后把你的回应绑定在这个事件上(event.setResult();)。
看命名就知道这是一个常量域,你无需取得MinecraftForge的实例就可直接调用这个public的域。如何?现在明白上一章的内容了没?豁然开朗是吧?什么?晕了?抱歉,这是进阶篇,挺住。。。
-
MinecraftForge.TERRAIN_GEN_BUS:
除去矿石,MC里还有各种地形地貌。这条总线就是用来汇总地形生成事件的。
TERRAIN_GEN_BUS主线所管理的事件应该都在net.minecraftforge.event.terraingen这个包下面,可以看到挺多的。为什么你看到了BiomeEvent(生物群落事件)?别忘了MC中的生物群落是伴随地貌的。你居然还看到了OreGenEvent?是的,矿石生成算是地形的一部分,放在这里面也无可厚非。所以,我把ORE_GEN_BUS放在了前面讲,因为它的辖域较小,记住OreGenEvent显然是由ORE_GEN_BUS专线管理,那么net.minecraftforge.event.terraingen包里其它的事件就交给TERRAIN_GEN_BUS了。什么?SaplingGrowTreeEvent?这个我也是醉了,估计树木生成也是在创建世界就开始了,而且树木生长本质也就是放置新的方块,所以就算入terraingen了吧。如何?这些事件够用不?不够?没关系,随便点开一个看看,就比如BiomeEvent,那么你会看到它里面又有几个静态内部类,其它的也一样如此,所以Forge提供的可用事件其实是非常丰富的。至于具体的用法,那么多事件怎么说的完,如果用的到我再说吧。其实认真看前面的内容,掌握了方法,那么你就应该有些思路了吧。然后就自己摸索呗,我不也是自己摸索的。
-
MinecraftForge.EVENT_BUS:
除去地形地貌,MC里还有很多其它的事件,这些就交给它了。
有了之前ORE_GEN_BUS到TERRAIN_GEN_BUS的经验,应该就不难理解这种从小范围逐一排除的分类方式了。除了net.minecraftforge.event.terraingen这个包以外的其它Forge定义的事件,基本散落在net.minecraftforge.event、net.minecraftforge.client.event这两个包及其子包,它们都归EVENT_BUS这条主线负责。一些诸如UI渲染之类的事件,显然只能发生在客户端,所以在net.minecraftforge.client.event包里找,其它去net.minecraftforge.event,这不难理解吧。
终于结束了,后面我们要涉及的问题越来越深入,文章也会越来越长。别怪我不分成小章节,这毕竟不是系统的书本教程,我需要让大家遇到什么问题后能很清楚的找到对它的分析与解决它的所有相关知识点,以免出纰漏,自然不能把关联的问题分的太散,否则可能需要你读好几章才能彻底搞明白一个问题,万一漏读了一章会出大麻烦。我只能说,我会尽量把章与章之间的界限划分的更加明显。
GitHub链接:https://github.com/zhengxiaoyao0716/DouroMod
斗罗大坑真不是我一个人能完成的,我甚至都不敢肯定自己会不会弃坑,所以我公开源码,分析源码,还专门写这些文章来分享我的经验,就是为了拉你一起入坑。都说咱国人盗版强,MC如此开放,Notch如此宽容,咱倒是做一个震撼点的mod出来啊。