Unity的C#編程教程_53_類 Class 詳解及應用練習(二)
1. Class Inheritence
2. Bank System Inheritance Example
Protected Data Members
Virtual Methods and Overriding
Q and A on Using MonoBehaviour Custom Classes
Structs, Memory Management, and Value vs. Reference Types
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,可以看到名字變了,相當於在該方法中是一個封閉空間。
這一點非常重要!而且經常會作為面試題!