"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个总线很好区分,分工也很明确,我就不详细说明了...(群众:我们去年买了个表)
谢谢提供建议, 我去好好思考一下.
我目前用你提的方法, 先自制一个叫 EntityMagic 的class, 然后让EntityMagic01和EntityMagic02使用的参数都改写为 EntityMagic , 编译可以过, 但执行下去就会出错了.
EntityMagic.java 内容如下:
import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.projectile.EntityThrowable;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.MovingObjectPosition;
import net.minecraft.world.World;
public class EntityMagic extends EntityThrowable{
public EntityMagic(World world){
super(world);
}
public EntityMagic(World par1World, EntityLiving par2EntityLiving) {
super(par1World, par2EntityLiving);
// TODO Auto-generated constructor stub
}
@Override
protected void onImpact(MovingObjectPosition movingobjectposition) {
// TODO Auto-generated method stub
}
}
我这样算只完成了开头, 对吧!?
正确来看, 是不是我必须把 EntityThrowable.java 里的所有内容都照抄进来才对?
所以, 我暂时先用 Entity entity 代替 (EntityMagic01)entity.
另外, 我有另一个疑问, 是关于Render 的生命周期, 在 EntityMagic02.java 里的部分内容我使用如下:
@Override
protected void onImpact(MovingObjectPosition objMOP){
if(objMOP.typeOfHit == EnumMovingObjectType.ENTITY){
double OrgX = objMOP.entityHit.posX;
double OrgY = objMOP.entityHit.posY - 1;
double OrgZ = objMOP.entityHit.posZ;
for(int ypos = (int)OrgY; ypos < 3 + (int)OrgY; ypos++){
this.worldObj.setBlock(OrgX, ypos, OrgZ, Block.dirt.blockID);
}
}
this.setDead();
}
@Override
protected float getGravityVelocity(){
return 0.0F;
}
在这个碰撞事件里, 是不是表示这颗子弹最后有碰撞到任何一个东西他都会 setDead(), 是不是?
我现在可能就是卡在这边, 因为我有设置它射出去后是直线飞行, 永远不会往下掉, 所以, 当我
朝天空或者最远处没有任何障碍物射出去后, 我随后再沿着他直线飞行的路径去看, 结果最后会看
到他绘制的子弹停留在空中, 怎么摸怎么碰都无法消除他, 到底是 EntityMagic02.java的问题, 还是
RenderMagic.java的问题?
我该在哪里设置描绘这颗子弹的生命周期? (我希望他距离原始出发点20格后就自动消失)
希望我一直提问题不会影响你考试, 我可以等你考完试再来慢慢讨论.
你可以参考一下EntityFireball的做法,它重写了onUpdate,通过一个计时的int来限制火球的生命周期.另外this.setDead()也是必须在服务器端执行的,具体可以看EntitySnowball的onImpact中的写法.
但愿能解决子弹不消失的问题吧...
试了很多种方法, 最后发觉还是要用到NBT, 这方面我仿照EntityFireball的内容, 可以做一个计时器.
EntitySnowball方面不行, 它用的方法很单纯, 没用到计时器, 因为原生EntitySnowball没有使用NBT, 而且也没有
使用计时器的功能来让雪球自动消失.
而且, 我发现我提的问题点不在 onImpact() 事件里, 因为此事件是要这个Entity有击中任何的东西时, 它才会动作, 而我目前的需求是要让这个Entity在经过很久的时间后依然没击中任何东西就自动消失.
目前我额外加一个 onUpdate() 事件, 而且事件里还要 super.onUpdate() 才行, 在计时器方面我设定了40tick后, 如果此Entity还没击中任何目标会自动爆炸, 执行后, 我试着射向天空, 画面中没看到我自制的子弹(射击出去的一瞬间有看到而已), 但有看到射击目标很远的地方它自爆了.
我在想一个问题, onUpdate 跟 Render 是不是有很大的关系?
如果我不加onUpdate 事件就可以看到子弹射出去的样子, 但是就无法让计时器动作.
如果我加了onUpdate 事件就看不到子弹射出去的样子, 但是计时器可以正常动作.
目前暂时解决生命周期的问题了, 不过我还是想不通 onUpdate 跟 Render 之间到底有什么关联.
我是在猜, 因为我 extends EntityThrowable 这个entity, 而EntityThrowable档案里我有看了很多遍, 是不是因为这档案里它是把物体移动的一些绘描指令写在 onUpdate 事件里, 而造成我不能再使用 onUpdate 来做其它事, 因为只要我注解掉 EntityMagic02里的 onUpdate 就可以看到子弹飞出去的图片, 没注解掉时就只有发射的一瞬间有看到图片, 之后就什么都没看到.
在MOD主档我是用 RenderingRegistry.registerEntityRenderingHandler 来注册Render, 假如果我不做注册的动作, 改在 EntityMagic02.java 档里的 onUpdate 事件自己写描绘子弹的程序, 不知道这方法行不行得通?
请问您的KABOOM的代码里
取消了事件之后那几行代码有什么用吗?我感觉就是一个无关的自定义事件...试过注释掉也似乎没问题啊...
不错的教程,但是我看完后有个问题想请教下。在客户端mod监听什么事件才能在登录服务器时触发指定函数?如在一个client side mode中检测登入指定服务器时弹出gui. 谢谢!
在客户端监听玩家登陆服务器好像没有太好的方法...FML有个PlayerEvent但仅供服务器使用,我的想法是在客户端监听EntityJoinWorldEvent事件,然后判断"event.entity instanceof EntityClientPlayerMP",同时还要有个变量用来记录玩家是否是刚刚进入服务器,因为EntityJoinWorldEvent是实体每次被添加到世界时触发,因此玩家复活和进入其他世界时也会触发,同时还要在玩家退出服务器时置零这个变量...总之感觉是个很蹩脚的实现.假如是客户端和服务器协同工作的话,可以是服务器用PlayerLoggedInEvent监听到玩家进入服务器后,用EntityPlayer的openGui来在客户端显示一个界面,不过这还需要在NetworkRegistry中注册一个GuiHandler.
“同时还要在玩家退出服务器时置零这个变量”这个需要怎么实现呢?监听什么事件?
感谢你的教程,不过说到弓呢我刚好请问一下,我新建了弓,里面所有代码都照抄ItemBow,但是为什么拿在手上的显示方法还是和一般物品一样而不是像弓一样?(以第三视角看的话)还有我新建了一个EntityArrow,可是我找不到EntityArrow的贴图路径该在哪里改.十分不好意思请教你,不过我真的找不到办法...
基本上凡是跟渲染相关的东西都是在RenderXXX中,比如EntityArrow的渲染是RenderArrow. MCBBS上的老版教程中的实体章节有说如何改渲染,但新版中漏掉了,算是我的一个疏忽... (其实就是懒)
有两个个问题想问一下:
1、注册一个事件的订阅必须在Init过程或者Init之后是吗?我在ClientProxy的preInit过程注册结果出错了qwq
2、对于像是RenderPlayerEvent这类和渲染有关的的事件,是可以只在客户端进行注册,还是两边都要进行注册(或者直接在两边都会有的类里面统一注册一次),还是只可以在客户端注册呢?
1.按说注册事件是什么时候都行的,那个出错提示是啥...
2.只在客户端注册
怎么绑定一个按键 然后点击那个按键模拟玩家发送命令
(我只是想做个傻瓜版输入命令的233)
在客户端可以用ClientCommandHandler的executeCommand来模拟玩家向服务器发送命令,返回值代表执行结果.
关于如何绑定按键我好像在GUI那篇里写了.
你好,我用1.7.10的版本虽然能监听和拦截,但一调用entityPlayer里面的方法游戏就闪退了(entityPlayer!=null) ,不知道出于什么原因呢