是男人就下100層—Unity實(shí)現(xiàn)歡樂(lè)球球(上)Mesh生成

作者:沈琰
本篇難度: ★★★
前言
大家好,今天我們來(lái)學(xué)習(xí)做一個(gè)稍微復(fù)雜點(diǎn)的項(xiàng)目 。
以年初的《跳一跳》為爆發(fā)點(diǎn),短短半年時(shí)間,微信小游戲已經(jīng)呈遍地開(kāi)花的趨勢(shì)。據(jù)統(tǒng)計(jì),截止6月7日,微信小游戲的數(shù)量已經(jīng)超過(guò)了1000款。很多人都認(rèn)為這是下一個(gè)風(fēng)口級(jí)的機(jī)遇。

當(dāng)然,我們這里不談市場(chǎng),只談技術(shù)。林林總總的小游戲可以說(shuō)是用來(lái)練手的項(xiàng)目示例寶庫(kù)。今天的主角,是一款叫做《歡樂(lè)球球》的游戲。用Unity實(shí)現(xiàn)這款作品會(huì)用到一些進(jìn)階的知識(shí),所以本次內(nèi)容可能會(huì)比入門(mén)級(jí)的項(xiàng)目稍微難那么一點(diǎn)。
話(huà)不多說(shuō),我們開(kāi)始。
1.實(shí)現(xiàn)方法的思考
通常每次準(zhǔn)備實(shí)現(xiàn)或者復(fù)刻一個(gè)游戲,都免不了這一步。然而這一次在開(kāi)頭我們就遇到了一個(gè)大麻煩:我們沒(méi)有素材!

球和圓柱體都還好說(shuō),Unity自帶的3D模型里就有。但是我們沒(méi)有這關(guān)鍵的圓環(huán)形的模型,尤其我們還需要在這個(gè)圓環(huán)模型上開(kāi)出一個(gè)自定義的"缺口"。
這關(guān)系到游戲的核心玩法,所以即使是素材商店有現(xiàn)成的圓環(huán)模型都無(wú)法滿(mǎn)足我們的需求。
腫么辦?

涼拌。。。好了,本期文章到此結(jié)束。
........
開(kāi)個(gè)玩笑,辦法當(dāng)然是有的。現(xiàn)在要用到Unity的進(jìn)階內(nèi)容:Mesh編程。我們來(lái)自己動(dòng)手生成我們想要形狀的3D物體。
2.使用Mesh實(shí)現(xiàn)自定義3d物體
顧名思義,Mesh指的是組成3D物體的網(wǎng)格。在Unity里所有的3D物體網(wǎng)格都是由大小不等的三角形拼接而成。

在Unity里每一個(gè)能顯示在場(chǎng)景的3D物體都需要兩個(gè)組件:MeshFilter和MeshRenderer。
前者負(fù)責(zé)存儲(chǔ)物體的Mesh信息,后者則根據(jù)信息把物體渲染到場(chǎng)景中。

所以現(xiàn)在要做的事就明確了:我們通過(guò)計(jì)算得到Mesh信息來(lái)給一個(gè)空的Mesh賦值,生成我們想要的形狀的物體。那么Mesh里的信息是如何計(jì)算和儲(chǔ)存的呢?
要組成一個(gè)最基本的3D物體,需要一組在空間中確定坐標(biāo)的點(diǎn)和一組以這些點(diǎn)為頂點(diǎn)的三角形,在Mesh信息里是以一個(gè)Vector3類(lèi)型的頂點(diǎn)數(shù)組和一個(gè)int類(lèi)型的三角形數(shù)組保存的。其中三角形數(shù)組保存的是以頂點(diǎn)數(shù)組下標(biāo)為序號(hào)的三角形順序,所以三角形數(shù)組的長(zhǎng)度剛好是頂點(diǎn)數(shù)組的三倍。
當(dāng)我們按照頂點(diǎn)順序繪制出一個(gè)三角形時(shí),這個(gè)三角形只有一個(gè)面是可見(jiàn)的。而具體哪個(gè)面可見(jiàn)則是由頂點(diǎn)序號(hào)的方向來(lái)確定。簡(jiǎn)而言之就是頂點(diǎn)順序方向是順時(shí)針則正面可見(jiàn),頂點(diǎn)順序方向?yàn)槟鏁r(shí)針則背面可見(jiàn)。

有了以上的基礎(chǔ),我們就可以開(kāi)始著手定制我們的3D物體了。我們先簡(jiǎn)單點(diǎn),試著去畫(huà)一個(gè)有缺口的圓片,自己指定缺口的弧度大小。
我們新建一個(gè)腳本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
?
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class TestDisk : MonoBehaviour
{
??? MeshFilter MeshFilter;
??? MeshRenderer MeshRenderer;
?
??? public float height = 0.4f;
??? public float radius = 1f;
??? public int details = 20;
?
??? static float EPS = 0.01f;
?
??? void Start()
??? {
??????? MeshFilter = transform.GetComponent<MeshFilter>();
??????? MeshRenderer = transform.GetComponent<MeshRenderer>();
?
??? }
??? //生成一個(gè)弧度隨機(jī)的圓片
??? [ContextMenu("GeneratePie")]
??? public void GeneratePieByRadian()
??? {
??????? var arcs = new List<float>();
??????? // 按比例隨機(jī),0代表空的,1代表整圓
??????? float r = Random.Range(0.1f, 0.9f);
?? ?????r *= 2 * Mathf.PI;
?
??????? arcs.Add(0);
??????? arcs.Add(r);
?
??????? GeneratePie(arcs);
??????
??? }
?
?
??? // 參數(shù):每個(gè)弧用兩兩個(gè)弧度(float)表示,每個(gè)餅可以有多個(gè)三角塊,就和切披薩一樣
??? public void GeneratePie(List<float> arcs)
??? {
??????? List<Vector3> verts = new List<Vector3>();
??????? List<Vector2> uvs = new List<Vector2>();
??????? List<int> tris = new List<int>();
?
??????? List<Vector3> _verts = new List<Vector3>();
??????? List<Vector2> _uvs = new List<Vector2>();
??????? List<int> _tris = new List<int>();
?
????? ??for (int i = 0; i < arcs.Count; i += 2)
??????? {
??????????? _verts.Clear();
??????????? _uvs.Clear();
??????????? _tris.Clear();
?
??????????? //先把中心點(diǎn)添加進(jìn)頂點(diǎn)List中
??????????? _verts.Add(new Vector3(0, 0, 0));
??????????? _verts.Add(new Vector3(0, -height, 0));
?
??????????? AddArcMeshInfo(arcs[i], arcs[i + 1], _verts, _uvs, _tris);
?
??????????? //把頂點(diǎn)序號(hào)填進(jìn)三角形list里
??????????? foreach (int n in _tris)
??????????? {
??????????????? tris.Add(n + verts.Count);
??????????? }
??????????? verts.AddRange(_verts);
?? ?????????uvs.AddRange(_uvs);
??????? }
??????? Mesh mesh = new Mesh();
??????? // 填寫(xiě)mesh
??????? mesh.vertices = verts.ToArray();
??????? mesh.triangles = tris.ToArray();
??????? mesh.uv = uvs.ToArray();
?
??????? //根據(jù)頂點(diǎn)和三角形數(shù)據(jù)自動(dòng)生成體積框,法線(xiàn)和切線(xiàn)
??????? mesh.RecalculateBounds();
??????? mesh.RecalculateNormals();
??????? mesh.RecalculateTangents();
?
??????? MeshFilter.mesh = mesh;
?
??? }
?
??? void AddArcMeshInfo(float begin, float end, List<Vector3> verts, List<Vector2> uvs, List<int> tris)
??? {
??????? // begin和end是開(kāi)始弧度、結(jié)束弧度
??????? // verts里面已經(jīng)有了頂面中心點(diǎn)、底面中心點(diǎn),下標(biāo)分別是0和1
?
??????? float eachRad = 2 * Mathf.PI / details;
?
??????? // 用三角函數(shù)計(jì)算出圓的周長(zhǎng)上的頂點(diǎn)
??????? float a;
??????? for (a = begin; a <= end; a += eachRad)
??????? {
??????????? Vector3 v = new Vector3(radius * Mathf.Sin(a), 0, radius * Mathf.Cos(a));
??????????? verts.Add(v);
??????????? Vector3 v2 = new Vector3(radius * Mathf.Sin(a), -height, radius * Mathf.Cos(a));
??????????? verts.Add(v2);
??????? }
??????? if (a < end + EPS)
??????? {
??????????? Vector3 v = new Vector3(radius * Mathf.Sin(end), 0, radius * Mathf.Cos(end));
??????????? verts.Add(v);
??????????? Vector3 v2 = new Vector3(radius * Mathf.Sin(end), -height, radius * Mathf.Cos(end));
??????????? verts.Add(v2);
??????? }
?
??????? // 頂面頂點(diǎn)序號(hào)
??????? int n = verts.Count;
??????? for (int i = 2; i < n - 2; i += 2)
??????? {
??????????? tris.Add(i); tris.Add(i + 2); tris.Add(0);
??????? }
?
??????? // 側(cè)面頂點(diǎn)序號(hào)
??????? for (int i = 2; i < n - 2; i += 2)
??????? {
??????????? tris.Add(i); tris.Add(i + 1); tris.Add(i + 2);
??????????? tris.Add(i + 2); tris.Add(i + 1); tris.Add(i + 3);
??????? }
?
??????? // 封住兩個(gè)直線(xiàn)邊
??????? tris.Add(2); tris.Add(0); tris.Add(1);
??????? tris.Add(3); tris.Add(2); tris.Add(1);
??????? tris.Add(n - 1); tris.Add(0); tris.Add(n - 2);
??????? tris.Add(1); tris.Add(0); tris.Add(n - 1);
??? }
}
?
把腳本掛載到一個(gè)空物體上,設(shè)置好數(shù)據(jù)后運(yùn)行場(chǎng)景:

可能會(huì)感覺(jué)有點(diǎn)蒙。別方,我們可以用協(xié)程按照頂點(diǎn)順序依次顯示出構(gòu)成圓片的三角形,便于我們更加形象的理解其構(gòu)成順序。

? ?IEnumerator SequenceTest()
??? {
??????? for (int i = 0; i < MeshFilter.mesh.triangles.Length; i += 3)
??????? {?????????
??????????? Debug.DrawLine(MeshFilter.mesh.vertices[MeshFilter.mesh.triangles[i]], MeshFilter.mesh.vertices[MeshFilter.mesh.triangles[i + 1]], Color.red, 100f);
?
??????????? yield return new WaitForSeconds(0.2f);
??????????? Debug.DrawLine(MeshFilter.mesh.vertices[MeshFilter.mesh.triangles[i + 1]], MeshFilter.mesh.vertices[MeshFilter.mesh.triangles[i + 2]], Color.yellow, 100f);
?
??????????? yield return new WaitForSeconds(0.2f);
??????????? Debug.DrawLine(MeshFilter.mesh.vertices[MeshFilter.mesh.triangles[i + 2]], MeshFilter.mesh.vertices[MeshFilter.mesh.triangles[i]], Color.blue, 100f);
?
??????????? yield return new WaitForSeconds(0.2f);
?
??????? }
??? }
?
用紅黃藍(lán)三種顏色的順序來(lái)顯示三角形的三條邊,效果如圖:

可以看到三角形的構(gòu)建順序就是我們賦值時(shí)候的順序。
搞明白了半圓片是怎么實(shí)現(xiàn)的了,相信環(huán)形也就難不倒大家了。無(wú)非是中心點(diǎn)換成了環(huán)形內(nèi)徑的頂點(diǎn),這就留給大家自己去思考如何實(shí)現(xiàn)。

關(guān)于Mesh編程的內(nèi)容網(wǎng)上有很多,此處只做簡(jiǎn)單說(shuō)明,有興趣的童鞋可以自己了解一下。
傳送門(mén):https://blog.csdn.net/qq_29579137/article/details/77369734
3.小球的重力和反彈模擬
現(xiàn)在素材我們算是有了,該考慮小球了。不然怎么叫歡樂(lè)球球呢。
首先想一下這么一個(gè)問(wèn)題:我們能不能直接用小球掛載的剛體組件自帶的重力和碰撞盒子上的物理材質(zhì)的彈力去實(shí)現(xiàn)游戲中小球反彈的效果?畢竟這樣比較省事。
答案是不能。因?yàn)槲锢碛?jì)算本身是對(duì)數(shù)據(jù)的一種近似計(jì)算,會(huì)有一定的誤差。而當(dāng)游戲運(yùn)行一段時(shí)間后,這種誤差會(huì)逐漸積累到影響游戲正常邏輯。比如在球觸碰到地面時(shí)如果移動(dòng)地面,球就會(huì)獲得一個(gè)水平方向分量的力,球的反彈移動(dòng)速度的向量就會(huì)有一點(diǎn)偏差。所以我們得自己來(lái)模擬重力和反彈果。
先想一下現(xiàn)實(shí)世界的重力是如何生效的:任何物體都會(huì)受到重力加速度的影響,自由落體時(shí)速度會(huì)逐漸增大直到碰到底面或者重力加速度與空氣阻力達(dá)成平衡。碰到地面時(shí)則速度值會(huì)因?yàn)閺椓Ω淖?,然后又受到重力影響,如此往?fù)。所以代碼也按照這個(gè)思路來(lái)寫(xiě)。
新建小球的腳本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
?
public class BallTest : MonoBehaviour
{
??? SphereCollider sphereCollider;
?
??? public float bounceSpeed;
??? public float gravity;
??? public float maxSpeed = 1;
?
??? float radius = 0;
??? float speed;
?
??? void Start ()
??? {
??????? sphereCollider = GetComponent<SphereCollider>();
??????? radius = sphereCollider.radius;
?
???????? }
??? //球掉落移動(dòng)計(jì)算
??? void Drop()
??? {
??????? //每幀減去模擬重力帶來(lái)的速度影響
??????? speed -= gravity * Time.deltaTime;
??????? //限制球能達(dá)到的最大速度
??????? speed = Mathf.Clamp(speed, -maxSpeed, maxSpeed);
?
??????? transform.position += new Vector3(0, speed, 0);
??? }
??? void Bounce()
??? {
??????? //當(dāng)球反彈回升的時(shí)候跳過(guò)檢測(cè)
??????? if (speed >= 0)
??????? {
??????????? return;
??????? }
?
??????? //確定一個(gè)位置合適的立方體,比球略小,位置偏下
??????? Vector3 p = transform.position + new Vector3(0, -radius, 0);
??????? Vector3 size = new Vector3(radius * 0.5f, radius * 0.5f, radius * 0.5f);
??????? if (Physics.OverlapBox(p, size, Quaternion.identity, LayerMask.GetMask("Ground")).Length > 0)
??????? {???????
??????????? speed = bounceSpeed;?????
??????? }
?
??? }
??
?? void Update()
??? {
??????? Bounce();
??????? Drop();
?
??? }
??
??
}
?
在場(chǎng)景中新建一個(gè)球體和一個(gè)平面,把腳本掛載在球體上,修改平面的Layer為"Ground",然后運(yùn)行場(chǎng)景:

可以看到小球如我們所期望的方式運(yùn)動(dòng)起來(lái),并且由于數(shù)值都是我們?cè)O(shè)定好的,每幀更新調(diào)用,所以不管運(yùn)行多久都不會(huì)有誤差。如此一來(lái)我們就簡(jiǎn)單的實(shí)現(xiàn)了小球的重力和反彈。
結(jié)束
這期文章我們把游戲前期的準(zhǔn)備工作基本做完了,下期我們會(huì)開(kāi)始著手實(shí)現(xiàn)游戲邏輯,比如分?jǐn)?shù)的計(jì)算,場(chǎng)地復(fù)用等等。
關(guān)于Mesh這期只介紹了基本的形狀的構(gòu)建,把一個(gè)3D物體渲染到場(chǎng)景里還需要計(jì)算頂點(diǎn)法線(xiàn),
貼圖的UV坐標(biāo),切線(xiàn)等更復(fù)雜的數(shù)據(jù),有興趣的同學(xué)可以自行深入研究。
本期文章工程地址:https://github.com/tank1018702/unity_002
想系統(tǒng)學(xué)習(xí)游戲開(kāi)發(fā)的童鞋,歡迎訪(fǎng)問(wèn)?http://levelpp.com/? ? ? ??
游戲開(kāi)發(fā)攪基QQ群:869551769? ? ? ? ?
微信公眾號(hào):皮皮關(guān)