上一章我们学习了Mod制作的基本要素,以及Block和Item的创建.
本章我们要进行:
对实体进行操纵
创建新的实体
为物品添加额外的功能
什么是Entity
“一切事物都依存于Entity,即是说,没有做不出的东西.”
–考验东方众的时刻到了,这句话的原型出自哪?
知识点:什么是实体(Entity)
///////////////////////////////////////////////////////////////////////////////////////////////////////////
Entity是续Block,Item后第三个MC世界的重要组成部分,在游戏中,地上跑的动物,洞穴里潜行的炸弹魔,怪物死后掉落的经验球,水上漂浮的舟,它们都是不同的Entity.
一个Entity除了具有XYZ坐标以外还具有一些特殊的参数,比如高度,宽度(某些摸不着的Entity没有这些参数)生命值(无敌的Entity无需考虑)Yaw,Pitch,速度…
什么是Yaw和Pitch?按照立体几何的定义,在右手坐标中,Yaw是物体围绕Y轴(即高度轴)旋转的参数,在MC中他通常用来控制有形实体的模型角度.Pitch是物体围绕X轴旋转的参数,游戏默认实体的正朝向是Z轴正方向,所以Pitch控制的是实体的面朝上和面朝下.
(如果你看过第一版教程的话,你会记得那时候我说过Pitch控制的是左倾和右倾,事实上这是错误的结论…那时候我误以为实体的正朝向是X轴正方向.游戏也不是通过Pitch来控制实体死掉时的侧倒,而是通过渲染器(Render)来实现.)
然而我们很少直接使用Entity,我们会根据需要从Entity派生出新的类,比如EntityBoat(已放置的船)就是从Entity中派生出来的.
然而说到Entity就不得不提NBT.
///////////////////////////////////////////////////////////////////////////////////////////////////////////
知识点:NBT初解
///////////////////////////////////////////////////////////////////////////////////////////////////////////
NBT是MC的数据储存格式,它是Notch设计的一种树形数据储存格式(类似XML).它与地图存档密不可分,在存档时,NBT会将Entity的数据写入NBT文件,读档时NBT会读取NBT文件并将数据还原给实体.
NBT是个很复杂的东西,幸运的是,在这一部分中,我们还无需操作NBT.
///////////////////////////////////////////////////////////////////////////////////////////////////////////
对实体的简单操作
接下来我们创建一个被称为DiracWand的物品,先让它实现个最简单的功能:击飞实体
首先新建一个叫ItemDiracWand的物品,并让它继承Item类,让Eclipse补上必要的构造函数.
hitEntity的返回值代表是否算作一次成功的攻击,目前仅用来统计玩家数据.
知识点:Minecraft的坐标系
///////////////////////////////////////////////////////////////////////////////////////////////////////////
本来这个东西在第一版教程是没有的,因为当时作为高二党的我认为数学与坐标系是不用说就明白的,但现在高考后我发现数学这东西不用就立刻忘光了…所以我补上了坐标系这一节.
现在Minecraft采用标准的(三维)右手坐标系,即向东为X轴正方向,向南为Z轴正方向,向上为Y轴正方向.
在空间计算上,并没有什么太多需要注意的,只要记住Y轴代表高度就好了.
但在角度计算上比较麻烦,首先,Java的Math类使用的是笛卡尔直角坐标系(简称直角坐标系),即X轴向右为正,Y轴向上为正.第二,实体的rotationYaw字段表示实体所朝向的水平角度,它以右手坐标系的Z轴正方向为0度,沿Y轴顺时针转动为正角度转动.而我们知道在直角坐标系中角度是以X轴正方向为0度,逆时针为正角度转动.因此如果要在直角坐标系中使用rotationYaw的话很可能需要换算.
然后,实体的rotationPitch表示实体朝向的高度角度,即表示实体面朝上还是面朝下.常识认为Pitch为正表示的是面朝上,然而在游戏中Pitch为负数才表示面朝上,正数表示面朝下.比如rotationPitch为-90.0时表示头朝正上方仰望,视野与Y轴平行且同向,-45.0表示向斜向上45度仰视,45.0表示面朝斜向下45度在找金子,90.0表示在看自己的事业线(如果有的话).因此你在实际运算时可能需要变换符号.
右手坐标系,红色为正朝向.
笛卡尔坐标系
此外,rotationYaw还有个很要命的特性,通常我们认为使用弧度制表示角度时,取值范围在[0,360]或[-180,180],但rotationYaw不会在游戏中对角度进行约束,换句话说如果你现在的rotationYaw是90,那么你原地向右转一圈后rotationYaw会变为450,再向左转两圈rotationYaw会变为-270.
不过好在Minecraft的MathHelper类提供了wrapAngleTo180_float和wrapAngleTo180_double方法来将角度约束在[-180,180]的范围内.这样正好和直角坐标系中的角度范围([-180,180]或[-π,π])相匹了.(顺便一提,MathHelper类的sin和cos也是采用直角坐标系.)
///////////////////////////////////////////////////////////////////////////////////////////////////////////
之后要添加相关的代码使玩家可以使用物品击飞NPC,Entity的motionX,motionY,motionZ三个字段分别表示其在该方向上的分速度,我们可以用正交分解算出实体的分速度,至于击飞的角度,由于玩家必须面朝NPC才能攻击他们,因此可以假设飞出的方向就是玩家的朝向方向,但对于角度的计算,这里面还有些学问.
知识点:左手坐标系的计算,以及与笛卡尔直角坐标系的换算
///////////////////////////////////////////////////////////////////////////////////////////////////////////
以前我一直不明白为什么有人说左手系与右手系各有优缺点,今天在思考换算时才明白原来直角坐标系不能直接映射到左手系的XZ平面上,因为直角系的Y轴与左手系的Z轴方向是相反的…
为了方便计算,有时我们会将立体坐标系中的东西投影到平面坐标系上计算,对于右手系(微软的DirectX使用的就是右手系,壮哉!微软!壮哉!DirectX!),我们可以像左下图那样直接按照”右手系X轴-平面系X轴,右手系Z轴-平面系Y”的映射关系来计算,但对于左手系(Minecraft采用的坐标系,哦对了OpenGL也使用的是左手系,不过你不用管它.)我们却会发现很难直接将平面系映射到立体系上,因为映射完后平面系的Y轴和直角系的Z轴方向是相反的,更要命的是Minecraft中角度以顺时针为正方向,笛卡尔坐标系却是以逆时针为正方向,两角的起始角度又不同,更进一步加大了计算难度.
对程序员而言拥有数学背景是再好不过的事,遗憾的是并非所有人都能做到,因此这里给出个最简单最土的方法:如果你想用直角系计算左手系中的XZ轴,就把角度加上90度,然后一切按照直角系中的方法计算.
这个方法的原理是利用错误来抵消错误,比如你要计算一条直线在X轴和Z轴上的投影,那么就是:
其中angle为直线相对正方向的夹角度数,采用角度制,rad为换算为弧度制的结果.length为线长.很显然在Minecraft的左手系中这样计算是错误的,X轴肯定不能用Cos来计算,但这个土算法的微妙之处就在于用错误抵消了错误…总之,如果你想使用这个,就记得一切都要按照直角系中的方式来.
不过,我相信你肯定不会满足于止步于此,既然我们已经了解左手系的秘♂密,那就直接在左手系中计算好了,如果你使用这种方法计算,那么就记住Z轴使用Cos计算,X轴使用Sin计算并且结果要变负.
以此方法书写,则上面的算法应写成:
两种方法怎么写都无所谓,但我更倾向于第二种,刚开始看上去很难习惯它,但多用用的话就会习惯.
///////////////////////////////////////////////////////////////////////////////////////////////////////////
既然我们已经完全了解原理那么便可以开始着手编写代码了,将DiracWand类中的hitEntity方法写成:
暂且先忽略那条对par3EntityLivingBase.worldObj.isRemote的判断,下一节我们再解释它,这段代码的意义是:首先,它降低了物品的耐久(关于物品方法的参数的含义可以参见Plus篇的Item部分,par1ItemStack为当前物品的物品栈,par2EntityLivingBase是被攻击的实体,par3EntityLivingBase是攻击者).然后根据攻击者的角度算出击飞的方向,然后算出速度在各方向上的分量,并通过setVelocity来设置速度.
(其实你也可以通过直接修改实体的motionX等字段来直接修改速度.)
然后在Mod主类中添加相关的代码.
之后就开游戏测试一下吧,在生存模式下用/give调出物品(别忘了物品ID和实际ID换算一事,在创造模式下直接刷物品也行,只不过就没有物品损坏效果了)然后找只动物敲一下试试…
实体的部署
相比简单地修改实体的参数,大多数人更对创造一个实体感兴趣,实体的创造其实是很简单的呢,至少是对站着的人来说.
知识点:在游戏中放置一个实体
///////////////////////////////////////////////////////////////////////////////////////////////////////////
World类的spawnEntityInWorld方法允许你在游戏中放置一个实体,例如:
这便是一个在游戏中创建一个已点燃的TNT实体,world是它所属的世界,xyz是坐标,player是TNT实体的放置者.但重点不是这些,而是那个world.isRemote.
///////////////////////////////////////////////////////////////////////////////////////////////////////////
知识点:代码执行位置(客户端or服务器),以及一些关于数据同步的浅谈
///////////////////////////////////////////////////////////////////////////////////////////////////////////
这一个知识点比较长,事实上它应该被放在Extra篇中来讲,网络就是这么操蛋的东西,”你要不谈网络,咱们还是好朋友”
从字面上讲,isRemote是判断一个游戏世界是否是远程的,即是否是在客户端上,Remote引发人许多的遐想,从只敢远观不敢亵玩的花瓶游戏Distant Worlds,到风雪交加的守矢神社下的一曲动人的Last Remote…艹,扯远了.
我们暂且将isRemote=false的世界称为服务器世界,将isRemote=true的称为客户端世界,显然我们需要将运算内容放在服务器世界,将效果表达部分放在客户端世界,但实际上要想判断谁放在客户端谁放在服务器并不容易,就拿上个章节的代码举例:
par1ItemStack.damageItem(1, par3EntityLivingBase)被放到了客户端/服务器判断之前,也就是说它在无论哪个端都是可执行的,这显然违背了我们的原则,因为damageItem的作用是降低物品的耐久值,显而易见它是个逻辑运算,只应当在服务器端运行,但实际上它的”正确”用法就是在两个端都执行…因为对玩家(客户端)而言,物品被损坏是可以看得到的(物品栏中物品的耐久度条缩短),换而言之,对客户端,damageItem让玩家能看到自己的物品慢慢损坏,对服务器,damageItem让服务器默默记下物品的耐久度损坏状况,并适时告诉客户端”您的话费余额已用尽,快给我们塞钱吧”.
那么,如果我们只让damageItem在服务器端运行,那是不是就成了像中国电信的■翼那样只管停机不管通知的坑爹货,只管销毁玩家的物品,不告诉玩家物品耐久程度?实际测试一下你会发现玩家依然能看到耐久度慢慢下降…这是因为游戏会定期进行一次”大”同步(“大”只是相对的,并不一定真的是一次大规模的全部数据同步),将玩家手持物品的信息同步到各客户端注1.那时玩家便会看到自己的物品耐久度下降了.
很难说清两种方法哪个更好,Notch采用的是第一种方法,因此在延迟很高时你可能会把手中的物品用到损坏,结果过了几秒后发现它又复原了…
然而,也有些东西是强制要求必须在服务器进行,比如实体的创建,如果在客户端执行的话,就会变出来一个无法正常运行的幽灵实体,正常的实体必须创建在服务器,然后由服务器向客户端同步实体信息,你可能会想为何不像damageItem那样双方同时进行?那样不是更能克制延迟问题吗?但这里却没有任何可商量的余地,实体的创建就是必须在服务器进行.注2
(其实要想解释也能解释的通,同一个实体在服务器和客户端的EntityID(实体ID)一定是相同的,两端同时进行无法保证ID相同或不出现冲突,因此只能由服务器统一进行实体ID管理和分配,然后同步给客户端.)
废了则么多话,就总结一下我对代码执行位置和数据同步的看法吧:
1.客观要求最优先
就像实体的创建那样,它就是为在服务器执行而设计的!因此就不要想着怎么拿到客户端了!
2.考虑硬件能力
这个其实可以和第一条合并,这条的意思是:”从硬件条件考虑,有些代码确实无法在某些端执行”,比如图像渲染,你在服务器端执行图象渲染的代码?请允许我做一个悲伤的表情.
既然说这一条是”从硬件条件考虑”,那么上一条或许就可以称为”从软件条件考虑”了.上一条的不能执行是因为代码编写成那尿性,这一条的不能执行是因为服务器端没有相关的类!Minecraft源代码中任何名字形如xxx.client或xxx.client.ooo的包下的类在独立服务器(minecraft_server.jar)中都是不存在的.
3.异步与同步
(注:这里的”异步”并非是指通常意义上的异步(Async),而是一种比喻,与同步相对应,泛指服务器允许客户端在短时间内与服务器不同步的行为,事实上,有一个专业的术语来描述这种行为:轨迹推测法(Dead Reckoning))
C/S模式的多人游戏多允许客户端与服务器间适当异步运行,比如Minecraft中实体的移动就并非绝对同步的,服务器会在实体移动时发送它的的移动方向和速度等信息,而精确的大规模同步则是每隔一段时间进行一次,在大规模同步之前,实体的移动在客户端就是异步的,客户端会根据服务器之前的信息来计算实体的移动,因此在网络环境较差的情况下,玩家会看见实体走一段距离后突然又瞬移到另一个位置,这就是客户端收到了实体移动的信息,但没及时收到精确同步的信息.之前提到的damageItem也是如此,客户端无需等待服务器的信息便可先异步降低物品耐久度,等收到服务器的同步信息后再确定物品准确耐久度,但倘若网络环境差,就会出现物品用坏后过几秒突然又恢复的情况.
说完异步再说同步,Minecraft中依赖同步的有实体的创建,玩家对实体的攻击,实体的死亡等等…对于依赖同步的行为来说,在糟糕的网络状况下可能会出现玩家进行操作很久后才有响应的情况,比如打开箱子,高延迟时开箱子过很久才会出界面.
何时同步何时允许适当异步,除了客观条件外,就要看开发者的取舍了…
4.特效
大多数人认为特效无需同步,事实上,无需同步的是特效的细节,特效的发生是需要同步的…你总不希望看到一个玩家喊”听,是炸弹魔的声音!”,别的玩家嘲讽说”哪jb有声,你丫耳拙了吧…”然后被突然冒出的Creeper炸死吧…再比如粒子特效,你需要同步特效的发生,即何时出现粒子,但不必精确地同步每个粒子(事实上,我想你即使想做也做不到…),不过,如果有需要还可以同步一下粒子的数量或密度,争取给每个玩家提供相同的体验.
说了这么多,其实归根结底就只有一句话:”凭经验判断”……如果是这样那么对新手来说这就是个无解的问题了?其实还有两种办法可以解决,第一种是:所有操作都先按两端同时执行编写(一些明显不能的就不要这样写了…比如产生实体),单人游戏下成功运行后再在多人游戏下调试,找出那些不能正常运行的功能,再重新编写.第二种是:所有操作都仅在服务器端执行,然后再找出不能正常运行的功能并进行修正.我比较推荐第一种,因为第二种中由于未能同步造成的bug可能与程序中的逻辑bug混杂在一起,难以除错.
///////////////////////////////////////////////////////////////////////////////////////////////////////////
看了那么一大段话,估计大家也都累了(写的人也累呀…),那就糊弄个简单的功能来展示实体的创建 – 让DiracWand能右键发射TNT.
Minecraft中物品右键引发的方法是onItemRightClick方法,通过重写它来实现按右键执行某功能.
(注:onItemUse与onItemRightClick的区别 – onItemUse只会在有效范围内右键一个砖块时引发,onItemRightClick在任何时候按右键时都会引发.其中,如果onItemUse可以触发的话,它会在onItemRightClick之前触发.onItemUse是否会阻止onItemRightClick的发生取决于onItemUse的返回值,true会阻止onItemRightClick的发生,false会允许其被触发.)
之后进游戏试试,按住右键然后就毁灭世界吧.你可以开一个独立服务器然后自己在客户端中联进去测试,可以正常运行哦.掌握了这么强力的物品(Item),你已经可以去ITEM组申请取代芙兰达的职位了! (笑)
设计一种新Entity
在旧教程中,这个是第四篇的第一节,而在新教程中却刚刚到一半,这可真要人命啊…
(2013.8.3 因为明天要出去旅游所以今晚把这一部分草草写完然后发上来吧,没有时间做示例只好纯写知识点了,时间略紧迫所以写的比较凌乱)
知识点:实体的原理与创建
///////////////////////////////////////////////////////////////////////////////////////////////////////////
老实说写到这时我一头雾水,这个知识点的标题是我过去起的,但现在想想,这玩意有什么原理可说呢…能说的都已经在前头的”什么是原理”中说了…反正还是扯一些能说的吧…
每一个世界都有一个实体列表(loadedEntityList字段)和玩家实体列表(playerEntities字段,包括所有位于此世界的玩家实体,玩家实体会同时存在于两个列表当中),用以维持实体循环,此外,为了进一步优化性能,Minecraft为每一个Chunk还准备了16个子实体列表(Chunk类的entityList字段),若将一个Chunk(16x256x16个砖块)按高度(Y轴)均分成16个区域的话,每个区域内的实体会属于一个代表该区域内实体的子列表中(地表下的实体会被归为最下面的子列表,256高度以上的实体同理),子列表主要用于优化数据存取以及使用AABB盒搜索实体.
最后就是关于创建一种新的实体的办法,我们需要考虑5个问题.
1.实体的原型
我知道这个名字叫的不好,这几天JavaScript写多了…我想说的是,砖块和物品采用的是类似单例模式的方式,在游戏中放置一个砖块,实际上只是将那个位置的砖块ID改为新砖块的ID,而实体不同,实体不需要你在Mod主类中new XXX一个原型,那样做实际上近似于放置一个实体,创建一种新的实体,只需要创建一个继承Entity类的类就好了.此外它还有个讲就:在客户端中被放置的实体只会被调用参数为World的构造函数,如果不明白的话可以看第四个问题.
2.实体的注册
当然,我们不可能什么都不做,单单地新建一个类便创建了一个实体了,我们还需要注册它,注册的最基本的目的是为了给实体一个名字和一个类型ID,名字是用于NBT存储时使用,类型ID用于网络通信时使用.此外还有个叫做EntityTracker的东西,它是下面提到的网络同步的组成部分之一,用于检测和决定实体数据的同步,Minecraft对它编写的很死,新增的实体无法被EntityTracker识别,因此就需要注册器来解决.注册的办法是调用EntityRegistry的registerModEntity方法或registerGlobalEntityID方法,这里我推荐registerModEntity方法.
registerModEntity方法的参数包括”实体的类”,”名字”,”类型ID”,”所属的Mod”,”实体交互范围”,”信息更新频率”,”是否同步速度数据”.例如:
最后再蛋逼一句实体循环.
知识点:实体循环
///////////////////////////////////////////////////////////////////////////////////////////////////////////
用一句话来解释:实体循环就是游戏每一帧对所有实体进行的运算…
稍微了解游戏引擎原理的人都会知道游戏循环(Game Loop)这个东西,简单来说,就是每一秒钟游戏都会对一段程序循环运算几十遍(通常是25~30遍),更进一步了解引擎原理的人会知道通常游戏循环分为两部分:Update(数据运算,也有写作Tick(帧),因为一次游戏循环算一帧)和Render(图像绘制,也有写作Draw)
Minecraft类的runTick方法以及MinecraftServer类的tick方法就相当于Update,在runTick中,游戏会将玩家所在的世界中的所有实体的onUpdate方法调用一遍.(tick更为复杂一点,它要将所有世界的实体依次调用一遍)onUpdate方法会执行对实体的运算…
另外,Update和Render并非一一对应关系,即不一定每次Render时都有一次Update.但对于不涉及图形学的Modder来说,就不用太在意这个了…
///////////////////////////////////////////////////////////////////////////////////////////////////////////
注释:
注1 对玩家物品的同步:事实上这里有个疑点,从代码的角度看,游戏只有在玩家切换了物品的情况下才会同步玩家手中或服饰栏中的物品,但实测即使玩家不换物品也会正常同步数据.
注2 实体的创建必须在服务器进行:这里说的实体的创建是World类的spawnEntityInWorld,单纯地实例化一个实体类是在任意端都允许的.另外,有些开发者喜欢利用幽灵实体的特性,故意单方面在客户端创建实体,我说不出这样做有何坏处…但就我个人而言我是绝对不会这么做的…
欢迎光临 最MC论坛 (http://www.zuimc.com/) | Powered by Discuz! X3.2 |