MCBBS Wiki欢迎您共同参与编辑!在参与编辑之前请先阅读Wiki方针

如果在编辑的过程中遇到了什么问题,可以去讨论板提问。

为了您能够无阻碍地参与编辑 未验证/绑定过邮箱的用户,请尽快绑定/验证

MCBBS Wiki GitHub群组已上线!

您可以在回声洞中发表吐槽!

服务器状态监控。点击进入

本站由MCBBS用户自行搭建,与MCBBS及东银河系漫游指南(北京)科技有限公司没有从属关系。点此了解 MCBBS Wiki 不是什么>>

用户:MashKJo/1.12.2模组开发教程/11.高级物品和方块 - 前置概念:修订间差异

来自MCBBS Wiki
跳到导航 跳到搜索
(// Edit via Wikiplus)
→‎在世界中放置方块:​ // Edit via Wikiplus
 
(未显示同一用户的8个中间版本)
第3行: 第3行:
很简单,如果我们排除掉使用TileEntity的方块,那么剩下的那些高级的物品/方块,几乎都是通过覆写Item类或Block类下的若干方法实现的,这些方法有的是Minecraft原生支持的,还有一些是Forge塞进去的。你可能会看到Minecraft有些原生支持的方法被@Deprecated了,那可能是因为Forge觉得这个方法提供的形参不够多,对于对应功能的控制还不够精细,因此patch了一个更为强大的同功能重载方法。另外,如果你实在找不到可以覆写的方法,那么可以考虑监听一些事件。
很简单,如果我们排除掉使用TileEntity的方块,那么剩下的那些高级的物品/方块,几乎都是通过覆写Item类或Block类下的若干方法实现的,这些方法有的是Minecraft原生支持的,还有一些是Forge塞进去的。你可能会看到Minecraft有些原生支持的方法被@Deprecated了,那可能是因为Forge觉得这个方法提供的形参不够多,对于对应功能的控制还不够精细,因此patch了一个更为强大的同功能重载方法。另外,如果你实在找不到可以覆写的方法,那么可以考虑监听一些事件。


对于高级物品,Item类有如下常用的方法值得覆写:
然而,覆写这些方法通常涉及到一些很重要也比较复杂的类,这些类比较琐碎,应用也非常广泛,如果真的按照“讲到了对应的专题再详细讲”这个原则安排内容,会导致一系列问题——例如,World类是一个非常重要的类,用到的地方超级多,但是读者想必也不希望掌握如何创建一个新维度后,再回来写高级的物品/方块吧?因此这一节就专门讲这些琐碎的类以及它们涉及到的一些重要概念。
* onItemUseFirst、onItemUse:在物品被玩家手持,且右键非空气方块时触发,均返回一个EnumActionResult,前者先于后者被触发。对于前者而言,返回EnumActionResult.PASS,则代表会继续执行原版的相关逻辑。
* onItemUseFinish:该方法和上述2个方法有所区别——该方法限定的实体并不仅仅是玩家(EntityPlayer)了,而是所有生物(EntityLivingBase)。它返回一个ItemStack,代表某个ItemStack被使用后的结果。
* onItemRightClick:该方法会在玩家手持物品右键时触发——没错,它不要求玩家必须选中某个非空气方块,只要右键就行,实际上它也先于onItemUseFirst和onItemUse触发。Minecraft本身只提供了修改右键物品的逻辑的该方法,那么如果我们想干涉左键逻辑怎么办?考虑监听这两个事件:PlayerInteractEvent.LeftClickBlock和PlayerInteractEvent.LeftClickEmpty,具体在监听时可以判断玩家的手持物品,再写入相关逻辑,即可。
* canHarvestBlock:返回一个布尔值,注意该方法有2个重载,其中一个是原版的,还有一个是Forge塞进来的,当然推荐使用后者。用于判断某个BlockState是否可以被该Item对应的某个ItemStack挖掘并产生掉落物,原版中该方法的实现很丑陋,全是面向结果编程的味道,且鉴于Forge patch的<code>Block#setHarvestLevel</code>这一方法的存在,一般你不该动这个方法,不过如果你想做出像剪刀那样比较特殊的挖掘、掉落效果,就可以覆写这个方法。
* onUpdate:这是一个比较重要的方法,它在游戏主循环中,每一tick都会被调用一次,也就是说理想状态下每秒钟内两个逻辑端各会调用该方法20次。因此如果我们覆写了该方法,便可以赋予相应的ItemStack一秒刷新20次的能力,我们利用它可以做出一些很高级的物品,例如计时器之类的东西。从该方法中我们可以拿到持有该ItemStack的实体(没错,Entity,甚至都非EntityLivingBase),以及“该物品是否被选中”(isSelected)。
* onEntityItemUpdate:决定当前ItemStack对应的EntityItem的刷新逻辑。
* onCreated:在该物品被合成出来后触发。
* getMaxItemUseDuration:用于指定物品被右键按住使用后,持续的最长时间——如右键弓后弓会逐渐拉伸至蓄满力状态,蓄满力后一段时间内,如果不松开右键,弓会一直紧绷着。注意这个方法的返回值是一个int值,单位实际上是tick。
* onPlayerStoppedUsing:配合着getMaxItemUseDuration使用的——即玩家松开右键后执行的游戏逻辑,原版的弓在这里即会召唤出一发箭矢实体(EntityArrow)。
* addInformation:@SideOnly(Side.CLIENT),用于给ItemStack添加tooltip,具体为向一个List<String>中添加新元素的过程,一般是搭配<code>I18n#format</code>实现,甚至我们可以通过当前ItemStack的附加NBT信息来决定添加的tooltip。
* hasEffect:@SideOnly(Side.CLIENT),用于决定该ItemStack是否有附魔光泽——没错,ItemStack有无附魔和有无附魔光泽的对应关系并不是写死的,只是Item类对该方法的默认实现即为:有附魔才有附魔光泽,你大可给你的Item子类在这里无脑返回一个true。
* getItemBurnTime:如果你想添加熔炉燃料,那么覆写这个方法即可。返回一个正整数,则默认它是一个熔炉燃料,且单位为tick;若返回0,则代表它不可燃;返回-1,则代表它的可燃状态交给原版Minecraft去决定。
* getIsRepairable:用于决定该ItemStack可被什么ItemStack修复。两个参数中,toRepair是被修复的,repair是修复材料。

对于高级方块,Block类下有如下常用方法值得覆写:
* isPassable:决定实体是否可以从该方块中穿过,参考原版的门方块,就不难理解了。
* isReplacable:决定该方块是否可以在其他方块被放置时取代——如果不能,则方块会被放置在该方块的侧面。如原版的液体方块和空气方块在这里返回true。
* neighborChanged:覆写该方法,即可自定义该种方块的周围发生方块更新时执行的游戏逻辑。一般有两个用途:1.激发红石信号,如观察者方块;2.触发对该方块的状态的合法性检验(这里的“状态”不仅仅包括BlockState,是一种抽象的概念),如悬空的沙子在周围有方块更新时会立刻下坠。
* breakBlock:在方块被破坏后触发,注意不限于被玩家破坏(如果想要限定被玩家破坏才执行逻辑,可以考虑覆写onBlockDestroyedByPlayer),但使用<code>World#setBlockState</code>替换该方块时,不会触发这个方法。
* onBlockActivated:玩家右键点击该方块后触发,注意两只手都会触发一次,所以最好先判定传入的EnumHand。原版的工作台和熔炉,在这里,就是让玩家打开相应的GUI界面。
* getStateForPlacement:上一节已经提到过,不再赘述。
* onBlockClicked:这个方法从MCP得到的名字可能会很有迷惑性,读者可能会以为它在左键、右键被按下时都会触发,实际上不对,只有左键点击它会触发该方法。
* onBlockPlacedBy:在某个生物实体放置该方块时被触发,注意不限于玩家,像末影人这种非玩家生物也可以。
* isFlammable、getFlammability等一系列方法:用于决定该方块是否可燃、燃烧能力等一系列和火方块相关的参数。按照Minecraft原版的设计,这些东西本来应该通过<code>BlockFire#setFireInfo</code>这个公开方法指定,不过经过Forge的patch后,我们便可以在Block类中覆写方法指定了。
* getDrops:得到该方块所有可能的掉落物(以一个NonNullList<ItemStack>的形式呈现),由于每破坏一次该种方块,该方法就会调用一次,所以在这里可以利用利用随机数。
* getExpDrop:得到该方块被采集时,掉落的经验量。
* isReplacableOreGen:决定:在区块生成的过程中,已经被生成的该种方块是否可以被矿石取代。

想必读者也看到了,覆写这些方法通常涉及到一些很重要也比较复杂的类,这些类比较琐碎,应用也非常广泛,如果真的按照“讲到了对应的专题再详细讲”这个原则安排内容,会导致一系列问题——例如,World类是一个非常重要的类,用到的地方超级多,但是读者想必也不希望掌握如何创建一个新维度后,再回来写高级的物品/方块吧?因此这一节就专门讲这些琐碎的类以及它们涉及到的一些重要概念。
== 一些枚举类 ==
== 一些枚举类 ==
在给我们的高级的物品/方块实现一些功能时,我们会和很多枚举类打交道,如:
在给我们的高级的物品/方块实现一些功能时,我们会和很多枚举类打交道,如:
* EnumAction:这个枚举类专用于物品,具体地说,是表示物品被使用时,产生的动画效果的“类型”(说是动画效果,实际上就是一层薄薄的贴图转了个角度后的一堆平移、伸缩或旋转变换而已),由<code>Item#getItemUseAction</code>决定。原版有5种EnumAction,分别是:NONE、EAT、DRINK、BLOCK、BOW。很显然,后四者分别用于食物、水瓶和药水、方块的物品形式、弓。那么可能会有读者问了:如果我想自定义物品使用时的动画效果,怎么办,如何去做到?答案是通常情况下做不到,除非你用黑魔法。一般而言,新的EnumAction可以通过Forge提供的EnumHelper来通过反射生成,但是Minecraft具体处理这个EnumAction并实际产生动画的代码中,Forge实际上没有发布任何事件,所以理论上我们无法新增自定义的物品使用动画,除非你用Mixin大法,但除非你Modding水平够高,否则执意在Forge框架下开发时用Mixin实现自己的需求,还不如自己换个需求。
* EnumAction:这个枚举类专用于物品,具体地说,是表示物品被使用时,产生的动画效果的“类型”(说是动画效果,实际上就是一层薄薄的贴图转了个角度后的一堆平移、伸缩或旋转变换而已),由<code>Item#getItemUseAction</code>决定。原版有5种EnumAction,分别是:NONE、EAT、DRINK、BLOCK、BOW。很显然,后四者分别用于食物、水瓶和药水、方块的物品形式、弓。那么可能会有读者问了:如果我想自定义物品使用时的动画效果,怎么办,如何去做到?答案是通常情况下做不到,除非你用黑魔法。一般而言,新的EnumAction可以通过Forge提供的EnumHelper来通过反射生成,但是Minecraft具体处理这个EnumAction并实际产生动画的代码中,Forge实际上没有发布任何事件,所以理论上我们无法新增自定义的物品使用动画,除非你用Mixin大法,但除非你Modding水平够高,否则执意在Forge框架下开发时用Mixin实现自己的需求,还不如自己换个需求。
* EnumActionResult:这个枚举类有3个实例:SUCCESS、PASS和FAIL,它用于描述某个交互逻辑进行后,返回的结果(或者说执行结果)。其实它的意义不是那么大,因为它也就只是一个返回值而已,你可以在写一堆你自己的自定义逻辑后返回FAIL,本质上就是一个标记而已。不过对于物品的使用而言,SUCCESS和FAIL都不会在执行完主手上的物品的相关逻辑后,继续执行副手上对应物品相关逻辑,而PASS则会,这个特性需要小小地注意一下
* EnumActionResult:这个枚举类有3个实例:SUCCESS、PASS和FAIL,它用于描述某个交互逻辑进行后,返回的结果(或者说执行结果)。其实它的意义不是那么大,因为它也就只是一个返回值而已,你可以在写一堆你自己的自定义逻辑后返回FAIL,本质上就是一个标记而已。3个返回值没有固定含义,读者在使用前请先查看相关的代码
* EnumBlockRenderType:顾名思义,这个枚举类用于描述方块的渲染类型——INVISIBLE、LIQUID、ENTITYBLOCK_ANIMATED、MODEL。
* EnumBlockRenderType:顾名思义,这个枚举类用于描述方块的渲染类型——INVISIBLE、LIQUID、ENTITYBLOCK_ANIMATED、MODEL。
** MODEL:默认值,就是最普通的渲染类型——从指定的模型文件中读取出一个模型(实际上是通过一个ICustomModelLoader得到了一个IModel),并经历一系列加工过程,最后渲染出来。
** MODEL:默认值,就是最普通的渲染类型——从指定的模型文件中读取出一个模型(实际上是通过一个ICustomModelLoader得到了一个IModel),并经历一系列加工过程,最后渲染出来。
第21行: 第50行:


== 一些向量类 ==
== 一些向量类 ==
Minecraft在<code>net.minecraft.util.math</code>包下提供了三个向量类:Vec2f、Vec3f和Vec3d。实际上读者应该能很清楚地看出来,数字代表空间维度数,第一个是平面向量,后两个是空间向量;而字母代表坐标精度,f代表float,d代表double。一般而言,我们用的都是Vec3d。
Minecraft在<code>net.minecraft.util.math</code>包下提供了三个向量类:Vec2f、Vec3i和Vec3d。实际上读者应该能很清楚地看出来,数字代表空间维度数,第一个是平面向量,后两个是空间向量;而字母代表坐标精度,i代表int,f代表float,d代表double。一般而言,我们用的都是Vec3d。


合理运用向量类能给不少问题中的数学运算带来极大的便利,如Vec3d下就有很多简化计算过程的好用方法,读者可以去自行察看。
合理运用向量类能给不少问题中的数学运算带来极大的便利,如Vec3d下就有很多简化计算过程的好用方法,读者可以去自行察看。
第28行: 第57行:


== ActionResult<T> ==
== ActionResult<T> ==
这是对EnumActionResult的一个扩展——有时候,Minecraft处理游戏逻辑时,在获取对某个对象的处理的结果时,不仅想知道处理是否成功(SUCCESS、PASS、FAIL这些结果),还想知道该对象的最终值,这个时候就需要用到这个类了,该类的构造很简单,就是<code>(EnumFacing typeIn, T value)</code>。实际上我们一般能见到的是ActionResult<ItemStack>。
这是对EnumActionResult的一个扩展——有时候,Minecraft处理游戏逻辑时,在获取对某个对象的处理的结果时,不仅想知道处理是否成功(SUCCESS、PASS、FAIL这些结果),还想知道该对象的最终值,这个时候就需要用到这个类了,该类的构造很简单,就是<code>(EnumActionResult typeIn, T value)</code>。实际上我们一般能见到的是ActionResult<ItemStack>。


== AxisAlignedBB ==
== AxisAlignedBB ==
第53行: 第82行:


== RayTraceResult ==
== RayTraceResult ==
RayTrace,意思为“光线追踪”,如该词语的含义所言,RayTrace在Minecraft中的含义,即为“计算生物实体的视线,确定其视线选中的到底是一个实体,还是一个非空气方块,亦或者就是空气(即等价于什么都没选中)”。玩家在游戏中在准星指向的位置放置方块时,就会有RayTrace的过程。描述RayTrace结果的类型便是RayTraceResult。从一个RayTraceResult中,你可以拿到很多相关的有用信息。

World类下有与RayTrace相关的一些方法,我们需要的时候可以直接拿来用。

另外,实体的碰撞结果也是用RayTraceResult来描述的(最常见的例子就是实体投掷物砸中某个方块或实体)。


== World ==
== World ==
其实在笔者看来,World类是个设计得比较奇怪的类,从字面意思上看,它就是代表着某个维度——这么理解的确是对的。然而,World类可不仅承载了维度这一概念,甚至还包括了存档这一概念——World中相关信息的保存路径、世界生成时选择的设置(如是否有奖励箱生成),甚至“是否开启了极限模式”这种全局通用设置,你都能从一个World对象中获取。Mojang显然是把“维度”和“存档”这两个概念揉到了一个类中——即World类。

World类的设计极为复杂,相关的方法、用途也很多,这里只做简要说明。很多其他的东西,会在将来讲到添加新维度时,一并说明的。

=== 判断当前逻辑端 ===
没错,我们终于说到了,判断当前所处的逻辑端的方法——检查非静态字段isRemote的值——是true,则代表当前为逻辑客户端,否则为逻辑服务端。虽然也有其它一些方法来判断逻辑段,但都没有这种方法好用,因为World对象在Minecraft各处代码中均可见到。

=== 在世界中放置方块 ===
在一个World中获取某坐标处的方块状态很简单,getBlockState这个方法即可胜任。与之类似地,放置方块,可以用setBlockState。

但我们注意到:后者有2个重载,形参列表的区别在于:有一个重载较另一个重载而言,多了一个:<code>int flags</code>。flags即标记,但它又是一个int类型的数——显然,它是一个幻数,具体含义到底如何,全看Mojang的底层设计如何了。如果去看看那个较为简单的方法,可以发现,它实际上是调用了另一个重载方法,并指定flags为3——为什么是3?

显然,这很让人摸不着头脑。不过Forge在<code>Constants.BlockFlags</code>类下提供了相应的常量,并通过注释和常量名说明了flags的具体含义。简单地说,Minecraft会从传入的幻数中获取很多信息,每一种信息的具体值都由传入的幻数对应的二进制数的每一位决定:

{| class="wikitable"
|-
! 常量名及含义 !! 数值(二进制形式)
|-
| NOTIFY_NEIGHBORS(邻近方块更新) || 0b00001
|-
| SEND_TO_CLIENTS(将相关数据同步到客户端) || 0b00010
|-
| NO_RENDERER(该方块的渲染状态不会被更新) || 0b00100
|-
| RENDERER_MAIN_THREAD(让方块的渲染状态在游戏主线程中立刻被更新) || 0b01000
|-
| NO_OBSERVERS(该行为不会被观察者方块观测到) || 0b10000
|-
|}

Minecraft的默认行为是采取前两项行为,因此默认的幻数值是把前两项行为对应的数进行按位或运算,即0b00011——实际上也就是上文提到的3。你可能会注意到,第三条和第四条是相冲突的,当发生冲突时,以第三条为准。

== 注释与外部链接 ==

2024年12月29日 (日) 22:52的最新版本

首先,笔者想对能看到这里的读者,道一声恭喜!因为能学到这里,读者应该已经掌握了创建一个新物品或新方块的基本的标准化流程,且已经熟悉了相关的重要概念(如ItemStack和NBT格式的运用),这已经超越了许多新手modder了。不过应该也有读者存有疑问:之前学习的东西,充其量只能搞出一些没有任何额外行为和高级功能的,仅能被当作合成材料或建材使用的物品/方块,那么那些知名模组中的看起来很高级的物品/方块,都是怎么做出来的呢?

很简单,如果我们排除掉使用TileEntity的方块,那么剩下的那些高级的物品/方块,几乎都是通过覆写Item类或Block类下的若干方法实现的,这些方法有的是Minecraft原生支持的,还有一些是Forge塞进去的。你可能会看到Minecraft有些原生支持的方法被@Deprecated了,那可能是因为Forge觉得这个方法提供的形参不够多,对于对应功能的控制还不够精细,因此patch了一个更为强大的同功能重载方法。另外,如果你实在找不到可以覆写的方法,那么可以考虑监听一些事件。

对于高级物品,Item类有如下常用的方法值得覆写:

  • onItemUseFirst、onItemUse:在物品被玩家手持,且右键非空气方块时触发,均返回一个EnumActionResult,前者先于后者被触发。对于前者而言,返回EnumActionResult.PASS,则代表会继续执行原版的相关逻辑。
  • onItemUseFinish:该方法和上述2个方法有所区别——该方法限定的实体并不仅仅是玩家(EntityPlayer)了,而是所有生物(EntityLivingBase)。它返回一个ItemStack,代表某个ItemStack被使用后的结果。
  • onItemRightClick:该方法会在玩家手持物品右键时触发——没错,它不要求玩家必须选中某个非空气方块,只要右键就行,实际上它也先于onItemUseFirst和onItemUse触发。Minecraft本身只提供了修改右键物品的逻辑的该方法,那么如果我们想干涉左键逻辑怎么办?考虑监听这两个事件:PlayerInteractEvent.LeftClickBlock和PlayerInteractEvent.LeftClickEmpty,具体在监听时可以判断玩家的手持物品,再写入相关逻辑,即可。
  • canHarvestBlock:返回一个布尔值,注意该方法有2个重载,其中一个是原版的,还有一个是Forge塞进来的,当然推荐使用后者。用于判断某个BlockState是否可以被该Item对应的某个ItemStack挖掘并产生掉落物,原版中该方法的实现很丑陋,全是面向结果编程的味道,且鉴于Forge patch的Block#setHarvestLevel这一方法的存在,一般你不该动这个方法,不过如果你想做出像剪刀那样比较特殊的挖掘、掉落效果,就可以覆写这个方法。
  • onUpdate:这是一个比较重要的方法,它在游戏主循环中,每一tick都会被调用一次,也就是说理想状态下每秒钟内两个逻辑端各会调用该方法20次。因此如果我们覆写了该方法,便可以赋予相应的ItemStack一秒刷新20次的能力,我们利用它可以做出一些很高级的物品,例如计时器之类的东西。从该方法中我们可以拿到持有该ItemStack的实体(没错,Entity,甚至都非EntityLivingBase),以及“该物品是否被选中”(isSelected)。
  • onEntityItemUpdate:决定当前ItemStack对应的EntityItem的刷新逻辑。
  • onCreated:在该物品被合成出来后触发。
  • getMaxItemUseDuration:用于指定物品被右键按住使用后,持续的最长时间——如右键弓后弓会逐渐拉伸至蓄满力状态,蓄满力后一段时间内,如果不松开右键,弓会一直紧绷着。注意这个方法的返回值是一个int值,单位实际上是tick。
  • onPlayerStoppedUsing:配合着getMaxItemUseDuration使用的——即玩家松开右键后执行的游戏逻辑,原版的弓在这里即会召唤出一发箭矢实体(EntityArrow)。
  • addInformation:@SideOnly(Side.CLIENT),用于给ItemStack添加tooltip,具体为向一个List<String>中添加新元素的过程,一般是搭配I18n#format实现,甚至我们可以通过当前ItemStack的附加NBT信息来决定添加的tooltip。
  • hasEffect:@SideOnly(Side.CLIENT),用于决定该ItemStack是否有附魔光泽——没错,ItemStack有无附魔和有无附魔光泽的对应关系并不是写死的,只是Item类对该方法的默认实现即为:有附魔才有附魔光泽,你大可给你的Item子类在这里无脑返回一个true。
  • getItemBurnTime:如果你想添加熔炉燃料,那么覆写这个方法即可。返回一个正整数,则默认它是一个熔炉燃料,且单位为tick;若返回0,则代表它不可燃;返回-1,则代表它的可燃状态交给原版Minecraft去决定。
  • getIsRepairable:用于决定该ItemStack可被什么ItemStack修复。两个参数中,toRepair是被修复的,repair是修复材料。

对于高级方块,Block类下有如下常用方法值得覆写:

  • isPassable:决定实体是否可以从该方块中穿过,参考原版的门方块,就不难理解了。
  • isReplacable:决定该方块是否可以在其他方块被放置时取代——如果不能,则方块会被放置在该方块的侧面。如原版的液体方块和空气方块在这里返回true。
  • neighborChanged:覆写该方法,即可自定义该种方块的周围发生方块更新时执行的游戏逻辑。一般有两个用途:1.激发红石信号,如观察者方块;2.触发对该方块的状态的合法性检验(这里的“状态”不仅仅包括BlockState,是一种抽象的概念),如悬空的沙子在周围有方块更新时会立刻下坠。
  • breakBlock:在方块被破坏后触发,注意不限于被玩家破坏(如果想要限定被玩家破坏才执行逻辑,可以考虑覆写onBlockDestroyedByPlayer),但使用World#setBlockState替换该方块时,不会触发这个方法。
  • onBlockActivated:玩家右键点击该方块后触发,注意两只手都会触发一次,所以最好先判定传入的EnumHand。原版的工作台和熔炉,在这里,就是让玩家打开相应的GUI界面。
  • getStateForPlacement:上一节已经提到过,不再赘述。
  • onBlockClicked:这个方法从MCP得到的名字可能会很有迷惑性,读者可能会以为它在左键、右键被按下时都会触发,实际上不对,只有左键点击它会触发该方法。
  • onBlockPlacedBy:在某个生物实体放置该方块时被触发,注意不限于玩家,像末影人这种非玩家生物也可以。
  • isFlammable、getFlammability等一系列方法:用于决定该方块是否可燃、燃烧能力等一系列和火方块相关的参数。按照Minecraft原版的设计,这些东西本来应该通过BlockFire#setFireInfo这个公开方法指定,不过经过Forge的patch后,我们便可以在Block类中覆写方法指定了。
  • getDrops:得到该方块所有可能的掉落物(以一个NonNullList<ItemStack>的形式呈现),由于每破坏一次该种方块,该方法就会调用一次,所以在这里可以利用利用随机数。
  • getExpDrop:得到该方块被采集时,掉落的经验量。
  • isReplacableOreGen:决定:在区块生成的过程中,已经被生成的该种方块是否可以被矿石取代。

想必读者也看到了,覆写这些方法通常涉及到一些很重要也比较复杂的类,这些类比较琐碎,应用也非常广泛,如果真的按照“讲到了对应的专题再详细讲”这个原则安排内容,会导致一系列问题——例如,World类是一个非常重要的类,用到的地方超级多,但是读者想必也不希望掌握如何创建一个新维度后,再回来写高级的物品/方块吧?因此这一节就专门讲这些琐碎的类以及它们涉及到的一些重要概念。

一些枚举类

在给我们的高级的物品/方块实现一些功能时,我们会和很多枚举类打交道,如:

  • EnumAction:这个枚举类专用于物品,具体地说,是表示物品被使用时,产生的动画效果的“类型”(说是动画效果,实际上就是一层薄薄的贴图转了个角度后的一堆平移、伸缩或旋转变换而已),由Item#getItemUseAction决定。原版有5种EnumAction,分别是:NONE、EAT、DRINK、BLOCK、BOW。很显然,后四者分别用于食物、水瓶和药水、方块的物品形式、弓。那么可能会有读者问了:如果我想自定义物品使用时的动画效果,怎么办,如何去做到?答案是通常情况下做不到,除非你用黑魔法。一般而言,新的EnumAction可以通过Forge提供的EnumHelper来通过反射生成,但是Minecraft具体处理这个EnumAction并实际产生动画的代码中,Forge实际上没有发布任何事件,所以理论上我们无法新增自定义的物品使用动画,除非你用Mixin大法,但除非你Modding水平够高,否则执意在Forge框架下开发时用Mixin实现自己的需求,还不如自己换个需求。
  • EnumActionResult:这个枚举类有3个实例:SUCCESS、PASS和FAIL,它用于描述某个交互逻辑进行后,返回的结果(或者说执行结果)。其实它的意义不是那么大,因为它也就只是一个返回值而已,你可以在写一堆你自己的自定义逻辑后返回FAIL,本质上就是一个标记而已。3个返回值没有固定的含义,读者在使用前请先查看相关的代码。
  • EnumBlockRenderType:顾名思义,这个枚举类用于描述方块的渲染类型——INVISIBLE、LIQUID、ENTITYBLOCK_ANIMATED、MODEL。
    • MODEL:默认值,就是最普通的渲染类型——从指定的模型文件中读取出一个模型(实际上是通过一个ICustomModelLoader得到了一个IModel),并经历一系列加工过程,最后渲染出来。
    • INVISIBLE:顾名思义,就是代表该方块什么都不渲染,如空气方块,如果你的模组中有一些方块纯粹是用于技术性用途,那可以设定成INVISIBLE这个渲染类型,再附加一个setHardness(-1.0F)即可。
    • LIQUID:用于流体方块的渲染,代表例子是原版的水方块。
    • ENTITYBLOCK_ANIMATED:这个渲染类型很特殊,它单独起到的效果和INVISIBLE其实是大差不差的,只是它用于标记——该方块有TileEntity[1],且该方块的渲染全部由该TileEntity对应的一些相对接近于底层(实际上是lwjgl的OpenGL)的渲染代码来完成,包装成的东西名为TileEntitySpecialRenderer,一般简称TESR,TESR能帮我们实现很多单纯的模型文件做不到的效果,如:在方块表面渲染文字、动态贴图甚至实现动画效果。但是注意:用了TESR的方块实体,对应的方块的渲染类型,一般也还是MODEL。为何?因为这代表先让Minecraft默认的渲染机制为我们渲染好方块的大致轮廓和基础材质,再用TESR作补充;然而如果用了ENTITYBLOCK_ANIMATED,代表:方块的基础轮廓也是要我们手动去渲染的,典型的例子是原版的箱子。当它开启时,它的顶部的全部材质和四个侧面的一部分材质都会抬升并旋转一个角度——由于四个侧面都仅仅有一部分材质会参与这个过程,因此Minecraft的默认渲染机制确实不能胜任,所以才有必要用这个渲染类型。所以记住——除非你有特殊的需求,否则就算你用TESR渲染TileEntity,你也该用MODEL作为渲染类型而非ENTITYBLOCK_ANIMATED。有关TileEntity和TESR的内容,将会在后面讲到,这里先做了解即可。
  • BlockRenderLayer:这个枚举类同样应用于方块,只是它用于指定方块的材质的特性。它有4个实例:SOLID、TRANSLUCENT、CUTOUT、CUTOUT_MIPPED。SOLID代表方块的材质完全不透明,TRANSLUCENT则代表半透明,CUTOUT则是材质中有一部分全透明而另外一部分完全不透明,至于CUTOUT_MIPPED,它是CUTOUT的抗锯齿版本,和CUTOUT实际上差别不是很大。
  • EnumFacing:这是一个表示方向的枚举类,可以表示东、南、西、北、上、下六个方向,前面已经提过,此处不再赘述。
  • EnumHand和EnumHandSide:二者都用于表示主手和副手,不知道Mojang出于怎样的考量写出了这两个定位几乎一样的枚举类,反正用的时候也没区别,注意一下到底是哪个类型就行。
  • EntityEquipmentSlot:用于描述一个有物品栏的生物的物品栏槽位种类,总共有6种:主手、副手、头部、胸部、腿部、脚部。

以上就是一些常用的枚举类,多亏了MCP,它们的类名和实例名都很容易让人顾名思义,用起来也比较简单,这里就不多说了。

一些向量类

Minecraft在net.minecraft.util.math包下提供了三个向量类:Vec2f、Vec3i和Vec3d。实际上读者应该能很清楚地看出来,数字代表空间维度数,第一个是平面向量,后两个是空间向量;而字母代表坐标精度,i代表int,f代表float,d代表double。一般而言,我们用的都是Vec3d。

合理运用向量类能给不少问题中的数学运算带来极大的便利,如Vec3d下就有很多简化计算过程的好用方法,读者可以去自行察看。

实际上,虽然这三个类被MCP取名为向量(Vector),然而它果真只能代表平面中或空间中一个向量或坐标吗?显然不是。它们本质上,其实是对有序数对或三维有序数组的简单封装而已。所以理论上它们也可以用来表示除向量和坐标之外的其他东西。比如,Vec3d也被经常用来表示维度的天空和虚空迷雾的颜色——在这里,Vec3d构造方法中传入的3个数值,其实就是相当于该颜色的R、G、B三个通道的各自的值。

ActionResult<T>

这是对EnumActionResult的一个扩展——有时候,Minecraft处理游戏逻辑时,在获取对某个对象的处理的结果时,不仅想知道处理是否成功(SUCCESS、PASS、FAIL这些结果),还想知道该对象的最终值,这个时候就需要用到这个类了,该类的构造很简单,就是(EnumActionResult typeIn, T value)。实际上我们一般能见到的是ActionResult<ItemStack>。

AxisAlignedBB

AxisAlignedBB,全称AxisAlignedBoundingBox,简称AABB。这个类描述的是一个长宽高都确定的长方体——在实际应用中用于指定某个方块的碰撞箱,以及鼠标指向某方块时产生的勾选框。这个类下有许多运算用方法,如取交集(intersect)、取并集(union),但实际上这里的取交集和取并集并不等同于数学意义上的交集与并集,具体是怎样的读者去看看它们的具体实现就好了,简单来说这两个方法都返回一个AxisAlignedBB,然而空间中两个长宽高都平行于坐标轴的长方体,其并出来的新的几何图形可不一定是一个标准的长方体,所以这两个方法实际上意义不大了。

但实际上,Block类中用于获取碰撞箱的方法,返回值类型就是AxisAlignedBB,这是不是意味着我们的方块的碰撞箱只能是一个长方体了?实际上并不是,我们可以通过覆写addCollisionBoxToList来实现,具体操作为向一个List<AxisAlignedBB>中添加新元素,这就很简单了。

顺带一提,似乎Block类下几乎所有和碰撞箱有关的方法都被打上了@Deprecated注解,读者对此不要感到有疑虑,放心大胆地用即可。有的时候不要在乎这个@Deprecated,毕竟Block类下的getStateFromMetagetMetaFromState都被@Deprecated了——你总不能不覆写这两个方法吧?不覆写的话,注册多BlockState方块就会崩游戏了。实际上这都是因为Mojang即将在1.13大改这些类和相关方法,你一般会看到Forge对此写的一些注释如“to be removed in 1.13”,可问题是我们现在是在写1.12的模组,所以有的时候这些@Deprecated也挺让人不知所谓的。

BlockPos

BlockPos,顾名思义,即方块坐标(Block Position)。由于是用来描述方块的,因此它存储的三个坐标值也是int类型的——哪怕它有一个三个参数全为double的构造方法。不过注意,实体的三个坐标值则是double类型的,这需要区分一下。

DamageSource

这个类用于描述实体所受到的伤害类型——如虚空伤害、魔法伤害等等。这个类不是枚举类,所以我们可以自由地new新的DamageSource,供我们自己用。

原版的所有已经定义好的DamageSource都在DamageSource类下以静态字段的形式呈现,需要的时候直接用即可。DamageSource主要有2个用途:第一个用途为参与一些判断,如有些生物会判断自己受到的伤害的DamageSource,并决定是否免疫它,如末影人就自动免疫弹射物造成的伤害;一系列特化版的保护附魔(如火焰保护、弹射物保护等)也是通过判断DamageSource来减免带有附魔的盔甲的穿戴者受到的伤害的。第二个用途是决定玩家死亡时的聊天栏提示信息——DamageSource的构造器中接受一个String参数:damageType,如果玩家因为某种DamageSource死亡,那么聊天栏发送的消息的本地化键名为"death.attack." + damageType + ".player"

I18n和ITextComponent

在模组开发中,我们时常会遇到这样一种需求:给某个玩家发送聊天栏信息,很简单,用EntityPlayer#sendMessage即可(注意不是sendStatusMessage),我们注意到这个方法接受一个ITextComponent,实际上出于本地化的需求与考虑,我们用的最多的该接口的实现是TextComponentTranslation。这个类的构造方法接受一个String——实际上就是用于充当该消息内容的本地化键名。我们在发送消息时,应该先判断逻辑端,否则消息会发送两次——由于我们处理逻辑一般是在逻辑服务端,因此发送消息一般也在这一端执行。

如果我们在发送消息之外的场景中,还有本地化的需求怎么办?答案是使用net.minecraft.client.resources.I18n#format,该方法返回一个本地化之后的String量,用法和TextComponentTranslation基本一样,但从该类的包名即可看出——该类只在物理客户端有意义,使用它时要尤其注意这一点。

另外,你或许也注意到了,无论是TextComponentTranslation的构造方法还是I18n#format,它都还可以接受一个Object变长参数,这实际上是为了满足一个需求:需要在本地化文本中插入动态的内容,具体用法类似于C语言中的printf函数,如字符串的占位符是%s

RayTraceResult

RayTrace,意思为“光线追踪”,如该词语的含义所言,RayTrace在Minecraft中的含义,即为“计算生物实体的视线,确定其视线选中的到底是一个实体,还是一个非空气方块,亦或者就是空气(即等价于什么都没选中)”。玩家在游戏中在准星指向的位置放置方块时,就会有RayTrace的过程。描述RayTrace结果的类型便是RayTraceResult。从一个RayTraceResult中,你可以拿到很多相关的有用信息。

World类下有与RayTrace相关的一些方法,我们需要的时候可以直接拿来用。

另外,实体的碰撞结果也是用RayTraceResult来描述的(最常见的例子就是实体投掷物砸中某个方块或实体)。

World

其实在笔者看来,World类是个设计得比较奇怪的类,从字面意思上看,它就是代表着某个维度——这么理解的确是对的。然而,World类可不仅承载了维度这一概念,甚至还包括了存档这一概念——World中相关信息的保存路径、世界生成时选择的设置(如是否有奖励箱生成),甚至“是否开启了极限模式”这种全局通用设置,你都能从一个World对象中获取。Mojang显然是把“维度”和“存档”这两个概念揉到了一个类中——即World类。

World类的设计极为复杂,相关的方法、用途也很多,这里只做简要说明。很多其他的东西,会在将来讲到添加新维度时,一并说明的。

判断当前逻辑端

没错,我们终于说到了,判断当前所处的逻辑端的方法——检查非静态字段isRemote的值——是true,则代表当前为逻辑客户端,否则为逻辑服务端。虽然也有其它一些方法来判断逻辑段,但都没有这种方法好用,因为World对象在Minecraft各处代码中均可见到。

在世界中放置方块

在一个World中获取某坐标处的方块状态很简单,getBlockState这个方法即可胜任。与之类似地,放置方块,可以用setBlockState。

但我们注意到:后者有2个重载,形参列表的区别在于:有一个重载较另一个重载而言,多了一个:int flags。flags即标记,但它又是一个int类型的数——显然,它是一个幻数,具体含义到底如何,全看Mojang的底层设计如何了。如果去看看那个较为简单的方法,可以发现,它实际上是调用了另一个重载方法,并指定flags为3——为什么是3?

显然,这很让人摸不着头脑。不过Forge在Constants.BlockFlags类下提供了相应的常量,并通过注释和常量名说明了flags的具体含义。简单地说,Minecraft会从传入的幻数中获取很多信息,每一种信息的具体值都由传入的幻数对应的二进制数的每一位决定:

常量名及含义 数值(二进制形式)
NOTIFY_NEIGHBORS(邻近方块更新) 0b00001
SEND_TO_CLIENTS(将相关数据同步到客户端) 0b00010
NO_RENDERER(该方块的渲染状态不会被更新) 0b00100
RENDERER_MAIN_THREAD(让方块的渲染状态在游戏主线程中立刻被更新) 0b01000
NO_OBSERVERS(该行为不会被观察者方块观测到) 0b10000

Minecraft的默认行为是采取前两项行为,因此默认的幻数值是把前两项行为对应的数进行按位或运算,即0b00011——实际上也就是上文提到的3。你可能会注意到,第三条和第四条是相冲突的,当发生冲突时,以第三条为准。

注释与外部链接

  1. 没错,EntityBlock指的其实就是TileEntity,实际上高版本它也叫BlockEntity,意思其实都一样,翻译成中文都是方块实体。