逗比刘真是菜啊,天天就知道写这种无聊的东西,写了也不一定有人看的。看了也不一定有人喜欢。喜欢也没有什么卵用啊。先分享一波图吧。
大家第一眼看见是个什么?(逗比刘第一眼说 是精子小蝌蚪啊,逗比刘好邪恶啊)
做一个这样的demo可以说基本用不到很多素材,最重要的素材就是那个圆圈,这个圆圈对于一个程序员来说画起来应该蛮简单的吧。前天我们讲到如果绘制一个2D的圆环,今天我们就绘制一个3D的圆环,如果我们把一个2D的圆绕着一个轴旋转一周就可以做出一个3D的圆环了。
那么我们首先的建一个基本的类Pipe用来构建我们的圆环。我们做出一般的情况就是做出一个整环,然后继续延伸到半个乃至固弧度的圆环。首先我们先从圆环开始。我先发一波图看我们最后的节点的信息是怎么分布的。
我们可以从这张图可以看出,整个圆环的点分布我们可以由最上面在yz平面上的一个圆绕着z轴旋转就构成了我们整个圆环,旋转180度就是个半圆,旋转90度就是一个四分之一圆,这样我们可以通过旋转多少度来控制整个圆环的长度。我们需要控制圆环的换的宽度和圆环的半径,位于yz平面的圆控制我们圆环的宽度。位于yz平面的圆中心点和该圆的旋转点的距离就是构成我们圆环的半径。那么我们就可以得出整个3D圆环上的点的规律。给出点在yz平面圆的的角度和yz平面的圆围绕着中心点旋转了多少度就可以计算出了该点。位于yz平面上的那个圆
上的点在y轴分量最大的那个 点就是最高的那个点,从图中我们可以清晰的看到。 现在我们做个假设假如位于yz平面那个圆上的最高的那个点为a1,那么将yz平面绕着z轴旋转90度的时候我们定义其为y1z1平面,那么这时的a1点就变成了在x轴上分量最大了。所以他们所有点在x轴和y轴的分量我们定义为x=r*sin(旋转角度),y=r*cos(旋转角度)。因为位于yz平面圆上不同位置上的点他们绕着z轴构成的半径都不一样的,所以半径r=圆环的半径+圆环的宽度*cos(点在圆上的角度,位于最高点的角度为0,位于最低点角度为180度,角度的递增顺序是逆时针),所以它的z轴分量就应该为z=圆的宽度*sin(点在圆上的角度)。如果理解有点困难的话大家在大脑里面意淫一下可以出来了。
所以理解这个规律之后,我们就需要建立一个基本的类来生成一个3D的圆环了。同时将它的圆环的宽度和圆环的半径暴露到inspect上方便我们控制。同时我们还应该写出我们刚刚分析通过点在圆上的角度和圆绕z轴形成的角度来得到我们的点方法,同时我们还是定义绘制网格的3个数组,还有我们圆上总共有多少个点,及我们可以旋转的角度(这里所说的我们不可能每隔一度就绘制这些点,我们的做法是每隔20度添加一个圆上的点了,或者我们绘制更加稀疏每隔30度添加一个圆的点)。
using UnityEngine; using System.Collections; using System.Collections.Generic; public class Pipe : MonoBehaviour { public float PipeRadius;//圆环的宽度 public float CurveRadius;//圆环的半径 public int PipesegmentCount;//定义圆上有顶点的个数 public int CurvesegmentCount;//定义圆的个数 private List<Vector3> _verticles; private List<int> _indictInts; private List<Vector2> _uvList; [SerializeField] private Material _meshMaterial;//定义圆环的材质 private void Awake() { _verticles = new List<Vector3>(); _indictInts = new List<int>(); _uvList=new List<Vector2>(); } private Vector3 GetVector3(float pipeAngle, float curveAngle)//通过点在圆上的角度和圆围绕z轴旋转的度数 { float r = CurveRadius + PipeRadius * Mathf.Cos(pipeAngle); float x = r * Mathf.Sin(curveAngle); float y = r * Mathf.Cos(curveAngle); float z = PipeRadius * Mathf.Sin(pipeAngle); return new Vector3(x, y, z); } public void OnDrawGizmos() { for (int i = 0; i < _verticles.Count; i++) { Gizmos.DrawSphere(_verticles[i],0.1f); } } }
建完了基本的类后我们开始添加一个添加点的方法。我们还是一个圆一个圆的添加。然后将他们的绘制顺序和顶点的uv信息同样也给出来。
private Mesh CreateMesh(int pipecout, int curvecout, float angle) { Mesh myMesh = new Mesh(); float pipeoffset = Mathf.PI * 2 / PipesegmentCount; float curveoffset = angle * Mathf.Deg2Rad / (CurvesegmentCount - 1); CurveAngle = angle; for (int i = 0; i < CurvesegmentCount; i++) { for (int j = 0; j < PipesegmentCount; j++) { Vector3 temp = GetVector3(pipeoffset * j, curveoffset * i); _verticles.Add(temp); } } for (int i = 0; i < CurvesegmentCount; i++) { for (int j = 0; j < PipesegmentCount; j++) { _uvList.Add(new Vector2(i % 2, j % 2)); } } for (int i = 0; i < CurvesegmentCount - 1; i++) { for (int j = 0; j < PipesegmentCount; j++) { _indictInts.Add(i * pipecout + j); _indictInts.Add((i + 1) % curvecout * pipecout + j); _indictInts.Add((i + 1) % curvecout * pipecout + (j + 1) % pipecout); _indictInts.Add(i * pipecout + j); _indictInts.Add((i + 1) % curvecout * pipecout + (j + 1) % pipecout); _indictInts.Add(i * pipecout + (j + 1) % pipecout); } } myMesh.vertices = _verticles.ToArray(); myMesh.triangles = _indictInts.ToArray(); myMesh.uv = _uvList.ToArray(); return myMesh; }
做完这些我们就可以做出一个给定角度的3D圆环了,我们继续给它添加响应的组件。
_uvList=new List<Vector2>(); <span style="background-color: rgb(255, 153, 0);"> var mesh = CreateMesh(PipesegmentCount, CurvesegmentCount, 360); this.GetComponent<MeshFilter>().mesh = mesh;</span>
做完这些只是第一步。接下里我们要怎么让2段3D圆环无缝的链接起来,如果第一段圆环的长度是90度(我们用角度来定义圆环的长度),那么第二段圆环的那么绕z的角度那么就是上一段圆环的角度的一个负值对应第一段的圆环长度90就是-90度了。那么下一段就是前面2段长度之和的一个负值了,如果我们这样做那岂不是很麻烦么?当我们生成第二段圆环的时候,我们将第二段的父物体设置成第一段,那么它相对于第一段的围绕z的角度就是上一段圆环角度的负值了,当我们生成第三段圆环的时候,我们将该段圆环的父物体设置成第二段圆环,那么它相对于第二段圆环围绕z轴的角度就是第二段圆环的角度的赋值了,一次类推。
从右边的视图中我们就可以清晰的看见游戏物体的父子关系了。如果我们就只是把几段这样的圆环连接起来的话,那我们还做成几段干嘛直接做成一大段就行了啊。那么我们接下来要做的就是把我们的第二段让它旋转围绕着第一段。而不是让所有的圆环都在一个平面上。截一波图看看。
要让第二段和第一段不在一个平面上,所以我们就需要对其旋转了。那么该怎么旋转和平移呢。截一波图分析分析。
首先我们需要让第二段圆环围绕着圆环本地y轴移动圆环的半径的距离(不是圆环的宽度)
图片截的不怎么好,但是步骤依次从左到右从上到下的顺序的看就明白了如何操作的了。好了我们就给出对应的代码,这个方法的作用应该写在我们这个pipe类中。你也可以写到一个pipe管理类中 其实都一样了。
public void ConnectPipe(Pipe prePipe) { float relativeRotation = Random.Range(0, PipesegmentCount) * 360f / PipesegmentCount;//首先我们定义它需要旋转多少度 transform.SetParent(prePipe.transform, false);//第一步确定父子关系,即让他变成上一段的孩子 transform.localPosition = Vector3.zero; transform.localRotation = Quaternion.Euler(0f, 0f, -prePipe.CurveAngle);//然后就是设置它的本地坐标的欧拉角 transform.Translate(0f, prePipe.CurveRadius, 0f);//开始沿着y轴移动(本地坐标系) transform.Rotate(relativeRotation, 0f, 0f);//开始绕着x轴移动了(本地坐标系) transform.Translate(0f, -CurveRadius, 0f);//然后移回到响应的点 transform.SetParent(prePipe.transform.parent);//最后重新设置父亲节点 transform.localScale = Vector3.one; }
接着我们做一个管理圆环的管理类。
public class PipeSystem : MonoBehaviour { [SerializeField] private int _pipecout; private List<Pipe> _pipes; private int _curindex = 0; void Awake () { _pipes = new List<Pipe>(); } }
我们一般设置这个pipecount的数量不能太多,我个人觉得4个就比较好了。我们虽然的是地图无限的拼接,但是我们只用4段圆环,当我们玩家跑到第二段圆环的时候,我们不删除第一段圆环而是让第一段圆环接到第四段圆环的后面这样就可以做到不生成额外的圆环,对于障碍物的生成我们采用对象池对其进行一点小小的优化。对于这个管理类他应该提供一些基本的方法,例如我们玩家跑到第二段圆环上去了之后这个时候我们的管理类应该通知我们的第一段环接到第四段圆环上去同时重新生成这一段圆环的障碍物同时返回这一段圆环的引用,同时在awake方法中初始化我们的对象池和圆环的位置及相应的信息。
<span style="background-color: rgb(255, 153, 0);"> [SerializeField] private GameObject _pipeGameObject; [SerializeField] private GameObject _cubeObs; [SerializeField] private GameObject _cylinderObs;</span>
<span style="background-color: rgb(255, 153, 0);"> Pools.ManageRecyclePool(new ObjectPool() { Prefab = _cubeObs, InstancesToPreallocate = 50 }); Pools.ManageRecyclePool(new ObjectPool() { Prefab = _cylinderObs, InstancesToPreallocate = 50 });</span> _pipes = new List<Pipe>(); <span style="background-color: rgb(255, 153, 0);"> for (int i = 0; i < _pipecout; i++) { GameObject ob = GameObject.Instantiate(_pipeGameObject); ob.transform.SetParent(this.transform); _pipes.Add(ob.GetComponent<Pipe>()); if (i != 0) { _pipes[i].ConnectPipe(_pipes[i-1]); } }</span>
接下来添加切换圆环即当我们玩家跑到第二段圆环的时候这时我们当前圆环数组中的索引需要发生变幻的。我们知道做一个横版的跑酷游戏例如那种某大型游戏公司的跑酷游戏一样我们的做法有时并不一定非要让玩家移动,而是让背景移动。所以这个的做法就是让背景移动,可是这是一个由很多不同角度的圆环组成的一个地图,那么有移动地图谈何容易啊。所以我们做法是将所有的圆环设置一个父亲节点对其统一管理。我们在开始的时候我们需要将第一段圆环和我们父亲节点保持相对不动,在update方法中我们让我们的父亲节点它的位于z轴的欧拉角随着时间的流逝不断的增大这样就好比我们在移动我们的第一段的圆环一样。这样玩家就在第一段圆环上移动一样。当我们父亲节点的所发生的总偏移量大于圆环的长度(用角度来模拟)的时候。这是我们父亲节点的总偏移量重置为0,如果我们还是按照原来的方式进行移动的话,那么肯定会出错。这时我们就需要重置我们的父亲节点的位置了,这时我们需要做几步来完成这样一份工作。1
把所有的圆环子节点把他们的父亲节点设置为空,然后把原来圆环的父亲节点位置设置成我们的第二段圆环的位置。然后再把所有的圆环又设置成原来的父亲节点了。具体的代码如下。
private int _tempindex = 0; public Pipe SwitchPipe() { _tempindex = _curindex; _curindex++; _curindex = _curindex%_pipecout; for (int i = 0; i < _pipecout; i++) { _pipes[i].transform.parent=null; } this.transform.localPosition = _pipes[_curindex].transform.position; this.transform.localEulerAngles = _pipes[_curindex].transform.eulerAngles; for (int i = 0; i < _pipecout; i++) { _pipes[i].transform.SetParent(this.transform); } return _pipes[_curindex]; }
这个方法里面差个障碍物的重置。所以我重新写了一个方法专门用来重置障碍物的。
public void ResetPipe() { _pipes[_tempindex].ResetObstacle(); _pipes[_tempindex].ConnectPipe(_pipes[(_tempindex + _pipecout - 1) % _pipecout]); }
接下来就是添加player类,这个类就比较简单,主要就是一个update方法。我就直接贴出代码了。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEngine; public class Player :MonoBehaviour { public PipeSystem PipeSystem; public float Velocity; private Pipe _currentPipe; private float _totalAngle = 0; [SerializeField] private float _speed; private void Start() { _currentPipe = PipeSystem.SetupFirstPipe(this.transform); } private bool _resetPipe = false; private void Update() { float delta = Time.deltaTime * _speed; _totalAngle += delta; if (_totalAngle >= _currentPipe.CurveAngle) { delta = _currentPipe.CurveAngle - _totalAngle + delta; PipeSystem.transform.localEulerAngles += new Vector3(0, 0, delta); _currentPipe = PipeSystem.SwitchPipe(); _resetPipe = true; delta = 0; _totalAngle = 0; } if (_totalAngle > 20 && _resetPipe) { PipeSystem.ResetPipe(); _resetPipe = false; } UpdatePlayerRotation(); PipeSystem.transform.localEulerAngles += new Vector3(0, 0, delta); } private void UpdatePlayerRotation() { if (Input.GetMouseButton(0)) { transform.localRotation *= Quaternion.AngleAxis(2, Vector3.right); } } }
添加完了玩家,我们圆环还有一些障碍物还没有生成,所以这里我们就需要添加添加障碍物的方法了。然后我们回到Pipe类中添加如下代码。
private void CreateObstacleItem() { float pipeoffset = Mathf.PI * 2 / PipesegmentCount; float curveoffset = CurveAngle * Mathf.Deg2Rad / (CurvesegmentCount - 1); for (int i = 0; i < CurvesegmentCount-1; i++) { int b = Random.Range(0, 10); if (b % 2 == 0) { int index = Random.Range(0, PipesegmentCount); int type = Random.Range(0,10) % (int)ObscaleEnum.Cylinder; GameObject obj = null; switch (type) { case 1: obj = CreatAwlObstacle(i, index, pipeoffset, curveoffset); break; case 0: obj = CreatePrimitiveTypeObscale(ObscaleEnum.Cube,i, index, pipeoffset, curveoffset); break; case 2: obj = CreatePrimitiveTypeObscale(ObscaleEnum.Cylinder, i, index, pipeoffset, curveoffset); break; } _obstacleObjects.Add(obj); } } }
然后添加几个创建障碍物的方法,首先添加创建圆锥体障碍物,后面就是添加创建圆柱体和长方体障碍物的方法了。
private GameObject CreatAwlObstacle(int curvindex, int pipeindex, float pipeoffset, float curveoffset) { List<Vector3> pointList = new List<Vector3>(); int index = pipeindex; int temp = (index + 1) % PipesegmentCount; Vector3 t1 = GetVector3(pipeoffset * index, curveoffset * curvindex); Vector3 t2 = GetVector3(pipeoffset * index, curveoffset * (curvindex + 1)); Vector3 t3 = GetVector3(pipeoffset * temp, curveoffset * (curvindex + 1)); Vector3 t4 = GetVector3(pipeoffset * temp, curveoffset * curvindex); Vector3 mid = (t1 + t3) / 2; float raiuds = Vector3.Magnitude(t2 - t1); Vector3 dirx = (t2 - t1).normalized; Vector3 diry = (t3 - t2).normalized; for (int i = 0; i <= 20; i++) { float rad = i*18*Mathf.Deg2Rad; var pos =raiuds* dirx*Mathf.Sin(rad)/2 +raiuds*diry*Mathf.Cos(rad)/2; pointList.Add(pos); } Vector3 normal = Vector3.Cross(dirx, diry).normalized; Vector3 t5 = normal * 0.5f; pointList.Add(t5); var obj = _meshBuilder.CreateAwlMeshObj(pointList); obj.transform.SetParent(this.transform,false); obj.transform.localPosition = mid + normal * 0.01f; pointList.Clear(); return obj; } private GameObject CreatePrimitiveTypeObscale(ObscaleEnum type,int curvindex, int pipeindex, float pipeoffset, float curveoffset) { int index = pipeindex; int temp = (index + 1) % PipesegmentCount; Vector3 t1 = GetVector3(pipeoffset * index, curveoffset * curvindex); Vector3 t2 = GetVector3(pipeoffset * index, curveoffset * (curvindex + 1)); Vector3 t3 = GetVector3(pipeoffset * temp, curveoffset * (curvindex + 1)); Vector3 mid = (t1 + t3)/2; Vector3 dirVector3 = (t3 - t2).normalized; Vector3 normal = Vector3.Cross(t2 - t1, t3 - t2); GameObject cube = Pools.Spawn(type==ObscaleEnum.Cube?_cube:_cyl); float scaley = Random.Range(0.05f, 1.5f); float offset = Random.Range(0, 10)%2 == 0 ? 0 : -0.3f; cube.transform.localScale = new Vector3(0.1f, scaley, 0.1f); cube.transform.up = normal; cube.transform.SetParent(this.transform, false); cube.transform.localPosition = mid + normal * scaley * 2 + dirVector3 * offset; cube.GetComponent<MeshRenderer>().material = _meshMaterial; return cube; }
然后我们害的给出网格创建的基本方法了。
public class MeshBuilder { private Material _meshMaterial; public void Intilized(Material mat) { _meshMaterial = mat; } public GameObject CreateAwlMeshObj(List<Vector3> pointList) { GameObject ob=new GameObject("Obstacle"); List<int> indicts=new List<int>(); Mesh mesh=new Mesh(); mesh.vertices = pointList.ToArray(); int temp = pointList.Count - 1; for (int i = 0; i < temp; i++) { indicts.Add(i); indicts.Add(temp); indicts.Add((i + 1) % temp); } mesh.triangles = indicts.ToArray(); var meshfilter = ob.AddComponent<MeshFilter>(); meshfilter.mesh = mesh; ob.AddComponent<MeshRenderer>().material = _meshMaterial; return ob; } public GameObject CreateCubeGameObject(List<Vector3> pointList) { GameObject ob=new GameObject(); List<int> indicts = new List<int>(); Mesh mesh = new Mesh(); mesh.vertices = pointList.ToArray(); int onelinelength = pointList.Count/4; for (int i = 0; i < 4; i++) { for (int j = 0; j < onelinelength-1; j++) { int temp = (i + 1) % 4; indicts.Add(i * onelinelength + j); indicts.Add(temp * onelinelength + j); indicts.Add(temp * onelinelength + j + 1); indicts.Add(i * onelinelength + j); indicts.Add(temp * onelinelength + j + 1); indicts.Add(i * onelinelength + j + 1); } } indicts.Add(0); indicts.Add(0 + 2 * onelinelength); indicts.Add(0 + 1 * onelinelength); indicts.Add(0 + 3 * onelinelength); indicts.Add(0 + 2 * onelinelength); indicts.Add(0); indicts.Add(3 * onelinelength - 1); indicts.Add(onelinelength - 1); indicts.Add(2 * onelinelength - 1); indicts.Add(onelinelength - 1); indicts.Add(onelinelength - 1); indicts.Add(3 * onelinelength - 1); var meshfilter = ob.AddComponent<MeshFilter>(); meshfilter.mesh = mesh; mesh.vertices = pointList.ToArray(); mesh.triangles = indicts.ToArray(); ob.AddComponent<MeshRenderer>().material = _meshMaterial; return ob; } }
public enum ObscaleEnum { None, Awl, Cube, Cylinder }
回到pipe把响应的awake方法补齐和一些字段补齐
private List<Vector2> _uvList; <span style="background-color: rgb(255, 153, 0);">public float CurveAngle = 0; private List<GameObject> _obstacleObjects; private MeshBuilder _meshBuilder;</span>
_uvList=new List<Vector2>(); <span style="background-color: rgb(255, 153, 0);"> _obstacleObjects=new List<GameObject>(); var mesh = CreateMesh(PipesegmentCount, CurvesegmentCount, Random.Range(60, 90)); this.GetComponent<MeshFilter>().mesh = mesh; _meshBuilder=new MeshBuilder(); _meshBuilder.Intilized(_meshMaterial); CreateObstacleItem();</span>
一个基本的sb又无聊的无限跑酷类型的游戏差不多做完了,这里只是一个demo完整度并不高,玩家的碰撞器啊,死亡判断这些我都没有去做,如果大家有时间可以做做,这个都比较简单,稍微写个碰撞检测就行了,另外没做完还有一部分原因是没有ui美术所以做不成一个完整的小游戏了。当我们做完这些然后去做一个赛车的赛道应该会比较简单。如果还有什么不懂的话可以看下面的源码链接。同时你可以qq联系我。qq:185076145 欢迎各位骚扰 哈哈。http://pan.baidu.com/s/1mirn2be