跳至正文

物块制作第二节:多物块与动态物块

各位早午晚上好哈喽Ciao~星星电网捕捉你的眼睛,谁在奇述,是我……不好意思串台了,我是瓶中微光,让我们继续物块的制作吧。

在看完第一节教程之后,你有没有自己尝试制作一个自己的物块呢?

相信你目前对于物块应该有了一个大致的了解,本期就让我们讲讲多物块与动态的物块,请确保你有学习过弹幕基础教程,这篇文章中将会使用到弹幕相关内容。


多物块

什么是多物块?在这里指的是宽度或高度大于一格的物块,请注意帧图较大并不意味着它就是一个多物块。例如原版中的珊瑚的贴图长宽就都大于16,但它仍然是一个单块的物块。

创建你的多物块

这部分对于你来说应该已经很熟悉了吧,我们也就简单略过一下。同时我使用的贴图为泰拉1.4.4中新增的弹力巨石,如果你有玩过天顶世界的话,你可能对它印象深刻。

using Terraria;
using Terraria.ID;
using Terraria.ModLoader;
using Terraria.ObjectData;

namespace MyMod
{
    public class MyMultTile : ModTile
    {
        public override void SetStaticDefaults()
        {
            Main.tileFrameImportant[Type] = true;
            Main.tileSolid[Type] = true;
            Main.tileNoAttach[Type] = true;
            Main.tileBlockLight[Type] = true;

            //是否为巨石,该属性原版中仅在世界生成时使用了
            //不过鉴于我们贴图使用的是弹力巨石,所以也将该属性设为true了
            TileID.Sets.Boulders[Type] = true;
            //为true时将绘制该物块背后的墙
            TileID.Sets.DrawsWalls[Type] = true;
            //绘制时是否忽略周围的半砖
            //如果它为false,周围有半砖时将额外绘制一些东西,不过这个属性其实较为无关紧要
            TileID.Sets.IgnoresNearbyHalfbricksWhenDrawn[Type] = true;

            //是不是没见过的东西?接下来就要介绍它
            TileObjectData.newTile.CopyFrom(TileObjectData.Style2x2);
            TileObjectData.newTile.LavaDeath = false;
            TileObjectData.addTile(Type);

            HitSound = SoundID.Dig;
            DustType = DustID.Stone;
            MineResist = 1f;
            MinPick = 0;
        }
    }
}

CopyFrom

当你写多了就会发现,设置TileObjectData 中的各种属性真是一件麻烦的事啊,每次都要有一大堆,就算是复制黏贴的看上去也很糟糕。那么有没有什么办法简化一下设置物块属性的操作呢?

有的,原版就为我们(或者说是为了方便他们自己)准备了一些“预设”,它们都是静态的TileObjectData 类型的变量,只需要对newTile 使用CopyFrom 这个方法就可以将“预设”中的内容复制过来,使用例如下

TileObjectData.newTile.CopyFrom(TileObjectData.Style6x3);
所有的预设

那么我如何知道这些预设中具体包含哪些内容呢?这个就只能通过阅读源码来查看了。在源码的TileObjectData 这个文件中搜索你想要了解的StyleXXX,并找到addBaseTile(StyleXXX) 所在的位置,在上一个addTileaddBaseTile 之间的就是这个预设中的内容,示例如下

红线之间的即为预设内容
如果你有正确设置属性,它看上去应该是这样的

真正的巨石

都用上弹力巨石的贴图了,不把它真的做成一个巨石不就浪费了?

Slope

首先我们来修正一个小问题,那就是…这个巨石居然可以用锤子锤成半砖和斜坡!?

这不对吧?

使用下面这个重写方法来阻止它被锤击。

public override bool Slope(int i, int j)
{
    return false;
}

KillMultiTile

如果你对巨石有些了解,相信你应该知道会滚动的巨石它其实是个弹幕。所以我们的任务就是在这个物块被破坏时生成一个巨石弹幕。使用下面这个重写方法来在多物块被破坏时执行一些操作,比如生成物品,弹幕等,它仅在多物块被破坏时被调用一次

public override void KillMultiTile(int i, int j, int frameX, int frameY)
{
    //伤害,击退等你可以随意调整
    Projectile.NewProjectile(new EntitySource_TileBreak(i, j), new Vector2(i + 1, j + 1) * 16, 
        Vector2.Zero, ProjectileID.Boulder, 100, 5f);
}

需要注意的是该方法所提供的ij 为多物块最左上角的物块坐标,所以我们需要加1来得到这个巨石的中心。将物块坐标转为世界坐标只需要简单的乘16就可以了,或者也可以使用泰拉给Vector2 加的扩展方法ToWorldCoordinates 这个方法的两个变量默认为8,我们不需要这个额外的偏移量,请将它们设为0。

(另外由于教程编写时TML还并未更新到1.4.4版本,所以使用了普通巨石的弹幕)

效果不错!

动态物块

接下来让我们做一个会动的物块吧!我使用的贴图是原版中的中式灯笼。

虽然它只有2帧,但是够用了,基础设置如下

Main.tileFrameImportant[Type] = true;
Main.tileLighted[Type] = true;
Main.tileNoAttach[Type] = true;
Main.tileLavaDeath[Type] = true;

//第一节中讲过的内容,别忘了哦!
AddToArray(ref TileID.Sets.RoomNeeds.CountsAsTorch);

TileObjectData.newTile.CopyFrom(TileObjectData.Style2x2);
//这些是用来干什么的?请看下文
TileObjectData.newTile.Origin = new Point16(1, 0);
TileObjectData.newTile.AnchorTop=new AnchorData(AnchorType.SolidTile | AnchorType.SolidSide,2,0);
TileObjectData.newTile.AnchorBottom = AnchorData.Empty;
TileObjectData.newTile.DrawYOffset = -2;
TileObjectData.addTile(Type);

//下面这个是用来添加小地图上的名字的
//格式基本固定,就不单独介绍了
ModTranslation name = CreateMapEntryName();
name.SetDefault("灯笼");
AddMapEntry(Color.Yellow, name);

HitSound = SoundID.Dig;
DustType = DustID.Torch;

依照惯例,先来介绍一下你没见过的设置

各种绘制偏移

以下内容都是你可以在TileObjectData.newTile 中可以设置的属性。

Origin 它是一个Point16 类型的变量,你可以理解为一个X和Y都是short 类型的”Vector2“,详细的就不多说了。这个属性的作用是改变在放置时鼠标与该物块左上角的位置关系,单位为格,具体可以看下图。

默认(0, 0)时
为(1, 1)时

可以看到放置预览的左上角的物块坐标X和Y各加一后就是鼠标所在的物块坐标。需要注意的是泰拉的Y轴正方向是向下的。

当你设置为奇奇怪怪的值时…

int DrawYOffset 该属性用于设置在物块绘制时Y方向上的偏移,这仅仅会改变绘制,不会改变物块的实际碰撞,这个物块该在哪就还是在哪。它的单位是像素,16像素就是一格,具体的还是看看图理解一下吧。

设置为16之后的效果,它腾空了!

这个灯笼将它设置为-2的原因是为了避免那些表面有凹陷的物块造成的让它看上去像是腾空的样子。例如各种草方块的贴图就是表面凹凸不平的。

以上两个是较为重要的属性,而接下来的就都是仅会改变放置预览的用处较少的属性了。

int DrawXOffset 放置预览时的X方向绘制偏移,具体如图。

TileObjectData.newTile.DrawXOffset = 8;

可以看到它向右偏了半格,但一放置下来后就变回来了

bool DrawFlipHorizontal 放置预览时水平方向是否翻折。

bool DrawFlipVertical 放置预览时垂直方向是否翻折。

int DrawStepDown 一个较为玄幻的属性,大概是鼠标上方有实心物块时额外添加的放置预览的绘制偏移。

int DrawStyleOffset 更加玄幻的属性,作用就是在放置预览时根据Style的不同选用不同的贴图。简单举个例子,原版的人体模型其实在放置下来之后就只会绘制个底座,上面的木头人是用TileEntity 额外绘制了个玩家,但如果放置预览时也只绘制个底座的话那岂不是显得很奇怪?所需就用到了这个属性。一般有多少Style这个属性就要被设置为多少,另外如果StyleWrapLimit 大于1时在这个属性设置时还要乘上你所设置的StyleWrapLimit

人体模型的贴图

Anchor系列

首先是用于判断放置时该物块上下左右有没有指定类型的物块的属性,分为AnchorTopAnchorBottomAnchorLeftAnchorRight。它们都是AnchorData 类型的变量,要设置它们你需要自己new 一个出来,它的构造器一共需要提供3个变量,分别是什么样的物块,多少个物块和偏移量

首先是什么样的物块,各种类型详见下图,如果需要多种类型,它们之间应该使用& 或者| 连接,其中涉及到比较麻烦的位运算,这里不详细说,总之用法和你在if 里使用的&&|| 基本一致。

看名字应该也就能知道它们什么作用了

至于countstart 两个变量还是看图来理解一下吧

TileObjectData.newTile.AnchorLeft=new AnchorData(AnchorType.SolidTile | AnchorType.SolidSide,4,4);
TileObjectData.newTile.AnchorRight=new AnchorData(AnchorType.SolidTile | AnchorType.SolidSide,2,2);
TileObjectData.newTile.AnchorTop=new AnchorData(AnchorType.SolidTile | AnchorType.SolidSide,2,2);
TileObjectData.newTile.AnchorBottom = AnchorData.Empty;

其中start 不能大于count,也不能是负数,不然就会出BUG说数组越界,源码也挺神秘的没看懂出这个问题的具体原因,总之是记住就好了。

bool AnchorWall 放置在墙上的物块把这个设置为true 就行,比方说画之类的。

int[] AnchorValidTiles 设置了它之后在判定能否放置时如果目标物块不是你指定的物块那么将无法放置,建议配合上面那几个AnchorTop 之类的使用。比方说树苗的下面必须是实心物块,而这个物块又必须是各种草方块才能放置下来。

int[] AnchorInvalidTiles 和上面的类似,只是反过来了,变成不能放置在你指定的物块上了。

int[] AnchorAlternateTiles 用于设置不同摆放样式时额外能放置在哪些物块上。比方说我想让它右边只能是实心物块和椅子,可以这样设置。AnchorType.AlternateTile 也就是专门为它准备的。

TileObjectData.newTile.AnchorRight = new AnchorData(AnchorType.SolidTile | AnchorType.SolidSide |
        AnchorType.AlternateTile, 2, 0);
TileObjectData.newTile.AnchorAlternateTiles = new int[] { TileID.Chairs };

int[] AnchorValidWalls 字面意思就是只有指定的墙壁才能放下来,但原版中并没有任何物块用到了这个属性。

RightClick

终于是讲完枯燥的基础设置了,接下来就来做些有趣的吧。首先我们想让这个灯笼能右键开关,实际上就是改变它的帧图,使用下面这个重写方法。

public override bool RightClick(int i, int j)
{
    //为什么是36?因为我们的贴图一帧的总宽度为2*18=36,中间空的也需要算上
    short changeFrames = 36;

    //这两个用于表示玩家右键到的时多物块的具体哪一块,通过这个来得到物块左上角
    //同时用X去判断我们的灯笼当前处于第一帧还是第二帧
    //TileFrameX和TileFrameY的单位都是像素,与你的贴图是完全对应的
    int mouse2TopLeftX = Main.tile[i, j].TileFrameX / 18 * -1;
    int mouse2TopLeftY = Main.tile[i, j].TileFrameY / 18 * -1;

    //如果小于-1那么就代表我们的灯笼现在处于第二帧
    //你可以自己计算一下。如果在第二帧那么就要把它变回去
    //所以就-36就能回到第一帧了
    if (mouse2TopLeftX < -1)
    {
        mouse2TopLeftX += 2;
        changeFrames = -36;
    }

    //加上i和j以得到这个多物块的左上角位置
    mouse2TopLeftX += i;
    mouse2TopLeftY += j;

    //从左上角向右下角遍历物块,改变它们的帧图
    for (int x = mouse2TopLeftX; x < mouse2TopLeftX + 2; x++)
        for (int y = mouse2TopLeftY; y < mouse2TopLeftY + 2; y++)
            if (Main.tile[x, y].TileType == Type)
                Main.tile[x, y].TileFrameX += changeFrames;

    //记得要返回true哦!
    return true;
}

这部分需要比较多的思考,建议自己代入数据算一算,同时要多尝试,虽说出问题的话很可能游戏就直接崩溃了,但试错是不可避免的,如果有能力的话也可以去看看源码里那些个会动的物块怎么写的。

我这里的仅仅是一个只有2个帧图的2×2物块,如果换成10张帧图的3×4物块呢?

效果图

HitWire

这个重写方法会在物块上的电线被激活时触发。

可以用于来改变物块帧图,具体如何改变于上面的右键改变是一致的,就不再写一遍了。

public override void HitWire(int i, int j)
{
    //在这里改变你的物块
}

唯一需要多注意的是后面要进行同步一下物块

//这个2,2是我们灯笼的长宽
NetMessage.SendTileSquare(-1, i, j, 2, 2);

AnimateTile

接下来就来尝试做一个真正会自己动的物块吧,这部分还是要靠发挥你的想象力,我这里只是做一个简单的效果。

首先使用AnimateTile 这个重写方法提供的2个引用变量来改变这个物块的“帧”和“帧计数”2个属性,这两个属性可以通过Main.tileFrameMain.tileFrameCounter 来获取到,但它们实际上并不会直接改变物块的帧图。

public override void AnimateTile(ref int frame, ref int frameCounter)
{
    frameCounter++;

    //我这里写的是每30帧切换一下
    if (frameCounter >= 30)
    {
        //在0到1之间切换,因为我们的灯笼只有2帧
        frame = frame switch
        {
            0 => 1,
            1 => 0,
            _ => 0
        };

        frameCounter = 0;
    }
}

改变了属性之后接下来自然就是要使用啦,用下面这个重写方法来使用它们,这个重写方法在多物块的每一格物块上都会执行。

public override void AnimateIndividualTile(int type, int i, int j, ref int frameXOffset, ref int frameYOffset)
{
    //根据帧去改变所有的物块帧
    frameXOffset = Main.tileFrame[type] * 36;
}
效果还可以!

虽然这部分内容看上去可能很少,但这是最能发挥你想象力的部分,我写的只能起到一些参考作用,重要的还是你自己的想法

另外这里的方法和上面的右键和电线触发时使用的方式有时会起冲突,导致出现BUG,最好不要同时使用这2种方法,或者你可以试着自己解决一下这个冲突。

ModifyLight

让我们来加点发光,毕竟都用上灯笼的贴图了,这个重写方法仅在Main.tileLighted[Type] = true 时才会执行。

public override void ModifyLight(int i, int j, ref float r, ref float g, ref float b)
{
    //我这里的意思是第一帧时才会发出光
    //第一种方式,也就是上面的右键和电线触发里写的改变帧图的方法
    bool method1 = Main.tile[i, j].TileFrameX / 18 < 2;
    //第二种方式,就是上面写的AnimateTile里的用来改变帧图的方式
    bool method2 = Main.tileFrame[Type] == 0;

    //你可以根据你自己选用的方式去决定它是否发光
    if (method1  && method2)
    {
        r = 0.9f;
        g = 0.2f;
        b = 0.2f;
    }
}
这颜色稍微有点阴间了…

KillMultiTile

最后来简单设置一下它的掉落物,这个重写方法在上面已经介绍过了,所以这里就一笔带过了。请像我一样在多物块里面都加上这个,如果你不想让你的多物块挖掉后掉落好多份物品的话。这样自己生成物品就不需要再去设置ItemDrop 属性了。

public override void KillMultiTile(int i, int j, int frameX, int frameY)
{
    Item.NewItem(new EntitySource_TileBreak(i, j), new Vector2(i, j) * 16, 
        ModContent.ItemType<MyItem>());
}

小作业

尝试着制作一个会自己变化并会发光的视域之魂瓶

下一期将来做一个可以生长的植物,同时继续介绍一些重写方法,我们下期见!

发表回复