基于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;