iT邦幫忙

2024 iThome 鐵人賽

DAY 19
0
自我挑戰組

Unity入門遊戲開發與實作系列 第 19

【Day 19】製作一個簡單 Unity 小遊戲(八)GameOver 和 Restart 機制

  • 分享至 

  • xImage
  •  

昨天,我們加上了 UI 還有一些微調,讓遊戲畫面變得更精緻一點點。今天我們要來製作 Game Over 的機制,也就是當玩家撞到障礙物或者是從賽道外墜落,意味著遊戲結束,我們要讓玩家重新回到起始點。

我們需要再建立一個腳本來負責控制所有有關遊戲開始和遊戲結束,而這個腳本並沒有要套用到場景中的任何物體上,所以我們可以建立一個空物件,並將新的腳本套用到該空物件上,這樣程式才有辦法運作。
Hierarchy 中右鍵 -> Create Empty ,命名這個空物件為 GameManager 。並且再創立一個腳本,也把他命名為 GameManager.cs ,接著把這個腳本套用到空物件上,我們就可以來撰寫我們的程式了!

// GameManager.cs
using UnityEngine;

public class GameManager : MonoBehaviour
{
    public void EndGame()
    {
		Debug.Log("Game Over");
    }
}

當我們碰到物體時,只需要呼叫這個腳本裡面的 EndGame() 就可以了,我們先初步的輸出 Game Over 就好,接著要回到 PlayerCollision.cs ,並且加入呼叫的程式碼 FindObjectOfType<>

using UnityEngine;
public class PlayerCollision : MonoBehaviour
{
    public PlayerMovement movement;

    void OnCollisionEnter (Collision collisionInfo)
    {
        if(collisionInfo.collider.tag == "Obstacle")
        {
            movement.enabled = false;
            FindObjectOfType<GameManager>().EndGame();
        }
    }
}

FindObjectOfType<GameManager> 會幫助我們找到 GameManager.cs 這個檔案,這種呼叫的方法比起我們之前在腳本中創立一另外一個腳本的 object 的方法更為安全。因為後者需要人們手動連結腳本到這個變數上,如果有遊戲的機制是玩家死亡之後腳色需要消失,在某個地方又重新出現,那麼這種連結腳本的方法就會出問題。例如我們先建立一個 Player 的 Prefab:將 PlayerHierarchy 面板中拖回 Prefab 資料夾
https://ithelp.ithome.com.tw/upload/images/20241003/20169301xhUNVNQPXy.png
我們開始模擬這個過程

  • 一開始 Player 之間的腳本都是我們手動連結上去的,所以一切工作正常
  • 接著模擬玩家死亡後腳色從場景中消失,所以 delete 場景中的 Player
  • 玩家又會重生在場景裡,所以我們重新將 Player 拖回場景中
  • 接下來我們開始遊戲,就會產生一大堆的錯誤訊息:
    https://ithelp.ithome.com.tw/upload/images/20241003/201693014PlRR4RE2p.png
    這是因為我們失去了腳本之間的連結,原本 Player 有建立 Main CameraText 之間的連結,但現在這些連結都因為原本的 Player 被刪除,使得相機和文字都無法獲取到正確的數值,新建立的 Plyaer 和原本的並沒有關聯。我們必須再重新把 Player 分別拖拉給 Main CameraText 的腳本,才能讓遊戲重新恢復正常。 這就是為什麼我們使用 FindObjectOfType<GameManager>
    除了和物體發生碰撞我們需要結束遊戲之外,我們還需要在玩家從側邊墜落時結束遊戲,所以我們回到 PlayerMovement.cs 加上檢測 y 軸方向的程式碼
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    public Rigidbody rb;

    public float forwardForce = 2000f;

    public float sideForce = 100f;

    // Update is called once per frame
    void FixedUpdate()
    {
        rb.AddForce(0, 0, forwardForce * Time.deltaTime);
        if ( Input.GetKey("d"))
        {
            rb.AddForce(sideForce * Time.deltaTime, 0, 0, ForceMode.VelocityChange);
        }
        if ( Input.GetKey("a") )
        {
            rb.AddForce(-sideForce * Time.deltaTime, 0, 0, ForceMode.VelocityChange);
        }
        if (rb.position.y < -1f)
        {
            FindObjectOfType<GameManager>().EndGame();
        }
    } 
}

當玩家的 y 軸座標小於 -1 的時候,我們也會呼叫這個 EndGame() 函式。
接著執行遊戲看看 Game Over 是不是都在正確的時間被呼叫:
https://ithelp.ithome.com.tw/upload/images/20241003/20169301HRdeC5bJe7.png
當玩家掉出賽道時,系統一直不斷地輸出 Game Over ,這是因為每一幀都檢測到 y 軸座標小於 -1 ,這會導致遊戲被重新啟動很多次,所以必須加入判斷機制,使得遊戲最多只會被重啟一次

// GameManager.cs
using UnityEngine;

public class GameManager : MonoBehaviour
{
	bool gameHasEnded = false;
	
    public void EndGame()
    {
		if(gameHasEnded == false)
		{
			gameHasEnded = true;
			Debug.Log("Game Over");
		}
    }
}

我們使用布林值來操控,最終不論如何,遊戲最多都只會重啟一次
https://ithelp.ithome.com.tw/upload/images/20241003/201693018HSSqNgKZT.png

不論是碰撞還是墜落,遊戲都最多都只會輸出一次的重啟訊息

接下來要來真正撰寫重新開啟遊戲的程式

// GameManager.cs
using UnityEngine;
using UnityEngine.SceneManageent;

public class GameManager : MonoBehaviour
{
	bool gameHasEnded = false;
	
    public void EndGame()
    {
		if(gameHasEnded == false)
		{
			gameHasEnded = true;
			Debug.Log("Game Over");
			Restart();
		}
    }
    void Restart()
    {
	    SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
}

我們獨立寫了一個 Restart 函式,裡面使用的 SceneManager 有需多的功能,但要記得在最上方寫上 using UnityEngine.SceneManageent; ,其中的SceneManager.LoadScene() 可以用來重新啟動場景,例如我們可以寫 SceneManager.LoadScene("level_01") 來啟動第一關的場景。而我們可能擁有很多關卡,我們需要啟動的是當前執行的場景,所以為了得到當前場景的名字,我們可以一樣使用 SceneManager 底下的功能 SceneManager.GetActiveScene().name


在實際遊玩的時候發現我們啟動場景有點太快了,一碰到東西馬上場景就重新啟動場景,所以我們要加入 Invoke

// GameManager.cs
using UnityEngine;
using UnityEngine.SceneManageent;

public class GameManager : MonoBehaviour
{
	bool gameHasEnded = false;
	public float restartDelay = 1f;
	
    public void EndGame()
    {
		if(gameHasEnded == false)
		{
			gameHasEnded = true;
			Debug.Log("Game Over");
			Invoke("Restart", restartDelay);
		}
    }
    void Restart()
    {
	    SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
}

Invoke 有兩個傳入值,分別是要執行的東西,和延遲的時間,也就是說,當檢測到遊戲結束,會再等待一秒鐘,讓玩家看清楚我們究竟是跟什麼東西發生了碰撞,然後才重啟關卡,整個過程變得流暢多了!

最後,如果在重新啟動場景的時候發現光線使得地板變得怪怪的,只要到 Lighting 面板中把下方的 Auto 框框取消打勾就好了
https://ithelp.ithome.com.tw/upload/images/20241003/20169301prCSfoXey0.png
選項如果打勾,表示 Unity 會自動偵測場景的光線變化,並且重新計算所有物體的光影效果並取代掉之前的。但是當我們重新啟動一個場景時, Unity 並沒有時間計算這些東西,所以我們可能會得到一個光線奇怪的場景,所以我們取消打勾,並按下旁邊的重新生成光影,問題就能夠得到解決!

重新執行遊戲就順暢的很多了!會自動重新開始的關卡省的每次都要按重新執行的按鈕。今天完成了遊戲重新啟動的偵測,並且修正了許多遊戲可能會有的問題。明天要來製作早該出現但消失很久的...勝利畫面!當玩家通過關卡時應該顯示的 UI 介面。


上一篇
【Day 18】製作一個簡單 Unity 小遊戲(七)數字 UI 顯示
下一篇
【Day 20】製作一個簡單 Unity 小遊戲(九)通關偵測和通關 UI 動畫
系列文
Unity入門遊戲開發與實作30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言