我在上一篇博客中提到,要实现2d弹坑的功能,代码比较复杂,但是思路其实不难。
简单来说,要实现弹坑功能最关键的部分就是找到顶点的位置,以及连线的顺序。具体可以分为以下几步:
(1)在碰撞点生成一个Mesh来模拟爆炸半径;
(2)获取爆炸半径内的顶点;
(3)将在爆炸范围内的顶点从网格中移除;
(4)根据新的顶点数组绘制网格;
(5)更新网格贴图;
(6)更新网格碰撞器。
下面我们先实现第一步,绘制Mesh
绘制Mesh
Unity对于物体的渲染,是基于三角形网格的。如果我们给定了一个顶点数组(一个Vector3矢量),就可以根据这些顶点的位置来创建三角形面,原则上可以创造出任何我们想要的图形。
为了实现圆形弹坑的效果,我们先来创造一个圆形Mesh,用来模拟爆炸的半径。要画一个圆我们有很多种方法,这里我介绍两种方法,(1)即通过圆心和圆周上的点来画圆;(2)通过圆周上的点画圆。
List<Vector3> vertices = new List<Vector3>(); //这里我们用链表来存储顶点的位置,这样可以不用事先计算数组的容量,
//也可以方便增添删除元素(之后实现弹坑需要频繁的增添删除顶点)。
List<int> triangles = new List<int>();//存储顶点顺序,长度必须是3的整数倍
///这里的pos代表传入的位置,radiu代表要生成的圆半径,verticesCount代表圆周上的顶点数
void CreateSphereMesh(Vector3 pos,float radiu,int verticesCount)
{
vertices.Clear();
triangles.Clear();
GameObject sphereMeshGO = new GameObject();//生成一个新的物体,挂载mesh组件
sphereMeshGO.transform.position = pos;//将这个物体的位置设置为鼠标点击的位置
MeshFilter sphereMeshFilter = sphereMeshGO.AddComponent<MeshFilter>();//给物体添加两个组件
MeshRenderer sphereMeshRenderer = sphereMeshGO.AddComponent<MeshRenderer>();
Vector3 center = Vector3.zero;
vertices.Add(pos);//首先将圆心位置存入,位置为0.
float delta = 360f / verticesCount;
float i =0;
while (i < 360)
{
Vector3 temp = new Vector3(radiu * Mathf.Cos(i*Mathf.Deg2Rad), radiu * Mathf.Sin(i * Mathf.Deg2Rad), 0);
vertices.Add(temp);
i += delta;
}
}
现在,我们有了圆心和圆周上的点,我们就可以按照如下方式创造三角面。这里需要注意,三角形顶点的顺序很重要,要按顺时针方向来绘制,不然你可能需要反转镜头来寻找创建的mesh到底去哪了。
List<int> triangles = new List<int>();//这里同样用列表存储三角形顶点顺序
void CreateSphereMesh(Vector3 pos,float radiu)
{
...//承接上边的代码
for (int k=0; k < vertices.Count-2;k++)
{
triangles.Add(0);
triangles.Add(k + 2);
triangles.Add(k + 1);
}
//按照这种生成方法,最后一个三角形网格的顶点需要用到前边的点,不方便在循环中表示,所以单独添加
//如果只用圆周上的点画圆,就不需要单独添加。
triangles.Add(0);
triangles.Add(1);
triangles.Add(defaultVerticeCount);
//有了上边这两个列表,我们就可以创建一个mesh来渲染这个圆了
Mesh sphereMesh = new Mesh();//创造一个新的Mesh
sphereMesh.vertices = vertices.ToArray();
sphereMesh.triangles = triangles.ToArray();
sphereMeshFilter.sharedMesh = sphereMesh;
}
现在我们可以用Debug.DrawLine方法来看一下顶点的位置是不是和我们想的一样。
...
int a, b, c;
for (int j = 0; j < triangles.Count; j = j + 3)
{
a = triangles[j];
b = triangles[j + 1];
c = triangles[j + 2];
//yield return new WaitForSeconds(0.03f);//在这里使用协程,慢放三角形的绘制顺序。
Debug.DrawLine(vertices[a], vertices[b], Color.red, 100.0f);
Debug.DrawLine(vertices[b], vertices[c], Color.red, 100.0f);
Debug.DrawLine(vertices[c], vertices[a], Color.red, 100.0f);
}
利用上边的代码可能看不出来三角形的顶点是不是我们预想的顶点?这里我们可以用协程来实现延时的功能,每画一个三角形之前等待0.03s,同时把方法前的Void设置为IEnumerator。
然后我们在Update方法中实现该协程方法。
public void Update()
{
if (Input.GetMouseButtonDown(0)){
Vector3 screenPosition = Camera.main.WorldToScreenPoint(transform.position);
Vector3 mousePosition = Input.mousePosition;
mousePosition.z = screenPosition.z;
mousePosition = Camera.main.ScreenToWorldPoint(mousePosition);
StartCoroutine(CreateSphereMesh(mousePosition, 2.0f, 20));
}
}
三角形绘制如下图所示,可以看到三角形的绘制顺序确实和我们想的一样。
将Sprite转换为Mesh
通过上边的操作我们已经可以创建一个简单的mesh了,那么如何利用SpriteRenderer创建Mesh呢?如果我们明白了上边的原理,就可以很轻松的来实现这一功能。因为SpriteRenderer.Sprite有现成的顶点,三角形,UV数组。我们只需要按照顺序把这些数组赋值给新的Mesh的顶点,三角形,UV数组即可(因为所需要的数组类型不同,所以这里不能直接拿sprite的数组来用)。完整代码如下:
IEnumerator GenerateFormSprite() {
//sprite的三角形数组
ushort[] triangles0 = _spriteRenderer.sprite.triangles;
//mesh的三角形数组
int[] meshTriangles = new int[triangles0.Length];
//mesh的三角形顶点
Vector3[] vertices0 = new Vector3[_spriteRenderer.sprite.vertices.Length];
//存储顶点数组
for (int i = 0; i < _spriteRenderer.sprite.vertices.Length; i++)
{
vertices0[i] = new Vector3(_spriteRenderer.sprite.vertices[i].x, _spriteRenderer.sprite.vertices[i].y, 0);
}
//存储三角形数组
for (int i = 0; i < meshTriangles.Length; i +=3)
{
meshTriangles[i] = triangles0[i];
meshTriangles[i + 1] = triangles0[i + 1];
meshTriangles[i + 2] = triangles0[i + 2];
yield return new WaitForSeconds(0.02f);
Debug.DrawLine(vertices0[meshTriangles[i]], vertices0[meshTriangles[i + 1]], Color.red, 100.0f);
Debug.DrawLine(vertices0[meshTriangles[i + 1]], vertices0[meshTriangles[i + 2]], Color.red, 100.0f);
Debug.DrawLine(vertices0[meshTriangles[i + 2]], vertices0[meshTriangles[i]], Color.red, 100.0f);
}
//存储sprite的texture
Texture2D texture = _spriteRenderer.sprite.texture;
//生成一个新的物体挂载mesh组件
GameObject go = new GameObject();
MeshFilter meshFilter = go.AddComponent<MeshFilter>();
MeshRenderer meshRenderer = go.AddComponent<MeshRenderer>();
Mesh _mesh = new Mesh();
meshFilter.mesh = _mesh;
//给mesh赋值
_mesh.vertices = vertices0;
_mesh.uv = _spriteRenderer.sprite.uv;
_mesh.triangles = meshTriangles;
//设置物体位置,名字等
go.name = "MeshSprite";
go.transform.position = gameObject.transform.position;
go.transform.localScale = gameObject.transform.localScale;
go.transform.rotation = gameObject.transform.rotation;
//设置mesh的属性
meshRenderer.material = _spriteRenderer.material;
meshRenderer.sharedMaterial.mainTexture = texture;
meshRenderer.sharedMaterial.color = _spriteRenderer.color;
meshRenderer.sortingLayerName = _spriteRenderer.sortingLayerName;
meshRenderer.sortingLayerID = _spriteRenderer.sortingLayerID;
meshRenderer.sortingOrder = _spriteRenderer.sortingOrder;
//销毁原Sprite
Destroy(gameObject);
在创建Mesh的过程中,我们用到了顶点,三角形和UV数组,效果如下图所示。
这其中,只有顶点数组存储的是世界坐标的位置,三角形数组存储的是顶点的顺序,是int类型的整数,而UV数组存储的是(0,0)到(1,1)范围的值,大家如果感兴趣,可以把它们都输出一下,也可以想上边这样绘制线来表示。
现在,我们已经可以根据顶点创建网格了,可是这个网格现在还没有碰撞器。那么下一步我们要考虑的就是如何根据顶点来正确的创建一个碰撞器以及如何再网格里边挖坑呢?
创建碰撞器的原理,其实也就是将点与点连接的过程,不过关键就是点与点之间的顺序了。不然得到的可能是?
本篇博客就先给大家分享这些,如果对大家有帮助最好。另外,如果本篇博客有什么不足之处,还望大家指正。