Unity2D 自定义Scriptable Tiles的理解与使用(三)——开始着手构建一个基于Tile类的自定义tile(上)

本文章使用的为 Unity201Unity版本8.4.36f1,脚本语言为C#

引言:

在上篇文章中,我们学习了Tile类的相关知识,了解了继承自Tile类的好处。那么现在,激动人心的时刻终于来了,我们要自己创建一个自定义瓦片了!在这篇文章中,我们将会一步步地实现一个带有简单功能的自定义瓦片。

正文:

首先先确定我们的目标,在这个文章中我们的目标是建立一个有如下功能的瓦片:

1.可以根据上下左右相邻的格子状态(有无瓦片)来确定要显示的图片。

(其实这个功能基本就是Pipeline瓦片的功能,而且是RuleTile功能的简化版,之所以要实现这个功能是因为这个功能简单,方便展示)

2.可以附加一个小人模样的游戏对象,并且每个小人的颜色都是随机得到的。

好那开始构建我们的瓦片

扫描二维码关注公众号,回复: 14780881 查看本文章

首先创建一个脚本资源,我在这里把它命名为MyTile。(other文件夹是和该文章无关的文件夹,直接忽视就好),如下图所示:

然后双击脚本进入脚本编辑界面(我这里用visual studio 2019),如下图所示:

我们先将Start方法和Update方法删掉。

接着我们需要引用UnityEngine.Tilemaps命名空间,这样我们就可以调用和Tilemap相关的API了。接着我不再让这个脚本继承自MonoBehaviour,而是改为继承自Tile,如下图所示:

 好,以上的步骤完成之后,我们先来实现我们想要的第一个功能,这个功能要求要根据上下左右的格子有无瓦片(这个瓦片是专指我创建的Mytile瓦片,其他的任何系统自带瓦片或自定义瓦片不包括在内)来确定瓦片所展现的图像。

以下为功能细节设想:

我们先在任意位置放置一个瓦片,如下为示意图:

这时瓦片上下左右都没有瓦片, 我就想让该瓦片呈现默认图像(竖着的道路)。如果我在这个瓦片右边再放一个瓦片,普通的瓦片会是这样的:

 而我想要的效果显然是这样的,请看gif动图

应该变为两个横着的道路

要想实现这个功能,首先我们来看看一下整个过程中发生了什么事。

一开始场景中已经有一个方块了,然后我的笔刷接近这个方块,在接近的一瞬间,发生了两件事:

1.笔刷控制的方块的图像改变了(从原来竖着的变为横着的)

2.这个已有的方块的图像也改变了(从原来竖着的变为横着的)

不仅笔刷所在的位置的瓦片图像改变了,它相邻的方块图像也改变了。所以我们不仅要让笔刷位置的瓦片进行刷新,还要让它相邻(上下左右)的瓦片也进行刷新。

还记得这个系列的第一篇文章里写到的RefreshTile方法吗,我们现在应该知道要想实现这个功能,要怎么写这个方法了。

先把这个方法写出来,动手写了”public override“之后,如果是直接选了vs智能提示的选项,就是下图这样的:

动图展示:

这里的 base.RefreshTile(position, tilemap) 是调用父类的被重写方法,父类的被重写方法已经实现了刷新笔刷位置瓦片的功能,所以我们要做的是刷新它上下左右的瓦片。

这里我们要学习一个新的知识。

方法参数中ITilemap类型的参数tilemap可以调用实例方法RefreshTile(Vector3Int position)

这个方法的名字和我们写的瓦片的这个RefreshTile方法是一致的,但是是两个完全不同的方法,它是真正让tilemap刷新我们瓦片的方法。我们写的MyTile类的爷爷类(Tile的父类)是TileBase类,在TileBase类的RefreshTile里就写了一个tilemap.RefreshTile(position)来刷新笔刷所在位置的瓦片。

以下是官方对这个方法的介绍:

ITilemap.RefreshTile

Leave feedback

Declaration

public void RefreshTile(Vector3Int position);

Parameters

position Position of the Tile on the Tilemap.   翻译:瓦片在Tilemap上的位置

Description

Refreshes a Tile at the given XYZ coordinates of a cell in the :Tilemap.翻译:刷新用xyz坐标指定位置的Tilemap格子上的瓦片。

The Tilemap will retrieve the rendering data, animation data and other data for the Tile and update all relevant components. 翻译:Tilemap将检索Tile的渲染数据、动画数据和其他数据,并更新所有相关组件。

那我们就可以开始利用这个方法刷新周围位置瓦片了,只需要上下左右四个位置刷新一遍就行。瓦片的RefreshTile方法里的参数position指代了我们当前笔刷的位置。所以我们就可以写出如下图所示的代码来:

 怎么样,是不是很简单,只需要多写四行代码就行。可以在下面复制代码:

public override void RefreshTile(Vector3Int position, ITilemap tilemap)
    {
        base.RefreshTile(position, tilemap);
        tilemap.RefreshTile(position + new Vector3Int(1, 0, 0));
        tilemap.RefreshTile(position + new Vector3Int(-1, 0, 0));
        tilemap.RefreshTile(position + new Vector3Int(0, 1, 0));
        tilemap.RefreshTile(position + new Vector3Int(0, -1, 0));
    }

好那我们现在解决了刷新的问题。但是这个功能仍然不能运行,因为还有一个最核心的工作没有做,那就是给予tilemap正确的图片用以显示。

回忆一下第一篇讲的哪个方法是用来让tilemap获取自定义瓦片渲染信息

答案是GetTileData方法。

我们先将这个方法写出来,如下图所示:

 这个方法可以将笔刷所在位置的瓦片渲染信息交给tilemap,让tilemap可以正确地渲染出我们的瓦片。

我们在类体里写一个Spirte[]数组类型的成员变量sprites。用来存放所有可能显示的图片。

 现在我们在GetTileData方法里面写三行代码,如下图:

 这里我们先写了一个int类型的变量来表示该瓦片应该显示的图片的编号,再通过我们自己编写的方法getIndex获取到瓦片应该显示的图片编号,最后根据编号在sprites里找到对应的图片赋值给参数tileData里成员变量的sprite。

好,现在我们来写getIndex方法。如下列代码:

  public int getIndex(Vector3Int position, ITilemap tilemap)
    {
        string roadSituation = "";
        if (tilemap.GetTile(position + new Vector3Int(-1, 0, 0))==this)
            roadSituation += "a";
        if (tilemap.GetTile(position + new Vector3Int(1, 0, 0)) == this)
            roadSituation += "b";
        if (tilemap.GetTile(position + new Vector3Int(0, 1, 0)) == this)
            roadSituation += "c";
        if (tilemap.GetTile(position + new Vector3Int(0, -1, 0)) == this)
            roadSituation += "d";

        switch (roadSituation)
        {
            case "a":
                return 1;
            case "b":
                return 1;
            case "c":
                return 0;
            case "d":
                return 0;
            case "ab":
                return 1;
            case "ac":
                return 2;
            case "ad":
                return 4;
            case "bc":
                return 3;
            case "bd":
                return 5;
            case "cd":
                return 0;
            case "abc":
                return 6;
            case "abd":
                return 7;
            case "acd":
                return 9;
            case "bcd":
                return 8;
            case "abcd":
                return 10;
        }
        return 0;
    }

首先写一个String类型变量roadSituation来表示瓦片周围的情况(默认为空字符串)。

然后判断左边相邻位置有没有瓦片:

 if (tilemap.GetTile(position + new Vector3Int(-1, 0, 0))==this)

看看这行代码,这里又引入一个新方法:tilemap.GetTile(Vector3Int position)

这个方法可以得到该tilemap指定位置的瓦片,得到的实例就是我们写的MyTile脚本的实例,如同所有ScriptableObject一样,不了解ScritableObject的朋友可以先去了解一下,就明白什么意思了。所以检测的是有没有MyTile瓦片,而不是检测所有瓦片。

如果等于this,即这个实例本身,那就说明左边是有瓦片的,那就让roadSituation加上“a”表示左侧有瓦片。

 roadSituation += "a";

下面的几行代码都是同样原理,右边有瓦片就加“b”,上面有加“c”,下面有加“d”。这时的

if (tilemap.GetTile(position + new Vector3Int(1, 0, 0)) == this)
            roadSituation += "b";
if (tilemap.GetTile(position + new Vector3Int(0, 1, 0)) == this)
            roadSituation += "c";
if (tilemap.GetTile(position + new Vector3Int(0, -1, 0)) == this)
            roadSituation += "d";

roadSituation就表示了周围的真实情况,例如它的值为“a”就表示只有左侧有瓦片,为“ab”表示左右有瓦片而上下没有,“abcd”就表示上下左右都有瓦片。

接着就是根据roadSituation的值来返回一个指定的编号了。

 switch (roadSituation)
        {
            case "a":
                return 1;
            case "b":
                return 1;
            case "c":
                return 0;
            case "d":
                return 0;
            case "ab":
                return 1;
            case "ac":
                return 2;
            case "ad":
                return 4;
            case "bc":
                return 3;
            case "bd":
                return 5;
            case "cd":
                return 0;
            case "abc":
                return 6;
            case "abd":
                return 7;
            case "acd":
                return 9;
            case "bcd":
                return 8;
            case "abcd":
                return 10;
        }
        return 0;

若周围没有任何瓦片,即roadSituation的值为 ""(空字符串),就返回0(默认值)。

其实我们要先确定好sprites图片数组的各个下标对应的图片,我这里采用的方案是这样的

这样sprites[0]就是编号为0的图片,sprites[8]就是编号为8的图片,等会在Inspector中就用这个顺序放图片。

例如情况是“ac”的话(左边和上边有瓦片),返回的就是2

GetTileData方法里面的index变量就会被赋值为2,就表示选择sprites[2]也就是这里的编号为2的图片,刚好是这张连接上面和左面的图片。然后将图片赋值给tileData.sprite。

好,现在我们已经把第一个功能的代码部分基本搞定了,我们可以测试一下了。但这时我们发现,这个脚本是绑定不了任何游戏对象的,这是因为该脚本继承自TileBase,而TileBase又继承自ScriptableObject。ScriptableObject是不能绑定在任何游戏对象上的,而是要通过创建资产(Asset)的方式创建,我们需要在我们的MyTile上面加上一行代码。如下图所示:

 这行代码的意义就是创建一个资产菜单,并命名为"MytTile"。

现在,我们可以通过在资产框中按右键创建一个MyTile类型的瓦片资产啦!

如下面动图所示: 

给这个瓦片起一个有意义或你喜欢的名字,然后点击我们的瓦片资产,在右边选择Inspctor窗口。

会出现这样的界面。

 展开Sprites,在size处输入11,建立一个容量为11的图片数组。

 按照约定好的图片顺序放入我们的图片,图片顺序如下图:

                                                        按照顺序放图片

那么我们的第一个功能算是彻底做完啦!

来测试一下!

没毛病!

以下是MyTile类目前的所有代码,只实现了第一个功能。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

[CreateAssetMenu(menuName = "myTile")]
public class MyTile : Tile
{
    public Sprite[] Sprites;

    public override void RefreshTile(Vector3Int position, ITilemap tilemap)
    {
        base.RefreshTile(position, tilemap);
        tilemap.RefreshTile(position + new Vector3Int(1, 0, 0));
        tilemap.RefreshTile(position + new Vector3Int(-1, 0, 0));
        tilemap.RefreshTile(position + new Vector3Int(0, 1, 0));
        tilemap.RefreshTile(position + new Vector3Int(0, -1, 0));
    }

    public override void GetTileData(Vector3Int position, ITilemap tilemap, ref TileData tileData)
    {
        base.GetTileData(position, tilemap, ref tileData);
        int index = 0;
        index = getIndex(position, tilemap);
        tileData.sprite = Sprites[index];
    }

    public int getIndex(Vector3Int position, ITilemap tilemap)
    {
        string roadSituation = "";
        if (tilemap.GetTile(position + new Vector3Int(-1, 0, 0)) == this)
            roadSituation += "a";
        if (tilemap.GetTile(position + new Vector3Int(1, 0, 0)) == this)
            roadSituation += "b";
        if (tilemap.GetTile(position + new Vector3Int(0, 1, 0)) == this)
            roadSituation += "c";
        if (tilemap.GetTile(position + new Vector3Int(0, -1, 0)) == this)
            roadSituation += "d";

        switch (roadSituation)
        {
            case "a":
                return 1;
            case "b":
                return 1;
            case "c":
                return 0;
            case "d":
                return 0;
            case "ab":
                return 1;
            case "ac":
                return 2;
            case "ad":
                return 4;
            case "bc":
                return 3;
            case "bd":
                return 5;
            case "cd":
                return 0;
            case "abc":
                return 6;
            case "abd":
                return 7;
            case "acd":
                return 9;
            case "bcd":
                return 8;
            case "abcd":
                return 10;
        }
        return 0;
    }
}

那这篇文章我们先实现第一个功能,第二个功能我将在下一篇文章中一步步地实现。

谢谢大家观看,希望这篇文章对大家学习有帮助,有什么建议或意见可以打在评论区噢 :)

猜你喜欢

转载自blog.csdn.net/scombropidae/article/details/125770787
今日推荐