是男人就下100層——Unity實現(xiàn)歡樂球球(下)鏈表對象池

作者:沈琰
本篇難度: ★★★
前言
大家好。
我,來填上期挖出來的坑了。摸魚的日子過于舒服以至于差點就把更新給忘了。

進入正題,首先回顧下上期的內容。
上期傳送門:是男人就下100層—Unity實現(xiàn)歡樂球球(上)Mesh生成
在上期的內容里我們只做了兩件事:
1.用Mesh實現(xiàn)了自定義的3D物體作為接下來游戲內的素材。
2.用代碼計算簡易的模擬了小球的重力和反彈效果。
我們首先思考下還差哪些東西沒做:
1.控制邏輯與游戲場景的生成復用。
2.分數(shù)的UI顯示與計算。
3.一些提升游戲表現(xiàn)力的特效。
其中前兩項都是歸屬于游戲邏輯的的范疇,并且也是游戲的核心,所以我們放到一起實現(xiàn)。
本篇同樣會涉及到意想不到的知識點:用鏈表做對象池。

控制邏輯
原版游戲是用手指滑動手機屏幕讓整個場景轉動,同樣的,我們可以用鼠標拖動屏幕來控制場
景轉動,用代碼去實現(xiàn)其實非常簡單,每次Update獲取鼠標這一幀和上一幀坐標的X值的變
化,用這個值計算旋轉。
using System.Collections;
using UnityEngine;
?
public class InputLogic : MonoBehaviour
{
??? //記錄上一幀鼠標的位置
??? Vector3 lastMousePos;
?
??? void TouchRotate()
??? {
??????? //鼠標左鍵按下時轉動
??????? if (Input.GetMouseButton(0))
??????? {
??????????? //計算這一幀鼠標位置與上一幀的差值,然后以Y軸轉動
??????????? float moveX = (Input.mousePosition - lastMousePos).x;
??????????? transform.Rotate(0, -moveX, 0);
??????? }?
??? }
?
?????? void Update ()
???? {
??????? TouchRotate();
??????? lastMousePos = Input.mousePosition;
???? }
}
?
由于我們只想讓場景內物體橫著轉,所以計算出來的旋轉幅度后用這個值沿著Y軸旋轉。把腳本
掛在一個空節(jié)點上,場景內所有需要旋轉的物體掛到空節(jié)點下,則其下所有子物體都會跟著空
節(jié)點一起旋轉。

場景的生成與復用
上期內容中,我們已經(jīng)能做到給定一個弧度,生成一個缺口大小與之對應的圓環(huán)了:

現(xiàn)在要做的就是在小球下落的過程中每隔一定高度就生成一個隨機弧度的圓環(huán)。但是為性能考
慮我們肯定不能無限制的生成,得在已經(jīng)生成的環(huán)移動到攝像機的視野之外時重新拿來復用。
應該已經(jīng)有聰明的同學想到可以寫一個對象池來實現(xiàn)這個功能,在小球與環(huán)的距離大于一個閾
值的時候把圓環(huán)加入對象池中,然后每次生成新的環(huán)時到對象池里面去取。
這個做法當沒毛病,并且通常也是這么做的。但我們今天用個更簡潔直觀的方法,用一個
鏈表來實現(xiàn)這個功能。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
?
public class GameMode : MonoBehaviour
{
??? public Circle PrefabCircle;
??? public Transform Root;
?
??? LinkedList<Circle> CircleQueue;
??? LinkedListNode<Circle> curNode;
?
?? ?public float gap = 8.0f;
??? float lowestCircleY;
?
??? Transform cam;
??? Transform pillar;
?
??? Ball ball;
??? void Start ()
??? {
??????? cam = Camera.main.transform;
??????? pillar = GameObject.Find("Pillar").transform;
??????? ball = GameObject.Find("ball").GetComponent<Ball>();
?
??????? CircleQueue = new LinkedList<Circle>();
?
??????? CircleQueue.AddLast(GetNewCircle());
??????? curNode = CircleQueue.Last;
??? }
?
??? Circle GetNewCircle()
??? {
??????? Circle circle = Instantiate(PrefabCircle, Root);
??????? circle.Init();
??????? return circle;
??? }
?
??? Circle GetNextCircle()
??? {
??????? // 讓鏈表循環(huán)使用的算法
??????? LinkedListNode<Circle> next = curNode.Next;
??????? if (next == null)
??????? {
??????????? // 如果達到了結尾就回到開頭
??????????? next = CircleQueue.First;
??????? }
??????? //如果圓環(huán)太高就隱藏等待復用
??????? if (next.Value.transform.position.y > lowestCircleY + 2f * gap)
??????? {
??????????? next.Value.gameObject.SetActive(false);
??????? }
??????? //如果鏈表的下一個環(huán)在場景中是隱藏的就返回這個環(huán)并在場景中顯示
??????? if (!next.Value.gameObject.activeInHierarchy)
??????? {
??????????? curNode = next;
??????????? curNode.Value.gameObject.SetActive(true);
??????? }
??????? //如果下一個環(huán)在場景中顯示,生成新的環(huán)并添加到鏈表next之前
??????? else
??????? {
??????????? curNode = CircleQueue.AddBefore(next, GetNewCircle());
??????? }
?
??????? return curNode.Value;
??? }
??
???????? void Update ()
??? {
??????? while (lowestCircleY + gap > ball.transform.position.y)
??????? {
??????????? var circle = GetNextCircle();
??????????? //每次得到新的圓環(huán)時改變高度
??????????? circle.transform.position = new Vector3(0, lowestCircleY - gap);
??????????? circle.GenerateCircleByLevel();
??????????? lowestCircleY = circle.transform.position.y;???
??????? }
?
??????? pillar.position = new Vector3(pillar.position.x, cam.position.y, pillar.position.z);
??? }
}
?
這段代碼的作用就是使用一個鏈表來達到場景復用的目的,就像這樣:

此處要注意的是鏈表并沒有成為一個環(huán)形,我們只是在下一個節(jié)點為null的時候把返回值賦值為
鏈表的第一個元素而已。
然后我們讓攝像機在球下落時跟著球一起移動,新建如下腳本掛載到主攝像機上。
public class ChaseBallCam : MonoBehaviour
{
?
??? Transform ball;
??? float baseY;
??? float camOffsetY;
?
??? void Start ()
??? {
??????? ball = GameObject.FindGameObjectWithTag("Player").transform;
?
??????? camOffsetY = ball.transform.position.y - transform.position.y;
??? }
????????
???????? void Update ()
??? {
??????? //當球的位置低于偏移值時讓攝像機和球一起動
?????????????????? float diffY = ball.transform.position.y - transform.position.y - camOffsetY;
??????? if (diffY < 0)
??????? {
??????????? // 比必要的位置再低一些,可以防止抖動
??????????? transform.position += new Vector3(0, diffY -0.15f, 0);
??????? }
??? }
}
?
最后再把攝像機的Y軸坐標每一幀同步賦值給中間的圓柱,最后運行效果如下:
可以看到鏈表的總長度只有4個,但已足夠實現(xiàn)場景內物體的循環(huán),如此一來場景的基本結構
就搭好了。

分數(shù)的計算與UI顯示
計算分數(shù)的算法最關鍵的問題,是如果連續(xù)通過多個缺口且沒有觸碰到圓環(huán),那么獲得的分數(shù)是
累加的。
這里的思路是用小球在圓環(huán)上反彈的時間來計算當次的下落獲得的分數(shù)。在Unity里通過調用
Time.time能夠獲取從游戲開始到現(xiàn)在總共經(jīng)過的時間。
我們在GameMode里每一次獲取下一個圓環(huán)的時間,就是小球經(jīng)過當前圓環(huán)的時間。所以我們
可以把計分函數(shù)放在生成圓環(huán)的循環(huán)里調用。在球的腳本里記錄每一次反彈的時間,當最后一
次反彈的時間大于上一次計分的時間時,說明小球碰到了圓環(huán),累加的計分需要重置。
? ? void AddScore()
??? {
??????? if (ball.lastBounceTime > lastAddScoreTime)
??????? {
???????????
??????????? combo = 1;
??????? }
??????? else
??????? {
??????????? combo++;
????????? ??if (combo > 9) { combo = 9; }
??????? }
??????? lastAddScoreTime = Time.time;
???????
??????? var numObj = Instantiate(prefabNumber, canvas);
??????? numObj.GetComponent<ScoreAnim>().SetNumber(combo);
??????? totalScore += combo;
??????? totalScoreText.text = string.Format("Score:{0}", totalScore);
??? }
?
然后在網(wǎng)上找一套數(shù)字的貼圖資源,從1-9按數(shù)字修改一下圖片的名字。

新建一個物體,在腳本里把圖片存入一個list中。在子物體中添加Image組件,每次新建時通過
傳入的分數(shù)來顯示與之對應的圖片,順便加上一些透明漸變和移動的UI動畫效果。別忘Image
組件只有在Canvas下才能正確的顯示圖片,所以場景中還需要新建一個Canvas。
sing System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
?
public class ScoreAnim : MonoBehaviour {
?
??? Image numImage;
??? public List<Sprite> nums;
??? int num;
????????
???????? void Start ()
??? {
??????? numImage = transform.Find("Image").GetComponent<Image>();
??????? numImage.sprite = nums[num % 10];
??????? numImage.CrossFadeAlpha(0, 0.5f, false);
??????? Destroy(gameObject, 1.0f);
??? }
?
??? public void SetNumber(int n)
??? {
??????? this.num = n;
??? }
????????
???????? void Update ()
??? {
??????? transform.Translate(new Vector3(0, 50*Time.deltaTime, 0));???????
???????? }
}
?
最后在Canvas下新建一個Text組件顯示總共獲得的分數(shù),在每次計分時更新分數(shù)。

游戲表現(xiàn)力的提升
到這里游戲的主體基本就完成了,但是我們還有活干,適當添加一些特效或者擴展功能讓我們
的游戲顯的更有趣一些。
比如可以在小球上加上一些特效:添加TrailRenderer組件實現(xiàn)尾跡,小球在圓環(huán)上反彈時
添加一張痕跡貼圖,改變小球的大小顯示反彈動畫等等。

也可以仿照原版加入小球經(jīng)過圓環(huán)時掉落的效果,添加障礙物的圓環(huán)讓小球碰到就結束游戲,
順便另外做個UI實現(xiàn)游戲的重新開始。這些都可以在前面的基礎上稍微修改下實現(xiàn),限于篇幅
就不詳細展開說明,可在后面的工程鏈接中下載下來研究。
大家完全可以按照自己的喜好來做一些自己認為更有趣的改動。

結束
這個小游戲到這里就做完了,這個工程難度總體來說是略微高于入門工程的,但是就算是初學
者慢慢來也能完整的做出來。希望能通過這個工程幫助大家減少一些初學者常有的畏難情緒,
即便缺少素材,我們也能用代碼去實現(xiàn)我們想要的功能。
本期文章工程地址:https://github.com/tank1018702/unity_002/tree/master/JumpBall
想系統(tǒng)學習游戲開發(fā)的童鞋,歡迎訪問?http://levelpp.com/? ? ? ???
游戲開發(fā)攪基QQ群:869551769? ? ? ? ??
微信公眾號:皮皮關