"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只应由服务器端来创建.