文章目录
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,可以看到名字变了,相当于在该方法中是一个封闭空间。
这一点非常重要!而且经常会作为面试题!