Unity Game Development

第8&9話 實用UI、場景切換與管理

Lecturer:水獺

目錄(可點)

本堂課內容

本堂課內容

遊戲開始UI

今天所有的操作

皆由上星期的專案繼續

新建一個Scene

「Scene」,也就是場景,是個我們非常熟悉的東西

在開啟一個Unity專案的時候

Unity Editor會自動生成一個「Sample Scene」

你可以直接在裡面做開發

但當我們要做一些完全不同的遊戲畫面時

新增不同的Scene來管理是一個比較明智的方式

Canvas

「Canvas」,畫布,是Unity所有UI遊戲物件的基礎

所有的UI物件都要被放在Canvas裡

作為他的子物件才能夠運作

在Hierarchy裡按右鍵 --> UI --> Canvas創建一個新的Canvas,命名Start

Canvas

Editor會同時創建一個Event System物件

那跟後面的互動部分(按鈕、輸入)相關

點到Canvas物件

你會發現他非常大

我們要來調整一些設定

原本的編輯區

Canvas

Canvas

看到右邊Inspector裡的Canvas Scaler組件

裡面有一個UI Scale Mode

這會影響到的是Canvas的大小如何訂定

以像素為單位訂定的固定大小,除非調整Canvas組件設定否則無法變動

符合螢幕大小,可指定螢幕的大小數值,通常用這個

沒用過,但一律不建議固定

改成Scale With Screen Size

Canvas

把Reference Resolution改成1920*1080(Full HD)

Game Panel的顯示比例也要記得改16:9(原本是Free Aspect)才不會跑掉

Canvas

之後他就會長這樣:

如果想預覽編輯後的結果可以按到Game Panel去檢視~

Image

對Start Canvas按右鍵選UI --> Image新增一個Image物件

裡面的Image組件有點類似一般的Sprite Renderer

可以理解為UI裡的Sprite Renderer

上面的Rect Transform也可以理解成一般Transform的進階版

Image

長寬,因為是背景所以設成跟螢幕一樣大(1920*1080)

位置

中心點(一個藍色圓圈,可以跟父物件的中心點對在一起,預設在中心,也可以影響座標基準)

錨點(不用改,可以影響座標基準點)

旋轉

大小

圖片,可以理解成一般的Sprite Renderer放圖像資源的地方

沒有圖片時的純色填充

材質

可否被Mask蓋住

Image

長寬,因為是背景所以設成跟螢幕一樣大(1920*1080)

位置

中心點(一個藍色圓圈,可以跟父物件的中心點對在一起,預設在中心,也可以影響座標基準)

錨點(不用改,可以影響座標基準點)

旋轉

大小

圖片,可以理解成一般的Sprite Renderer放圖像資源的地方

沒有圖片時的純色填充

材質

可否被Mask蓋住

點一下上面的錨點圖可以用GUI改

Image

目前長這樣

TextMeshPro

這是Unity新版的文字

舊版的可以改的東西比較少,同時也可能比較快被移除或成為過時組件

所以本堂課以新版為例

在Start Canvas上點右鍵 --> UI --> Text - TextMeshPro

創建一個新的TextMeshPro物件

TextMeshPro

之後可能會出現這個視窗

按Import TMP Essentials

等他跑一下之後 下面那個Extras也可以新增一下

TextMeshPro

右邊Inspector的上面一樣是Rect Transform和Canvas Renderer兩個組件,不管它們

我們看到下方的TextMeshPro - Text (UI)組件

文字,預設New Text

各種預設好的文字樣式,可以自己試試

字型,等等教你們怎麼新增

材質

文字樣式,由左到右:粗體、斜體、下劃線、刪除線、小寫、大寫、小寫用大寫表示但矮一點(w我不會形容,自己試試看)

文字大小

是否根據目前文字框長寬調整文字大小至完全符合換行

TextMeshPro

接續上一頁

文字顏色

可以調整字元之間、單字之間、行之間跟段落之間的間隔大小

對齊選項

啟用/停用自動換行

當超出大小後的顯示模式,預設顯示,也可以調遮住等等

TextMeshPro

調整文字、文字框長寬、位置和文字大小之後大概會長這樣

TextMeshPro

字體好醜,我們來自定字體吧

首先下載你想使用的字體

並且把字體檔(.ttf)放進Assets/TextMesh Pro/Fonts裡面

我是用粉圓體

TextMeshPro

接著對字體檔點右鍵

選Create --> TextMeshPro --> Font Asset

TextMeshPro

再來就可以點到剛剛的TextMeshPro物件

把Font Asset拖進TextMeshPro - Text(UI)組件裡的Font Asset欄位

字體就定義好了

Button

最後我們來做開始按鈕

順便把後面的商店按鈕也做一做

Start Canvas上點右鍵之後Create --> UI --> Button - TextMeshPro

創建一個帶有TextMeshPro文字物件的按鈕

他特小

Button

物件旁邊有個箭頭可以下拉

有TextMeshPro物件可以調整按鈕上的文字

先來看一下Image組件

上面的Source Image可以改變按鈕的背景圖

Button

接著是Button組件

按鈕互動時會變換顏色的圖(預設是按鈕背景)

一般狀況下的顏色

滑鼠移動到上面的顏色

按下瞬間的顏色

按下後還沒點擊其他地方的顏色

禁用(不可點擊)時的顏色

漸變時長

點擊時的動作

Button

先來講個觀念

大家可以點左上角File --> Build Settings

這裡是調整各種輸出遊戲的設定的地方

不管是遊戲平台、全螢幕設定或要輸出的Scene之類的都可以調整

現在我們要看的是上面的Scenes In Build

如果這裡只有上個禮拜的Scene而沒有今天的Start的話

我們可以點右下角的Add Open Scene把現在正在編輯的Scene加進來

Button

好了之後會長這樣

看到右邊的數字了嗎?

那叫Build Index,也就是Unity Editor給要被輸出的Scene的編號

現在遊戲畫面的Index是0,開始Scene是1

等等我們就要利用這個數字來取得要變換到的Scene

Button

來寫程式吧!

這次的程式很簡單

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement; //記得加這個才能對Scene做操作

public class StartMenu : MonoBehaviour
{
    public void ChangeScene() //記得加public不然等等在加按鈕事件的時候讀不到
    {
        //用LoadScene(buildIndex)來更換目前顯示的Scene
        SceneManager.LoadScene(0); 
    }
}

用一個函式就可以換Scene了!

Button

不過要讓按鈕能在備按下時執行腳本的步驟有點複雜

大家可以先注意聽,等等再操作

首先是腳本要加在哪個遊戲物件上

其實都可以

但是因為這個腳本是針對開始介面的

所以我會習慣把它放在Start Canvas上

Button

Button

接下來進到Button組件裡面

找到On Click()列表

按「+」加入一個事件

Button

會長這樣

附帶有按鈕按下後要執行的函式的遊戲物件

我們剛把換場景的腳本放在誰身上?

Start Canvas!

Button

把Start Canvas拖進來之後

這裡就是按下按鈕之後要執行的事情

有幾種常用的類別

Button

如果你想讓Start Canvas在按下按鈕後消失(停用)

就將事件設為 Canvas --> bool enabled

並且把值設為false(框框不打勾)

按鈕按下後就會把Start Canvas的Canvas組件設為停用

也就不會顯示所有在Start Canvas中的物件了

適用於所有組件,也可以丟不同的物件進來

不打勾

Button

如果你想讓整個GameObject被停用

則要把事件設為GameObject --> SetActive (bool)

並且把值設為false(框框不打勾)

按鈕按下後就會把整個GameObject設為停用

適用於所有遊戲物件

不打勾

Button

如果你想讓改一個GameObject的名稱

那就要把事件設成GameObject --> string name

並且把值設為你想改成的名字

按下按鈕後名稱即會被變更

適用於所有遊戲物件

Button

還有其他很多,比如整數型的layer、GameObject的SetParent以及很多的內建函式等

基本上就是秉持一個原則:

如果是要修改值的,填入的都是修改後的值(要把A改成B則欄位中填B)

沒有要修改值的就是直接選擇函式就可以執行了

而我們的變更Scene已經把所有要更改的都寫在腳本裡了 所以屬於第二個

可以直接選StartMenu(或你自己的腳本名稱) --> ChangeScene()(或你自己的函式名稱)

來執行

如果你發現找不到,有可能是以下幾個問題:

  • OnClick()事件的Object區沒有放進有附帶腳本的物件
  • 腳本中的函式前方沒有加public
  • 腳本附加錯遊戲物件

Button

Button

好了之後按下按鈕,應該就可以切換Scene到遊戲畫面啦

本堂課內容

本堂課內容

血條和遊戲選單

Bossbar

加工一下遊戲

把敵人有關尋路範圍(TrackingRange)的物件和腳本拿掉或停用

讓他們一出生就會朝畫面上的玩家走去

再加一個生成敵人的腳本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyGenerator : MonoBehaviour
{
    [SerializeField] GameObject enemyPrefab; //Enemy的Prefab參考
    bool inGenerateCD = false; //生成是否已冷卻
    private void Update() {
        if(!inGenerateCD){ //如果生成已經冷卻
            StartCoroutine(generate()); //啟動生成敵人的協程(下堂會講)
        }
    }

    IEnumerator generate(){
        inGenerateCD = true; //進冷卻
        Instantiate(enemyPrefab, new Vector3(7.8f, Random.value*5,0),Quaternion.identity); //在x軸7.8、y軸隨機的地方生成一個敵人
        yield return new WaitForSecondsRealtime(Random.value*3); //進入隨機0~3秒的冷卻(等待0~3秒才執行接下來的程式)
        inGenerateCD = false; //冷卻完畢
    }
}

Bossbar

再來把遊戲角色的轉向腳本寫好並加進Player

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerRotate : MonoBehaviour
{
    Vector2 dir; //角色->滑鼠方向向量
    void Update()
    {
        dir = Camera.main.ScreenToWorldPoint(Input.mousePosition-transform.position).normalized; //取得方向向量
        transform.up = Vector2.Lerp(transform.up, dir, 0.2f); //漸變方向
    }
}

Bossbar

再來把遊戲角色的射擊腳本寫好並加進Player

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerShoot : MonoBehaviour
{
    [SerializeField] GameObject bulletPrefab; //子彈Prefab參考
    bool inCD = false;
    void Update()
    {
        if(Input.GetMouseButtonDown(0) && !inCD){ //按下左鍵時
            Instantiate(bulletPrefab,transform.position,transform.rotation); //涉及
            StartCoroutine(CD()); //冷卻協程
        }
    }

    IEnumerator CD(){
        inCD = true; //進CD
        yield return new WaitForSecondsRealtime(0.5f); //等待0.5秒
        inCD = false; //CD結束
    }
}

Bossbar

再來把遊戲角色的射擊腳本寫好並加進Player

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerShoot : MonoBehaviour
{
    [SerializeField] GameObject bulletPrefab; //子彈Prefab參考
    bool inCD = false;
    void Update()
    {
        if(Input.GetMouseButtonDown(0) && !inCD){ //按下左鍵及不在CD時
            Instantiate(bulletPrefab,transform.position,transform.rotation); //涉及
            StartCoroutine(CD()); //冷卻協程
        }
    }

    IEnumerator CD(){
        inCD = true; //進CD
        yield return new WaitForSecondsRealtime(0.1f); //等待0.1秒
        inCD = false; //CD結束
    }
}

Bossbar

新建一顆圓形子彈

加入Collider2D、Rigidbody2D並調整大小

之後寫一個子彈的腳本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour
{
    float speed = 5; //子彈速度
    [SerializeField] Rigidbody2D rb2D; //子彈剛體參考
    void Start() //被生成時
    {
        StartCoroutine(Delete()); //啟動協程,4秒後刪除自己
    }

    private void Update() {
        rb2D.velocity = transform.up * speed; //指定剛體速度讓子彈飛一會
    }

    IEnumerator Delete(){
        yield return new WaitForSecondsRealtime(4); //等待四秒
        Destroy(gameObject); //刪除自己
    }

    private void OnCollisionEnter2D(Collision2D col) {
        if(col.gameObject.CompareTag("Obstacle")){ //如果碰到障礙物
            Destroy(gameObject); //刪除自己
        }
        else if(col.gameObject.CompareTag("Enemy")){ //碰到敵人
            Destroy(gameObject); //刪除自己
        }    
    }
}

Bossbar

再來我們安排一下畫面

新增一個Image當底

在上面點右鍵之後新增另一個Image當空的血量(會是子物件)

大概長這樣

Bossbar

接著再新增一個Image在空血量上面(會是子物件)

當成血量條

Bossbar

接下來非常重要的一步

我們要把血量條的Pivot(中心點)的X座標從0.5改成0

這樣在縮放的時候才會向左邊縮

而不會向中間縮

Bossbar

再來是最後的兩個程式

首先是敵人生成器加入總血量計算和血量條長度更新

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyGenerator : MonoBehaviour
{
    int generateLimit = 50;

    public int totalBlood; //新增總血量

    public int totalBloodtmp; //新增總血量初始值(不會變)

    [SerializeField] GameObject enemyPrefab; //Enemy的Prefab參考

    [SerializeField] GameObject bossbar; //血量條參考

    bool inGenerateCD = false; //生成是否已冷卻

    float bossbarStartLength; //初始血條長度
    
    private void Start() {
        totalBlood = enemyPrefab.GetComponent<EnemyNoRange>().blood * generateLimit; //從EnemyPrefab的Enemy腳本取得單隻血量並更新總血量
        totalBloodtmp = enemyPrefab.GetComponent<EnemyNoRange>().blood * generateLimit; //從EnemyPrefab的Enemy腳本取得單隻血量並更新總血量初始值
        bossbarStartLength = bossbar.GetComponent<RectTransform>().localScale.x; //初始血條長度
    }

    private void Update() {
        if(!inGenerateCD && generateLimit != 0){ //如果生成已經冷卻
            StartCoroutine(generate()); //啟動生成敵人的協程(下堂會講)
            generateLimit --; 
        }
        //新增:修改bossbar的scale的寬(x),高(y)不改,寬變成寬度初始值 * 目前總血量 / 總血量初始值
        bossbar.GetComponent<RectTransform>().localScale = new Vector2(bossbarStartLength * totalBlood/totalBloodtmp,bossbar.GetComponent<RectTransform>().localScale.y);
    }

    IEnumerator generate(){
        inGenerateCD = true; //進冷卻
        Instantiate(enemyPrefab, new Vector3(7.8f, Random.value*5,0),Quaternion.identity); //在x軸7.8、y軸隨機的地方生成一個敵人
        yield return new WaitForSecondsRealtime(Random.value*3); //進入隨機0~3秒的冷卻(等待0~3秒才執行接下來的程式)
        inGenerateCD = false; //冷卻完畢
    }
}

Bossbar

然後敵人程式加入被打到的扣血和沒血消失

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class EnemyNoRange : MonoBehaviour
{
    public int blood = 9; //敵人血量
    public int bloodMinus = 3; //敵人被打到的損血量
    public GameObject Player; //被追蹤的Transform
    NavMeshAgent agent; //一個叫NavMeshAgent的腳本,是尋路物件的核心Component
    
    GameObject enemyGenerator; 
    private void Start() {
        agent = GetComponent<NavMeshAgent>(); //取得參考
        agent.updateRotation = false; //不轉Sprite方向
        agent.updateUpAxis = false; //不更新朝上軸(上方永遠為Z軸正向)
        agent.speed = 1;
        enemyGenerator = GameObject.Find("GameManager"); //取得敵人生成器參考
    }

    private void Update() {
        Player = GameObject.FindGameObjectWithTag("Player");
        if(Player != null){
            agent.SetDestination(Player.transform.position); //如果在追蹤狀態就一直把尋路終點設為被追蹤者的位置
        }
        else{
            agent.SetDestination(transform.position); //如果不在追蹤狀態就把尋路終點設為自己的位置
        }
    }
    private void OnCollisionEnter2D(Collision2D col) {
        if(col.gameObject.CompareTag("Player")){ //如果碰到被追蹤者
            Destroy(col.gameObject); //就摧毀
        }
        
        if(col.gameObject.CompareTag("Bullet")){ //新增:如果碰到子彈
            blood -= bloodMinus; //扣血
            enemyGenerator.GetComponent<EnemyGenerator>().totalBlood -= bloodMinus; //更新目前總血量
        }
        if(blood == 0){ //如果沒血
            Destroy(gameObject); //刪除自己
        }
    }
}

Bossbar

就結束了

再來將剛剛有寫SerializeField的遊戲物件全部填充好基本上就可以了

沒填充好一定會報錯

可以自己檢查看看

真的有問題可以舉手問我

血條

可以自己練習看看

基本上跟剛剛的bossbar是相同的概念

只不過血條要放在玩家/敵人的頭上才會跟著他們

並且可以用空物件+SpriteRenderer來代替Image

因為它一定要Canvas才能運作,對於會移動的角色來說有點麻煩

像醬

暫停/遊戲結束

當玩家生命歸零或死掉

則遊戲結束

這邊以玩家Hardcore來示範

暫停/遊戲結束

很簡單

在敵人腳本裡加上一個Canvas參考

一般情況停用

在玩家死掉時啟用即可

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class Enemy : MonoBehaviour
{   
    public bool Tracking = false; //是否在尋路狀態
    public Transform Player; //被追蹤的Transform
    NavMeshAgent agent; //一個叫NavMeshAgent的腳本,是尋路物件的核心Component
   
    [SerializeField] Canvas GameOverCanvas; //遊戲結束Canvas參考
    private void Start() {
        agent = GetComponent<NavMeshAgent>(); //取得參考
        agent.updateRotation = false; //不轉Sprite方向
        agent.updateUpAxis = false; //不更新朝上軸(上方永遠為Z軸正向)
    }

    private void Update() {
        if(Tracking){
            agent.SetDestination(Player.position); //如果在追蹤狀態就一直把尋路終點設為被追蹤者的位置
        }
        else{
            agent.SetDestination(transform.position); //如果不在追蹤狀態就把尋路終點設為自己的位置
        }
    }
    private void OnCollisionEnter2D(Collision2D col) {
        if(col.gameObject.CompareTag("Player")){ //如果碰到被追蹤者就摧毀
            Tracking = false;
            Destroy(col.gameObject);
        }
        GameOverCanvas.enabled = true; //新增:啟用遊戲結束Canvas
    }
}

暫停/遊戲結束

之後把Canvas放入參考框

啟動遊戲之後應該就可以ㄌ(記得先把Canvas停用)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class Enemy : MonoBehaviour
{   
    public bool Tracking = false; //是否在尋路狀態
    public Transform Player; //被追蹤的Transform
    NavMeshAgent agent; //一個叫NavMeshAgent的腳本,是尋路物件的核心Component
   
    [SerializeField] Canvas GameOverCanvas; //遊戲結束Canvas參考
    private void Start() {
        agent = GetComponent<NavMeshAgent>(); //取得參考
        agent.updateRotation = false; //不轉Sprite方向
        agent.updateUpAxis = false; //不更新朝上軸(上方永遠為Z軸正向)
    }

    private void Update() {
        if(Tracking){
            agent.SetDestination(Player.position); //如果在追蹤狀態就一直把尋路終點設為被追蹤者的位置
        }
        else{
            agent.SetDestination(transform.position); //如果不在追蹤狀態就把尋路終點設為自己的位置
        }
    }
    private void OnCollisionEnter2D(Collision2D col) {
        if(col.gameObject.CompareTag("Player")){ //如果碰到被追蹤者就摧毀
            Tracking = false;
            Destroy(col.gameObject);
        }
        GameOverCanvas.enabled = true; //啟用遊戲結束Canvas
    }
}

暫停/遊戲結束

也可以自己加入一些比如重新開始等等的功能

如果有時間我會示範

暫停畫面則是寫一個暫停按鍵偵測

並且把暫停Canvas叫出來

裡面也可以放Button跳回遊戲

等等有時間也會示範

本堂課內容

本堂課內容

商店UI

用舊遊戲來舉例

貨幣

商店標題

商品名稱

商品價格

商品圖示

購買按鈕

裝備欄(今天不會教)

回主畫面按鈕

商品展示框

畫面滾動

如果你的商品無法在一個畫面中顯示

那就需要畫面滾動來讓玩家能瀏覽所有商品

現在我們就要來實做這個功能

*以下內容有關Canvas、Image、TextMeshPro及Button的內容請見前兩小節*

*本堂課只教橫向滾動,有需要直向滾動的可以自己研究看看,不行再問我*

畫面滾動

新建一個Scene當成商店的場景

畫面滾動

快速點兩下打開空白的Scene、新建一個Canvas

然後新建(全部都是UI):

一個TextMeshPro當標題

一個Image + 一個TextMeshPro當貨幣計數的Icon以及數值

一個按鈕當回主畫面的按鈕(可以把造型改成三角形之類的)

好了之後大概長醬

畫面滾動

再來新建一個Image

把他的寬度調整到跟螢幕兩側只剩一點空隙

高度Depends on you

寬高會是到時候展示商品的範圍

好了長醬

畫面滾動

接著在Image物件上Add Component

新增一個Scroll Rect跟一個Rect Mask 2D

要被滾動的子物件

是否允許水平or垂直滾動

被滾動子物件移動模式,

分為Unstricted、Elastic及Clamped三種模式

其中Elastic及Clamped會將要被滾動的子物件

限制在父物件Image中,

差別在於Elastic在超出畫面時

會有彈簧的效果將子物件拉回父物件範圍,

而Clamped沒有;

Unstricted則允許子物件亂跑。

Elastic彈簧強度

是否啟用運動慣性(在滑鼠滾輪停止轉動停下來之後子物件還會持續運動一段時間)

慣性縮減率(建議0 < this < 1,越接近0則子物件繼續移動的距離越短)

滾動敏感度(越大則滾輪轉動或滑鼠拖曳一單位的距離會讓子物件移動的越多)

當滾動時要執行的東西(加入的方式跟Button一樣)

畫面滾動

接著在Image物件上Add Component

新增一個Scroll Rect跟一個Rect Mask 2D

我的設定

畫面滾動

接著在Image物件上Add Component

新增一個Scroll Rect跟一個Rect Mask 2D

四邊內距

X、Y方向柔邊

畫面滾動

再來對Image點右鍵新增一個空物件

把高調整到跟Image一樣,寬Depends on you(大於Image寬度才會有滾動效果)

寬高會是全部商品一字排開的大小

Add Component加入一個Horizontal Layout Group

這個組件可以幫我們做等間距的水平排版

四邊內距

子物件之間的間距大小

子物件相對於這個物件的對齊位置

是否啟用子物件倒序排列

是否讓排版組件控制子物件長寬

排版組件縮放時是否影響子排版組件之遊戲物件

是否強制子物件延伸以填充空白處

這我的設定

畫面滾動

最後把剛剛的空物件拖到Image Scroll Rect組件裡的「Content」

這部分就基本上完成了

物品展示

這是我這次的展示框Prefab

底圖是用Inkscape畫的

建議先在Scene中的Canvas底下拖好一個之後

再整個拖曳到Project做成Prefab不然會有點麻煩

商品名稱,Tag為Name

商品圖片

商品價目,Tag為Price

購買按鈕,文字Tag為Status

物品展示

做成Prefab之後你就可以對他做以下任意操作

  1. 一個一個新增商品再丟回Scene裡(這我上次)
  2. 用程式更改Prefab實例化出來的遊戲物件資訊(這我這次)

因為我發現如果要做到購買的功能一定要把商品資訊整理好

所以就加了一點東西達成第二個操作

程式碼如下

物品展示

商品資訊初始化(GoodsManager.cs)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GoodsManager : MonoBehaviour
{
    public static bool spend = false; //花錢狀態機(連到CoinsManager)

    public static string equipped_skin = "NS"; //目前裝備的造型名稱縮寫(因為使用static所以為唯一,等等會用)

    //一個Key為商品名稱縮寫、Value為「多個Key為商品資訊(含價格、名稱、購買&裝備狀態、類別及稀有度)、Value為資訊值的字典』的字典,用來存所有的商品資訊
    public static Dictionary<string,Dictionary<string,string>> goods = new Dictionary<string,Dictionary<string,string>>(){};

    //一個依照商品展示順序左到右存名稱縮寫的陣列
    public static List<string> names = new List<string>(){"NS","T1","T2","T3","EM"};

    //一個依照商品展示順序左到右存價格的陣列
    public static List<string> prices = new List<string>(){"000","010","020","030","040"};

    //一個依照商品展示順序左到右存名稱的陣列
    public static List<string> realName = new List<string>(){"Normal\nSkin","Test1","Test2","Test3","Evil\nMoon"};

    //一個依照商品展示順序左到右存購買&裝備狀態的陣列(0 = 尚未購買;1 = 已購買未裝備;2 = 已裝備)
    public static List<int> statuses = new List<int>(){2,0,0,0,0};

    //一個依照商品展示順序左到右存類別的陣列(Skin = 造型or皮膚)
    public static List<string> roles = new List<string>(){"Skin","Skin","Skin","Skin","Skin"};

    //一個依照商品展示順序左到右存稀有度的陣列(0 = 普通;1 = 稀有;2 = 史詩;3 = 神話;4 = 傳奇)
    public static List<int> rarities = new List<int>(){0,1,2,3,4};
    
    void Awake()
    {
        for(int i = 0;i < names.Count;i ++){
            Dictionary<string,string> good_info_temp = new Dictionary<string, string>() {}; //建立一個空字典
            
            //將價格、名稱、購買&裝備狀態、類別及稀有度等五個資訊加入字典
            good_info_temp.Add("Price",prices[i]); 
            good_info_temp.Add("RealName",realName[i]);
            good_info_temp.Add("Status",statuses[i].ToString());
            good_info_temp.Add("Role",roles[i]);
            good_info_temp.Add("Rarity",rarities[i].ToString());

            goods.Add(names[i],good_info_temp); //將字典加入商品資訊的大字典裡
        }
    }
}

物品展示

進入商店展示商品(StoreManager.cs)

using System.Collections.Generic;
using System.Globalization;
using UnityEngine;
using TMPro;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class StoreManager : MonoBehaviour
{
    [SerializeField] GameObject showcasePrefab;
    private void Start() {
        List<string> names = GoodsManager.names; //商品名稱縮寫

        Dictionary<string,Dictionary<string,string>> goods = GoodsManager.goods; //商品資訊

        List<string> statusText = new List<string>(){"Buy it!","Equip!","Unequip!"}; //三個按鈕文字狀態

        List<string> colorCodes = new List<string>(){"9FFF8A","8BEEFF","9C4CFF","FFE73D","FF3D43"}; //五個稀有度對應的顏色色碼

        for (int i = 0;i < names.Count;i++){
            //這個腳本放在Canvas,我要把展示框實例化在Canvas/Image/ShowcaseHorizontal裡,所以用兩次GetChild向下讀兩層
            GameObject showcase = Instantiate(showcasePrefab,transform.GetChild(0).transform.GetChild(0).transform); 

            showcase.name = names[i]; //改實例化的物件名稱
            string colorCode = colorCodes[int.Parse(goods[names[i]]["Rarity"])]; //讀色碼
            //按照色碼改展示框Image的顏色(原本是白色,覆蓋顏色上去就有不同顏色了~)
            showcase.GetComponent<Image>().color = new Color(int.Parse(colorCode.Substring(0,2),NumberStyles.HexNumber)/255f,int.Parse(colorCode.Substring(2,2),NumberStyles.HexNumber)/255f,int.Parse(colorCode.Substring(4,2),NumberStyles.HexNumber)/255f);
            
            RectTransform[] children = showcase.GetComponentsInChildren<RectTransform>(); //取得展示框內所有物件的RectTransform
            
            List<string> columns = new List<string>(){"RealName","Body","Head","Price","Status"}; //由上到下所需更改的子物件Tag
            
            int count = 0; //計數,共5個
            foreach(RectTransform gO in children){ //遍歷所有子物件
                if(gO.gameObject.tag == columns[count]){ //如果子物件的Tag跟list輪到的Tag一樣的化
                    TextMeshProUGUI Tmp = gO.gameObject.GetComponent<TextMeshProUGUI>(); //取得TMP參考
                    Image IMG = gO.gameObject.GetComponent<Image>(); //取得Image參考

                    if(Tmp != null){ //如果有TMP組件代表是文字
                        if(gO.parent.TryGetComponent(out Button button)){ //若嘗試取得Button參考成功
                            Tmp.SetText(statusText[int.Parse(goods[names[i]][gO.tag])]); //更新Status
                            //更新Button(文字物件的父物件)的顏色
                            gO.parent.GetComponent<Image>().color = new Color(int.Parse(colorCode.Substring(0,2),NumberStyles.HexNumber)/255f,int.Parse(colorCode.Substring(2,2),NumberStyles.HexNumber)/255f,int.Parse(colorCode.Substring(4,2),NumberStyles.HexNumber)/255f);
                            count --;
                            continue;
                        }
                        Tmp.SetText(goods[names[i]][gO.tag]); //沒有button組件則為其他文字
                    }
                    else{
                        //沒有TMP則為圖片,這裡用到一個小技巧,只要在Project裡新增一個Resources資料夾(跟Assets同層級)
                        //再將資源丟入這個資料夾,就可以用「Resources.Load<類型>(相對路徑)來讀取資源
                        //如果你有一個資料夾Folder,路徑是.../Resources/Folder,相對路徑指的是「Folder」,連/都不用
                        IMG.sprite = Resources.Load<Sprite>("Skin/"+names[i]+"/"+columns[count]); 
                    }
                    count ++;
                }
            }
        }
    }

    public void BackToStartMenu(){ //回主畫面
        if(GoodsManager.equipped_skin != ""){ //不可沒裝備造型
            SceneManager.LoadScene("StartScene"); //以Scene Name切換Scene
        }
    }
}

物品展示

不管你用哪種方式

商店運行之後應該都要長類似這樣

並且可以左右拖動

購買

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;

public class BuyAndEquipSkin : MonoBehaviour //放在展示框的購買按鈕上
{
    private string goods_name;
    private int price;
    void Start(){
        goods_name = gameObject.transform.parent.name; //取得展示框名稱,也就是該商品名稱縮寫
        price = int.Parse(GoodsManager.goods[goods_name]["Price"]); //取得商品價格
        //如果Status是0就不用變,初始為Buy it!
        if(int.Parse(GoodsManager.goods[goods_name]["Status"]) == 1){ //如果Status是1則將按鈕文字改成Equip!
            TextMeshProUGUI text = gameObject.transform.GetComponentInChildren<TextMeshProUGUI>();
            text.SetText("Equip!");
        }
        else if(int.Parse(GoodsManager.goods[goods_name]["Status"]) == 2){ //如果Status是2則將按鈕文字改成Unequip!
            TextMeshProUGUI text = gameObject.transform.GetComponentInChildren<TextMeshProUGUI>();
            text.SetText("Unequip!");
        }
    }
    public void OnClick(){ //當按鈕被按下
        //判斷目前的前夠不夠及此商品是否已被購買
        if(CoinsManager.Coins >= price && int.Parse(GoodsManager.goods[goods_name]["Status"]) == 0){ //若錢夠且未被購買
            GoodsManager.goods[goods_name]["Status"] = "1"; //購買狀態
            TextMeshProUGUI text = gameObject.transform.GetComponentInChildren<TextMeshProUGUI>(); //改按鈕文字
            text.SetText("Equip!");
            CoinsManager.Coins -= price; //花錢
            GoodsManager.spend = true; //花錢狀態機更新
        }
    }
}

裝備

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;

public class BuyAndEquipSkin : MonoBehaviour //放在展示框的購買按鈕上
{
    private string goods_name;
    private int price;
    void Start(){
        goods_name = gameObject.transform.parent.name; //取得展示框名稱,也就是該商品名稱縮寫
        price = int.Parse(GoodsManager.goods[goods_name]["Price"]); //取得商品價格
        //如果Status是0就不用變,初始為Buy it!
        if(int.Parse(GoodsManager.goods[goods_name]["Status"]) == 1){ //如果Status是1則將按鈕文字改成Equip!
            TextMeshProUGUI text = gameObject.transform.GetComponentInChildren<TextMeshProUGUI>();
            text.SetText("Equip!");
        }
        else if(int.Parse(GoodsManager.goods[goods_name]["Status"]) == 2){ //如果Status是2則將按鈕文字改成Unequip!
            TextMeshProUGUI text = gameObject.transform.GetComponentInChildren<TextMeshProUGUI>();
            text.SetText("Unequip!");
        }
    }
    public void OnClick(){ //當按鈕被按下
        //判斷目前的錢夠不夠及此商品是否已被購買
        if(CoinsManager.Coins >= price && int.Parse(GoodsManager.goods[goods_name]["Status"]) == 0){ //若錢夠且未被購買
            GoodsManager.goods[goods_name]["Status"] = "1"; //購買&未裝備狀態
            TextMeshProUGUI text = gameObject.transform.GetComponentInChildren<TextMeshProUGUI>(); //更新按鈕文字
            text.SetText("Equip!");
            CoinsManager.Coins -= price; //花錢
            GoodsManager.spend = true; //花錢狀態機更新
        }
        //新增
        else if(int.Parse(GoodsManager.goods[goods_name]["Status"]) == 1 && GoodsManager.equipped_skin == ""){ //若已被購買但未被裝備且目前沒裝備
            GoodsManager.goods[goods_name]["Status"] = "2"; //裝備狀態
            GoodsManager.equipped_skin = goods_name; //更新裝備造型名稱縮寫
            TextMeshProUGUI text = gameObject.transform.GetComponentInChildren<TextMeshProUGUI>(); //更新按鈕文字
            text.SetText("Unequip!");
        }
        else if(int.Parse(GoodsManager.goods[goods_name]["Status"]) == 2){ //若已被裝備
            GoodsManager.goods[goods_name]["Status"] = "1"; //未裝備狀態
            GoodsManager.equipped_skin = ""; //更新裝備造型名稱縮寫為無
            TextMeshProUGUI text = gameObject.transform.GetComponentInChildren<TextMeshProUGUI>(); //更新按鈕文字
            text.SetText("Equip!");
        }
    }
}

裝備

之後就把腳本放到展示框Prefab的按鈕上

再將剛剛寫的OnClick函式加入當按鈕被按下時執行的清單後就完成啦

(忘記去看前兩小節)

裝備

至於如何實現裝備的功能就交給大家自己研究ㄌ

本堂課的範疇只在UI的部分w

造型的部分應該算簡單

提示是Resources以及SpriteRenderer

當然有問題的話可以問我

如果對更進階的裝備方式

如技能以及前面的裝備欄等等 有興趣也可以來找我

本堂課內容

本堂課內容

協程

如果你需要做物件定時消失、定時生成、技能冷卻或倒數計時等等的功能

當然可以選擇用「數值-Time.deltaTime」來倒數

但缺點是每計一次時就要開一個變數

而且Time.deltaTime是一個你無法掌控的小數

有沒有什麼東西是我們容易控制而且可以同時用一打多的?

Coroutine!

協程

void coroutine(){
	//sth
    StartCoroutine(ExampleCoroutine()); //開始協程
}

IEnumerator ExampleCoroutine(){ //協程回傳的為一個IEnumerator物件,可以理解為「反覆計算」
	//sth before waiting...,在等待前執行的東西
    
    //yield在等待second秒之後發出反覆運算結束的訊號,WaitForSecond會受到Time.Scale的影響導致快慢
    yield return new WaitForSeconds(second); 
    //or,WaitForSecondRealtime不會受到Time.Scale的影響,時間跟現實世界一致
    yield return new WaitForSecondsRealtime(second);
    //sth after waiting...,在等待後執行的東西
}

基本架構

協程

IEnumerator QS_s(){
    Destroy(gameObject.GetComponent<PolygonCollider2D>()); //刪除碰撞器
    yield return new WaitForSecondsRealtime(10);
    gameObject.AddComponent<PolygonCollider2D>(); //加回碰撞器
}

例一、無敵一段時間(玩家or敵人)

例二、特定時間生成(食物、寶物or敵人)

IEnumerator EO_s(float sec){ //傳入等待時間參數
    yield return new WaitForSecondsRealtime(sec); //等待一段時間
    Instantiate(Wave,transform.position,transform.rotation); //生成
}

例三、定時統計(上線人數、生命值等)

IEnumerator ConnectionCount(){
    while(true){
        yield return new WaitForSecondsRealtime(2f);
        Debug.Log(NetworkManager.Singleton.ConnectedClients.Count);
    }
}

協程

IEnumerator SpawnOverTime(){
    while(true){ //無限迴圈
        yield return new WaitForSecondsRealtime(Random.Range(1,5)); //等待隨機取數1~5秒
        //進階多人連線語法不用管,基本上就是判斷場上的物件數量並生成
        if(NetworkManager.Singleton.ConnectedClients.Count > 0 && NetworkObjectPool.Singleton.Active(prefab) < MaxPrefabCount){
            SpawnFood();
        }     
    }
}

例四、隨機間隔生成(食物、寶物等)

例五、等待一段時間後摧毀(子彈等)

IEnumerator Example(float sec){ //傳入等待時間參數
    yield return new WaitForSecondsRealtime(sec); //等待一段時間
    Destroy(gameObject) //摧毀物件
}

以上是一些簡單應用,這些東西都可以依照

你的遊戲所需的功能做擴充和調整

Q&A&DO

建北電資Unity遊戲開發小社第8and9堂

By MH Yang

建北電資Unity遊戲開發小社第8and9堂

  • 85