最近开发的时候需要实现给定一组Tile,每个Tile都是独立的GameObject,需要返回这些tile所代表的区域的轮廓信息,然后用linerenderer进行描边。
效果如下方视频:
【游戏开发】9期-猛龙断空斩/警戒射击,近期开发的有趣机制分享
想了好一阵子才发现其实非常简单,就是从边界处任意一点开始沿着边界移动就行。
步骤如下(仅适用于单连通区域):
1、遍历所有Tile,记录每一个顶点的频次(例如一个正方形给四个顶点提供一次)
2、随便从一个频次为1的顶点开始,记为顶点a
3、找出顶点a相邻的所有顶点,排除已被识别为边界的顶点后,将频次最小的顶点记为顶点b
4、若顶点b等于起始顶点,结束;若不等于,则将顶点b识别为边界,将顶点b记为顶点a,执行第3步。
Unity的C#代码如下,tiles是输入的tile,tile.GetVertexPos()作用是返回该Tile四个顶点的坐标
public class TileOutlineCollect : MonoBehaviour
{
// Start is called before the first frame update
public List<BaseTile> tiles;
public LineRenderer lineRenderer;
Dictionary<Vector2, TileVertex> VertexMap = new Dictionary<Vector2, TileVertex>();
public void FindOutline()
{
VertexMap = new Dictionary<Vector2, TileVertex>();
TileVertex startTileVertex = null;
foreach (var tile in tiles)
{
foreach (var v in tile.GetVertexPos())
{
if (VertexMap.TryGetValue(v,out var TV))
{
TV.count++;
}
else
{
VertexMap[v] = new TileVertex()
{
count = 1,
position = v,
};
if (startTileVertex==null || startTileVertex.count> 1)
{
startTileVertex = VertexMap[v];
}
}
}
}
List<TileVertex> outlineVertex = new List<TileVertex>() { startTileVertex };
List<Vector2> directionVector = new List<Vector2>() { Vector2.up, Vector2.right,Vector2.down, Vector2.left};
for (int i=0;i<100;i++)
{
TileVertex NextVertex = null;
foreach (var direction in directionVector)
{
if (VertexMap.TryGetValue(outlineVertex.Last().position+ direction* CoreValue.current.tileLength, out var TV) && !outlineVertex.Contains(TV))
{
if (NextVertex==null || NextVertex.count> TV.count)
{
NextVertex = TV;
}
}
}
if (NextVertex!=null && NextVertex!= outlineVertex.First())
{
outlineVertex.Add(NextVertex);
}
else
{
break;
}
}
lineRenderer.positionCount = outlineVertex.Count;
lineRenderer.SetPositions(outlineVertex.Select(o=>(Vector3)o.position).ToArray());
}
public class TileVertex
{
public int count;
public Vector2 position;
}
}
拓展
1、用上面的方法对一些狭长的区域描边时,可能会出现“抄近路”的现象,想修正这个问题可以把节点检索的步长减半,同时每个tile需要提供8个节点的信息(4顶点+4边中点);
2、对于非单连通区域,可以考虑在每次生成轮廓集合时,检查是否有未被标记为轮廓,且频次为1的节点,从而继续检索剩下的轮廓集合。