基于unity2D像素游戏的 双网格系统

本文与代码基于以下视频原理攥写

原youtube地址:https://www.youtube.com/watch?v=jEWFSv3ivTg

b站搬运地址:给种田游戏添加程序化地形生成(双网格系统、无限地形)_哔哩哔哩_bilibili

引入:

我们都知道,unity2D平面像素游戏,都是使用Grid网格系统进行绘制。在unity中为我们提供了瓦片(tile)进行绘制。

基础的网格系统方便且使用,但当我们需要更优秀的画面表现时,基础网格系统就会出现某些麻烦,看看下面的例子:

图片来自上面的视频

基础网格系统的cell(格)边界处的边角非常僵硬,呈现在游戏内的表现很差。为了解决这个问题,我们需要修改cell的绘制策略,让每个cell的绘制不是单独将spline画上去就行。

我们想到的第一个办法:采样

每个cell采样直接相连的四个cell与不直接相连的对角线上的四个cell:

这样获取每个cell的采样数据,就可以将原本spline更换成更加丝滑的衔接spline

但这么做也有问题,假设我们为每一种情况都设置一个spline,那么仅仅一种地块的变形就需要2^8 = 256块!(假设只有两种地块)

所以我们需要优化,有三种优化方式,都是从spline变体数量上做文章。

16-块情况

首先是以15块为基础的方案。这个方案是将一个cell分化成四个小cell,这四个小cell分别填充。变体被减少的只需要16块。但它有个致命问题,如下图,这样的一块属于绿色方块还是白色方块?并且它还不是网格对齐的!这样导致我们的程序,在需要对这样的地块进行程序操作的时候会带来麻烦。

接下来是47块的方案,这个方案简单粗暴,绘制47种变体,来应对各种各样的情况。但是47个变体还是太多了。

还有一种16块的情况,它是基于47块的简略版本:

很可惜,它还是有问题,交界处会出现瑕疵。

为了解决上述所有的问题,我们需要使用双网格系统

双网格系统

双网格系统基于第一个思想:绘制与数据分离的思想,我们将绘制的网格与数据的存储的网格进行分离,以此来解决16-块情况的边界不明的问题。

随后我们将用于绘制的网格进行偏移,移动半个cell的距离来解决变体太多的问题。如图(绿色的是数据网格,红色的是绘制网格):

我们把存储数据的叫做数据网格,用于绘制的叫绘制网格

这样每个绘制网格cell只需要采样这个cell覆盖到的数据网格cell,这样每个绘制网格都只需要采样覆盖的4个数据网格!总数量2^4=16大大减少了需要绘制的spline!

并且由于我们的绘制网格偏移了0.5个cell,在16-块情况下完美实现了网格对其,解决了16-块情况的问题!

如果有学过抗锯齿原理,就会发现它非常像msaa抗锯齿,但是msaa抗锯齿是放大一倍分辨率,双网格系统是将网格进行平移。

实现细节

用于实现鼠标操作的代码:

private void Update()
{
    if (Input.GetMouseButton(0))
    {
        Vector3 mouseWorldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        Vector3Int coordinate = GetComponent<Grid>().WorldToCell(mouseWorldPos);
        Tilemap tilemap = transform.Find("Data").GetComponent<Tilemap>();
        tilemap.SetTile(coordinate, dataTile[0]);
        renderGrid(coordinate);
    }
    if (Input.GetMouseButton(1))
    {
        Vector3 mouseWorldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        Vector3Int coordinate = GetComponent<Grid>().WorldToCell(mouseWorldPos);
        Tilemap tilemap = transform.Find("Data").GetComponent<Tilemap>();
        tilemap.SetTile(coordinate, dataTile[1]);
        renderGrid(coordinate);
    }
}

用于实现绘制的代码:

public void renderGrid(Vector3Int coordinate)
{
    Tilemap tilemap = transform.Find("Render").GetComponent<Tilemap>();
    for (int i = 0; i < 2; i++)
    {
        for (int j = 0; j < 2; j++)
        {
            var temp = coordinate - new Vector3Int(j, i, 0);
            tilemap.SetTile(temp, renderTile[upRenderGrid(temp)]);
        }
    }
}

public int upRenderGrid(Vector3Int coordinate)
{
    int result = 0;
    Tilemap tilemap = transform.Find("Data").GetComponent<Tilemap>();
    List<Tile> tiles=new List<Tile>()
    {
        tilemap.GetTile<Tile>(coordinate+new Vector3Int(0,0,0)),  
        tilemap.GetTile<Tile>(coordinate+new Vector3Int(1,0,0)),
        tilemap.GetTile<Tile>(coordinate+new Vector3Int(0,1,0)),
        tilemap.GetTile<Tile>(coordinate+new Vector3Int(1,1,0)),
    };
    if (tiles[0]==dataTile[0]) 
    {
        result+=1;
    }
    if (tiles[1]==dataTile[0]) 
    {
        result+=2;
    }
    if (tiles[2]==dataTile[0]) 
    {
        result+=4;
    }
    if (tiles[3]==dataTile[0]) 
    {
        result+=8;
    }
    return result;
}

其他内容:

private Tile[] dataTile;
private Tile[] renderTile;

private void Start()
{
    dataTile = new Tile[2];
    renderTile = new Tile[16];
    for (int i = 0; i < 2; i++)
    {
        dataTile[i] = Resources.Load<Tile>("wp/TilePlaceholders"+i);
    }

    for (int i = 0; i < 16; i++)
    {
        renderTile[i] = Resources.Load<Tile>("wp/TilesDemo"+i);
    }
}

我们来详解一下绘制的代码:

首先我们在按键后触发更新操作,由于我们知道,每一块数据cell被四块绘制cell覆盖,所以我们需要对这四块进行更新:

if (Input.GetMouseButton(1))
{
    Vector3 mouseWorldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
    Vector3Int coordinate = GetComponent<Grid>().WorldToCell(mouseWorldPos);
    Tilemap tilemap = transform.Find("Data").GetComponent<Tilemap>();
    tilemap.SetTile(coordinate, dataTile[1]);
    renderGrid(coordinate);
}
public void renderGrid(Vector3Int coordinate)
    {
        Tilemap tilemap = transform.Find("Render").GetComponent<Tilemap>();
        for (int i = 0; i < 2; i++)
        {
            for (int j = 0; j < 2; j++)
            {
                var temp = coordinate - new Vector3Int(j, i, 0);
                tilemap.SetTile(temp, renderTile[upRenderGrid(temp)]);
            }
        }
    }

如何确定四块的索引?我们的绘制网格向右上平移了半个cell,因此每个绘制网格必然会覆盖它自身,右边,上边与右上的绘制网格。因此我们对这四块网格进行更新:

public void renderGrid(Vector3Int coordinate)
{
    Tilemap tilemap = transform.Find("Render").GetComponent<Tilemap>();
    for (int i = 0; i < 2; i++)
    {
        for (int j = 0; j < 2; j++)
        {
            var temp = coordinate - new Vector3Int(j, i, 0);
            tilemap.SetTile(temp, renderTile[upRenderGrid(temp)]);
        }
    }
}

它会采样所覆盖到的数据cell,之后将网格二进制编码,按左下,右下,左上,右上分别编制为二进制的第一第二第三第四位,为空地时为0,为草地时为1,最后返回我们需要的spline索引:

int result = 0;
Tilemap tilemap = transform.Find("Data").GetComponent<Tilemap>();
List<Tile> tiles=new List<Tile>()
{
    tilemap.GetTile<Tile>(coordinate+new Vector3Int(0,0,0)),  
    tilemap.GetTile<Tile>(coordinate+new Vector3Int(1,0,0)),
    tilemap.GetTile<Tile>(coordinate+new Vector3Int(0,1,0)),
    tilemap.GetTile<Tile>(coordinate+new Vector3Int(1,1,0)),
};
if (tiles[0]==dataTile[0]) 
{
    result+=1;
}
if (tiles[1]==dataTile[0]) 
{
    result+=2;
}
if (tiles[2]==dataTile[0]) 
{
    result+=4;
}
if (tiles[3]==dataTile[0]) 
{
    result+=8;
}
return result;