[举二反三深入模组开发 第二节] 彩虹桥法杖 + 建筑小助手 = _ - 编程开发 - Minecraft(我的世界)中文论坛 -.html

[举二反三深入模组开发 第二节] 彩虹桥法杖 + 建筑小助手 = ? - 编程开发 - Minecraft(我的世界)中文论坛 -

Minecraft(我的世界)中文论坛

 找回密码
 注册(register)

!header_login!

只需一步,立刻登录

查看: 3438|回复: 7
打印 上一主题 下一主题

[Mod开发教程] [举二反三深入模组开发 第二节] 彩虹桥法杖 + 建筑小助手 = ?

[复制链接]
兔肉煲 当前离线
积分
3676
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2014-3-29
查看详细资料

来自:吉林

跳转到指定楼层
楼主
发表于 2021-1-14 13:41:48 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

您尚未登录,立即登录享受更好的浏览体验!

您需要 登录 才可以下载或查看,没有帐号?注册(register)

x
本帖最后由 兔肉煲 于 2021-1-14 13:44 编辑

举二反三深入模组开发 彩虹桥法杖 + 建筑小助手 = ?

本节将分析彩虹桥方块的动画效果, 建筑小助手是如何触及极远的区域的, 最后我们会造一个便利的探矿洞法杖

知识速览:

  • TileEntity的ITickable实现
  • (附加内容: 动态材质的实现)
  • BlockRayTrace



天之苍苍,其正色耶?

说起Botania的彩虹桥法杖, 相信读者并不陌生, 其美丽的特效出自浪漫的Vazkii之手, 观察彩虹桥方块我们可以发现, 其模型应该是一个不断变色的方块, 并且不时会散发出有植物气息的粒子效果, 彩虹桥方块会在其产生后一段时间内自动消失, 不难想象应该是实现了ITickable接口的TileEntity.



从彩虹桥方块中我们可以发现如下三个特性, 我们一个一个来看

  • 透明方块的动态材质
  • 在方块周围生成指定的粒子效果
  • 过一段时间后自我消失

透明方块的动态材质

关于动态材质相信读者都不会太陌生, 因为模组的模型加载说到底还是跟随了原版的机制, 所以我们固然可以想到动态资源包中常用的mcmeta格式以及瀑布般的长条材质, 其实Minecraft中重复简单的方块动画都可以用mcmeta配合一个包含动画过程关键帧的图片格式轻松实现, 其实彩虹桥方块的动画也是这样的, 在Botania的resources目录中我们可以找到bifrost.json, 这就是彩虹桥方块的模型

  1. {
  2.   "parent": "minecraft:block/cube_all",
  3.   "textures": {
  4.     "all": "botania:blocks/bifrost"
  5.   }
  6. }
  7. Botania开源地址: https://github.com/Vazkii/Botania
  8. 节选自(1.12-final分支): /resources/assets/botania/models/block/bifrost.json
复制代码

显然彩虹桥方块的模型与普通方块是相同的, 换句话说我们只需要关注动画材质就可以了, 根据Model里面声明的路径我们去找对应的贴图以及mcmeta文件.



这里就不多说什么了, 如果我们也想要一个动态模型只需如图一样创建贴图和mcmeta标记文件就可以了

注: mcmeta的文件名应为贴图名+.mcmeta

frametime: 动画播放的帧率

interpolate: 是否开启差值补帧(使动画渐变更流畅)

之后我们来看透明的实现, 在Bifrost的方块声明中我们可以找到如下代码

  1. public class BlockBifrostPerm extends BlockMod implements ILexiconable {

  2.     public BlockBifrostPerm(String name) {
  3.         super(Material.GLASS, name);
  4.         setLightOpacity(0);
  5.         setLightLevel(1F);
  6.         setSoundType(SoundType.GLASS);
  7.     }

  8.     @Override
  9.     public boolean isOpaqueCube(IBlockState state) {
  10.         return false;
  11.     }

  12.     @Override
  13.     public boolean isFullCube(IBlockState state) {
  14.         return false;
  15.     }

  16.     @Override
  17.     public boolean shouldSideBeRendered(IBlockState state, @Nonnull IBlockAccess world, @Nonnull BlockPos pos, EnumFacing side) {
  18.         if (world.getBlockState(pos.offset(side)).getBlock() == this) {
  19.             return false;
  20.         }
  21.         return super.shouldSideBeRendered(state, world, pos, side);
  22.     }

  23.     @SideOnly(Side.CLIENT)
  24.     @Nonnull
  25.     @Override
  26.     public BlockRenderLayer getRenderLayer() {
  27.         return BlockRenderLayer.TRANSLUCENT;

  28.     }
  29. }
  30. Botania开源地址: https://github.com/Vazkii/Botania
  31. 节选自(1.12-final分支): /common/block/BlockBifrostPerm.java
复制代码

显然我们的彩虹桥方块需要像玻璃一样可以透光, 在Minecraft1.15.2中我们可以让方块直接继承自AbstractGlassBlock从而直接得到类似玻璃的透光能力, 但在1.12.2我们还不能这样做, 于是我们可以覆盖isOpaqueCube isFullCube getRenderLayer等一系列方法, 使方块可以透光, 为了拥有更好的显示效果, Vazkii还覆盖了shouldSideBeRendered方法, 重叠的面将不会渲染, 这里笔者就不多说了, 读者可以自行阅读上文该方法的代码

在方块周围生成指定的粒子效果

有心的读者如果去Github阅读了BlockBifrostPerm的源码可以发现我们列举的代码中缺少了如下部分

  1. @Override
  2. public void randomDisplayTick(IBlockState state, World world, BlockPos pos, Random rand) {
  3.     if(rand.nextBoolean())
  4.         Botania.proxy.sparkleFX(pos.getX() + Math.random(), pos.getY() + Math.random(), pos.getZ() + Math.random(), (float) Math.random(), (float) Math.random(), (float) Math.random(), 0.45F + 0.2F * (float) Math.random(), 6);
  5. }
  6. Botania开源地址: https://github.com/Vazkii/Botania
  7. 节选自(1.12-final分支): /common/block/BlockBifrostPerm.java
复制代码

显然这段代码的含义便是粒子效果的渲染, randomDisplayTick会在贴图刷新的随机游戏刻执行, 可以看到Vazkii在代码的开头使用if (rand.nextBoolean())使下面代码执行的概率变为50%, 防止生成太多粒子造成卡顿. Vazkii自己实现了一套proxy来运行sparkleFX()来保持客户端渲染, 并且让粒子可以运动, 其中大多使用GL直接渲染, 这里的实现略微有点复杂我们就不展开了, 读者如果希望粒子效果包含运动的效果, 可以自行阅读Botania源码, 其代码大多位于fx包中

若要渲染普通的粒子效果, 使用world.spawnParticle(), 并且保证位于ClientSide执行

过一段时间后自我消失

在BlockBifrost类中, 我们可以发现彩虹桥方块绑定了TileEntity, 其类为TileBifrost, 用于储存数据, 我们来看一下TileBifrost类

  1. public class TileBifrost extends TileMod implements ITickable {

  2.     private static final String TAG_TICKS = "ticks";

  3.     public int ticks = 0;

  4.     @Override
  5.     public void update() {
  6.         if(!world.isRemote) {
  7.             if(ticks <= 0) {
  8.                 world.setBlockToAir(pos);
  9.             } else ticks--;
  10.         }
  11.     }

  12.     @Nonnull
  13.     @Override
  14.     public NBTTagCompound writeToNBT(NBTTagCompound par1nbtTagCompound) {
  15.         NBTTagCompound ret = super.writeToNBT(par1nbtTagCompound);
  16.         ret.setInteger(TAG_TICKS, ticks);
  17.         return ret;
  18.     }

  19.     @Override
  20.     public void readFromNBT(NBTTagCompound par1nbtTagCompound) {
  21.         super.readFromNBT(par1nbtTagCompound);
  22.         ticks = par1nbtTagCompound.getInteger(TAG_TICKS);
  23.     }

  24. }
  25. Botania开源地址: https://github.com/Vazkii/Botania
  26. 节选自(1.12-final分支): /common/block/tile/TileBifrost.java
复制代码

显然TileBifrost实现了ITickable, 这意味着这个方块具备了刷新数据的能力, 阅读update方法我们发现, 其实质就是自减内部储存的tick值, 如果减没了, 那么就使方块消失, writeToNBT和readFromNBT的用途不言而喻, 常写TileEntity的读者一定对其有所了解, Minecraft会在合适的时候(一般是保存世界和读取世界的时候)调用这两个方法, 以便于在进入和退出存档之前读写方块里面的数据

善于思考的读者此时可能发问了, 这个TileEntity的tick初始值应该是什么呢, 在上述对彩虹桥方块的分析过程中我们并没有看到对于tick初始值的声明....不妨再来想想彩虹桥方块是怎么被放置在世界中的呢....对了, 就是通过彩虹桥法杖,  所以不难想象设置tick的初始值的代码应该写在法杖中, 读者可自行验证猜想, 代码位于/common/item/rod/ItemRainbowRod.java的onItemRightClick方法中



其远而无所至极邪?

建筑小助手是direwolf20作为模组开发者的处女作, 其中各种小助手可以隔数十格远放置方块, 手中握着小助手就可以追踪到目光所及之处的方块.



如果我们查阅建筑小助手对应工具中build方法的具体实现, 可以找到如下代码.

  1. RayTraceResult lookingAt = VectorTools.getLookingAt(player, stack);
  2. if (lookingAt == null) { //If we aren't looking at anything, exit
  3.     return false;
  4. }
  5. BlockPos startBlock = lookingAt.getBlockPos();
  6. EnumFacing sideHit = lookingAt.sideHit;
  7. coords = BuildingModes.collectPlacementPos(world, player, startBlock, sideHit, stack, startBlock);
  8. BuildingGadgets开源地址: https://github.com/Direwolf20-MC/BuildingGadgets
  9. 节选自(1.12.x分支): /common/items/gadgets/GadgetBuilding.java
复制代码

相信聪明的读者可以看出, 建筑小助手是先通过BlockRayTrace来追踪目光所看的方块, 在通过collectPlacementPos获取需要建筑的方块的坐标值的, 这组坐标值就是代码中的coords, 也不难想象节选部分的下面也就是在coords这些坐标值处放置方块的相关代码了. 感兴趣的读者可以自行查阅, 顺便一提, 在放置方块前其实direwolf20还添加了Undo的相关代码(不知道读者有没有使用建筑小助手时出错, 从而使用撤销功能的呢? 感兴趣的话也可以自行阅读相关部分)

话归正题, 我们来具体详细的看一看这个BlockRayTrace究竟有什么门道, 在VectorHelper.getLookingAt方法中, 我们可以看到如下代码

  1. public static RayTraceResult getLookingAt(EntityPlayer player, boolean rayTraceFluid) {
  2.         World world = player.world;
  3.         Vec3d look = player.getLookVec();
  4.         Vec3d start = new Vec3d(player.posX, player.posY + player.getEyeHeight(), player.posZ);
  5.         //rayTraceRange here refers to SyncedConfig.rayTraceRange
  6.         Vec3d end = new Vec3d(player.posX + look.x * rayTraceRange, player.posY + player.getEyeHeight() + look.y * rayTraceRange, player.posZ + look.z * rayTraceRange);
  7.         return world.rayTraceBlocks(start, end, rayTraceFluid, false, false);
  8. }
  9. BuildingGadgets开源地址: https://github.com/Direwolf20-MC/BuildingGadgets
  10. 节选自(1.12.x分支): /common/tools/VectorTools.java
复制代码

可以看到, 经过一系列计算后, 我们调用的world.rayTraceBlocks()方法来获取到了RayTrace的结果, 读者可能这时对这些复杂的坐标计算产生了疑问, 希望我们了解了world.rayTraceBlocks()以后可以解答这个疑问, Ctrl+左键点击, 我们可以看到Forge对这个方法的解释

Performs a raycast against all blocks in the world.

对世界中的所有方块进行光线追踪

通过注释以及参数的分析, 我们猜测这个方法是在给定范围内对向量方向上的方块进行追踪, 这也便印证了我们为什么要提供两个Vec3d类型的参数, 相信其他的参数读者可以自行根据参数名理解, 这里就不多说了, 这里笔者希望补充一点小技巧: 即当我们对一个复杂方法的使用不了解时, 不妨通过参数和解释进行理解, 而不是直接阅读代码, 通常这些方法的内部实现较为复杂, 勉强阅读包含数学计算或复杂逻辑的内部实现对于开发Mod中实现具体需求的帮助不大, 希望之后读者也可以使用我们探究world.rayTraceBlocks()方法的这种方式来了解未知的方法.

言归正传, 我们回到getLookingAt, 可以看到我们给rayTrace提供了两个Vector, 分别是start和end, 如下图中(x,y,z)即为start的位置, 而(x1+x2, y1+y2, z1+z2)即为end的位置



这也就解释了为什么代码中的end要加上look.x/y/z(终点处乘以range是为了增大向量的长度), 相信读者读到这里也已经知道了world.rayTraceBlocks()是如何为我们进行追踪的: 有了start和end便可以构建一条有向线段, 而游戏负责帮助你追踪这条有向线段中存在的方块, 这也便达成了我们的目标. 对返回值RayTraceResult再使用getBlockPos()方法, 我们就得到了我们想要的方块坐标了.



君子生非异也, 善假于物也

读者不妨想一想结合上文两个例子的分析我们能开发出什么来...........

我认为你可能已经猜出来答案了, 因为他就写在标题上.......哦这次没有写在标题上qwq.  我们这次要做的是一个法杖, 是一个非常便利的法杖, 是一个黑魔法驱动的法杖, 是会动的长方体......哦扯远了......

这次我们的需求是做一个探矿洞中使用很方便的法杖, 姑且先叫他探矿者法杖. 我们将利用RayTrace使我们的法杖可以触及一定范围内所有的方块, 然后利用TileEntity的ITickable创造供我们通行的"临时"通道(小心不要被卡到墙里呀)

方块的触及

打开自己的idea工程, 我们先简单造一个物品, 随意地添加上贴图不要忘记注册物品, 之后我们就可以开始给物品添加功能了, 作为探矿者的法杖, 至少要能让我们简单的破坏距离较远的方块吧, 我们先覆盖onItemRightClick方法, 并在其中加入破坏方块的代码, 而要破坏方块的位置, 则由我们刚刚介绍好的RayTrace来提供, 我们也顺便创建一个VectorUtils来方便我们调用静态方法

  1. public class MinerWand extends Item {
  2.     public MinerWand() {
  3.         this.setCreativeTab(CreativeTabs.TOOLS);
  4.         this.setRegistryName("miner_wand");
  5.         this.setTranslationKey("forgedev.minerWand");
  6.     }

  7.     @Override
  8.     public ActionResult<ItemStack> onItemRightClick(World worldIn, EntityPlayer playerIn, EnumHand handIn) {
  9.         RayTraceResult ray = VectorUtils.getLookingAt(playerIn, false); //getLookingAt方法直接来源于Building Gargets
  10.         if (ray != null) { //只有追踪到方块的时候再触发
  11.             worldIn.destroyBlock(ray.getBlockPos(), false);
  12.         }
  13.         return super.onItemRightClick(worldIn, playerIn, handIn);
  14.     }
  15. }
复制代码

相信读者已经很明白了, 这里笔者就不再多说了, 需要注意的是这里我们覆盖的是onItemRightClick方法, 而不是onItemUse方法, 其原因就是因为onItemUse只有在物品被使用时才会被派发, 但是我们对远处的方块使用右键时(在游戏看来是对空气点击), 是不算做使用了物品的. (这里笔者偷偷把destoryBlock的第二个参数给了false, 读者可以查一查false意味着什么)

临时隧道的建成

隧道的产生是很容易的, 我们直接将对应的方块setToAir就万事大吉了, 但是这不符合我们对于临时隧道的理解, 临时隧道是我们通过之后, 墙壁会恢复原样的一种设定, 这样到别人家偷东西的话别人就不会发现自己家的墙开了一个洞(雾). 所以这里的需求就很明显了, 让消失的方块过一段时间后再恢复, 这里我们就需要用到TileEntity的ITickable来实现了. 我们先来创建一个自己的TileEntity类, 并加上相关的实现, 不要忘记我们需要实现ITickable接口

  1. public class TileRecoveryBlock extends TileEntity implements ITickable {
  2.     public int ticks = 20 * 5; //5 seconds
  3.     private Block block;

  4.     public TileRecoveryBlock(Block block) {
  5.         this.block = block;
  6.     }

  7.     @Override
  8.     public void update() {
  9.         if(!world.isRemote) {
  10.             ticks--;
  11.             if(ticks <= 0) {
  12.                 world.removeTileEntity(pos);
  13.                 world.setBlockState(pos, block.getDefaultState());
  14.             }
  15.         }
  16.     }

  17.     //不要忘记重写writeToNBT和readFromNBT方法, 以保证方块正确还原
  18.     @Nonnull
  19.     @Override
  20.     public NBTTagCompound writeToNBT(NBTTagCompound tagCompound) {
  21.         NBTTagCompound tag = super.writeToNBT(tagCompound);
  22.         tag.setInteger("ticks", ticks);
  23.         return tag;
  24.     }

  25.     @Override
  26.     public void readFromNBT(NBTTagCompound tagCompound) {
  27.         super.readFromNBT(tagCompound);
  28.         ticks = tagCompound.getInteger("ticks");
  29.     }
  30. }
复制代码

当Ticks达到我们所限制的5秒以后, update方**自己删除对应位置的TileEntity, 并且对方块进行复原, 需要被复原的方块在我们创建TileEntity的时候提供就好了, 所以我们的MinerWand类就变成了这样

  1. @Override
  2. public ActionResult<ItemStack> onItemRightClick(World worldIn, EntityPlayer playerIn, EnumHand handIn) {
  3.     if (!worldIn.isRemote) {
  4.         RayTraceResult ray = VectorUtils.getLookingAt(playerIn, false);
  5.         if (ray != null) {
  6.             if (playerIn.isSneaking()) { //潜行的时候创建隧道
  7.                 Block block = worldIn.getBlockState(ray.getBlockPos()).getBlock();
  8.                 worldIn.destroyBlock(ray.getBlockPos(), false);
  9.                 worldIn.setTileEntity(ray.getBlockPos(), new TileRecoveryBlock(block));
  10.             } else { //非潜行的时候单纯的破坏方块
  11.                 worldIn.destroyBlock(ray.getBlockPos(), false);
  12.             }
  13.         }
  14.     }
  15.     return super.onItemRightClick(worldIn, playerIn, handIn);
  16. }
复制代码

相信聪明的读者已经一目了然了, 我们在方块摧毁后在同一位置创建一个TileEntity, 并且把这个方块的相关信息传给TileEntity, 让TileEntity进行接下来的操作.

最后, 不要忘记在注册表中注册TileEntity, 不然最后可能会焦头烂额找不到问题的所在的.

看效果

如果读者已经正确的注册好所有的物品和TileEntity, 完成了相关操作的话, 应该会得到以下拔群的效果(笔者把我们创建的MinerWand的材质直接改为了原版的烈焰棒(偷懒嘤嘤嘤))

破坏16格以内方块



生成临时隧道



小结

本节内容的小结:

  • 通过Botania的彩虹桥法杖, 了解TileEntity实现接口ITickable的功能
  • 通过建筑小助手, 了解RayTrace的使用方式, 对尚未了解向量的读者进行简单的介绍
  • 在1和2的基础上制作了探矿者法杖, 使读者对1, 2有更深了解
  • 为读者留下了思考, 希望读者能够继续创新

课后小思考:

  • TileEntity的存在是否一定与方块绑定在一起呢
  • 使用本节的方法探究一下world.setBlockState的具体细节

本文代码已开源: https://github.com/TROU2004/forgedev

评分

参与人数 5人气 +9金粒 +95宝石 +27贡献 +3收起 理由
风的旋律+ 1+ 5MCBBS有你更精彩~
乙烯_中国+ 27MCBBS有你更精彩~
羽希氷華+ 2MCBBS有你更精彩~
海螺螺+ 4+ 50+ 3MCBBS有你更精彩~ 优秀奖励
+ 2+ 40MCBBS有你更精彩~

查看全部评分

帖子永久链接: 

Minecraft中文论坛 - 论坛版权1、本主题所有言论和图片纯属会员个人意见,与本论坛立场无关
2、本站所有主题由该帖子作者发表,该帖子作者享有帖子相关版权
3、其他单位或个人使用、转载或引用本文时必须同时征得该帖子作者的同意
4、帖子作者须承担一切因本文发表而直接或间接导致的民事或刑事法律责任
5、本帖若有内容转载自其它媒体,不代表本站赞同其观点和对其真实性负责
6、若本帖涉及任何版权问题,请立即告知本站,本站将及时予以删除并致以最深的歉意
7、Minecraft(我的世界)中文论坛管理员和版主有权不事先通知发贴者而删除本文

雪尼 当前离线
积分
5168
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2013-9-23
查看详细资料

来自:山东

沙发
发表于 2021-1-14 14:51:39 | 只看该作者
放置TileEntity的实现挺怪的。而且这么做会做会把原方块的metadata和TileEntity data擦除掉
回复

使用道具 举报

1820732766 当前离线
积分
102
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2014-1-29
查看详细资料

来自:江西

板凳
发表于 2021-1-19 21:54:49 | 只看该作者

神乎其技!6的飞起!
回复

使用道具 举报

苏子佩 当前离线
积分
1939
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2012-10-3
查看详细资料

来自:广东

地板
发表于 2021-1-20 14:17:08 | 只看该作者
学习了,我在服里试试
回复

使用道具 举报

modist 当前离线
积分
564
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2018-1-1
查看详细资料

来自:上海

5#
发表于 2021-1-22 16:15:10 | 只看该作者
雪尼 发表于 2021-1-14 14:51
放置TileEntity的实现挺怪的。而且这么做会做会把原方块的metadata和TileEntity data擦除掉 ...

确实,遇上流体也不行,而且在方块还没有恢复的时期玩家如果退出游戏,或是放上了其他带TileEntity的方块,事情会变得很麻烦
回复

使用道具 举报

Xue1r_Minn 当前离线
积分
27
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2021-3-21
查看详细资料

来自:北京

6#
发表于 2021-3-21 16:34:00 | 只看该作者
透过彩虹桥方块看东西变彩色是在哪里写的?
回复

使用道具 举报

兔肉煲 当前离线
积分
3676
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2014-3-29
查看详细资料

来自:吉林

7#
 楼主| 发表于 2021-3-21 22:59:59 | 只看该作者
Xue1r_Minn 发表于 2021-3-21 16:34
透过彩虹桥方块看东西变彩色是在哪里写的?

方块颜色变化就是一个动态材质, 把不同颜色方块的各个帧变成一个瀑布流, 然后加上meta信息就可以了
透过方块看就是玻璃的实现,上面有提到
回复

使用道具 举报

老章 当前离线
积分
108
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2020-4-7
查看详细资料

来自:山东

8#
发表于 2022-3-28 15:45:43 | 只看该作者
mcbbs有你更精彩
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册(register)

本版积分规则

Archiver|小黑屋|Mcbbs.net ( 京ICP备15023768号-1 ) | 京公网安备 11010502037624号 | 手机版

GMT+8, 2023-12-29 22:55 , Processed in 0.050781 second(s), Total 23, Slave 22 queries, Release: Build.2023.11.27 0934, Gzip On, Redis On.

"Minecraft"以及"我的世界"为美国微软公司的商标 本站与微软公司没有从属关系

© 2010-2023 我的世界中文论坛 版权所有 本站内原创内容版权属于其原创作者,除作者或版规特别声明外未经许可不得转载