"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也提供了一个相应的方法来进行反注册,也就是将已注册的实例解除掉)
注:这些图是我在旧版时截的,因此sendChatToPlayer和新版有差别...
之后进入游戏测试一下(注意要在生存/H模式下,创造模式玩家触发不了LivingFallEvent事件哟...)
到了游戏中确实有效...可是这坑爹玩意却一口气来了两个啊!
这是因为新版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的系统.使得玩家免遭伤筋断骨之苦.
事件的结果
有时我们我们处理完一个事件后,需要告诉后来人我们已经处理掉它了,还有些情况下,这个事件最终会传回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() + "."));
之后进入游戏测试一下.
事件的取消
有时在我们执行完一个事件的操作后,希望能取消掉这次事件,让后面的订阅者无法处理它,同时也让游戏内核认为这个事件没有发生.对于任何具有Cancelable(可取消)属性的事件来说,我们都可以实现这一目标.
要判断一个事件是否是可取消的,就调用它的isCancelable方法,如果返回的是true就代表它是可取消的,此外你也可以通过在IDE中直接观察那个事件是否具有@Cancelable注释来判断它能否被取消.
图: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); } }
事件的发布
事件的发布不比发一颗卫星容易多少,首先你要掌握轨道事件学,应用事件学,量子事件学,理论事件学,高维度事件学,隙间事件学,虚数维度事件学,亚空间事件学,节点空间事件学,迁跃断层事件学,翘曲事件学,子维度事件学,相对事件学,形而上事件学,混沌事件学,核事件学,高等事件学,代数事件学,几何事件学,事件工程学,事件哲学,事件心理学.此外你还得了解十事件问题,最小事件问题,事件树等算法.最后你还得祈祷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的类好了.
并没有太多需要注意的东西,一个是在构造函数中要调用基类的构造函数(super();),第二就是如果你不希望订阅者修改你的事件中的参数的话,就将它们声明为final.
此外我们可以设定它有无Result,能否被取消.
设定一个事件有Result,是在它的类上方加入@Event.HasResult
设定一个事件可以取消,是在它的类上方加入@Cancelable
之后我写了一个很无聊的代码,它的作用是在玩家输入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,然后看烟花吧...不过有些耐操的动物比如猪可能无法一发炸死.
结尾
"别这样,紫,我们都是女孩子..."
-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个总线很好区分,分工也很明确,我就不详细说明了...(群众:我们去年买了个表)
あの...那个test2b是闹哪样啊?就是那个调戏z......呜......呜...
-v-~
SZ会写Java?
为什么不会...
szszss ,謝謝你在第二章給我的"字符串的演示package", 裡面只有一個檔案, 而且程式碼相當簡潔, 正合我胃口.
程式碼內容看了很多遍, 我是拿著你的程式碼一步一步的KEY進去我的新專案, 在編輯過程中一直遇到兩個Errors, 訊息是
RenderGameOverlayEvent cannot be resolved to a type.
TEXT cannot be resolved or is not a field.
然後我就去查最上面的 import, 我發覺我的Eclipse它一直找不到這兩個
import net.minecraftforge.client.event.RenderGameOverlayEvent;
import net.minecraftforge.client.event.RenderGameOverlayEvent.ElementType;
然後, 我又去查Package Explorer顯示所載入 Minecraft 的package, 我找了
net.minecraft.client.renderer
net.minecraft.client.renderer.culling
net.minecraft.client.renderer.entity
net.minecraft.client.renderer.texture
net.minecraft.client.renderer.tileentity
這五個, 它裡面都沒有.
我想請問一下
net.minecraftforge.client.event.RenderGameOverlayEvent;
net.minecraftforge.client.event.RenderGameOverlayEvent.ElementType;
這兩個包你是從哪裡得來的, 為什麼我的1.5.1版裡都沒看到?
正常情況下, 如果程式編輯器偵測到我們輸入的程式碼有需要的package , 只要我們選擇 import 那一項後它會自動幫我補進JAVA文件的import區裡面, 現在的問題就是我在Eclipse編輯器裡看Minecraft的所有包裡面沒有這兩個package.
ElementType.TEXT 這個欄位也找不到, 我把這字串刪到剩 ElementType, 重新輸入, 後面輸入 點 就出現小選單一堆可以選的欄位, 裡面就是沒有TEXT 這欄位.
我用的Minecraft 是標準的1.5.1正常版, Forge 也是 minecraftforge-src-1.5.1-7.7.1.611.zip 標準版, MCP是由forge安裝時自動下載自動安裝的, 應該也是標準版, 我用的這些軟體版本跟你的不一樣嗎?
7.7.1.611確實有些舊了...RenderGameOverlayEvent是7.7.1.665新增的哦^ ^ 快去http://files.minecraftforge.net/下新版吧
謝謝你告訴我, 我查看了一下我目前用的Forge版本與 http://www.minecraftforge.net/forum/index.php/board,3.0.html
官網的版本, 真的差很大...
我又得重灌了XD
我下載了 minecraftforge-src-1.5.1-7.7.2.678.zip 這版本來使用.
雖然能成功顯示了, 但有一點我不明白, 不知道是不是Eclipse編輯器的問題, 在
if(event.type == ElementType.TEXT && RenderManager.instance.getFontRenderer() != null)
這一段程式碼裡, ElementType.TEXT 這句一直用不出來, import 方面它也沒有自動幫我載入
import net.minecraftforge.client.event.RenderGameOverlayEvent.ElementType;
然後我改輸入elementtype.TEXT 後, 滑鼠指過去停留一下, 它就顯示一些可以選的項目, 沒有出現 import 的項目, 後來我就選 change 那一項, 結果它自動幫我變成了這樣子
if(event.type== net.minecraftforge.client.event.RenderGameOverlayEvent.ElementType.TEXT && RenderManager.instance.getFontRenderer()!= null){
然後紅色底線的字都沒有再出現了, 也就是沒有errors的狀況.
我在猜, 是不是Eclipse編輯器的問題, 我目前用的是這個
http://www.eclipse.org/downloads/packages/eclipse-ide-java-developers/junosr2
Windows 64-bit 版的.
名字叫 Eclipse IDE for Java Developers
純英文界面.
我想好好的學習, 所以, 我想我該用跟你一樣的編輯器才行, 可以推薦哪個軟體哪個版本的較好上手?
另外, 我們目前在寫程式使用startclient.bat來啟動的, 這樣子的環境是不是叫Debug環境? 在這樣子的環境下, 我要怎麼安裝小地圖模組(ZansMinimap1.5.1)進去這種開發環境?
因為平時用小地圖習慣了, 一下子沒小地圖, 用起來就覺得礙手礙腳的XD
這是由于Java的類重名現象導致,名字叫ElementType的類有兩個,一個是Forge的ElementType,一個是Java的,你import了java的ElementType所在的package...所以就有了那個情況,你的代碼中肯定有一行import java.lang.annotation.ElementType;,將它改爲import net.minecraftforge.client.event.RenderGameOverlayEvent.ElementType;就好了.
我確實是一直在用Eclipse來啓動Minecraft,具體的方法可以看一下第二篇Extra教程.(至于小地圖的問題,這個似乎是無解的...網上下載的小地圖Mod都是混淆過的,無法用在開發環境下...似乎新版FML自帶反混淆器,但我不太確定...)
至于Eclipse的版本,這個並不重要,我因爲個人需要用的Eclipse for RCP and RAP Developers,Eclipse IDE for Java Developers足以滿足Java開發的需要了.
原來是CLASS名稱重覆的現象, 了解.
看來小地圖MOD是沒希望了...
這張圖是我最近的學習成果: http://imageshack.us/photo/my-images/546/20130504161147.png/
非常謝謝你的指導.
最近研究到 world 裡放置一個自己想要的方塊, 使用 setBlock 這指令.
但是, 有個疑問, 我要如何得知由 setBlock 指令所產生的方塊的ID, 因為我想用這些我所產生的ID來進行後續處理.
例如我在 X=10, Y=15~17, Z= 12 的地方用 setBlock產生3塊石頭, 我想在後續處理時, 只針對這3塊我產生的石頭進行 setBlockToAir 的動作. 請問, 較簡易的作法為何?
另一個問題是音效, 目前版本所使用的音效播放規則跟舊有的版本使用方式一樣嗎? 看了很多遍依然抓不到頭緒, 例如 playAuxSFX, playAuxSFXAtEntity, playSound, playSoundAtEntity, playSoundEffect, playSoundToNearExcept這些指令, 為何單單只想放出一個音效卻要分成那麼多種類的指令? 它們之間異性在哪裡?
今天如果我想要按滑鼠右鍵時放出一個方塊, 然後播放一個簡短的音效, 有比較簡易的作法嗎?
而且, 我到現在還是想不透, 那些音效檔放的目錄以及存取時使用的指令規則是什麼 -.-?
很遺憾Minecraft中沒有磚塊的ID編號這個說法...爲了優化性能和節約存儲空間,Minecraft只記錄下每個坐標所放置的磚塊是什麽,以及它的Metadata是什麽.需要有特殊功能的磚塊通過TileEntity來實現...(正好新版教程還沒寫到TileEntity,你可以去看看MCBBS上的老版教程中TileEntity的部分).如果要記錄某幾個磚塊的位置,我的想法是使用NBT來儲存...(很遺憾似乎新版教程也沒寫到NBT的部分,去看老版吧...離高考只剩一個月,時間不太充裕(其實這只是借口)沒時間碼太多字,如果還不會的話再來問我吧XD)
音效問題我還從來沒研究過,那我就再發揚一次現學現賣的傳統吧...
首先是RenderGlobal類中的playAuxSFX,它是用來播放一個已經設定好的聲音特效,我強烈不推薦使用它,因爲它的所有可播放的音效都是被預先設定好的,而且精度有問題(雖然非常不明顯吧...)
然後是World類,playSoundAtEntity是在一個實體上播放3D音效,它的參數分別是"實體","音效名"(如:"mob.ghast.fireball"是亡魂的火球聲),"音量"(1.0爲標准音量),以及..."音調"(pitch...我也不知道專業術語叫什麽,同樣1.0是標准音調).
playSoundToNearExcept是在某玩家身上播放一個3D音效,神奇的是只有其他玩家能聽見,該玩家本人聽不見,它的參數和上一個類似,只不過"實體"換爲"玩家實體".
playSoundEffect是在某點播放一個3D音效,參數是"x","y","z","音效名","音量","音調".
playSound不推薦使用,建議使用playSoundEffect.
最後是SoundManager類,比較值得一提的是playSoundFX指令,它是播放一個2D音效,比如玩家操作菜單時的音效就是2D音效,沒有音源方位等影響因素.另外它的addSound指令是加載一個聲音文件,參數是"音效名","文件".
NBT部分似乎難度很高, 我先放一旁好了.
音效方面我大概瞭解了, 謝謝.
假設我想要創造一個新的屬性用於玩家的基本能力值, 例如增加一個魔法力(MP), 基礎值 100
我應該把此變數設定在哪個地方較好?
像這種的屬性是不是應該屬於全域變數比較好?
因為我想要此變數在日後進行施法或使用道具後, 能對此變數做增減值的動作.
好吧,我試驗了幾次後才發現原來沒有那麽簡單...我以前只想到了數據的存儲,但沒想到數據的同步,存儲可以通過NBT存儲在EntityPlayerMP(服務器中的玩家Entity)中,但數據沒法從服務器端同步到客戶端,我想用DataWatcher來同步數據,結果試了幾次都失敗了...也許以後我會試試別的辦法,比如用自定義封包(即Packet,好像在台灣管它叫分組)來同步數據啥的...
多人模式的資料傳輸我還沒資格碰, 不懂XD
目前碰到一個難題, 我有2個Entity都使用相同的Render來畫子彈, 日後還會增加更多的Entity, 但也是使用相同的Render來畫子彈而已, 以下我使用目前的程式碼, 會出錯:
MOD主類裡的設定:
EntityRegistry.registerModEntity(EntityMagic01.class, "Magic01", 1, this, 64, 1, true);
RenderingRegistry.registerEntityRenderingHandler(EntityMagic01.class, new RenderMagic());
EntityRegistry.registerModEntity(EntityMagic02.class, "Magic02", 1, this, 64, 1, true);
RenderingRegistry.registerEntityRenderingHandler(EntityMagic02.class, new RenderMagic());
ItemA 裡的設定:
world.spawnEntityInWorld(new EntityMagic01(world, myplayer));
ItemB 裡的設定:
world.spawnEntityInWorld(new EntityMagic02(world, myplayer));
Render裡的設定:
public class RenderMagic extends Render{
public RenderMagic(){ super(); }
public void doRenderMagic(EntityMagic01 entityThrownItem, double d0, double d1, double d2,float f, float f1){
GL11.glPushMatrix();
~畫子彈過程省略~
GL11.glPopMatrix();
}
public void doRender(Entity entity, double d0, double d1, double d2, float f, float f1) {
this.doRenderMagic((EntityMagic01)entity, d0, d1, d2, f, f1);
}
}
請問, 有什麼寫法可以讓 RenderMagic() 給很多個Entity 共用?
我想問題應該是出在doRender裏,this.doRenderMagic((EntityMagic01)entity, d0, d1, d2, f, f1)將entity強制類型轉換爲EntityMagic01,所以導致了EntityMagic02沒法使用這個Render...我的想法是讓EntityMagic01和EntityMagic02都繼承一個類,比如EntityMagic,然後將Render寫成:
public class RenderMagic extends Render{
public RenderMagic(){ super(); }
public void doRenderMagic(EntityMagic entityThrownItem, double d0, double d1, double d2,float f, float f1){
GL11.glPushMatrix();
~畫子彈過程省略~
GL11.glPopMatrix();
}
public void doRender(Entity entity, double d0, double d1, double d2, float f, float f1) {
this.doRenderMagic((EntityMagic)entity, d0, d1, d2, f, f1);
}
}
這種做法類似于RenderLiving,基本上任何繼承EntityLiving的類都能使用RenderLiving作爲渲染器,同樣的,任何繼承EntityMagic的類也都能使用RenderMagic.
另外對于world.spawnEntityInWorld(new EntityMagic01(world, myplayer));我建議寫成:
if (!world.isRemote)
{
world.spawnEntityInWorld(new EntityMagic01(world, myplayer));
}
這是爲多人遊戲而考慮的,多人遊戲中Entity只應由服務器端來創建.