Unity的C#编程教程

    科技2022-08-03  109

    文章目录

    Class Inheritence1. Class Inheritence2. Bank System Inheritance Example Protected Data MembersVirtual Methods and OverridingQ and A on Using MonoBehaviour Custom ClassesStructs, 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,可以看到名字变了,相当于在该方法中是一个封闭空间。

    这一点非常重要!而且经常会作为面试题!

    Processed: 0.012, SQL: 8