我們來用Unity復(fù)刻一下《黃金礦工》

作者:繁華如夢
前言
空洞騎士的項(xiàng)目算是告一段落了,開新坑開新坑。
這里只有一個(gè)問題:
不想再肝了。滿腦子裝的都是摸魚。
要是有在摸魚的同時(shí)又能填坑的操作就好了。

對(duì)了。
以前玩過一個(gè)摸魚必備游戲《黃金礦工》,開這個(gè)坑就可以邊摸魚邊填坑了。

經(jīng)過上述不超過5分鐘的、大起大落的心理過程后,便有了這期的文章。
雖然黃金礦工比較簡單,但其中細(xì)枝末節(jié)較多。文章只會(huì)講解較為重要的技術(shù)點(diǎn),其余部分歡迎參考后續(xù)工程代碼食用。
模擬繩子
黃金礦工游戲中,玩家通過按鍵操作并發(fā)射鉤子來挖取金礦,鉤子與絞盤之間有一條繩子進(jìn)行連接,這條繩子我們使用Unity提供的LineRenderer組件來進(jìn)行實(shí)現(xiàn)。
由于素材的限制,我們使用鉤子的這張圖片做為玩家的本體,并在上面添加LineRenderer組件,然后在Materials選項(xiàng)中選擇默認(rèn)的精靈圖材質(zhì)(Sprites-Default),然后將Order in Layer選項(xiàng)修改為1,防止被背景2D物體遮擋。其中,Positions選項(xiàng)是用來設(shè)置線段的2點(diǎn),LineRenderer組件會(huì)在游戲運(yùn)行時(shí)自動(dòng)在2點(diǎn)之間進(jìn)行連線。而我們也是在代碼中動(dòng)態(tài)修改這2點(diǎn)的值,來實(shí)現(xiàn)繩子的效果。大致設(shè)定如圖:

接下來,在絞盤的位置新建一個(gè)空物體,用于確定線段的起點(diǎn)。然后在代碼中更新Positions選項(xiàng)中點(diǎn)的位置即可。代碼如下:
public class Player : MonoBehaviour {
??? public Transform startTrans;??? //起始點(diǎn)
??? LineRenderer lineRenderer;
??? void Start() {
??????? lineRenderer = GetComponent<LineRenderer>();
??????? lineRenderer.startWidth = 0.1f;//修改線條寬度
??? }
??? void Update() {
??????? UpdataLine();
??? }
??? public void UpdataLine()
??? {
??????? lineRenderer.SetPosition(0, startTrans.position);
??????? lineRenderer.SetPosition(1, transform.position);//設(shè)置線條2點(diǎn)的位置
??? }
}
?
完成后效果如下:

旋轉(zhuǎn)
游戲中,鉤子總是繞著絞盤來進(jìn)行旋轉(zhuǎn)。使用RotateAround這個(gè)函數(shù)就可輕松解決,難點(diǎn)是如何限制鉤子只在下方進(jìn)行旋轉(zhuǎn)操作,而不會(huì)旋轉(zhuǎn)到上方。此處,我們使用向量之間的夾角來進(jìn)行判斷,鉤子是否旋轉(zhuǎn)出邊界。由于素材的問題,鉤子對(duì)應(yīng)的方向是Up軸的正下方。如下圖:

我們就需要通過Up軸的反方向,與絞盤的Right軸之間的夾角,來進(jìn)行旋轉(zhuǎn)的處理。代碼如下:
public enum RotaDir
{
??? left,
??? right,
}//定義旋轉(zhuǎn)方向枚舉
?? ?public RotaDir nowDir;??? //玩家當(dāng)前的旋轉(zhuǎn)方向
??? public float angleSpeed;? //旋轉(zhuǎn)速度
??? public void PlayRotate()
??? {
?
??????? float rightAngle = Vector3.Angle(transform.up * -1, Vector3.right);//計(jì)算玩家前進(jìn)方向與Right的夾角
?
?
??????? if (nowDir == RotaDir.left)
??????? {
??????????? if (rightAngle < 170)
??????????? {?? //在可旋轉(zhuǎn)范圍內(nèi)按當(dāng)前方向繼續(xù)旋轉(zhuǎn)
??????????????? transform.RotateAround(startTrans.position, Vector3.forward, angleSpeed * Time.deltaTime);
??????????? }
??????????? else
??????????? {
??????????????? nowDir = RotaDir.right;//超出范圍,改變方向進(jìn)行旋轉(zhuǎn)
??????????? }
?
??????? }
??????? else
??????? {
??????????? if (rightAngle > 10)
??????????? {
??????????????? transform.RotateAround(startTrans.position, Vector3.forward, -angleSpeed * Time.deltaTime);
??????????? }
??????????? else
?? ?????????{
??????????????? nowDir = RotaDir.left;
??????????? }
?
??????? }
??? }
?
完成后效果如下:

移動(dòng)
通過上面的步驟,我們可以很輕松的知道,鉤子的朝向總是自己的Up軸的反方向。那么移動(dòng)的方向也是如此。只要一直朝這個(gè)方向移動(dòng)就行了,返回也是同樣的道理。代碼如下:
??? public float angleSpeed;
??? public void PlayMoveForward()//前向移動(dòng)
??? {
??????? transform.position += transform.up * -1 * moveSpeed * Time.deltaTime;
??? }
??? public void PlayBackMove()//返回移動(dòng)
??? {
??????? transform.position += transform.up * moveSpeed * Time.deltaTime;
??? }
?
完成后效果如下:

道具交互
不同的道具有不同的交互效果,本作中主要的效果就是得分增加,道具增加,以及玩家移動(dòng)減速了。
道具效果
新建一個(gè)PropScript腳本,專門用來記錄當(dāng)前道具的接觸效果或者獲得分?jǐn)?shù)。其中通過道具的類型來進(jìn)行處理。代碼如下:
public enum PropType
{
??? None,
??? Fraction,??? // 分?jǐn)?shù)類型道具
??? Boom,??? // 炸彈
??? Potion,??? // 雙倍藥劑
} //定義道具類型枚舉
public class PropScript : MonoBehaviour {
?
??? public int fraction;// 當(dāng)前道具分?jǐn)?shù)
??? public PropType nowType;// 當(dāng)前道具類型
??? public int scaleLevel=1;? //當(dāng)前道具的縮放道具? 默認(rèn)為1? 用于計(jì)算玩家鉤住時(shí)的速度
??? public void UseProp()//使用道具的方法
??? {
??????? switch (nowType)
??????? {
??????????? case PropType.Fraction:
??????????????? GameMode.Instance.AddFraction(fraction);//分?jǐn)?shù)增加
??????????????? break;
??????????? case PropType.Potion:
??????????????? GameMode.Instance.isDouble = true;//開啟雙倍開關(guān)
??????????????? break;
??????????? case PropType.Boom:
??????????? ????GameMode.Instance.AddBoomProp();//添加炸彈道具
??????????????? break;
??????? }
??? }
}
人物減速
我們使用觸發(fā)器來檢測鉤子是否抓中了物體,如果抓中了。就在此時(shí)更改玩家的速度。由于之前在PropScript中設(shè)置了縮放的等級(jí),我們可以通過這個(gè)來計(jì)算新的速度(PS:人物速度 = 原人物速度-原人物速度*減速系數(shù)*縮放等級(jí))。代碼如下:
public class Player : MonoBehaviour {
...
??? private void OnTriggerEnter2D(Collider2D collision)
??? {
??????? PropScript propScript = collision.gameObject.GetComponent<PropScript>();
??????? if (propScript != null)
??????? {
??????????? float tempDistance = Vector3.Distance(transform.position, propScript.transform.position);
??????????? propScript.transform.position = transform.position + transform.up * -1 * tempDistance;//位置修正
??????????? propScript.transform.SetParent(transform);//設(shè)置父物體用于拖拽移動(dòng)
??????????? ComputeSpeed(propScript.scaleLevel);
??????? }
??? }
??? public void ComputeSpeed(int scaleLevel)//計(jì)算新的玩家速度
??? {
??????? moveSpeed = moveSpeed - moveSpeed * 0.15f * scaleLevel;
??? }
}
?
道具生成
黃金礦工游戲中,有多種道具來豐富游戲的內(nèi)容。在進(jìn)行道具生成之間,先準(zhǔn)備好對(duì)應(yīng)的游戲數(shù)據(jù),能夠使開發(fā)事半功倍。
數(shù)據(jù)準(zhǔn)備
首先制作所有的道具的2D預(yù)制體,并統(tǒng)一將他們的Order in Layer參數(shù)改為1(PS:由于背景的2D物體對(duì)應(yīng)參數(shù)設(shè)置的為0,為了能夠覆蓋在背景之上,就需要調(diào)高參數(shù)),然后將他們保存在Resources/Prefabs文件夾下(PS:子文件夾可以隨便設(shè)置,但是必須得是在Resources文件夾下),方面后續(xù)代碼的讀取。如下:

接下來,新建一個(gè)Gamemode腳本,來控制并管理游戲中的道具生成以及勝利相關(guān)功能等。然后在里面初始化對(duì)應(yīng)的道具數(shù)據(jù)。代碼如下:
public class GameMode : MonoBehaviour {
??? public Dictionary<string, GameObject> tempLates;??????? //存儲(chǔ)所有的道具預(yù)制體模板
??? public string[] objNames;?????? //所有分?jǐn)?shù)道具的物體名稱
??? public int[] fractionData;????? //道具的分?jǐn)?shù)數(shù)據(jù)
??? public int[] targetFraction;??? //每一關(guān)的目標(biāo)分?jǐn)?shù)
??? public float[] scaleData;?????? //縮放數(shù)據(jù)
??? public string[] propName;?????? //特殊道具名稱
??? public float minX;?????? //生成的最小X值 面板賦值
??? public float maxX;?????? //生成的最大X值 面板賦值
??? public float minY;?????? //生成的最小y值 面板賦值
??? public float maxY;?????? //生成的最大y值 面板賦值
??? void Start() {
??????? targetFraction = new int[] { 3000, 4000, 5000, 7000, 9000, 12000 ,15000,20000,25000};
??????? objNames = new string[] { "Diamonds", "gold", "goldTwo", "stoneOne", "stoneTwo" };
??????? fractionData = new int[] { 1000, 300, 500, 100, 150 };
??????? propName = new string[] { "explosive", "Potion" };
??????? scaleData = new float[] { 1.0f, 1.2f, 1.5f, 1.8f, 2.0f };//手動(dòng)填入關(guān)卡的相應(yīng)數(shù)據(jù)
??????? InitData();//初始化 讀取數(shù)據(jù)并填充字典
??? }
}
?
其中設(shè)置的道具生成的范圍大致如下圖:

道具生成
隨機(jī)位置
已經(jīng)準(zhǔn)備好了對(duì)應(yīng)的數(shù)據(jù),接下來就是根據(jù)對(duì)應(yīng)數(shù)據(jù)生成道具。首先隨機(jī)道具生成的位置,通過之前GameMode設(shè)置的生成數(shù)值,我們可以輕松得到在范圍內(nèi)的隨機(jī)坐標(biāo)。代碼如下:
??? public Vector3 RandomPos()
??? {
??????? float xVaule = Random.Range(minX, maxX);
??????? float yVaule = Random.Range(minY, maxY);
??????? Vector3 tempPoint = new Vector3(xVaule, yVaule, 0);
??????? return tempPoint;
??? }
?
隨機(jī)旋轉(zhuǎn):
??? public Quaternion RandomRotate()
??? {
???? ???float angle = Random.Range(0, 360);
??????? Quaternion tempQuat = Quaternion.AngleAxis(angle, Vector3.forward);
??????? return tempQuat;
??? }
?
由于之前設(shè)置了縮放等級(jí),隨機(jī)縮放就只需要從數(shù)組中取出對(duì)應(yīng)的值即可。于是代碼如下:
??? public float RandomScale(out int scaleLevel)??? // 隨機(jī)道具縮放 返回縮放值,用于計(jì)算
??? {
??????? int index = Random.Range(0, scaleData.Length);
??????? scaleLevel = index+1;
??????? float scaleVaule = scaleData[index];
??????? return scaleVaule;
??? }
?
通過之前設(shè)置的關(guān)卡分?jǐn)?shù)信息,以及各種道具的分?jǐn)?shù)。我們來生成當(dāng)前關(guān)卡的道具。主要是先隨機(jī)出道具,然后計(jì)算當(dāng)前已經(jīng)生成道具分?jǐn)?shù)的總和,查看是否超出目標(biāo)關(guān)卡分?jǐn)?shù),超出就不在生成,不超出就重復(fù)上面的操作。直到分?jǐn)?shù)超出(PS:道具的分?jǐn)?shù)=道具配置的分?jǐn)?shù)*道具的縮放值)。代碼如下:
public class GameMode : MonoBehaviour {
...
??? public int level;//當(dāng)前游戲關(guān)卡數(shù)
??? public void SwitchLevel()
??? {
??????? int creatfraction = 0;????? //現(xiàn)在已經(jīng)生成的分?jǐn)?shù)
??????? int tempfraction = targetFraction[level];//當(dāng)前關(guān)卡的目標(biāo)分?jǐn)?shù)
??????? tempfraction += minFraction;//為了降低游戲難度,讓生成道具的分?jǐn)?shù)總和,能夠超過目標(biāo)分?jǐn)?shù)+minFraction
??????? while (creatfraction < tempfraction)//如果現(xiàn)在生成的分?jǐn)?shù)小于目標(biāo)分?jǐn)?shù)
??????? {
??? ????????int objIndex = Random.Range(0, objNames.Length);
??????????? string tempName = objNames[objIndex];//隨機(jī)道具名稱
??????????? GameObject tempObj = RandomProp(tempName);
??????????? float tempScale = 1;//道具的縮放值
??????????? int scaleLevel = 1;//道具的縮放等級(jí)
??? ????????if (objIndex != 0)//鉆石不進(jìn)行旋轉(zhuǎn)縮放操作
??????????? {
??????????????? Quaternion tempQuat = RandomRotate();
??????????????? tempObj.transform.rotation = tempQuat;
??????????????? tempScale = RandomScale(out scaleLevel);
??????????????? tempObj.transform.localScale *= tempScale;//設(shè)置道具的縮放
??????????? }
??????????? var tempScript = tempObj.AddComponent<PropScript>();//給生成道具添加腳本
??????????? tempScript.nowType = PropType.Fraction;//設(shè)置類型
??????????? int fraction = fractionData[objIndex];//活動(dòng)設(shè)定的道具分?jǐn)?shù)
??????????? fraction = (int)(fraction * tempScale);//計(jì)算出現(xiàn)在的道具分?jǐn)?shù)
??????????? creatfraction += fraction;
??????????? tempScript.fraction = fraction;
??????????? tempScript.scaleLevel = scaleLevel;//設(shè)置道具的縮放等級(jí)
??????????? levelObjs.Add(tempObj);
??????? }
??????? int propCount = Random.Range(0, 3);?????? //場上最多2個(gè)特殊道具
??????? while (propCount > 0)
??????? {
??????????? propCount--;
??????????? GameObject tempObj = RandomSpecialProp();//同上
??????????? levelObjs.Add(tempObj);
??????? }
??? }
??? public GameObject RandomProp(string name)//隨機(jī)道具
??? {
??????? GameObject templateObj = tempLates[name];
??????? Vector3 tempPoint = RandomPos();
??????? GameObject tempObj = Instantiate(templateObj, tempPoint, Quaternion.identity);
??????? return tempObj;
??? }
}
?
游戲演示

結(jié)語
這樣,一個(gè)簡答的黃金礦工就算完成了。但是還缺少UI顯示,關(guān)卡切換等功能,文章沒有給出實(shí)現(xiàn)的講解。歡迎童鞋參考后續(xù)的完整版工程食用,來補(bǔ)上缺失的部分。
相關(guān)鏈接
工程與試玩文件鏈接:https://pan.baidu.com/s/1kTVE9mE6sylGdgDKOzePOw#list/path=%2F
慣例時(shí)間
有想系統(tǒng)學(xué)習(xí)游戲開發(fā)的童鞋,歡迎訪問:http://levelpp.com/?
另有專業(yè)開發(fā)交(gao)流(ji)群等待大家強(qiáng)勢插入:869551769