- 积分
- 3676
- 帖子
- 主题
- 精华
贡献- 份
爱心- 心
- 钻石
- 颗
- 人气
- 点
- 下界之星
- 枚
- 最后登录
- 1970-1-1
- 注册时间
- 2014-3-29
来自:吉林 | 本帖最后由 兔肉煲 于 2021-1-14 13:44 编辑
举二反三深入模组开发 彩虹桥法杖 + 建筑小助手 = ?
本节将分析彩虹桥方块的动画效果, 建筑小助手是如何触及极远的区域的, 最后我们会造一个便利的探矿洞法杖
知识速览:
- TileEntity的ITickable实现
- (附加内容: 动态材质的实现)
- BlockRayTrace
天之苍苍,其正色耶?
说起Botania的彩虹桥法杖, 相信读者并不陌生, 其美丽的特效出自浪漫的Vazkii之手, 观察彩虹桥方块我们可以发现, 其模型应该是一个不断变色的方块, 并且不时会散发出有植物气息的粒子效果, 彩虹桥方块会在其产生后一段时间内自动消失, 不难想象应该是实现了ITickable接口的TileEntity.
![]()
从彩虹桥方块中我们可以发现如下三个特性, 我们一个一个来看
- 透明方块的动态材质
- 在方块周围生成指定的粒子效果
- 过一段时间后自我消失
透明方块的动态材质
关于动态材质相信读者都不会太陌生, 因为模组的模型加载说到底还是跟随了原版的机制, 所以我们固然可以想到动态资源包中常用的mcmeta格式以及瀑布般的长条材质, 其实Minecraft中重复简单的方块动画都可以用mcmeta配合一个包含动画过程关键帧的图片格式轻松实现, 其实彩虹桥方块的动画也是这样的, 在Botania的resources目录中我们可以找到bifrost.json, 这就是彩虹桥方块的模型
- {
- "parent": "minecraft:block/cube_all",
- "textures": {
- "all": "botania:blocks/bifrost"
- }
- }
- Botania开源地址: https://github.com/Vazkii/Botania
- 节选自(1.12-final分支): /resources/assets/botania/models/block/bifrost.json
复制代码 显然彩虹桥方块的模型与普通方块是相同的, 换句话说我们只需要关注动画材质就可以了, 根据Model里面声明的路径我们去找对应的贴图以及mcmeta文件.
![]()
这里就不多说什么了, 如果我们也想要一个动态模型只需如图一样创建贴图和mcmeta标记文件就可以了
注: mcmeta的文件名应为贴图名+.mcmeta
frametime: 动画播放的帧率
interpolate: 是否开启差值补帧(使动画渐变更流畅) 之后我们来看透明的实现, 在Bifrost的方块声明中我们可以找到如下代码
- public class BlockBifrostPerm extends BlockMod implements ILexiconable {
- public BlockBifrostPerm(String name) {
- super(Material.GLASS, name);
- setLightOpacity(0);
- setLightLevel(1F);
- setSoundType(SoundType.GLASS);
- }
- @Override
- public boolean isOpaqueCube(IBlockState state) {
- return false;
- }
- @Override
- public boolean isFullCube(IBlockState state) {
- return false;
- }
- @Override
- public boolean shouldSideBeRendered(IBlockState state, @Nonnull IBlockAccess world, @Nonnull BlockPos pos, EnumFacing side) {
- if (world.getBlockState(pos.offset(side)).getBlock() == this) {
- return false;
- }
- return super.shouldSideBeRendered(state, world, pos, side);
- }
- @SideOnly(Side.CLIENT)
- @Nonnull
- @Override
- public BlockRenderLayer getRenderLayer() {
- return BlockRenderLayer.TRANSLUCENT;
- }
- }
- Botania开源地址: https://github.com/Vazkii/Botania
- 节选自(1.12-final分支): /common/block/BlockBifrostPerm.java
复制代码 显然我们的彩虹桥方块需要像玻璃一样可以透光, 在Minecraft1.15.2中我们可以让方块直接继承自AbstractGlassBlock从而直接得到类似玻璃的透光能力, 但在1.12.2我们还不能这样做, 于是我们可以覆盖isOpaqueCube isFullCube getRenderLayer等一系列方法, 使方块可以透光, 为了拥有更好的显示效果, Vazkii还覆盖了shouldSideBeRendered方法, 重叠的面将不会渲染, 这里笔者就不多说了, 读者可以自行阅读上文该方法的代码
在方块周围生成指定的粒子效果
有心的读者如果去Github阅读了BlockBifrostPerm的源码可以发现我们列举的代码中缺少了如下部分
- @Override
- public void randomDisplayTick(IBlockState state, World world, BlockPos pos, Random rand) {
- if(rand.nextBoolean())
- 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);
- }
- Botania开源地址: https://github.com/Vazkii/Botania
- 节选自(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类
- public class TileBifrost extends TileMod implements ITickable {
- private static final String TAG_TICKS = "ticks";
- public int ticks = 0;
- @Override
- public void update() {
- if(!world.isRemote) {
- if(ticks <= 0) {
- world.setBlockToAir(pos);
- } else ticks--;
- }
- }
- @Nonnull
- @Override
- public NBTTagCompound writeToNBT(NBTTagCompound par1nbtTagCompound) {
- NBTTagCompound ret = super.writeToNBT(par1nbtTagCompound);
- ret.setInteger(TAG_TICKS, ticks);
- return ret;
- }
- @Override
- public void readFromNBT(NBTTagCompound par1nbtTagCompound) {
- super.readFromNBT(par1nbtTagCompound);
- ticks = par1nbtTagCompound.getInteger(TAG_TICKS);
- }
- }
- Botania开源地址: https://github.com/Vazkii/Botania
- 节选自(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方法的具体实现, 可以找到如下代码.
- RayTraceResult lookingAt = VectorTools.getLookingAt(player, stack);
- if (lookingAt == null) { //If we aren't looking at anything, exit
- return false;
- }
- BlockPos startBlock = lookingAt.getBlockPos();
- EnumFacing sideHit = lookingAt.sideHit;
- coords = BuildingModes.collectPlacementPos(world, player, startBlock, sideHit, stack, startBlock);
- BuildingGadgets开源地址: https://github.com/Direwolf20-MC/BuildingGadgets
- 节选自(1.12.x分支): /common/items/gadgets/GadgetBuilding.java
复制代码 相信聪明的读者可以看出, 建筑小助手是先通过BlockRayTrace来追踪目光所看的方块, 在通过collectPlacementPos获取需要建筑的方块的坐标值的, 这组坐标值就是代码中的coords, 也不难想象节选部分的下面也就是在coords这些坐标值处放置方块的相关代码了. 感兴趣的读者可以自行查阅, 顺便一提, 在放置方块前其实direwolf20还添加了Undo的相关代码(不知道读者有没有使用建筑小助手时出错, 从而使用撤销功能的呢? 感兴趣的话也可以自行阅读相关部分)
话归正题, 我们来具体详细的看一看这个BlockRayTrace究竟有什么门道, 在VectorHelper.getLookingAt方法中, 我们可以看到如下代码
- public static RayTraceResult getLookingAt(EntityPlayer player, boolean rayTraceFluid) {
- World world = player.world;
- Vec3d look = player.getLookVec();
- Vec3d start = new Vec3d(player.posX, player.posY + player.getEyeHeight(), player.posZ);
- //rayTraceRange here refers to SyncedConfig.rayTraceRange
- Vec3d end = new Vec3d(player.posX + look.x * rayTraceRange, player.posY + player.getEyeHeight() + look.y * rayTraceRange, player.posZ + look.z * rayTraceRange);
- return world.rayTraceBlocks(start, end, rayTraceFluid, false, false);
- }
- BuildingGadgets开源地址: https://github.com/Direwolf20-MC/BuildingGadgets
- 节选自(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来方便我们调用静态方法
- public class MinerWand extends Item {
- public MinerWand() {
- this.setCreativeTab(CreativeTabs.TOOLS);
- this.setRegistryName("miner_wand");
- this.setTranslationKey("forgedev.minerWand");
- }
- @Override
- public ActionResult<ItemStack> onItemRightClick(World worldIn, EntityPlayer playerIn, EnumHand handIn) {
- RayTraceResult ray = VectorUtils.getLookingAt(playerIn, false); //getLookingAt方法直接来源于Building Gargets
- if (ray != null) { //只有追踪到方块的时候再触发
- worldIn.destroyBlock(ray.getBlockPos(), false);
- }
- return super.onItemRightClick(worldIn, playerIn, handIn);
- }
- }
复制代码 相信读者已经很明白了, 这里笔者就不再多说了, 需要注意的是这里我们覆盖的是onItemRightClick方法, 而不是onItemUse方法, 其原因就是因为onItemUse只有在物品被使用时才会被派发, 但是我们对远处的方块使用右键时(在游戏看来是对空气点击), 是不算做使用了物品的. (这里笔者偷偷把destoryBlock的第二个参数给了false, 读者可以查一查false意味着什么)
临时隧道的建成
隧道的产生是很容易的, 我们直接将对应的方块setToAir就万事大吉了, 但是这不符合我们对于临时隧道的理解, 临时隧道是我们通过之后, 墙壁会恢复原样的一种设定, 这样到别人家偷东西的话别人就不会发现自己家的墙开了一个洞(雾). 所以这里的需求就很明显了, 让消失的方块过一段时间后再恢复, 这里我们就需要用到TileEntity的ITickable来实现了. 我们先来创建一个自己的TileEntity类, 并加上相关的实现, 不要忘记我们需要实现ITickable接口
- public class TileRecoveryBlock extends TileEntity implements ITickable {
- public int ticks = 20 * 5; //5 seconds
- private Block block;
- public TileRecoveryBlock(Block block) {
- this.block = block;
- }
- @Override
- public void update() {
- if(!world.isRemote) {
- ticks--;
- if(ticks <= 0) {
- world.removeTileEntity(pos);
- world.setBlockState(pos, block.getDefaultState());
- }
- }
- }
- //不要忘记重写writeToNBT和readFromNBT方法, 以保证方块正确还原
- @Nonnull
- @Override
- public NBTTagCompound writeToNBT(NBTTagCompound tagCompound) {
- NBTTagCompound tag = super.writeToNBT(tagCompound);
- tag.setInteger("ticks", ticks);
- return tag;
- }
- @Override
- public void readFromNBT(NBTTagCompound tagCompound) {
- super.readFromNBT(tagCompound);
- ticks = tagCompound.getInteger("ticks");
- }
- }
复制代码 当Ticks达到我们所限制的5秒以后, update方**自己删除对应位置的TileEntity, 并且对方块进行复原, 需要被复原的方块在我们创建TileEntity的时候提供就好了, 所以我们的MinerWand类就变成了这样
- @Override
- public ActionResult<ItemStack> onItemRightClick(World worldIn, EntityPlayer playerIn, EnumHand handIn) {
- if (!worldIn.isRemote) {
- RayTraceResult ray = VectorUtils.getLookingAt(playerIn, false);
- if (ray != null) {
- if (playerIn.isSneaking()) { //潜行的时候创建隧道
- Block block = worldIn.getBlockState(ray.getBlockPos()).getBlock();
- worldIn.destroyBlock(ray.getBlockPos(), false);
- worldIn.setTileEntity(ray.getBlockPos(), new TileRecoveryBlock(block));
- } else { //非潜行的时候单纯的破坏方块
- worldIn.destroyBlock(ray.getBlockPos(), false);
- }
- }
- }
- return super.onItemRightClick(worldIn, playerIn, handIn);
- }
复制代码 相信聪明的读者已经一目了然了, 我们在方块摧毁后在同一位置创建一个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 | + 5 | | | MCBBS有你更精彩~ | 乙烯_中国 | | | + 27 | | MCBBS有你更精彩~ | 羽希氷華 | + 2 | | | | MCBBS有你更精彩~ | 海螺螺 | + 4 | + 50 | | + 3 | MCBBS有你更精彩~ 优秀奖励 | 雨 | + 2 | + 40 | | | MCBBS有你更精彩~ |
查看全部评分
|