基于FML的MinecraftMod制作教程(1) - Forge的事件系统

"That,good sir,is but a phantasy of yours."
-Arthur Dimmesdale,The Scarlet Letter

在等东方心绮楼的试玩版发布前有些无聊,因此就写一写Forge的事件系统吧.

事件(Event)系统是Forge在4.0版本后引入的,用最通俗的话解释,事件就是一件事发生后,就会触发与它相关联的事.比如一个玩家不幸掉进岩浆里受到伤害,就会引发一个事件(之后我们会了解到这个事件是LivingHurtEvent事件),Forge的系统会将这个事件派发给相关联的方法(Method),那些方法来决定是否处理,如何处理.
很不幸这仅仅只是通俗的解释,实际实现起来要比这个困难一些,首先Java不原生支持事件,它采用的替代方案(基于监听者模式,使用监听器和匿名类)相当令人恶心,因此Forge设计了一套自己的事件系统(基于发布/订阅模式).Forge的事件总线(EventBus)是事件系统的处理中枢,任何人都可以发布事件,任何人也都可以订阅事件并在事件被发布时接收到它.如果有复数个订阅者订阅了同一事件,那么系统会将事件按优先级让订阅者们依次传阅,事件在被传阅完毕后会重新交还给系统并返还给发布者.游戏中支持的事件类型是被预先编程好的,当然开发者可以自己定制事件.

订阅一个事件

事件的订阅是通过Annotation来实现,任何一个被添加了@SubscribeEvent的方法都会被视为事件订阅者,但要想实际起作用必须满足这三个条件:
①:方法有且只有一个参数,并且此参数的类型必须是Event类或其派生类.
②:方法的访问级是public,不能是静态(static)
③:方法必须属于一个非静态的类.

(注:这里所说的Event是指net.minecraftforge.event包下的Event类,而不是cpw.mods.fml.common.event包下的FMLEvent)

首先先添加一个处理事件的方法(我这里添加到了Mod主类当中),比如这里我想在订阅"生物下落"(一个生物从高处摔到地上时触发),就让它的参数是LivingFallEvent.然后为它加上@SubscribeEvent

@SubscribeEvent
	public void test1(LivingFallEvent event)
	{
		if(event.entityLiving instanceof EntityPlayer)
		{
			EntityPlayer entityPlayer = (EntityPlayer)event.entityLiving;
			entityPlayer.addChatMessage(new ChatComponentText("Falling Star! You fell "+event.distance+ " meters.That's cool, man!"));
		}
	}

然后我们要通过MinecraftForge.EVENT_BUS.register来注册方法所在的那个类,通常来说我们是在Mod载入时进行注册,如果你将处理事件的方法单独写在一个类中(假设叫做EventContainer),那么就在Mod主类的Init方法中添加
MinecraftForge.EVENT_BUS.register(new EventContainer());
如果你直接把处理事件的方法写在了Mod主类中,那就直接在Init方法中添加
MinecraftForge.EVENT_BUS.register(this);
(顺便一提,EVENT_BUS也提供了一个相应的方法来进行反注册,也就是将已注册的实例解除掉)

E1-1
注:这些图是我在旧版时截的,因此sendChatToPlayer和新版有差别...

之后进入游戏测试一下(注意要在生存/H模式下,创造模式玩家触发不了LivingFallEvent事件哟...)

e1-2
到了游戏中确实有效...可是这坑爹玩意却一口气来了两个啊!
这是因为新版Minecraft的单人模式采用了本地服务器模拟的方式,虽然你在运行着单人模式,但实质是你在本机运行着一个服务器,然后你的客户端连接着你自己的本机服务器.然而客户端和服务器都个会有一个玩家实体(EntityPlayer的派生类),客户端的是net.minecraft.client.entity.EntityClientPlayerMP,服务器端的是net.minecraft.entity.player.EntityPlayerMP.两者都继承自EntityPlayer,因此我们在验证event.entityLiving instanceof EntityPlayer会返回true.这时我们就需要讨论如何取舍了.

执行端的选择,事件的监听位置,事件的过滤

我们先依次说起这三件事,首先是执行端的选择,我们是在服务器端对事件进行响应,还是在客户端?通常来说,我们认为凡是需要执行实际操作的东西都要放在服务器端,毕竟客户端和服务器端存在延迟(对于更糟的情况,客户端还会存在作弊插件),当玩家跳到地上时,他可能会认为自己完成了一次"下落",它的客户端也是这样认为,因为EntityClientPlayerMP这个实体确确实实落地了,并在客户端引发了一个LivingFallEvent事件.然而由于延迟的存在(某些服务器能存在10秒的延迟),服务器端认为EntityPlayerMP依然停留在远处的一个平地,并没有进行下落,由此便产生了分歧,究竟玩家是否从高处落下来了?很不幸在这里服务器是大佬,它认为既然EntityPlayerMP没有下落,那玩家就是没有下落,不管客户端再怎么闹腾也没有作用,如果客户端是个图形效果增强Mod之类的不影响实际数据的Mod,也许问题不大,如果是会对游戏产生实际影响的Mod,只是神知道接下来会发生什么可怕的事情,有可能是数据不同步造成的断线.不过假如玩家足够幸运,在更糟糕的事情发生之前让自己的移动数据发送到了服务器那,让服务器做出及时更新的话,或许结果会好一点,但我们禁不起这样的风险,玩家会认为是我们的Mod有问题,而服务器管理者会考虑卸载掉我们的Mod,毕竟与其花高价租个更好的服务器,管理者更倾向于点点鼠标删掉一个Mod,然后随便敷衍玩家几句了事.在这里,我选择在服务器端处理这个事件.对于如何控制执行端,一个是通过事件的过滤,一个是通过@SideOnly,但我不建议使用@SideOnly,除非是必要,否则最好不要考虑它.
对于事件的监听位置,我建议在一个统一的类内执行,但也并非没有特例,设想这样的一个情景,我们做了一把很NB的剑,被它砍中的实体会立刻飞到高度255的地方然后信仰之坠,而且我们为了秀技术刻意不使用onLeftClickEntity方法,而使用一个订阅LivingHurtEvent事件的方法,那么我们该怎么做?我们可以将事件放在Mod主类里,也可以专门建立一个类来监听,但我们也可以把它直接放在那把剑的物品类里,并在"很NB"的剑的类的构造函数(希望你没看晕)内注册它自己.别忘了Minecraft的物品是采用类似于享元模式的方式啊...但注意不要让事件监听被注册的太多,否则会对性能产生巨大的负面影响.
最后我们到了事件的过滤这个问题了,Forge的事件系统的发布方式需要稍微解释一下,Forge的所有事件(再次强调,不是FML的事件)都继承自Event类,如果你订阅了一个事件(类),那么任何该类及其派生类都会在发布时发送给你.比如LivingFallEvent就是继承自EntityEvent,那么如果你订阅了EntityEvent的话,不光是EntityEvent事件发布时你会收到一份,LivingFallEvent事件发布时也会发给你一份,因为LivingFallEvent是EntityEvent的派生类."既然你连大事都管了,那么小事你也得管",这或许和现实以及大多数人的认识相反,但这确实是Forge的事件系统的实际写照.因此事件的过滤就分为两部分,首先是事件类型的过滤,如果你真的订阅EntityEvent事件的话我建议你一定要三思,因为LivingUpdateEvent这个事件是每一个实体在每一个Tick(帧)都要执行一次,你最好把它过滤掉,它的过滤判断表达式是"传来的事件 instanceof LivingUpdateEvent == false".第二部分的过滤是事件源的过滤,就拿我们实际遇到的情况举例,EntityClientPlayerMP和EntityPlayerMP都会在同一时间引发相同的事件,既然我选择在服务器端执行更新,那么我就过滤掉只存在于客户端的EntityClientPlayerMP,保留EntityPlayerMP.这个的过滤方法就有很多,对事件的参数进行判断就行了.
另外再提出一个忠告:订阅Event事件之前请三思.

小小修改一下代码,将

event.entityLiving instanceof EntityPlayer

改为

event.entityLiving instanceof EntityPlayerMP

(至于下面的"(EntityPlayer)event.entityLiving"是否修改,在这里是随便的,毕竟我只是想过滤一下事件源而已...)

再次测试,这一次我们成功了.(教练我懒得截图了...自行脑补成功的样子)
(说的轻巧,事实上作者为了研究这个问题烧了一个晚上的时间...)

事件的优先权

有些时候,会有复数个订阅者同时监听着一个事件,有些情况下他们是互相竞争的,因此就有了优先权的设定,优先权分为五级:最高,高,普通(这个是默认的),低,最低.事实上只有前三个才会被开发者考虑,SB才会让自己的事件排到最低级...对吧= = 所以必要时就有些公德心吧,别把无聊的东西排在高优先级处.
安排优先级的方法是修改@SubscribeEvent的priority属性.比如:

@SubscribeEvent(priority = EventPriority.HIGH)

这个订阅的优先级是High,它只会和同优先级的订阅者竞争,对于比它低的订阅者,它占有绝对的优先权.我们这就来试试它的效果,将我们之前的方法改为(即删掉整个test1然后输入下面的代码):

@SubscribeEvent(priority = EventPriority.HIGH)
 public void test2a(LivingFallEvent event)
 {
 if(event.entityLiving instanceof EntityPlayerMP)
 {
 EntityPlayer entityPlayer = (EntityPlayer)event.entityLiving;
 entityPlayer.addChatMessage(new ChatComponentText(
 "Falling Star! You fell "+event.distance+ " meters. That's cool, man! I will protect you from the falling damage!"));
 event.distance = 0.0f;
 }
 }

 @SubscribeEvent(priority = EventPriority.LOW)
 public void test2b(LivingFallEvent event)
 {
 if(event.entityLiving instanceof EntityPlayerMP)
 {
 EntityPlayer entityPlayer = (EntityPlayer)event.entityLiving;
 entityPlayer.addChatMessage(new ChatComponentText(
 "Falling fool! You had fallen... Damn it, WTF "+event.distance+ " meters? Are you fooling me?"));
 }
 }

再次进行测试,这次无论我们从多高的地方跳下去,都不会受伤(虽然会发出pia的声音),因为test2a方法先截获了跌落事件,并篡改了结果,因此它即糊弄了test2b方法,又欺骗了Minecraft的系统.使得玩家免遭伤筋断骨之苦.
e1-5

事件的结果

有时我们我们处理完一个事件后,需要告诉后来人我们已经处理掉它了,还有些情况下,这个事件最终会传回Minecraft内核,供它就事件的结果进行决策,因此我们就有了Result这个东西.
Result是一个Enum(枚举)类型.我们通过setResult和getResult来设置或读取.Result有三个值:ALLOW(允许),DENY(否决)和DEFAULT(默认),然而,这玩意耍起来有风险.
首先,有些事件的Result是供你随便耍的,有些则会影响到游戏的运行,前者我称之为无Result事件,后者我称其为有Result事件,判断它的办法是调用hasResult方法,如果它返回true就代表它有Result,那么就请仔细阅读这个事件的JavaDoc.比如玩家拿着水桶对着物体(不一定是实体)按下鼠标右键时引发的FillBucketEvent事件就是有Result的事件,如果在最终传回游戏内核时它的Result的结果是ALLOW的话,那么就会强制视为一次成功的操作.对于无Result的事件,它仅仅只是"Result这个参数对它没有意义",而不代表我们用不了Result.LivingFallEvent就是无Result事件,但我们接下来就用它展示一下Result的使用.
(另外,由于Forge制作组的乌龙,不要将Result和某些事件里的result参数搞混,前者才是我们现在所说的Result,后者只是代表着传来事件的某一参数.)

在test2a中加入:

event.setResult(Result.DENY);

在test2b中加入:

entityPlayer.addChatMessage(new ChatComponentText(
 "I understood... She made the event " + event.getResult().toString() + "."));

e1-4

之后进入游戏测试一下.

E1-2

事件的取消

有时在我们执行完一个事件的操作后,希望能取消掉这次事件,让后面的订阅者无法处理它,同时也让游戏内核认为这个事件没有发生.对于任何具有Cancelable(可取消)属性的事件来说,我们都可以实现这一目标.
要判断一个事件是否是可取消的,就调用它的isCancelable方法,如果返回的是true就代表它是可取消的,此外你也可以通过在IDE中直接观察那个事件是否具有@Cancelable注释来判断它能否被取消.
e1-6
图:LivingFallEvent具有@Cancelable注释,因此它是可取消的.

设置一个事件被取消的办法是调用它的setCanceled方法,setCanceled的参数是一个bool,true代表此事件取消,被取消的事件在调用isCanceled时会返回true,并且尚未接收它的订阅者们默认将不再会接收它,Forge的事件系统也将向发布者返回一个true(默认是返回false,具体的返回机制我会在后面解释),对于已取消的事件,你也可以通过setCanceled(false)来复活它.
(注意,对于不可取消的事件,千万不要setCanceled(true),游戏会立刻抛错的)

现在,将我们的test2a方法中的

event.distance = 0.0f;

替换为

event.setCanceled(true);

然后保存,测试.

这一次你依然是不会受到跌落伤害,甚至都不会收到第二个订阅者的信息,因为你将这个事件取消掉了.

那么被取消的事件该如何复活呢?如果要让别的订阅者复活它的话,首先我们得让那个订阅者能接收它,默认情况下订阅者不会再接收被取消的事件,但我们可以通过修改@SubscribeEvent的receiveCanceled属性来让它能接收被取消的事件.对于任何receiveCanceled = true的订阅者来说,它都可以接收被取消的事件.

将我们的test2b替换为

@SubscribeEvent(priority = EventPriority.LOW,receiveCanceled = true)
 public void test2b(LivingFallEvent event)
 {
 if(event.entityLiving instanceof EntityPlayerMP)
 {
 EntityPlayer entityPlayer = (EntityPlayer)event.entityLiving;
 entityPlayer.addChatMessage(new ChatComponentText("F@CK U Maribel!I know you are there!"));
 event.setCanceled(false);
 }
 }

E-10

再次测试,这一次玛艾露贝莉可保不了你了.
E-11

事件的发布

事件的发布不比发一颗卫星容易多少,首先你要掌握轨道事件学,应用事件学,量子事件学,理论事件学,高维度事件学,隙间事件学,虚数维度事件学,亚空间事件学,节点空间事件学,迁跃断层事件学,翘曲事件学,子维度事件学,相对事件学,形而上事件学,混沌事件学,核事件学,高等事件学,代数事件学,几何事件学,事件工程学,事件哲学,事件心理学.此外你还得了解十事件问题,最小事件问题,事件树等算法.最后你还得祈祷Yukari在和Cthulhu对轰时不会从你这里神隐走几个即将发送的事件,当做弹幕发射出去.

幸运的是,这些事情Forge全帮你包办了!所以发布一个事件的方法是很容易的!

开发者可以通过调用事件总线(EVENT_BUS)的post方法来发布一个事件,post方法会返回一个bool值,true代表这个事件被取消了,false代表它顺利执行到了最后.建议的格式是这样的.

初始化一个事件的实例
MinecraftForge.EVENT_BUS.post(事件)
判断事件的结果

看到的时候被雷了一下对吧...我这里破天荒地使用了伪代码,因为我自己实在懒得编一个抛出事件的例子,因此我就以Minecraft的ItemBow(弓)的拉弓部分为例:

ArrowNockEvent event = new ArrowNockEvent(par3EntityPlayer, par1ItemStack);
MinecraftForge.EVENT_BUS.post(event);
if (event.isCanceled())
{
return event.result;
}

ArrowNockEvent是拉弓事件,它的参数分别是触发玩家的实体和玩家所拿的弓.
MinecraftForge.EVENT_BUS.post(event)则是将刚刚初始化完毕的事件向订阅者发送出去,事件总线会完成事件的发布工作(为你省了很多事呢!笑).并最终返还给你返回值.
之后就到了对事件的处理工作了,由面向对象的特性可知,event这个事件在被发布出去后已经被各位订阅者们糟蹋了一通了.因此我们不必写一个专门接收处理结果的代码,直接对event操作就能获取处理结果.
(小吐槽:其实官方的那段代码可以被简化的,post返回的就是isCanceled的结果)

自定义事件

现在开始才是高潮吧,Satori?
自定义事件的发布和固有事件的发布没有差别,唯一问题在于如何创建自定义事件.
既然所有事件都是继承自Event类,那我们也创建一个继承自Event的类好了.

e1-9

并没有太多需要注意的东西,一个是在构造函数中要调用基类的构造函数(super();),第二就是如果你不希望订阅者修改你的事件中的参数的话,就将它们声明为final.

此外我们可以设定它有无Result,能否被取消.
设定一个事件有Result,是在它的类上方加入@Event.HasResult
设定一个事件可以取消,是在它的类上方加入@Cancelable
e1-10

之后我写了一个很无聊的代码,它的作用是在玩家输入KABOOM时让自己周围的生物全都遭遇一次爆炸.

	@SubscribeEvent
	public void letsrock(ServerChatEvent event)
	{
		if(event.message.equals("KABOOM"))
		{
			event.setCanceled(true);//截获玩家的指令并不让它显示在屏幕上,用来模拟游戏指令(Command)
			EntityPlayer player = event.player;
			EventHANDRU eventHANDRU = new EventHANDRU(player);//初始化一个事件
			MinecraftForge.EVENT_BUS.post(eventHANDRU);//发布它
			if(eventHANDRU.getResult() == Result.ALLOW)
			{
				//这个长的让人发指的东西是获取玩家附近的生物
				List list = player.worldObj.getEntitiesWithinAABB(EntityLiving.class, AxisAlignedBB.getBoundingBox(player.posX-30D, player.posY-20D, player.posZ-30D, player.posX+30D, player.posY+20D, player.posZ+30D));
				//值得一提的是我这里使用的是遍历器,传统的下标遍历因为无法锁定资源可能导致ConcurrentModificationException...
				for(Iterator iterator = list.iterator();iterator.hasNext();)
				{
					EntityLiving entity = (EntityLiving)iterator.next();
					if(entity.equals(player)) //别把自己也给炸了...
					{
						continue;
					}
					player.worldObj.createExplosion(player, entity.posX, entity.posY, entity.posZ, 4f, true);
				}
			}
		}
	}

	@SubscribeEvent
	public void goodbyeRenko(EventHANDRU event)
	{
event.entityPlayer.addChatMessage(new ChatComponentText("Have a nice day, Renko Usami."));//欢迎来到冥界,宇佐见莲子.
		event.setResult(Result.ALLOW);
	}

进入游戏后输入KABOOM,然后看烟花吧...不过有些耐操的动物比如猪可能无法一发炸死.
E-12

结尾

"别这样,紫,我们都是女孩子..."
-Reimu Hakurei,紫妹异闻录

就这样结束了吗...也许是吧,Forge的事件系统就只有这么点东西可说.然而如果你有兴趣,你可以去研究更加高深的东西,例如事件的getListenerList是什么东西?FML的事件系统怎么使用?它和Forge的事件系统有什么关系?学海无涯啊少年...愿你在Forge之路上愈行愈远吧,装哉我大Forge!F@CK THE BUKKIT!

附录1.OMG到底有几条总线!?
事实上,Forge不只有Event_Bus这1个事件总线,Forge总共有3个事件总线,分别是TERRAIN_GEN_BUS(地形生成总线),ORE_GEN_BUS(矿物生成总线)和EVENT_BUS(我们敬爱的,事件总线).3个总线很好区分,分工也很明确,我就不详细说明了...(群众:我们去年买了个表)