Unity的C#編程教程_53_類 Class 詳解及應用練習(二)

Class Inheritence

1. Class Inheritence

  • 類的繼承,用於創建新的類的時候,使用「現有類」的一些現成的屬性。

比如我們設計了一個類,用於存儲遊戲中所有物品的資訊:

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

[System.Serializable]
public class Item
{
    
    public string name;
    public int id;
    public string description;
    public Sprite icon; // 設定道具圖標
    
    public Item()
    {

    }

    public Item(string name, int id, string description)
    {
        
        this.name = name;
        this.id = id;
        this.description = description;
    }
    
}

那我們現在想要設計一個類,用於存儲所有武器的資訊。

武器是物品中的一個分支,所以物品有的屬性,武器都有,比如名字,id,價格,描述。

但是武器還有些別的物品沒有的屬性,比如攻擊力,或者還可以有攻擊頻率,熟練度等。

另外遊戲中一般還有消費品,比如補血瓶也是一種道具,但是除了基礎屬性,還有一些特有屬性,比如可以增加生命值等

那對於武器和消耗品,是不是都要單獨建立一個新的類呢?

答案是不用,我們可以復用一些 「道具」 的基礎屬性!

比如我們新建一個武器的類:

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

[System.Serializable]
public class Weapons : Item
{

    public int attack;

    public Weapons(string name,int id, string description,int attack) : base(name, id, description)
    {
        this.attack = attack;
    }

}

比如還可以新建一個消耗品類:

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

public class Consumables : Item
{

    public int addHealth;

    public Consumables(int addHealth)
    {
        this.addHealth = addHealth;
    }
}

然後我們新建一個空的遊戲對象 ItemDatabase,掛載上同名腳本:

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

public class ItemDatabase : MonoBehaviour
{

    public Item[] items; // 可以建立一系列道具
    public Weapons[] weapons; // 可以建立一系列武器
    public Consumables[] consumables; // 可以建立一個消耗品列表

    public Weapons bigSword; // 可以單獨建立一個武器

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

    }

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

編譯後,進入 Unity 可以看到可以自己修改和定義的道具列表和武器列表,還有一個名字叫做 Big Sword 的武器(具體屬性內容為空)。

另外由於我們的消耗品類前面沒有添加 [System.Serializable],所以在 unity 中不可見。

可以看到,武器類的道具除了有 attack 的特有屬性,也有一般道具的基礎屬性。

這個繼承的方法,可以很好地來拓展我們的道具分類,比如道具下面有武器分支,武器下面還有遠程武器分支,遠程武器下面還有魔法武器分支,等等。

如果不使用繼承,那可以想像,在我們的 Item 類下面需要放超多屬性,要涵蓋所有門類的道具的所有屬性才行!

2. Bank System Inheritance Example

  • 任務說明:
    • 設計一個銀行賬戶的類,包含屬性:銀行名稱,帳號,餘額
    • 包含方法:查詢餘額,存錢,取錢
    • 設計一個賬戶管理程式,管理所有的銀行賬戶
    • 另外還有一個專門的銀行賬戶,用於申請貸款(使用一個可以調用的方法,確定你能否貸款,能貸款多少錢)

首先定義一個銀行賬戶的類:

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


[System.Serializable]
public class BankAccount
{
    public string bankName; // 所屬銀行
    public int id; // 帳號
    public float money; // 餘額

    public void CheckBalance() // 顯示餘額
    {
        Debug.Log(bankName + id + " has money: " + money);
    }

    public void Withdraw(float moneyOut) // 取錢
    {
        Debug.Log("Withdraw money: " + moneyOut);
        money -= moneyOut;
        Debug.Log(bankName + id + " has money: " + money);
    }

    public void Deposit(float moneyIn) // 存錢
    {
        Debug.Log("Deposit money: " + moneyIn);
        money += moneyIn;
        Debug.Log(bankName + id + " has money: " + money);
    }
}

創建一個腳本,用於控制所有的銀行賬戶:

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

public class AccountManager : MonoBehaviour
{
    public BankAccount[] bankAccounts;

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

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

創建一個空的遊戲對象,用作掛載銀行賬戶的控制腳本,這就好比一個可以控制你所有賬戶的實體終端,類似網銀這種。然後把控制腳本掛載到這個空的遊戲對象下。

這個時候我們假設有 3 個銀行賬戶,則可以在 unity 的 inspector 中輸入 size 為 3,然後分別輸入 3 個銀行賬戶的資訊。

這裡的賬戶控制腳本是可以復用的,比如現在我假設有一台特定銀行的 ATM 機器,那麼我創建一個空的遊戲對象,並命名為 A Bank ATM,然後同樣可以掛載上面的控制腳本,按照實際情況,這裡可以設定 size 為 1,然後對應設定銀行名字,帳號和餘額即可。

有了以上的這些賬戶後,我們就需要對這些賬戶進行特定操作了,比如我們面對一台 ATM 機器,那應該可以查詢餘額,存錢和取錢才對。

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

public class AccountManager : MonoBehaviour
{
    public BankAccount[] bankAccounts;

    // Start is called before the first frame update
    void Start()
    {
        // 查詢餘額
        bankAccounts[0].CheckBalance();

        // 存錢 10 元
        bankAccounts[0].Deposit(10);

        // 取錢 5 元
        bankAccounts[0].Withdraw(5);
    }

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

    }
}

這裡只做了最基本的演示,如果要更真實,那我們要有特定的按鍵或者選擇菜單,然後還需要有對應金額的輸入。

然後我們可以定義一個用於申請貸款的賬戶,這個賬戶不但有銀行賬戶的基本功能,還能貸款:

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

[System.Serializable]
public class LoanAccount : BankAccount // 繼承基礎的銀行賬戶功能
{

    public bool status; // 是否可以貸款
    public float loanApprove; // 可以貸款多少錢
}

在賬戶管理下面,可以多生成一個貸款賬戶:

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

public class AccountManager : MonoBehaviour
{
    public BankAccount[] bankAccounts; // 普通銀行賬戶
    public LoanAccount loanAccount; // 貸款賬戶

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

    }

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

    }
}

然後在 Unity 中就可以編輯這個貸款賬戶的具體資訊。

自然,我們可以在這個貸款賬戶的定義腳本下添加貸款賬戶的專用方法,比如獲取貸款(賬戶餘額增加),償還貸款(賬戶餘額減少)。

需要注意的是,我們所定義的這些類下面的方法,只有在 MonoBehaviour 下面調用的時候才會運行,不會自動運行!

Protected Data Members

  • 前面我們接觸到的大多數是 private 和 public 的對象,其實還有一種叫做 protected 的對象
  • public 對象誰都可以訪問並修改
  • private 對象只有類本身的方法可以進行訪問和修改
  • 而 protected,介於兩者之間,除了本身的類可以訪問,繼承的子類也可以訪問

比如一個銀行賬戶的類:

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


[System.Serializable]
public class BankAccount
{
    public string bankName; // 所屬銀行
    public int id; // 帳號
    protected float money; // 餘額
    // private float money; 如果使用的是 private 則只能在類本身進行訪問,子類也不行

    public void CheckBalance() // 顯示餘額
    {
        Debug.Log(bankName + id + " has money: " + money);
    }

    public void Withdraw(float moneyOut) // 取錢
    {
        Debug.Log("Withdraw money: " + moneyOut);
        money -= moneyOut;
        Debug.Log(bankName + id + " has money: " + money);
    }

    public void Deposit(float moneyIn) // 存錢
    {
        Debug.Log("Deposit money: " + moneyIn);
        money += moneyIn;
        Debug.Log(bankName + id + " has money: " + money);
    }
}

其子類:

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

[System.Serializable]
public class LoanAccount : BankAccount // 繼承基礎的銀行賬戶功能
{

    public bool status; // 是否可以貸款
    public float loanApprove; // 可以貸款多少錢

    
    public void loanMoney()
    {
        money = 100; // 只有在 money 是 public 和 protected 時子類才能訪問
        loanApprove = money * 100;
    }
}

不僅僅是變數,類下面的方法也可以設定為 protected,一樣的效果。

在實際的遊戲中,用於「交互」的資訊,通常需要設定為public,比如一個角色的血量,別的角色攻擊可以造成扣血,所以需要外部訪問。而有的資訊不需要交互,比如玩家的動作是奔跑還是行走,這個通常是玩家自己控制而不是外部決定的,所以應該設計為 private 或者 protected。

Virtual Methods and Overriding

  • 除了繼承我們自己定義的類,也可以繼承 unity 的 MonoBehaviour 這個類
  • 假設我們要設計一個寵物系統
    • 新建一個腳本 Pat 繼承於 MonoBehaviour
    • 寵物有名字,會奔跑

首先新建一個腳本:

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

public class Pet : MonoBehaviour
{
    public string patName;

    public virtual void Run() // 設定為虛擬方法,子類可以重寫
    {
        Debug.Log("I'm running!");
    }

    // Start is called before the first frame update
    void Start()
    {
        Run(); // 調用移動方法
    }

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

    }
}


這個是基礎的寵物,我們可以衍生出一隻狗:

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

public class Dog : Pet
{

    public override void Run() // 重寫方法
    {
        Debug.Log("Dog is running!");
    }

}

然後在 unity 中創建一個 cube,假設是個寵物狗,然後掛載上這個腳本

還可以有別的寵物,移動方式不同:

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

public class Bird : Pet
{

    public override void Run() // 重寫方法
    {
        Debug.Log("Bird is flying!");
    }

}

由於最初的 Pet 繼承於 MonoBehaviour,所以有 Start 方法可以使用。

這個時候調用了一個 virtual 方法,程式自動會去檢查子類有沒有重寫該方法,沒有被重寫的則使用父類默認的方法。

這裡需要注意,我們也可以把 start 中調用 Run() 的語句放到子類中。另外雖然子類中沒有調用方法,但是由於繼承於父類,父類繼承於 MonoBehaviour,所以子類運行的時候,父類中的 start() 是自動運行的。

另外注意,虛擬方法和重寫方法的作用域必須統一,比如虛擬方法是 protected,那麼重寫方法也必須是 protected

Q and A on Using MonoBehaviour Custom Classes

  • 自定義類的一個常見問題是:什麼時候該繼承 MonoBehaviour
    • 當我們邏輯功能和行為的時候,需要繼承 MonoBehaviour
    • 比如我們設計的寵物系統,寵物需要有一些特定伴隨遊戲運行需要調用的方法,那寵物就需要繼承 MonoBehaviour,這樣我們把寵物的腳本掛載到遊戲對象上的時候,才能調用對應的方法,產生對應的動作
    • 另外比如遊戲中的道具,一般道具不會隨著遊戲運行而不斷變化,而是被玩家使用,所以不需要繼承 MonoBehaviour。
    • 還有比如遊戲裡面的各種敵人,那就應該是繼承於 MonoBehaviour 的,因為我們要為其添加各種行為和動作。

Structs, Memory Management, and Value vs. Reference Types

  • 現在 structs 常用於性能增強,或者代替 classes 的作用
    • 一般情況下,如果一個東西的衍生不超過 4 個領域,那可以考慮用 structs 代替類的繼承
    • 其實 structs 和 classes 基本差不多,區別在於 structs 不能繼承,不能繼承即表示不可通過繼承來重寫方法,或者添加其他東西,也即實現了「不可變性/統一性」
    • 常見的應用場景,比如不同的彈藥,屬性統一為 3 個:傷害,冷卻時間,範圍,我們不需要通過繼承來為其添加額外的東西

比如道具的案例:

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

public class Item // 引用類型 reference type
{
    public string itemName;
    public int itemId;

    public Item(string name, int id)
    {
        this.itemName = name;
        this.itemId = id;
    }

}

public struct Item2 // 值類型 value type
{
    public string itemName;
    public int itemId;

    public Item2(string name, int id)
    {
        this.itemName = name;
        this.itemId = id;
    }
}

public class StructTest : MonoBehaviour
{
    Item sword;
    Item2 spear;

    // Start is called before the first frame update
    void Start()
    {
        sword = new Item("Big Sword", 1);
        spear = new Item2("Small Spear", 2);
    }

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

值類型 value type 和 引用類型 reference type 的概念非常重要:

  • 值類型:佔用記憶體空間(棧 stack),所以訪問速度較快
  • 引用類型:可以理解為不包含具體的值(其在堆 heap 中分配記憶體空間),僅為一個記憶體地址,所以不佔用額外記憶體,相比值類型有更大的存儲規模,較低的訪問速度

在 C# 中有垃圾回收機制,不需要過多考慮記憶體管理,但是上面的概念依然值得了解!

常見的 int,float,long,bool,bytes,char的都是值類型:

    // Start is called before the first frame update
    void Start()
    {
        // value type
        int num = 12; // 這裡的 num 就是個值類型,有單獨的記憶體空間
    }

同樣,struct 也是值類型。

引用類型常見的是 string 字元串:

    // Start is called before the first frame update
    void Start()
    {
        // value type
        int num = 12; // 這裡的 num 就是個值類型,有單獨的記憶體空間

        // reference type
        string myName = "Hello World";
    }

這裡並不是在記憶體中直接存儲了 「Hello World」,而是存儲了一個地址,指向存儲位置。

引用類型還有:arrays,class,delegates。

當我們傳遞數據的時候,值類型的數據會被複制(增加記憶體佔用),原始存儲的值不會被改變。比如我們把一個 int 變數傳遞到一個函數中。但是引用類型傳遞到函數中的時候,傳遞的是記憶體地址,所以可以修改到原數據。

演示傳遞的區別:

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

[System.Serializable]
public class Item1 // 引用類型 reference type
{
    public string itemName;
    public int itemId;

    public Item1(string name, int id)
    {
        this.itemName = name;
        this.itemId = id;
    }

}

[System.Serializable]
public struct Item2 // 值類型 value type
{
    public string itemName;
    public int itemId;

    public Item2(string name, int id)
    {
        this.itemName = name;
        this.itemId = id;
    }
}


public class StructTest : MonoBehaviour
{
    public Item1 sword;
    public Item2 spear;

    // Start is called before the first frame update
    void Start()
    {
        //sword = new Item1("s", 1);
        sword.itemName = "Big sword";
        sword.itemId = 1;

        spear = new Item2("Small Spear", 2); // 也可以用上面那種初始化方法

        // 用改名程式驗證值類型和引用類型,對於數據傳遞的區別
        Debug.Log("Before: " + sword.itemName);
        ChangeName(sword); // 調用改名方法
        Debug.Log("After: " + sword.itemName);
        // 可以看到名字改了
        // 因為傳遞的是地址,所以直接對該地址存儲的數據進行了改動

        Debug.Log("Before: " + spear.itemName);
        ChangeName(spear); // 調用改名方法
        Debug.Log("After: " + spear.itemName);
        // 可以看到結果名字不變
        // 因為原來的值被複制了,所以改的是複製的值,原始值不變
    }

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

    void ChangeName(Item1 classItem) // 改名方法
    {
        classItem.itemName = "changed name sword";
    }

    void ChangeName(Item2 structItem) // 改名方法
    {
        structItem.itemName = "changed name spear";
    }
}

在 unity 的 console 窗口中可以看到,sword 的名字改了,spear 的名字沒改,這就是變數傳遞的區別。如果我們在改名方法中添加一個語句顯示 itemName,可以看到名字變了,相當於在該方法中是一個封閉空間。

這一點非常重要!而且經常會作為面試題!

Tags: