前言
這個游戲算是本Unity菜雞真正意義上從頭到尾跟着教程,一步步踩坑到完成的游戲了。
寫這篇博客主要是用於總結學習該游戲項目,嗯,下面開始吧。
(PS:這個游戲是跟着siki學院的憤怒的小鳥教程做的,b站地址:【SiKi學院Unity】Unity初級案例 - 憤怒的小鳥_嗶哩嗶哩_bilibili
官網的課程資料、源碼及筆記下載地址:http:// http://www.sikiedu.com/course/134)
提示:以下是本篇文章正文內容,下面案例可供參考
一、游戲邏輯與設計
本款游戲作為一個入門級的Unity項目,它的實現邏輯並不復雜,首先把游戲場景分為三個:
加載界面
關卡選擇界面
游戲界面
其中,加載界面和關卡選擇界面,可以選擇Unity自帶的UI功能進行實現,也就是用Image來顯示背景圖片和各個按鍵等組件布局;
關於游戲界面,例如小鳥、小豬、木塊、背景和草地等對象,這里是通過新建一個空物體,然后添加圖片的形式進行實現,然后例如暫停窗口、勝利窗口、失敗窗口就可以使用UI進行實現,把所有窗口放在一個Canvas里,然后默認取消顯示,當達成目標功能時(例如關卡勝利、失敗,點擊暫停按鍵等),就將它對應的組件設置為激活狀態,這樣就達成了界面顯示的功能;
然后是關於游戲的邏輯了,由於憤怒的小鳥它主要的游戲核心,其實就是——碰撞。
所以在這里,絕大部分的游戲邏輯其實都是通過碰撞和觸發進行實現,我們把小鳥、小豬、障礙物等組件加上剛體和碰撞器,然后根據需求針對特殊的個體組件添加觸發器,然后我們可以通過判斷碰撞和觸發的狀態,來決定是否發生了游戲對象的邏輯碰撞,然后選擇執行對應代碼。
二、游戲場景搭建
1.游戲背景
游戲背景就是由幾個圖片(背景圖片、地面、草叢)拼接而成,並給地面添加一個碰撞器(BoxCollider2D),以便小鳥和敵方單位不會一直落下。
2.玩家模塊
玩家模塊由以下幾個對象組成:
彈弓左部
彈弓右部
當前小鳥
預備小鳥
對於小鳥,需要添加其剛體與碰撞器組件,並根據不同小鳥的大小,調整碰撞范圍;
關於小鳥的“彈弓模擬”操作,這里使用了Spring Joint2D組件進行實現,中心點為小鳥中心,左右兩個點分別設置在左彈弓和右彈弓上的合適位置,然后可以根據情況調整距離和頻率,這樣就可以初步實現類似彈弓彈簧的效果(目前還無法飛出);
接下來關於拖拽小鳥,形成畫線的功能,這里使用LineRender進行畫線操作,在腳本中,當可以進行畫線操作時(當前小鳥已經激活,但是還沒有飛行),設置其每一段Line的兩點並畫線;
/// <summary>
/// 划線
/// </summary>
public void Line()
{
right.enabled = true; left.enabled = true; right.SetPosition(0, rightPos.position); right.SetPosition(1, this.transform.position); left.SetPosition(0, leftPos.position); left.SetPosition(1, this.transform.position); }
3.敵人模塊
敵人模塊就是靠自己進行發揮了,針對不同的物體對象(小豬、木塊、木條、鐵塊、柱子...)設置其剛體和碰撞器(主要是碰撞體大小),然后設置其血量(最小和最大承受速度),受傷圖片等等其它的變量,然后根據自己的想象和設計,設計出不同的關卡;
三、游戲代碼模塊
1.小鳥(包括特殊小鳥)
通過控制canMove來區分當前小鳥和等待小鳥的狀態,當該小鳥為激活狀態時,canMove為true,可以執行Line(彈弓畫線)和Fly(飛行過程)方法,進行彈弓的操控與飛行操作,當飛行狀態時,重新置canMove為false(防止重復操作),其中控制小鳥的操作由鼠標來執行,在代碼中即是,在OnMouseUp、OnMouseDown來進行監聽;
當飛行過程中,可以通過點擊鼠標調用ShowSkill方法執行特殊小鳥的技能操作,這個方法可以寫成虛方法,在后面的特殊小鳥中,進行重寫並調用;當該小鳥飛出后,通過延時調用Next方法,重新銷毀當前小鳥,並激活下一個等待小鳥;
using System.Collections;
using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; public class Bird : MonoBehaviour { public bool isClick = false; public float maxDis = 1.5f; [HideInInspector] public SpringJoint2D sp; protected Rigidbody2D rg; public LineRenderer right; public LineRenderer left; public Transform rightPos; public Transform leftPos; public GameObject boom; protected TestMyTrail myTrail; [HideInInspector] public bool canMove = false; public float amooth = 3; public AudioClip select; public AudioClip fly; private bool isFlay; public bool isReleased = false; public Sprite hurt; public SpriteRenderer render; public void Awake() { sp = GetComponent<SpringJoint2D>(); rg = GetComponent<Rigidbody2D>(); render = GetComponent<SpriteRenderer>(); myTrail = GetComponent<TestMyTrail>(); } private void OnMouseDown() { if (canMove) { AudioPlay(select); isClick = true; rg.isKinematic = true; } } private void OnMouseUp() { if (canMove) { isClick = false; rg.isKinematic = false; Invoke("Fly", 0.1f); right.enabled = false; left.enabled = false; canMove = false; } } public void Update() { //如果點擊的是UI界面,則直接返回 if (EventSystem.current.IsPointerOverGameObject()) { return; } if (isClick) { this.transform.position = Camera.main.ScreenToWorldPoint(Input.mousePosition); this.transform.position += new Vector3(0, 0, -Camera.main.transform.position.z); if(Vector3.Distance(this.transform.position,rightPos.position) > maxDis) { Vector3 pos = (this.transform.position - rightPos.position).normalized; pos *= maxDis; this.transform.position = pos + rightPos.position; } Line(); } //相機跟隨 CamereMove(); //飛行時,點擊左鍵 if (isFlay) { if (Input.GetMouseButtonDown(0)) { ShowSkill(); } } } public void CamereMove() { float posX = this.transform.position.x; Camera.main.transform.position = Vector3.Lerp(Camera.main.transform.position, new Vector3(Mathf.Clamp(posX,0,17),Camera.main.transform.position.y, Camera.main.transform.position.z), amooth*Time.deltaTime); } public void Fly() { isReleased = true; isFlay = true; AudioPlay(fly); myTrail.StartTrail(); sp.enabled = false; Invoke("Next", 3); } /// <summary> /// 划線 /// </summary> public void Line() { right.enabled = true; left.enabled = true; right.SetPosition(0, rightPos.position); right.SetPosition(1, this.transform.position); left.SetPosition(0, leftPos.position); left.SetPosition(1, this.transform.position); } public virtual void Next() { Gamemanager._instance.birds.Remove(this); Destroy(this.gameObject); Instantiate(boom, this.transform.position, Quaternion.identity); Gamemanager._instance.NextBird(); } public void OnCollisionEnter2D(Collision2D collision) { isFlay = false; myTrail.ClearTrail(); } public void AudioPlay(AudioClip clip) { AudioSource.PlayClipAtPoint(clip,this.transform.position); } public virtual void ShowSkill() { isFlay = false; } public void Hurt() { render.sprite = hurt; } }
剩下的黃鳥(加速)、綠鳥(回旋)和黑鳥(爆炸),就是繼承了redbird這個類,然后根據需求,重寫ShowSkill方法即可;
public class YellowBird : Bird
{
public override void ShowSkill() { base.ShowSkill(); rg.velocity *= 2; } } public class GreenBird : Bird { public override void ShowSkill() { base.ShowSkill(); Vector3 speed = rg.velocity; speed.x *= -1; rg.velocity = speed; } }
其中,BlackBird因為要通過觸發判斷爆炸效果,並且爆炸后直接進行銷毀,所以要額外多寫幾個方法進行實現
public class BlackBird : Bird
{
public List<Pig> blocks = new List<Pig>(); /// <summary> /// 進入觸發區域 /// </summary> /// <param name="collision"></param> private void OnTriggerEnter2D(Collider2D collision) { if(collision.gameObject.tag == "Enemy") { blocks.Add(collision.gameObject.GetComponent<Pig>()); } } /// <summary> /// 退出觸發區域 /// </summary> /// <param name="collision"></param> private void OnTriggerExit2D(Collider2D collision) { if (collision.gameObject.tag == "Enemy") { blocks.Remove(collision.gameObject.GetComponent<Pig>()); } } public override void ShowSkill() { base.ShowSkill(); if( blocks!=null && blocks.Count > 0) { for(int i = 0; i < blocks.Count; i++) { blocks[i].Dead(); } } OnClear(); } public void OnClear() { rg.velocity = Vector3.zero; Instantiate(boom, this.transform.position, Quaternion.identity); render.enabled = false; GetComponent<CircleCollider2D>().enabled = false; myTrail.ClearTrail(); } public override void Next() { Gamemanager._instance.birds.Remove(this); Destroy(this.gameObject); Gamemanager._instance.NextBird(); } }
2.敵人(包括小豬、木塊等障礙物)
這里主要就是調整敵方的“血量”,但是這里的血量並不是一點一點扣除的,而是進行一個判斷,當該物體碰撞的對象是Player(小鳥),並且當
碰撞速度>maxspeed時,敵方死亡
minspeed<碰撞速度<maxspeed時,敵方調整為受傷狀態
碰撞速度<minspeed時,敵方不受傷
然后寫一個Dead方法,用於處理死亡后的操作(播放死亡音樂、爆炸特效、銷毀物體等)
public class Pig : MonoBehaviour
{
public float maxSpeed = 10; public float minSpeed = 4; private SpriteRenderer render; public Sprite hurt; public GameObject boom; public GameObject score; public bool isPig = false; public AudioClip hurtClip; public AudioClip dead; public AudioClip birdCollision; private void Awake() { render = GetComponent<SpriteRenderer>(); } private void OnCollisionEnter2D(Collision2D collision) { if(collision.gameObject.tag == "Player") { AudioPlay(birdCollision); collision.transform.GetComponent<Bird>().Hurt(); } if(collision.relativeVelocity.magnitude > maxSpeed) { Dead(); } else if( collision.relativeVelocity.magnitude>minSpeed && collision.relativeVelocity.magnitude < maxSpeed) { AudioPlay(hurtClip); render.sprite = hurt; } } public void Dead() { if (isPig) { Gamemanager._instance.pigs.Remove(this); } AudioPlay(dead); Destroy(this.gameObject); Instantiate(boom, this.transform.position, Quaternion.identity); GameObject go = Instantiate(score, this.transform.position + new Vector3(0,0.5f,0), Quaternion.identity); Destroy(go, 1.5f); } public void AudioPlay(AudioClip clip) { AudioSource.PlayClipAtPoint(clip, this.transform.position); } }
3.游戲管理器
游戲管理器用於控制游戲邏輯,這里定義兩個列表,用於存儲所有小鳥和所有小豬,
在開始時,激活小鳥列表的第一個對象為當前小鳥,其它小鳥等待,當當前小鳥飛行完成后,調用Next方法,刪除已發射小鳥,並激活下一個等待小鳥進行操縱;
當小鳥數量=0或是小豬數量=0時,進行邏輯判斷
當小豬數量=0時,勝利!
當小豬數量<0,並且小鳥數量=0時,失敗!
然后顯示對應的UI界面,其中當勝利時,獲得的星星個數為 當前剩余小鳥個數+1,以此邏輯進行得分判斷,並通過Unity自帶的PlayerPrefs類,以鍵值對的形式(key為當前的關卡名稱),進行數據的存儲;
剩下的就是定義一些UI按鍵的綁定方法,例如
Next 下一關
SaveData 當游戲勝利時,保存當前關卡得分(取最大)
Home 返回游戲首頁
RePlay 重新開始當前游戲關卡
public class Gamemanager : MonoBehaviour
{
public List<Bird> birds; public List<Pig> pigs; public static Gamemanager _instance; public Vector3 originPos; //初始位置 public GameObject win; public GameObject lose; public GameObject[] starts; public int starsNum = 0; public int totalNum = 5; public void Start() { Initialized(); } public void Awake() { _instance = this; originPos = birds[0].transform.position; } /// <summary> /// 初始化小鳥 /// </summary> private void Initialized() { for(int i = 0; i < birds.Count; i++) { if (i == 0)//第一只小鳥 { birds[0].transform.position = originPos; birds[i].enabled = true; birds[i].sp.enabled = true; birds[i].canMove = true; } else { birds[i].enabled = false; birds[i].sp.enabled = false; } } } public void NextBird() { if (pigs.Count > 0) { if (birds.Count > 0) { //下一只小鳥 Initialized(); } else { //輸了 lose.SetActive(true); } } else { //贏了 win.SetActive(true); } } public void ShowStarts() { StartCoroutine("show"); //Debug.Log("勝利!!!" + birds.Count); } IEnumerator show() { for (; starsNum < birds.Count + 1; starsNum++) { if(starsNum >= starts.Length) { break; } yield return new WaitForSeconds(0.2f); //Debug.Log(starts[i].name); starts[starsNum].SetActive(true); } } public void RePlay() { SaveData(); SceneManager.LoadScene(2); } public void Home() { SaveData(); SceneManager.LoadScene(1); } public void Next() { SaveData(); string currentLevel = PlayerPrefs.GetString("nowLevel"); Debug.Log(currentLevel); int num = int.Parse(currentLevel.Substring(5,1)) + 1; string nextLevel = currentLevel.Substring(0, currentLevel.Length - 1) + System.Convert.ToString(num); Debug.Log("下一關為: " + nextLevel); //加載下一關 PlayerPrefs.SetString("nowLevel", nextLevel); SceneManager.LoadScene(2); } public void SaveData() { Debug.Log("當前關卡的星星數量為: " + starsNum); //當前的星星數目大於已存儲星星數目時,進行更新存儲 if (starsNum > PlayerPrefs.GetInt(PlayerPrefs.GetString("nowLevel"))) { PlayerPrefs.SetInt(PlayerPrefs.GetString("nowLevel"), starsNum); } //存儲所有的星星個數 int sum = 0; for(int i = 1; i <= totalNum; i++) { sum += PlayerPrefs.GetInt("level" + i.ToString()); //Debug.Log("第"+ i.ToString() +"關的星星為: " + PlayerPrefs.GetInt("level" + i.ToString())); //Debug.Log("sum為: " + sum); } Debug.Log("將要存儲的星星總數為: " + sum); PlayerPrefs.SetInt("totalNum", sum); } }
4.地圖選擇
地圖UI設計為以下四個部分,其中最后一個部分沒有功能實現,所以真正的關卡其實只有前面三部分
在開始時,我們先讀取所有已通關關卡的星星數目總和,當星星綜合大於設定的map星星數目時,該map才進行解鎖,設置isSelect=true,否則鎖定該關卡,設置isSelect=false;
當點擊該map時,隱藏map視圖,顯示關卡視圖level
public class MapSelect : MonoBehaviour
{
public int starsNum; public bool isSelect = false; public GameObject locks; public GameObject starts; public GameObject map; public GameObject panel; public Text startsText; public int startNum = 1; public int endNum = 5; public void Start() { //清除所有游戲數據 //PlayerPrefs.DeleteAll(); if(PlayerPrefs.GetInt("totalNum",0) >= starsNum) { Debug.Log("星星總數為: " + PlayerPrefs.GetInt("totalNum")); isSelect = true; } if (isSelect) { locks.SetActive(false); starts.SetActive(true); //TODO:Text顯示 TextShow(); } } public void TextShow() { int count = 0; for (int i = startNum; i <= endNum; i++) { count += PlayerPrefs.GetInt("level" + i.ToString(), 0); } startsText.text = count.ToString() + "/15"; } public void Selected() { if (isSelect) { panel.SetActive(true); map.SetActive(false); } } public void PanelSelect() { panel.SetActive(false); map.SetActive(true); } }
5.關卡選擇
當激活關卡視圖時,首先激活第一關,設置isSelect = true,然后遍歷剩下的關卡,通過PlayerPrefs獲取已存儲的數據,當目標關卡的星星數目>0時,激活該關卡,否則進行鎖定;
當激活某一關時,要通過PlayerPrefs得到該關卡的星星數目,然后通過控制star[i](星星列表)進行星星的顯示;
然后定義一個Slect方法,用於選擇關卡,當點擊某個關卡時,通過PlayerPrefs設置當前關卡為點擊關卡,並通過SceneManager讀取場景
public class LevelSelect : MonoBehaviour
{
public bool isSelect = false; public Sprite levelBG; public Image img; public GameObject[] stars; public void Awake() { img = GetComponent<Image>(); } public void Start() { //是第一關 if(this.transform.name == this.transform.parent.GetChild(0).name) { isSelect = true; } else { int beforeNum = int.Parse(this.gameObject.name) - 1; if( PlayerPrefs.GetInt("level"+beforeNum.ToString()) > 0) { isSelect = true; } } //激活關卡 if (isSelect) { img.overrideSprite = levelBG; this.transform.Find("num").gameObject.SetActive(true); //讀取星星個數 int count = PlayerPrefs.GetInt("level" + this.gameObject.name); if (count > 0) { for(int i = 0; i < count; i++) { stars[i].SetActive(true); } } } } public void Selected() { if (isSelect) { PlayerPrefs.SetString("nowLevel", "level" + this.gameObject.name); SceneManager.LoadScene(2); //Debug.Log("選擇成功"); } } }
6.暫停界面
通過UI設計一個暫停界面,當為激活時,它在游戲窗口外,當激活時,通過動畫,將UI界面移入游戲窗口;
定義方法Pause,當點擊暫停按鈕時,調用該方法,然后調用暫停動畫,將UI窗口移入游戲界面,
設置Time.timeScale = 0,以達到暫停游戲的效果,並隱藏該暫停按鈕;
然后定義Resume方法,當點擊還原按鈕時,調用該方法,調用還原動畫,重新將UI窗口移除游戲界面,並設置第一個小鳥為激活狀態,並顯示暫停按鈕;
接下來定義Home方法和Retry方法,分別實現返回首頁,和重新開始該關卡的操作,這里可以調用Gamemaneger中的Home和Retry方法,不過要注意提前設置Time.timeScale = 1,以免游戲繼續暫停;
public class PausePanel : MonoBehaviour
{
private Animator anim; public GameObject button; public void Awake() { anim = GetComponent<Animator>(); } /// <summary> /// Home按鍵 /// </summary> public void Home() { Time.timeScale = 1; Gamemanager._instance.Home(); } /// <summary> /// Retry按鍵 /// </summary> public void Retry() { Time.timeScale = 1; Gamemanager._instance.RePlay(); } /// <summary> /// Pause按鍵 /// </summary> public void Pause() { anim.SetBool("isPause", true); button.SetActive(false); //暫停 if (Gamemanager._instance.birds.Count > 0) { if(Gamemanager._instance.birds[0].isReleased == false) { Gamemanager._instance.birds[0].canMove = false; } } } /// <summary> /// Resume按鍵 /// </summary> public void Resume() { Time.timeScale = 1; anim.SetBool("isPause", false); //還原 if (Gamemanager._instance.birds.Count > 0) { if (Gamemanager._instance.birds[0].isReleased == false) { Gamemanager._instance.birds[0].canMove = true; } } } /// <summary> /// pause動畫結束后調用 /// </summary> public void PauseAnimEnd() { Time.timeScale = 0; } /// <summary> /// resume動畫結束后調用 /// </summary> public void ResumeAnimEnd() { button.SetActive(true); } }
四、場景間的組合搭配
一個游戲場景由以下幾個模塊構成:
Main Camera 主攝像機
Player 玩家模塊,包括彈弓、當前小鳥、准備小鳥
Enemy 敵人模塊,包括所有敵方單位:小豬、木塊等障礙物
env 游戲背景:背景圖片,地面,草叢
Cavas UI畫布,所有的UI組件都放在這里,用於管理UI的所有操作
UICamera UI攝像機,查看UI鏡頭
Gamemenager 游戲管理器,掛載Gamemaneger腳本,管理幾乎所有的游戲邏輯與功能模塊
五、游戲的發布
關於游戲的發布,首先在項目設置里,設置你的游戲畫面、游戲圖標、鼠標圖標等設置
然后進入Build Settings,設置需要發布的場景畫面Scenes,根據需求,在不同的平台上發布游戲,在這里我選擇發布的平台是Windows和Android(安卓發布,需要設置例如jdk、sdk,ndk等環境配置)
最后點擊Build,鐺鐺,大功告成~
總結
以上就是一個Unity入門菜雞開發的入門級2D游戲項目,本文主要用於自己學習總結,如有不對的地方望各位大佬指正。
下面是已發布成功的游戲本體:
PC和安卓:https://pan.baidu.com/s/1Q6SH-YWx7u4qF5DFDjOVqw 提取碼:9xpx
安卓(藍奏雲):https://www.lanzouw.com/b02oe9adi 密碼:2zp4