Unity的C#編程教程_54_靜態類型 Static Types 詳解及應用練習

C# Static Types

1.Working with Static Types

  • 所謂的 static type,使用的是 static 關鍵字

    • 比如可以有 static class,static method,或者 static variable
    • static type 存在於棧 stuck 中(記憶體),生命周期與程式運行一致
  • 任務說明:

    • 我們設計一個程式用於接收資訊,比如一堆數字,然後用程式加總
    • 假設每次按下空格鍵加 10

傳統的做法是首先新建一個 Score 的類,用於存放累加後的結果:

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

public class Score : MonoBehaviour
{

    public int num;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

這個腳本可以掛載到一個遊戲對象 Score 上面

然後再用一個腳本進行累加操作:

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

public class StaticTest : MonoBehaviour
{

    public Score score; // 實例化一個 Score 的類

    // Start is called before the first frame update
    void Start()
    {
        score = GameObject.Find("Score").GetComponent<Score>(); // 初始化
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            score.num += 10; // 對於類下面的 num 變數加 10
        }
    }
}

這個腳本也掛載到一個遊戲對象下面,運行遊戲後,按下空格鍵就可以看到 Score 裡面的 Num 每次會加 10

然後我們嘗試把 Score 類下面的 num 變數改為 static:

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

public class Score : MonoBehaviour
{

    public static int num;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

計數程式就要做對應修改:

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

public class StaticTest : MonoBehaviour
{

    // public Score score; // 不再需要實例化

    // Start is called before the first frame update
    void Start()
    {
        //score = GameObject.Find("Score").GetComponent<Score>(); // 也不需要初始化
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            //score.num += 10;
            Score.num += 10; // 直接對於類下面的 num 變數加 10
            Debug.Log(Score.num); // Inspector 中無法顯示,需要通過 debug 進行顯示
        }
    }
}

靜態變數直接存儲在記憶體中,無需通過類的實例化來分配記憶體,所以可以直接對其進行操作。

Static 的東西沒有辦法直接在 unity 的 inspector 中看到,所以可以通過 debug 的方式查看數值變化。

在運行大型項目的時候,由於靜態數據會佔據記憶體,需要額外關注。

不僅僅是變數可以 static,類和方法也可以是 static 的。

當你使用 static class 的時候,這個類無法繼承別的父類,所以自然 static class 無法繼承 MonoBehaviour。

2.Practical Example of Working with Static Types

  • 實際運用 static type
  • 創建一個 enemy spawn manager
    • 顯示 enemy 的數量,使用 UI 展現數字
    • 使用 cube 代表 enemy,設定為 prefab

首先創建一個 cube,命名為 Emeny,然後拖拽到 assets 中作為 prefab,原窗口中的 Enemy 可以刪除,然後創建腳本 SpawnManager:

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

public class SpawnManager : MonoBehaviour
{
    public GameObject enemyPrefab;
    public static int enemyCount; // 這裡使用靜態變數

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Instantiate(enemyPrefab); // 每次按下空格鍵生成 enemy
        }
    }
}

另外創建一個 Enemy 腳本,掛載到 Enemy 的 Prefab 上,用於控制 Enemy 的激活和關閉:

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

public class Enemy : MonoBehaviour
{
    private UIManager _ui; // 實例化

    public void OnEnable() // unity 自帶的方法,生成遊戲對象的時候自動調用
    {
        SpawnManager.enemyCount++; // 激活的時候,enemy數量+1
        // 靜態變數可以直接操作,非常方便!

        _ui = GameObject.Find("UIManager").GetComponent<UIManager>(); // 初始化
        _ui.UpdateEnemyNum(); // 調用方法更新數字

        Delet(); // 調用銷毀方法
    }

    public void OnDisable() // unity 自帶的方法,銷毀遊戲對象的時候自動調用
    {
        SpawnManager.enemyCount--; // 關閉的時候,enemy數量-1
        // 靜態變數可以直接操作,非常方便!

        _ui.UpdateEnemyNum(); // 調用方法更新數字
    }

    void Delet() // 銷毀方法
    {
        Destroy(this.gameObject, Random.Range(2.0f, 4.0f)); // 2~4秒後銷毀自己
    }
}

這裡我們發現,把 enemyCount 設置為靜態變數後,操作起來非常方便!不需要有實例化和初始化過程,直接可以對其進行操作!

將 Enemy 的 Prefab 拖拽到 SpawnManager 腳本上。

創建一個空的遊戲對象,命名為 UIManager,並掛載同名腳本,用於顯示現存的 Enemy 數量:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // 直接 using UI 庫方便使用

public class UIManager : MonoBehaviour
{
    public Text enemyNumText; // 實例化一個 Text

    public void UpdateEnemyNum()
    {
        enemyNumText.text = "Enemy Number: " + SpawnManager.enemyCount;
    }
}

在場景中新建一個 Text 遊戲對象,按照喜好修改大小顏色等參數,然後將其拖拽到 UI Manger 的腳本中。

運行遊戲,按下空格鍵就會生成 Enemy,UI會顯示數字增加,當 Enemy 自動銷毀後,數字會減小。

注意幾點:

  • static 的方法無法使用非 static 的變數。
  • 靜態的即不變的,在某個類中只有一個,不會因實例化對象的不同而不同。
  • 靜態類不能實例化,不能使用 new 關鍵字創建靜態類類型的變數。

Instance Members vs Static Members

  • 區別 instance 和 static
    • instance:實例化以後才存在
    • static:程式運行開始到結束都會存在,如果 class 中有一個靜態變數,那麼這個變數為所有該類的實例化進行共享
    • 這個問題也是面試常見題目
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class ItemTest // 自定義一個類
{
    public string name;
    public int id;

    public static int itemNum; // 總的道具數量,設為靜態變數
}


public class InstanceTest : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        // 當我們要使用自定義的類的時候,首先要進行實例化 instance
        ItemTest sword = new ItemTest();
        ItemTest spear = new ItemTest();
        // 這裡創建的兩個實例,擁有不同的變數 name 和 id
        // 但是 itemNum 這個變數只有一個,每個實例都共享
        // 這個時候我們無法通過 sword.itemNum 來訪問這個變數
        // 可以直接通過 ItemTest.itemNum 來進行訪問
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

那靜態變數如何使用的,舉個例子,通過構造函數:

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


public class ItemTest // 自定義一個類
{
    public string name;
    public int id;

    public static int itemNum; // 總的道具數量,設為靜態變數

    public ItemTest() // 每次實例化的時候自動調用構造函數
    {
        itemNum++; // 相當於每次實例化一個道具,道具總數+1
    }
}


public class InstanceTest : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        // 當我們要使用自定義的類的時候,首先要進行實例化 instance
        ItemTest sword = new ItemTest();
        ItemTest spear = new ItemTest();
        ItemTest axe = new ItemTest();

        Debug.Log("Item number: " + ItemTest.itemNum);
        // 前面生成了 3 個實例,所以這個得到的數字就是 3
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

比如在線網遊中,顯示的當前在線玩家數量,就可以通過這個方法實現。

Utility Helper Classes

  • 靜態方法和靜態類
    • SDK:Software Development Kit
    • utility classes:用來讓開發流程更流暢
    • 同時為多個應用程式開發時,我們不希望手動地複製粘貼一個通用方法,這個時候我們可以有一個 utility helper class 涵蓋該通用的方法。比如我們開發多個電腦視覺程式,每個程式都要用到圖片預處理,而且預處理的步驟是相同的,那我們就可以用一個 utility helper class 來涵蓋該預處理的方法。

我們就可以把這個 utility helper class 設置為靜態:

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

public static class UtilityHelper // 靜態類不能繼承父類,所以這裡不能繼承 MonoBehaviour
{

    // 靜態類下面的所有成員都必須是靜態的,包括變數和方法等

    public static void CreatCube() // 這個方法用於創建一個 cube
    {
        GameObject.CreatePrimitive(PrimitiveType.Cube); // 創建一個預設的 cube
    }

    public static void ChangePosition(GameObject obj) // 這個方法用於改變遊戲對象位置
    {
        obj.transform.position += new Vector3(1, 0, 0); // 把遊戲對象沿著 x 軸移動 1
    }
}

然後我們創建一個 cube 作為 Player 並掛載同名腳本:

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

public class Player : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space)) // 按下空格鍵
        {
            UtilityHelper.CreatCube(); // 調用生成 cube 的方法
        }

        if (Input.GetKeyDown(KeyCode.C)) // 按下 C 鍵
        {
            UtilityHelper.ChangePosition(this.gameObject); // 改變 player 的位置
        }
    }
}

以此來調用靜態類。

這裡我們使用的 utility helper class 下面可以放置多種靜態方法,便於我們在其他程式段落中調用。

當然,我們也可以根據需求使用具有返回值的靜態方法。需要注意的是,靜態類和靜態方法的生命周期是程式開始到程式結束,所以一直會佔用到記憶體!

Challenge: Random Color Helper

  • 任務說明:
    • 創建一個 helper class,用於改變遊戲對象的顏色
    • 該改變顏色的方法需要傳入遊戲對象和目標顏色
    • 測試:按下空格鍵改變 player 的顏色
    • 額外任務:改變為隨機顏色

首先創建 helper class:

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

public static class UtilityHelper // 靜態類不能繼承父類,所以這裡不能繼承 MonoBehaviour
{
    public static void ChangeColor(GameObject obj, Color color) // 改變遊戲對象顏色
    {
        obj.GetComponent<MeshRenderer>().material.color = color;
    }
}

然後創建 Player 並掛載同名腳本:

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

public class Player : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.C)) // 按下 C 鍵
        {
            UtilityHelper.ChangeColor(this.gameObject, Color.red);
            // player 變為紅色
        }
    }
}

隨機改變顏色:

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

public static class UtilityHelper // 靜態類不能繼承父類,所以這裡不能繼承 MonoBehaviour
{

    public static void ChangeColor(GameObject obj, Color color, bool randomColor = false) // 改變遊戲對象顏色
    {
        if (randomColor)
        {
            color = new Color(Random.value, Random.value, Random.value);
            // Random.value 返回的是 0.0~1.0 的值,包含了0.0和1.0兩個邊界
        }
        obj.GetComponent<MeshRenderer>().material.color = color;    
    }
}

對應修改 player 腳本:

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

public class Player : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {

        if (Input.GetKeyDown(KeyCode.C)) // 按下 C 鍵
        {
            UtilityHelper.ChangeColor(this.gameObject, Color.red, true);
            // player 變為隨機顏色
        }
    }
}

Initializing Static Members with a Static Constructor

  • 初始化一個靜態對象
    • 和構造函數初始化一個實例對象類似,這裡使用靜態構造函數
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class Players // 玩家的類
{
    public string name; // 姓名
    public int id; // id
    public int level; // 等級

    public static string group; // 組織為靜態變數

    public Players() // 一般的實例化類構造函數,初始化非靜態變數
    {
        Debug.Log("Instance Members initialized");
    }

    static Players() // 靜態構造函數,初始化靜態變數
    {
        group = "LOL";
        Debug.Log("Static Members initialized");
    }
}

public class InitialTest : MonoBehaviour
{

    public Players p1;
    public Players p2;

    // Start is called before the first frame update
    void Start()
    {
        p1 = new Players(); // 初始化 p1
        p2 = new Players(); // 初始化 p2
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

在 console 中可以看到,先顯示一個 “Static Members initialized”,然後顯示 2 個 “Instance Members initialized”,靜態變數只初始化一次,而且是在其他初始化之前進行。

注意:靜態變數的的初始化在實例化之前進行,因為靜態變數會在所有實例化對象之間進行共享。

Tags: