Unity 2D 曲面地图动态生成
发表于2018-10-11
游戏中2D的地图比较直观,关于2D平面地图就不做介绍了,生成也比较容易,这篇要介绍的是游戏中2D曲面地图动态生成问题,一起来看看吧。
第一张是在scence视图下的图,可以很清楚的看到这个地形的分布,第二张则是一个球在场景中的运行图,也就是我们的主角了。如果我们需要制作出一个曲面,毫无疑问的是我们得动态构造网格,然后给我们的网格贴uv。一个曲面是有很多个三角形绘制,所以我们知道如何绘制出一个三角形的话,然后间接的就可以绘制出一个四边形,乃至整个曲面网格了。如果不会绘制一个三角形的可以参考前面的博客了。如果我们先绘制出一个长方形的网格,然后通过变幻把直的变成弯的(如果我们能变魔术就好了,把直的变成弯的)。
我们先把直的绘制出来,上面就是一个方的地形了(逗比刘真是菜,一个方的地形还这样,逗比刘就只会装b了 哈哈)我们发现方的地形我们用了很多点来绘制其实是有必要的,这里便于我们理解,我们发现如果把上面的地形用sin曲线或者cos曲线做的话,那不是就是变成曲线了么。好我们先绘制出直的来,然后演变成曲的。
那么首先需要建一个类了。我们定义其为BKCurveEntity,
public class BKCurveEntity { }
我们先写个初始化方法,用来给定我们用什么材质,方形网格的高度,整个网物体的其实位置。
public Vector3 P1; public Vector3 P2; public Vector3 P3; public Vector3 P4; public Vector3 OriginVector3; private Material _bodyMeshMaterial; private Material _terrianMaterial; private float _factor; public void IntilizedBkEntity(List<Vector3> keyPoints, Material tm, Material bm, float factor) { P1 = keyPoints[0]; P2 = keyPoints[1]; P3 = keyPoints[2]; P4 = keyPoints[3]; _bodyMeshMaterial = bm; _terrianMaterial = tm; _factor = factor; }
p2,p3表示我们绘制网格的左上坐标和右上坐标。这里p1,p4的用处我们先不讲,tm代表网格黑色山体的材质,而bm代表的网格灰色山体上面草的材质了。后面的参数先不讲了。接下来我们添加一个主方法。GameObject GenerateMeshGameObject(Vector3 originPos, float startuv)用来生成一个gameobject。
public GameObject GenerateMeshGameObject(Vector3 originPos, float startuv) { var bk = new GameObject("BK"); bk.transform.position = originPos; OriginVector3 = originPos; return bk; }
我们还是先对一些参数进行赋值,比如生成网格他的初始位置,起始uv等信息。然后就是我们需要添加网格的点了,这里我们网格左下到右下的点我们可以根据左上到右上的点来进行计算,所以我们需要先得到左上到右上这之间的点了。
<span style="background-color: rgb(255, 153, 0);">_tempVector3S = new List<Vector3>();</span> var bk = new GameObject("BK");
OriginVector3 = originPos; <span style="background-color: rgb(255, 153, 0);"> float right = P3.x; float width = 8f; _tempVector3S.Add(P2); for (float i = P2.x; i < right; ) { i += 0.5f; if (i > right) i = right; var leftTopPoint = P2+new Vector3(i,0,0); _tempVector3S.Add(leftTopPoint); }</span>
这个地方我们采用的是线型采样点。把它存在一个临时数组中。接下来我们就生成山体和山体上面的草了。同时我们同样需要得到山体网格的顶点信息和顶点绘制顺序及顶点的uv。
OriginVector3 = originPos; <span style="background-color: rgb(255, 153, 0);"> _terrainBodyV = new List<Vector3>(); _terrainBodyT = new List<int>(); _terrainBodyU = new List<Vector2>(); _terrainSurfaceV = new List<Vector3>(); _terrainSurfaceT = new List<int>(); _terrainSurfaceU = new List<Vector2>(); </span> float right = P3.x;
上面分别定义2种网格所需要的基本信息:网格顶点信息,顶点绘制顺序,顶点uv信息。
for (float i = P2.x; i < right; ) { i += 0.5f; if (i > right) i = right; var leftTopPoint = P2+new Vector3(i,0,0); _tempVector3S.Add(leftTopPoint); } <span style="background-color: rgb(255, 153, 0);"> for (int i = 0; i < _tempVector3S.Count - 1; i++) { _terrainBodyV.Add(_tempVector3S[i]); _terrainBodyV.Add(new Vector3(_tempVector3S[i].x, 0, 0)); _terrainBodyV.Add(_tempVector3S[i + 1]); _terrainBodyV.Add(new Vector3(_tempVector3S[i + 1].x, 0, 0)); _terrainSurfaceV.Add(_tempVector3S[i] + new Vector3(0, 0.5f, 0f)); _terrainSurfaceV.Add(_tempVector3S[i] - new Vector3(0, 0.2f, 0f)); _terrainSurfaceV.Add(_tempVector3S[i + 1] + new Vector3(0, 0.5f, 0f)); _terrainSurfaceV.Add(_tempVector3S[i + 1] - new Vector3(0, 0.2f, 0f)); _terrainBodyT.Add(0); _terrainBodyT.Add(3); _terrainBodyT.Add(1); _terrainBodyT.Add(3); _terrainBodyT.Add(0); _terrainBodyT.Add(2); _terrainSurfaceT.Add(0); _terrainSurfaceT.Add(3); _terrainSurfaceT.Add(1); _terrainSurfaceT.Add(3); _terrainSurfaceT.Add(0); _terrainSurfaceT.Add(2); var terrainBodymesh = new Mesh(); var terrainFaceMesh = new Mesh(); terrainBodymesh.vertices = _terrainBodyV.ToArray(); terrainBodymesh.uv = _terrainBodyU.ToArray(); terrainBodymesh.triangles = _terrainBodyT.ToArray(); terrainFaceMesh.vertices = _terrainSurfaceV.ToArray(); terrainFaceMesh.uv = _terrainSurfaceU.ToArray(); terrainFaceMesh.triangles = _terrainSurfaceT.ToArray(); var ob = new GameObject("meshobject"); var surface = new GameObject("Surface Gameobject"); ob.transform.parent = bk.transform; ob.transform.localPosition = Vector3.zero; surface.transform.parent = bk.transform; surface.transform.localPosition = Vector3.zero; _terrainBodyV.Clear(); _terrainBodyT.Clear(); _terrainBodyU.Clear(); _terrainSurfaceV.Clear(); _terrainSurfaceT.Clear(); _terrainSurfaceU.Clear(); }</span>
这里我们只是对2种网格的顶点信息和顶点绘制顺序进行了赋值。还没有对他们uv赋值了。这里我们画一张图给大家理解一下。
我们添加点的顺序依次是左上,左下,右上,右下,所以我们我们这个绘制顺序数组就可以很轻松得出来,分别0,3,1和0,2,3.最后就是添加uv和碰撞器及材质了。
surface.transform.localPosition = Vector3.zero; <span style="background-color: rgb(255, 153, 0);"> AddColliderToChildMesh(_tempVector3S[i], _tempVector3S[i + 1], bk.transform); var ren = ob.AddComponent<MeshRenderer>(); var filter = ob.AddComponent<MeshFilter>(); ren.material = _bodyMeshMaterial; filter.mesh = terrainBodymesh; var ren1 = surface.AddComponent<MeshRenderer>(); var filter1 = surface.AddComponent<MeshFilter>(); ren1.material = _terrianMaterial; filter1.mesh = terrainFaceMesh;</span> _terrainBodyV.Clear();
这样一个方的网格就出来了。这里我们建一个类测试一下。
public class CreateTerrian : MonoBehaviour { }
先定义一个在控制面板上可以拖拽的2个材质。
<span style="background-color: rgb(255, 153, 0);">private BKCurveEntity _bkCurveEntity; [SerializeField] private Material bm; [SerializeField] private Material tm;</span>
然后再start方法中我们生成4段方的网格。
[SerializeField] private Material tm; <span style="background-color: rgb(255, 153, 0);"> private float _length; private float _originx; private float _x1, _x2, _x3, _x4; private List<float> _posFloats; private List<GameObject> _bkGameObjects; void Start () { _posFloats=new List<float>(); _bkCurveEntity=new BKCurveEntity(); List<Vector3> points=new List<Vector3>(); _bkGameObjects=new List<GameObject>(); _originx = -10;//起始位置的x值为-10 _length = 0; _x1 = 0; _x2 = 2; _x3 = 6; _x4 = 5; for (int i = 0; i < 3; i++) { points.Add(new Vector3(-10, _x1, 0)); points.Add(new Vector3(0, _x2, 0)); points.Add(new Vector3(10, _x3, 0)); points.Add(new Vector3(20, _x4, 0)); _bkCurveEntity.IntilizedBkEntity(points, bm, tm, 0.5f); var ob= _bkCurveEntity.GenerateMeshGameObject(new Vector3(_originx+_length, -5, 0), 0); _bkGameObjects.Add(ob); _length= _bkCurveEntity.GetBkLength(); _originx=_bkCurveEntity.OriginVector3.x; _posFloats.Add(_originx); _x1 = _bkCurveEntity.P2.y; _x2 = _bkCurveEntity.P3.y; _x3 = _bkCurveEntity.P4.y; _x4 = GetRandomValue(2,8,_x3); points.Clear(); } }</span>
这里第二段网格的最后一个点的y值我们用随机函数生成。
private float GetRandomValue(float min, float max, float referce) { while (true) { float endvalue = Random.Range(min, max); if (Mathf.Abs(endvalue - referce) > 2) { return endvalue; } } }
第二段网格的起始点就是第一段网格的终点。这样就轻松的搞定了一个方形的网格了,那么接下来我们想办法把一个方形的网格变成一个曲的网格了。这里我们可以用最常见的sin和cos函数来试试吧。
for (float i = P2.x; i < right; ) { i += 0.5f; if (i > right) i = right; <span style="background-color: rgb(255, 153, 0);"> var leftTopPoint = P2+new Vector3(i,Mathf.Sin(i),0);</span><span style="background-color: rgb(255, 204, 102);"> </span> _tempVector3S.Add(leftTopPoint); }
这就是效果,很显然我觉得很糟糕,因为2段之间没有连接起来,当然想让他们无缝连接起来也很简单。只需保证所有网格的起始点和终点他们的y保持一致就行了。这样虽然保证了网格是曲面,发现整个网格是有规则的没有随机性可言。所以我们只有想别的办法然他保证随机性和曲面了。(逗比刘这都不知道么,我来告诉你吧,用采样曲线啊,这你都不知道啊。)哦 我们可以用采样曲线,首先让我想到的就是贝塞尔曲线,那我们来看看如果想让2条贝塞尔曲线连接起来,2段曲线有什么关系呢?从下图来分析:
上图中有2段贝塞尔曲线分别为p0,c0,p1和p1,c2,p2。如果想让2段2次贝尔赛曲线连接起来,那么满足第一段贝尔赛曲线的终点和第二段杯赛曲线的起始点为同一个点,同样p1要在c0和c2线段上,我们在做完第一段贝塞尔曲线的时候,开始连接第二段贝塞尔曲线的时候,我们先通过p1c0向量求得c1控制点,然后在随机p2点就可以做出曲线了,思路已经很清晰了,我们可以试试,这里我就不试了,这样虽然可以做出曲线,但是对于y的取值是个费劲的问题,y的取值超出摄像机的视野范围,这对于我们来讲可以来说很简单同样也不简单。所以这里我们就不用这个曲线了,我们采用catmull rom曲线,不知道的朋友可以在wiki上查看https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline。接下来我们返回到BKCurveEntity这个类中,添加如下方法:
private Vector3 CatmullRom(Vector3 P0, Vector3 P1, Vector3 P2, Vector3 P3, float t) { Vector3 c0 = P1; Vector3 c1 = (P2 - P0) * _factor; Vector3 c2 = (P2 - P1) * 3.0f - (P3 - P1) * _factor - (P2 - P0) * 2.0f * _factor; Vector3 c3 = (P2 - P1) * -2.0f + (P3 - P1) * _factor + (P2 - P0) * _factor; Vector3 curvePoint = c3 * t * t * t + c2 * t * t + c1 * t + c0; return curvePoint; }
采样曲线前面2个点就相当于我们的控制点,factor为曲线平滑因子范围是[0,1]。 ?t的范围是[0,1]为0时 该曲线取得p1。为1时该曲线取得p2。这样我们想让我们2段catmull曲线连接起来只需要,第一段曲线的p1,p2,p3等于第二段曲线的p0,p1,p2即可。这样给我们的曲线构建带来很大的方便。这里我们就将他改为该曲线线,效果就如最上面的图了。
for (float i = P2.x; i < right; ) { i += 0.5f; if (i > right) i = right; <span style="background-color: rgb(255, 153, 0);"> var t = GetCatmullRomT(P1, P2, P3, P4, i); var leftTopPoint = CatmullRom(P1, P2, P3, P4, t);</span> _tempVector3S.Add(leftTopPoint); }
添加方法如下:
private float GetCatmullRomT(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4, float cur) { return 2 * (cur - p2.x) / (p3.x - p1.x); }
</pre>最后给草地添加碰撞器。<pre name="code" class="plain"> private void AddColliderToChildMesh(Vector3 originVector3, Vector3 endVector3, Transform parent) { var ob = new GameObject("Collider"); ob.tag = "Ground"; ob.transform.parent = parent; var length = Vector3.Distance(originVector3, endVector3); var midX = (endVector3.x + originVector3.x) / 2f; var midY = (originVector3.y + endVector3.y) / 2f; var offset = (endVector3 - originVector3); var eulerZ = Mathf.Atan2(offset.y, offset.x) * Mathf.Rad2Deg; ob.transform.localPosition = new Vector3(midX, midY, 0f); ob.transform.rotation = Quaternion.Euler(0, 0, eulerZ); var col = ob.AddComponent<BoxCollider2D>(); col.offset = new Vector2(0, -0.03f); col.size = new Vector2(length, 0.06f); }
这里给点建议,如果我们有时不是很特别想突出表现我们的山体,我们可以给曲线的点让他们分散的稀疏点,给我们的草地上的点添加密一点。大家也可以把这个资源文件下载下来,资源链接:http://pan.baidu.com/s/1mh66kDq
来自:https://blog.csdn.net/u012565990/article/details/51919570