本帖最后由 szszss 于 2012-8-8 23:34 编辑

MCP的Mod制作教程(4)
创建新的实体,NPC,添加特殊功能,创建粒子与屏幕文字
作者:szszss

索引贴地址:
http://www.mcbbs.net/thread-18949-1-2.html

注:本文最初基于MCP5.6和ModLoader1.1.0编写的.
MCP6.2和ModLoader1.2.4更新了大量方法的名字,导致我的教程几乎报废一半... 不管怎么说,我用了一个晚上的时间还是修正了教程的文字部分,使其和最新版的MCP与ModLoader接轨.但图片部分我实在是无力修改了...大家将就着看吧.
MCP6.2改掉了一大批方法的名字.以本教程的NBT部分为例,5.6的setTag在6.0改成了appendTag.或许这个看起来还有些道理.但有些就显得毫无理由了.比如判断一个世界是否是多人游戏世界.5.6是multiplayerWorld,6.0成了isRemote...


在读本篇之前,请确定你已读过并掌握上一篇的内容.

本篇我们将要:
创建一种新的Mob:Dirac Pig
添加新的功能,使Dirac Wand具有瞬间转移箱子内的物品的能力


创建一个新的活物Entity

"一切事物都依存于Entity,即是说,没有做不出的东西."
                                  --考验东方众的时刻到了,这句话的原型出自哪?


事实上这句话说得有些大了,比如Block就和Entity是截然不同的...

抛开这些不管,确实很多东西都是靠Entity(实体)来做成,比如NPC.
这里我们以NPC为例,教大家如何创建一个Entity.

知识点:Entity的原理,与NBT的使用
///////////////////////////////////////////////////////////////////////////////////////////////////////////
之前我们提到,Entity很少直接使用,我们多是使用从Entity中派生而来的实体.
按照规则,大部分实体被编写好后需要加入实体列表(EntityList),实体列表对一个实体储存3个信息:ID,里名字,实体本身.
通过ID可以获取对应的实体,同样如果你知道一个实体,你可以获取它的ID.
里名字和ID同理,知道里名字可以获得实体,知道实体也能获得里名字.
但ID和里名字无法相互获取,需要利用实体作为中介来间接获取.
一个实体可以用ID或里名字来间接获取,也可以通过实体类直接获取.
那实体列表到底有什么用?答案是:用于多人游戏下的网络传输.多人游戏中,MC不会把整个实体都写入封包发送过去,这会让玩家和服务器的带宽崩坏的,取而代之,他只会发送必要的数据,其中一点就是游戏会根据实体列表来获取要发送的实体的ID,然后将ID发送过去.
这样做,可以大大减小封包的体积,增加信息传输效率,提高多人游戏的流畅度.
另外实体列表在刷怪笼也有用到.刷怪笼是通过实体列表来查询所应刷的怪物.
里名字则是在NBT存储数据时用到.

在创建完一个Entity后,如果你想在游戏世界中放置一个Entity,你可以使用World类(注意,这不是一个静态类)的spawnEntityInWorld方法来在游戏世界中添加一个实体.
比如船(ItemBoat)的右键方法:
public ItemStack onItemRightClick(ItemStack itemstack, World world, EntityPlayer entityplayer)
{
     ......
     world.spawnEntityInWorld(new EntityBoat(world, (float)i + 0.5F, (float)j + 1.0F, (float)k + 0.5F));
     ......
}
游戏在引发onItemRightClick方法时会将玩家所在的世界作为参数传入,onItemRightClick方法会在玩家所在的世界(world参数)创建一个EntityBoat实体.

然而我们之前也提到过,Entity是通过NBT来储存特殊信息,基本上说,凡是你为一个实体添加了新变量,如果你希望这些变量即使在存读游戏后依然保留的话,就要写入NBT.

Entity自带两个方法,writeEntityToNBT方法是向NBT写入信息,readEntityFromNBT方法是从NBT读取信息,你无须担心它们的参数问题,游戏会向它们提供参数的,你所要做的只是为NBT节点(NBTTagCompound类)添加数据.节点可以被写入很多数据,同时节点也能派生出更多的子节点.
但是我们现在仅仅是学习NBT的使用,不会深入学习它的运作机理和复杂使用.如果可能,我会在教程的Extra编中撰写相关的NBT教程.你现在只需知道如何向一个NBT节点读写数据就行了.


以EntityLiving类(一切活物Entity的基类)的writeEntityToNBT方法的节选片段为例.
nbttagcompound.setShort("Health", (short)health);
它是向nbttagcompound(这个是作为参数输入的,不用担心它从哪里来,程序会负责处理这一切)这个NBT节点添加一个short(8位整数)类型的数据,数据名称为Health,数据数值是转换为8位整数的health变量.(health是int变量,转换为short要显式转换)

再以readEntityFromNBT方法的节选片段为例.
health = nbttagcompound.getShort("Health");
它是设置health为nbttagcompound节点中,名称为Health的short类数据的数值.
///////////////////////////////////////////////////////////////////////////////////////////////////////////

知识点:活物Entity的创建
///////////////////////////////////////////////////////////////////////////////////////////////////////////
说了这么多才说道本章重点...V587吧.
MC中,玩家和野怪的实体是按照这个顺序派生的.
Entity
  ↓
EntityLiving → EntityPlayer(玩家类)
  ↓
EntityCreature → EntityMob(野怪类)
  ↓
EntityAnimal(动物类)

Entity类:所有Entity的基础类
EntityLiving类:活物类,多了health(HP)等数据.
EntityPlayer类:玩家类,玩家拥有物品栏..可以重生..等等,很复杂.
EntityCreature类:NPC类,拥有寻路,移动等等...
EntityAnimal类:动物类,拥有喂养度等等...不会攻击玩家,可以饲养
EntityMob类:野怪类,有攻击力(主动不主动看它的派生类怎么设计)
///////////////////////////////////////////////////////////////////////////////////////////////////////////

你可能很奇怪我为什么没有在上个知识点内写如何创建NPC,因为我发现这实在太浪费空间了,我决定一边制作范例Mod一边解释如何创建NPC.
创建一个新类dcEntityDiracPig,将它的全部内容替换如下:

  1. package net.minecraft.src;
  2. public class dcEntityDiracPig extends EntityMob
  3. {
  4.     public dcEntityDiracPig(World world)
  5.     {
  6.         super(world);
  7.         texture = "/mob/diracpig.png";
  8.         setSize(0.9F, 0.9F);
  9.         attackStrength = 2;
  10.         experienceValue = 5;
  11.         moveSpeed = 1F;
  12.     }
  13.     public int getMaxHealth()
  14.     {
  15.         return 20;
  16.     }
  17.     public void writeEntityToNBT(NBTTagCompound nbttagcompound)
  18.     {
  19.         super.writeEntityToNBT(nbttagcompound);
  20.     }
  21.     public void readEntityFromNBT(NBTTagCompound nbttagcompound)
  22.     {
  23.         super.readEntityFromNBT(nbttagcompound);
  24.     }
  25.     protected String getLivingSound()
  26.     {
  27.         return "mob.pig";
  28.     }
  29.     protected String getHurtSound()
  30.     {
  31.         return "mob.pig";
  32.     }
  33.     protected String getDeathSound()
  34.     {
  35.         return "mob.pigdeath";
  36.     }
  37.     protected int getDropItemId()
  38.     {
  39.         return Item.porkRaw.shiftedIndex;
  40.     }
  41. }
复制代码

这是一个最简的Mob类,他包含了一个Mob所应有的全部功能.
texture = "/mob/pig.png";  - 设置它的纹理
setSize(0.9F, 0.9F);       - 设置它的体积
attackStrength = 2;       - 设置它的攻击力
experienceValue = 5;     -设置它掉落的经验
moveSpeed = 1F;        -设置它的移动速度

public int getMaxHealth()
    {
        return 20;
}
设置它的最大生命值为20(玩家的HP即20)

getLiving/Hurt/DeathSound()   设置它空闲/受伤/死亡时的声音

protected int getDropItemId()
    {
        return Item.porkRaw.shiftedIndex;
}
设置它死亡时掉落的物品

现在我们制作完了一个新的Mob,接下来要为他配置模型和Render,制作新的模型不在本文的讨论范围内,以后我会在Extra编中介绍如何制作新模型,我们现在自行设计一个新Render就行了.

创建一个叫dcRenderDiracPig的类,将它的全部代码替换如下:

  1. package net.minecraft.src;
  2. public class dcRenderDiracPig extends RenderLiving
  3. {
  4.     public dcRenderDiracPig(ModelBase modelbase, float f)
  5.     {
  6.         super(modelbase, f);
  7.     }
  8.     public void renderDiracPig(dcEntityDiracPig entitypig, double d, double d1, double d2,
  9.             float f, float f1)
  10.     {
  11.         super.doRenderLiving(entitypig, d, d1, d2, f, f1);
  12.     }
  13.     public void doRenderLiving(EntityLiving entityliving, double d, double d1, double d2,
  14.             float f, float f1)
  15.     {
  16.      renderDiracPig((dcEntityDiracPig)entityliving, d, d1, d2, f, f1);
  17.     }
  18.     public void doRender(Entity entity, double d, double d1, double d2,
  19.             float f, float f1)
  20.     {
  21.      renderDiracPig((dcEntityDiracPig)entity, d, d1, d2, f, f1);
  22.     }
  23. }
复制代码

这样我们就有了一个可以渲染DiracPig的Render了.
在mod_Diracon类中重写BaseMod的addRenderer方法


  1. public void addRenderer(Map map)
  2.     {   
  3.         map.put(dcEntityDiracPig.class, new dcRenderDiracPig(new ModelPig(), 0.7F));
  4. }
复制代码

Eclipse会在Map那里报一个错,因为你没有导入一个必要的包,将鼠标移到Map上,在弹出的窗口中选择Import"Map".让Eclipse自动完成修正.
addRenderer这个方法会获取游戏的渲染列表(系统会把渲染map发送给这个方法),你可以为渲染列表添加渲染,比如map.put(dcEntityDiracPig.class, new RenderPig(new ModelPig(), new ModelPig(0.5F), 0.7F));,dcEntityDiracPig.class是你的NPC的类,new RenderPig(new ModelPig(), new ModelPig(0.5F), 0.7F)是使用RenderPig类建立一个新的Render,并输入相关的参数,大部分Render只需要2个参数 - "模型类"和"大小".在这里的模型类是ModelPig类,大小是0.7F.

接下来在load方法内加入

  1. ModLoader.registerEntityID(dcEntityDiracPig.class, "DiracPig", 121);
  2. ModLoader.addSpawn(dcEntityDiracPig.class, 10, 4,4,EnumCreatureType.monster);
复制代码

我们向游戏的实体列表添加了一个实体,它的里名字是DiracPig,实体ID是121(121~199是一片空闲的实体ID),被注册的类是dcEntityDiracPig.class

getUniqueEntityId这个方法可以用来获取一个可用的空闲ID.但我不推荐使用这个,因为一旦你添加新mod导致读取顺序被改变,发生的事情会是未知的,你可以在EntityList.java中查看可用的实体ID.

最后我们向NPC刷新列表添加了一个新的刷新,EnumCreatureType.monster是设置刷新类型为Mob,这样它会在晚上刷出.10, 4,4是刷新频率,这个是大多数野怪的刷新参数,我们可以直接套用,具体的详细规则会在Extra编中讲解.
此时,你的load方法的末尾代码应该是(图中的AddRenderer应为addRenderer)
类的头部分应该是

最后我们为它绘制一个霸气的纹理.我们从Minecraft.jar中的mob目录内找到了猪的纹理pig.png,解压后将它乱涂一番,然后存到MCP目录下的lib\mob  (自行新建文件夹) 改名叫diracpig.png
然后保存,编译,测试.
入夜后,不幸神隐入迪拉克之海(xi jian)的紫皮猪会出现,并向玩家发动进攻...用你的迪拉克之杖痛扁他们吧!

让Dirac Pig去猎杀其他怪物

我们制作完了一个新的Mob,准确说是一个新的Entity,但现在感觉未免熊了点,我们的新怪物除了样子猎奇一点以外一点也不特别,所以我们现在来对他修改一下.让DiracPig能与玩家并肩作战.

知识点:OMGWTF!?怪物是怎么发现我的?
///////////////////////////////////////////////////////////////////////////////////////////////////////////
EntityMob类中的一个方法findPlayerToAttack()能获取一个怪物附近16格内可攻击并且可被怪物看见的玩家,它的原理是由World类的getClosestVulnerablePlayerToEntity方法获取最近的可攻击的玩家,然后用EntityLiving类的canEntityBeSeen方法检测是否可被怪物看见.我们只要重写findPlayerToAttack()方法就能让紫皮猪们自动寻找其他野怪去杀.

MC中每个活物都有EyeHeight(眼高)这个属性,它代表这个活物的眼睛到地面的高度,默认的眼高是最大身高的85%,canEntityBeSeen方法的运作机理是利用World类的rayTraceBlocks方法在野怪的眼部到玩家的眼部创建一条线段,并判断线段是否会被方块挡住.
///////////////////////////////////////////////////////////////////////////////////////////////////////////

但就在即将编写代码的时候我们又遇到了新的问题,游戏提供了getClosestVulnerablePlayerToEntity方法来查询距离某实体最近的玩家,但没有提供方法来查询距离某实体最近的怪物...但我们也有相应的解决办法,同样是World类,getEntitiesWithinAABB这个方法能够获取一个长方体内的指定类实体,它的运作机理很复杂,涉及到Chunk,这里就不过多解释了(读者:卧槽你其实是自己也没研究明白吧...)

为dcEntityDiracPig类添加这个方法

  1. protected Entity findPlayerToAttack()
  2.     {
  3.      double i = this.posX;
  4.      double j = this.posY;
  5.      double k = this.posZ;
  6.      List list = worldObj.getEntitiesWithinAABB(net.minecraft.src.EntityMob.class, AxisAlignedBB.getBoundingBox((double)i - 16, (double)j - 4, (double)k - 16, (double)i + 16, (double)j + 4, (double)k + 16));
  7.         for (int a=0;a<list.size();a++)
  8.         {
  9.          if(list.get(a) instanceof dcEntityDiracPig == false)
  10.          {
  11.          return (Entity)list.get(a);
  12.          }
  13.         }
  14.         return null;
  15. }
复制代码
(注:在MCP7.0a以前,getBoundingBox这个方法的名字是getBoundingBoxFromPool)
i,j,k是迪拉克猪的坐标,list是调用getEntitiesWithinAABB方法后获得的返回列表,返回列表包括以迪拉克猪自身为中心,一个长宽32x32,高8的长方体内所有的怪物(EntityMob)实体.(顺便一提,getEntitiesWithinAABB的效率不高...用多了会卡).
按照规范,我们应该先检查list是否是个空表,但在这里list一定包含了至少一个实体(迪拉克猪自己也会被列入list内)所以我们就不用检查了,之后我们会枚举list内所有的Object(懂得面向对象编程的人一定知道这是何物,顺便一问,Java有foreach这种枚举吗...我是学C#的=w=) 并判断它是否是dcEntityDiracPig(迪拉克猪)类,如果不是便将它转换为Entity并返回.(为什么我们要判断它是否等于迪拉克猪类?你总不会希望那紫皮猪自己打自己或同类相残吧...)

于是保存,编译,测试.

夜幕降临(~ Evening Star),紫皮猪再次出现,这次它们不但不会攻击玩家,反而会去猎杀怪物!虽然它们真的很弱...


强化物品的功能,物品栏的操作,NBT的操作,TileEntity初解

注意:这一部分所要实现的功能需要涉及修改游戏源文件.
对源文件的修改很容易导致Mod冲突,如果不是非常必要,请尽可能避免.

现在我们要制作迪拉克之杖(Dirac Wand)的强化功能,首先我们来设计一下它的功能:
右击一个箱子A,再右击箱子B,就能把箱子A的物品转移至箱子B.
箱子A的记录数据随物品绑定,存读游戏后依旧保留.

知识点:使用物品会引发的方法
///////////////////////////////////////////////////////////////////////////////////////////////////////////
我们已经知道hitEntity这个方法是在物体击中一个实体时引发(即左键攻击一个实体且命中时),接下来我会列出在其他情况下引发的方法.

左键挥动击中砖块时.
getStrVsBlock  返回一个物品对指定砖块可造成的伤害(注意你无法通过这个方法来获得一个具体的方块!)
(2012.3.28备注:我猜想应该还是有办法来获取一个具体的方块,我会在Extra编中写)

左键挥动击中实体(NPC)时.
hitEntity  通常来说是降低指定物品的耐久度.实质是让玩家对物品造成伤害.
getDamageVsEntity  返回一个物品对实体可造成的伤害.

敲碎一个砖块时
onBlockDestroyed  通常来说是降低物品的耐久度.实质是让玩家对物品造成伤害.
canHarvestBlock  判断是否可以获得捣碎的砖块...这个功能比较含糊,建议还是使用上一个.

右键使用一个物品时
onItemRightClick  默认没有什么特殊功能.
onItemUse  对砖块使用时引发,和上面那个相比,它多了几个参数
useItemOnEntity  对某个实体使用物品时触发.

松开右键时
onPlayerStoppedUsing 好吧这个功能不是很可靠,最好不要依赖它...

物品受到损伤时
getMaxDamage  返回一个物体的最大耐久度

物品被创建时
onCreated  充当构造函数的功能吧...

Update物品时(更新物品的状态)
onUpdate  没有太多可解释的...

这些方法的访问级都是public或protected,换句话说你可以随意在继承类中重写它们!HOYA!
///////////////////////////////////////////////////////////////////////////////////////////////////////////

之后我们还要考虑如何存储这些数据,请记住,一切物品(Item)都是以物品栈(ItemStack)的形式在游戏世界中体现,比如当你拿着之前制作的迪拉克之杖时,你手中拿着的并非是一个叫diracwand的Item,而是一个类型为diracwand,数量为1的ItemStack.同理,一切数据也都要在储存在ItemStack的NBT节点中(如果你够范可以学着ItemMap专门写一个类来储存数据,不过这样只会让你的工作量激增,最后你还是不得不求助于NBT).

现在便可以开始第一个规则的编写了,为dcItemDiracWand添加一个方法重写

  1. public boolean onItemUse(ItemStack itemstack, EntityPlayer entityplayer, World world, int i, int j, int k, int l)
  2. {
  3.     //待会在这里添加代码
  4.         return true;
  5.     }
复制代码

这样我们便添加了一个onItemUse的重写,它会在玩家对一个砖块按右键时引发.
之后为继续为方法添加代码

  1. NBTTagCompound nbttagcompound = itemstack.getTagCompound();
  2. if(nbttagcompound == null)
  3. {
  4.      itemstack.setTagCompound(new NBTTagCompound());
  5.      nbttagcompound = itemstack.getTagCompound();
  6. }
  7. if(nbttagcompound.getBoolean("chestAExist") == false)
  8. {
  9.      if(world.getBlockId(i, j, k) == Block.chest.blockID)
  10.      {
  11.           nbttagcompound.setInteger("chestAx", (int)i);
  12.           nbttagcompound.setInteger("chestAy", (int)j);
  13.           nbttagcompound.setInteger("chestAz", (int)k);
  14.           nbttagcompound.setBoolean("chestAExist", true);
  15.      }
  16. }
复制代码

这个代码会先获取物品栈的NBT节点,然后判断NBT节点是否存在,如果不存在则创建,之后检查节点的chestAExist数据是否是false(即使chestAExist不存在也没关系,不存在的Boolean类数据默认视为false)即判断是否设置过箱子A,如果没有那么便判断玩家右键的砖块是否是箱子,如果是就储存下玩家所右击的箱子的位置,并将chestAExist设为true.
此时你的代码应该是这样
那么如果chestAExist存在且为true呢?自然就是转移箱子的物品了,在写下一步代码前先了解一下MC的物品储存机制和TileEntity的原理.

知识点:物品存储机制
///////////////////////////////////////////////////////////////////////////////////////////////////////////
当我写到这部分时我都畏葸了...因为我自己都没完全研究明白MC的物品存储系统的运作机理,但毕竟自己是写教程的...明白多少就得写多少了.
以单人模式下(多人模式下也类似,只不过运算会交给服务器端)的箱子的物品存储运作机理为例.
当玩家放置一个箱子后,就会在箱子的位置同时创建一个TileEntity(待会我会解释TileEntity为何物)
如果两个箱子连接为一个大箱子,它们俩的TileEntity不会合并.
当玩家打开一个箱子时,系统会分析这是一个小箱子还是一个大箱子,如果是小箱子就将它的TileEntityChest按照IInventory接口转换并作为参数输入玩家实体的displayGUIChest方法.如果是大箱子就将2个箱子的TileEntityChest转化为一个InventoryLargeChest类,最后再按照IInventory接口转换并作为参数输入玩家实体的displayGUIChest方法.这个方法经过一系列传导后最终会显示给玩家物品栏界面.

解释:
TileEntityChest类包含了实体的创建和删除等方法,此外物品数据也存储于此
IInventory接口定义了最基本的几个方法:物品的存取等,它可以过滤掉TileEntityChest多余的方法.
InventoryLargeChest类是一个合集,它包括2个已按照IInventory接口转换过的TileEntityChest,分别对应两只箱子.它是基于IInventory接口创建的类,所以说它与IInventory接口完全匹配.

不管你是否看没看或看没看懂上面的文字,我们只要知道一个道理就能继续本篇的教程了:物品数据存储在TileEntityChest内.
顺便一提,玩家的物品数据储存在EntityPlayer的inventory变量里.
///////////////////////////////////////////////////////////////////////////////////////////////////////////

知识点:TileEntity初解
///////////////////////////////////////////////////////////////////////////////////////////////////////////
但愿你已经知道NBT是何物了吧...
众所周知,MC中Entity(实体)会随着游戏保存进存档内,玩家是实体,所以玩家的背包也能被保存.然而砖块呢?
答案是:当然也被保存,但为了缩小地图数据,所有的砖块(不管你是无用的泥砖还是神奇的火炉)都会被一视同仁地以最简形式(只有简单的ID等信息,没有坐标数据,没错,正是因为如此,你不能依靠一个Block类来获取一个砖块的坐标!)储存起来,结果就是一些专有数据丢失了,比如箱子的储物,火炉内尚未取出的矿物.
(2012.3.28备注:我认为也许可以通过遍历对比一片区域内的Block来确定一个砖块的坐标,也许这里太难表达我的想法了,我会在Extra编中解释)
为此,Notch使用了一个愚蠢的办法(比一[哔-]两制还要愚蠢):专门创造一种实体,用于储存砖块的信息.
这种实体就是TileEntity
TileEntity也和普通的Entity一样,直接存在于游戏世界中,然后有些TileEntity可以看到(比如牌子的TileEntitySignRenderer,它是用来在游戏世界中渲染文字),有些则看不见摸不着(比如箱子的TileEntityChest,它用来记录箱子内的物品).TileEntity与它所属的砖块是重叠在一起的,也就是说它与所属砖块的XYZ坐标都是一样的.游戏也是根据XYZ坐标来定位TileEntity,换句话说,如果你请求获取一个火炉的TileEntityFurnace,游戏会根据火炉的XYZ坐标来查找它所在的位置的TileEntityFurnace.这也是为什么有时你通过特殊手段刷出的特殊砖块会失效.

TileEntity和Entity一样,都需要将它的特殊信息保存入NBT中.
///////////////////////////////////////////////////////////////////////////////////////////////////////////

了解这些后我们就可以开始编写了.
准备在if结构的下面,return true的上面,加入这些代码...


  1. else
  2. {
  3.         int Ax = (int)nbttagcompound.getInteger("chestAx"); //读取NBT数据
  4.         int Ay = (int)nbttagcompound.getInteger("chestAy");
  5.         int Az = (int)nbttagcompound.getInteger("chestAz");
  6.         int Bx = i;  //将当前敲得砖块坐标设为Bx,By,Bz
  7.         int By = j;
  8.         int Bz = k;
  9.         if(world.getBlockId(Ax, Ay, Az) != Block.chest.blockID) //如果箱子A不存在了,就返回
  10.         {
  11.                 return true;
  12.         }
  13.         if(world.getBlockId(Bx, By, Bz) != Block.chest.blockID) //如果玩家敲得不是一个箱子,就返回
  14.         {
  15.                 return true;
  16.         }
  17.         Object obj = (TileEntityChest)world.getBlockTileEntity(Ax, Ay, Az); //取得箱子A的TileEntity并强转换为TileEntityChest
  18.         //之后我们需要检测箱子A的前后左右有没有和他相连的箱子,如果有,将它们俩的TileEntity拼接成一个InventoryLargeChest
  19.         if (world.getBlockId(Ax - 1, Ay, Az) == Block.chest.blockID)
  20.         {
  21.                 obj = new InventoryLargeChest("Large chest", (TileEntityChest)world.getBlockTileEntity(Ax - 1, Ay, Az),((IInventory) (obj)));
  22.         }
  23.         if (world.getBlockId(Ax + 1, Ay, Az) == Block.chest.blockID)
  24.         {
  25.                 obj = new InventoryLargeChest("Large chest", ((IInventory) (obj)), (TileEntityChest)world.getBlockTileEntity(Ax + 1, Ay, Az));
  26.         }
  27.         if (world.getBlockId(Ax, Ay, Az - 1) == Block.chest.blockID)
  28.         {
  29.                 obj = new InventoryLargeChest("Large chest", (TileEntityChest)world.getBlockTileEntity(Ax, Ay, Az - 1),((IInventory) (obj)));
  30.         }
  31.         if (world.getBlockId(Ax, Ay, Az + 1) == Block.chest.blockID)
  32.         {
  33.                 obj = new InventoryLargeChest("Large chest", ((IInventory) (obj)), (TileEntityChest)world.getBlockTileEntity(Ax, Ay, Az + 1));
  34.         }
  35.         IInventory inv1 = (IInventory) obj; //将obj按照IInventory接口转换,并将结果存为Inv1.Inv1即箱子A的物品.
  36.         obj = (TileEntityChest)world.getBlockTileEntity(Bx, By, Bz);//取得箱子B的TileEntity并强转换为TileEntityChest
  37.         //同样,我们需要检测箱子B的前后左右有没有和他相连的箱子,如果有,将它们俩的TileEntity拼接成一个InventoryLargeChest
  38.         if (world.getBlockId(Bx - 1, By, Bz) == Block.chest.blockID)
  39.         {
  40.                 obj = new InventoryLargeChest("Large chest", (TileEntityChest)world.getBlockTileEntity(Bx - 1, By, Bz), ((IInventory) (obj)));
  41.         }
  42.         if (world.getBlockId(Bx + 1, By, Bz) == Block.chest.blockID)
  43.         {
  44.                 obj = new InventoryLargeChest("Large chest", ((IInventory) (obj)), (TileEntityChest)world.getBlockTileEntity(Bx + 1, By, Bz));
  45.         }
  46.         if (world.getBlockId(Bx, By, Bz - 1) == Block.chest.blockID)
  47.         {
  48.                 obj = new InventoryLargeChest("Large chest", (TileEntityChest)world.getBlockTileEntity(Bx, By, Bz - 1), ((IInventory) (obj)));
  49.         }
  50.         if (world.getBlockId(Bx, By, Bz + 1) == Block.chest.blockID)
  51.         {
  52.                 obj = new InventoryLargeChest("Large chest", ((IInventory) (obj)), (TileEntityChest)world.getBlockTileEntity(Bx, By, Bz + 1));
  53.         }
  54.         IInventory inv2 = (IInventory) obj; //将obj按照IInventory接口转换,并将结果存为Inv2.Inv2即箱子B的物品.
  55.         int ASize = inv1.getSizeInventory();//获取Inv1的箱子容量,箱子容量是所有格子的综合,一个小箱子是27容量.
  56.         int BSize = inv2.getSizeInventory();
  57.         for (int loop=0;loop<ASize;loop++) //遍历Inv1的所有格子,在内部计数中,一个容量为N的箱子,所拥有的格子的编号为[0~(N-1)] 所以我们的循环范围是严格小于ASize
  58.         {
  59.                  ItemStack IS = inv1.getStackInSlot(loop); //获取Inv1在第loop个格子上的物品栈
  60.                  if(IS != null)  //如果物品栈不是null(不存在)的话.
  61.                  {
  62.                          for(int loop2=0;loop2<=BSize;loop2++)  //遍历Inv2的格子以寻找可插入物品栈的空位,注意由于我们需要处理特殊情况:Inv2没有可用的格子.所以我们让循环范围是小于等于BSize
  63.                          {
  64.                          if(loop2 == BSize) //如果loop等于箱子容量,即再也没有任何可用的空位,就让物品栈从箱子B中弹出来.
  65.                           {
  66.                                  EntityItem entityitem = new EntityItem(world, Bx, By + 1f, Bz, IS); //创建一个EntityItem,它代表在游戏中被扔在地上的物品栈
  67.                                  entityitem.delayBeforeCanPickup = 10; //设置拾取延迟.
  68.                                  world.spawnEntityInWorld(entityitem); //生成实体
  69.                                  inv1.setInventorySlotContents(loop, null); //将Inv1中的对应物品栈抹除(设为null)
  70.                                  break; //中止循环
  71.                          }
  72.                          if(inv2.getStackInSlot(loop2) == null) //如果发现了可用空位.
  73.                          {
  74.                                  inv2.setInventorySlotContents(loop2, IS); //在Inv2的可用空位制造一个一模一样的物品栈
  75.                                  inv1.setInventorySlotContents(loop, null); //清除Inv1中对应的物品栈
  76.                                  break; //中止循环
  77.                          }
  78.                  }
  79.              }
  80.         }
  81.         nbttagcompound.setBoolean("chestAExist", false); //将NBT中的"已存在箱子A"设为false
  82. }
复制代码

很高能吧...它和你的其他代码的位置关系是这样的.
但如果你在此时编译测试的话你会发现它在游戏里毫无效果,因为游戏默认设置如果你持有具有右键功能的物品对具有特殊功能的砖块按右键的话,只会执行右键砖块的效果,所以我们必须修改游戏源文件(再次说明,为了防止Mod冲突,对源文件修改一定要尽量避免,但如果迫不得已也不要吊死在一棵树上,该修改时就修改...)打开PlayerControllerSP.java,找到onPlayerRightClick方法,将
if (i > 0 && Block.blocksList.blockActivated(par2World, par4, par5, par6, par1EntityPlayer))
         {
             return true;
         }
修改为
  1. if(par1EntityPlayer.isSneaking() == false)
  2.         {
  3.                 if (i > 0 && Block.blocksList[i].blockActivated(par2World, par4, par5, par6, par1EntityPlayer))
  4.                 {
  5.                     return true;
  6.                 }
  7.         }
复制代码

这样如果玩家按住Shift的话,始终只会执行物品的功能.

编译程序,然后开始测试.

使用迪拉克之杖按住Shift对着一个箱子按右键,然后对着另一个箱子按右键,第一个箱子的物品就会转移至第二个箱子,如果空间不够的话就会弹出在第二个箱子的上方.

添加特效与屏幕文字

现在做成了一个迪拉克之杖,但我们发现它缺乏足够的提示,有时候我们不清楚到底有没有设置成功,所以我们希望添加一些提示,比如特效什么的.

知识点:波与粒的境界
///////////////////////////////////////////////////////////////////////////////////////////////////////////
游戏中有一些粒子特效,比如你敲砖块时崩发出的岩石碎片,敌人倒地后出现的灰色烟雾,紫色的传送门中散发出的光粒,这些粒子特效全部派生自一个类:EntityFX(特效实体)
EntityFX派生自Entity类,默认的可输入参数有World,d,d2,d3,d4,d5,d6(某些特殊的派生类根据需求不同改掉了一些参数,但大体仍不变),World是特效所在的世界,d,d2,d3对应xyz坐标,d4,d5,d6对应速度向量在xyz三个轴上的分速度.

至于你问波在哪,我也不知道...
///////////////////////////////////////////////////////////////////////////////////////////////////////////
我们决定使用使用EntityPortalFX(传送门的光粒特效)作为粒子特效,我们设置箱子时能够让箱子崩发出一些粒子特效.

在NBTTagCompound nbttagcompound = itemstack.getTagCompound();的下方加入

  1. Random random = new Random();
复制代码

之后Eclipse会报错,用纠错功能添加缺少的包

在设定箱子A的部分中的nbttagcompound.setBoolean("chestAExist", true);的下方加入

  1. for(int number = 0;number <= 10;number++)
  2. {
  3.         world.spawnParticle("portal",(double)i+0.5,(double)j,(double)k+0.5,(double)random.nextFloat() - 0.5D,(double)random.nextFloat(),(double)random.nextFloat() - 0.5D);
  4. }
复制代码

这个代码能够创建10个粒子并他们向上方喷发(在游戏内不明显,这是由于EntityPortalFX的特性),并且随机带有一些角度.portal是传送门的光粒子的名字.关于各种粒子的名字,可以在RenderGlobal.java内找到.

在为箱子B传送物品的部分中的nbttagcompound.setBoolean("chestAExist", false);的下方加入相同的代码.

  1. for(int number = 0;number <= 10;number++)
  2. {
  3.         world.spawnParticle("portal",(double)i+0.5,(double)j,(double)k+0.5,(double)random.nextFloat() - 0.5D,(double)random.nextFloat(),(double)random.nextFloat() - 0.5D);
  4. }
复制代码

这样我们就做完粒子特效了,那不妨再加一点别的,比如文字提示.

知识点:向玩家屏幕的左下角显示文字
///////////////////////////////////////////////////////////////////////////////////////////////////////////
事实上,实现这个功能非常简单,你只要调用一个玩家实体的addChatMessage方法就行了.唯一的难度就是如何获取玩家实体.告诉你个办法,Wolrd类的playerEntities列表包含所有位于这个世界的玩家.如果你问我如何获取Wolrd...
///////////////////////////////////////////////////////////////////////////////////////////////////////////
在我们添加粒子特效的代码下新起一行,分别加入

  1. entityplayer.addChatMessage("已设置传送起点");
复制代码
  1. entityplayer.addChatMessage("物品传送完毕");
复制代码

这时你的代码应该是这个样子.
于是保存,编译,测试.

如果你完成了上面的几章,那么现在你已经有能力制作一些更加复杂的mod了.你现在能创建新的Mob,能重写Mob的行为模式,能为物品添加功能,创建粒子特效,向玩家屏幕输出文字.下一篇我们会学习最后两个部分,地形生成器和TileEntity的创建.

但愿你还没有读晕吧=w=反正作为写的人我还没晕.


下一篇:地形生成器,制作新的TileEntity