https://learn.u3d.cn/tutorial/intermediate-gameplay-scripting?chapterId=63562b2bedca72001f21d33f#604077094d432c002110d130

01.Properties 属性

位于类外的代码访问这个类的成员变量的两种方法:

  • 使用public公开变量
  • 使用属性

属性本身可以当做变量,并且可以封装成员变量,也称之为字段

一个属性可以有两个访问器 get 和 set ,在 get 中我们返回所封装的字段,在 set 中使用value给字段赋值

     private int experience;
 ​
     // 创建一个属性
     public int Experience
    {
         get
        {
             return experience;
        }
         set
        {
             experience = value;
        }
    }

使用属性可以执行两个公共变量无法实现的操作:

  • 通过省略 get 和 set 可以有效将字段设置为只写或只读
  • 可以将访问器视为函数,可以在其内部运行其他代码或调用其他函数
     private int experience;
 ​
     // 创建一个属性
     public int Experience
    {
         get
        {
             // some other code
             return experience;
        }
         set
        {
             // some other code
             experience = value;
        }
    }
     public int Level{
         get
        {
             // 可以返回运算后的等级而不是经验值
             return experience / 1000;
        }
         set{
             // 可以接收玩家等级和计算玩家所获得的经验数量,并存储在experience字段中
             experience = value * 1000;
        }
    }

可以使用简写语法,Visual Studio 可以输入prop快速创建属性

 public int Health { get; set; }

其他脚本调用

 public class B01_Game : MonoBehaviour
 {
     void Start()
    {
         B01_Player myPlayer = new B01_Player();
 ​
         //属性可以像变量一样使用
         myPlayer.Experience = 5;
         int x = myPlayer.Experience;
    }
 }

02.三元运算符

三元运算符用于根据布尔表达式,在两个值直接做出选择

 int health = 10;
 string message;
 message = health > 0 ? "Player is Alive" : "Player is Dead";

三元运算符可相互嵌套,但会导致语句繁琐,难以理解,

 message = health > 0 ? "Player is Alive" : health == 0 ? "Player is Barely Alive" : "Player is Dead";

使用三元表达式而不是if语句的情况:

  • 代码需要简单的if-else结构
  • 且每种情况只需要一个短表达式

03.Statics 静态成员

静态成员如变量和方法,是跨类的所有实例共享的成员。此外,静态成员可以直接通过类访问,无需先对类的对象进行实例化。

  • 通常,成员变量对于类的每个对象是唯一的,即使类的没搞对象具有相同的变量,但它们都有自己的值
  • 但对于静态成员,类的每个对象具有相同的变量和相同的值,如果在一处更改,其他也同时更改
 public class B03_Enemy : MonoBehaviour
 {
     //静态变量是在类的所有实例之间共享的变量。
     public static int enemyCount = 0;
 ​
     public void Enemy()
    {
         //通过递增静态变量了解,已创建此类的多少个对象。
         enemyCount++;
    }
 }

在另一个脚本生成几个敌人

 public class B03_Game : MonoBehaviour
 {
     void Start()
    {
         B03_Enemy enemy1 = new B03_Enemy();
         B03_Enemy enemy2 = new B03_Enemy();
         B03_Enemy enemy3 = new B03_Enemy();
 ​
         //可以使用类名和点运算符,来访问静态变量。
         int x = B03_Enemy.enemyCount;
    }
 }

只要脚本挂在在对象上,玩家数量就会增加

 public class B03_Player : MonoBehaviour
 {
     public static int plauerCount=0;
     void Start()
    {
         plauerCount++;
    }
 }

可以在其他脚本访问静态变量来查看玩家数量

 public class B03_PlayerManager : MonoBehaviour
 {
     void Start()
    {
         int x = B03_Player.plauerCount;
    }
 }

与静态变量一样,静态方法属于类,而不属于类的特定对象

 public class B03_Utilities : MonoBehaviour
 {
     public static int Add(int num1,int num2){
         return num1 + num2;
    }
 }

在另一个方法中就可以用类的名称和点来调用Add方法,无需通过实例化类的对象来使用其静态成员

 public class B03_UtiltiesExample : MonoBehaviour
 {
     void Start()
    {
         int x = B03_Utilities.Add(6,7);
    }
 }

不能在静态方法内部,使用非静态类成员变量,静态变量属于类,而非静态变量属于类的实例

也可将整个类变成静态类,只需要在calss前加入static,类变成静态但不能创建类的实例

 public static class B03_UtiltiesExample : MonoBehaviour
 {
     void Start()
    {
         int x = B03_Utilities.Add(6,7);
    }
 }

04.Method Overloading 方法重载

通过重载过程可以为单个方法提供多个定义,意味着可以使用同一方法名称执行两项不同的操作

重载方法的操作是为新方法指定名称相同,但形参不同

 public class B04_SomeClass : MonoBehaviour
 {
     public int Add(int num1, int num2)
    {
         return num1 + num2;
    }
     public string Add(string str1, string str2)
    {
         return str1 + str2;
    }
 }

在其他类中,当我们尝试访问Add方法时,我们可以看到有两个版本,可能出现三种情况

  1. 与传入参数完全匹配,运行这个版本的已重载方法
  2. 如果不匹配,根据匹配项,选择需要最少转换量的方法
  3. 如果没有可能的匹配项,或者多版本所需的转换量相同,则抛出错误

05.Generics 泛型

泛型是一种特征,通过该特征类型可以作为参数,传递给类和方法等,实际上这允许你在不了解所处理数据的确切情况下进行一般编程

 public class B05_SomeClass : MonoBehaviour
 {
     //这是一个通用方法。注意通用类型“T”。该“T”将在运行时替换为实际类型。
     public T GenericMethod<T>(T param)
    {
         return param;
    }
 }

如果要添加多个泛型参数,可以继续添加,明明惯例常常遵循T

 public T GenericMethod<T,U,V>(T param)

T只是个占位符,运行时会替换为实际类型,无论T最终成为什么类型,这个类型也将成为方法的返回类型和参数类型。因为它们都是用T作为其类型

对泛型参数施加限制,冒号后为具体的限制,通常有四种:class, struct, new(), MonoBehaviour, Ienumerable

  • class 确保T是引用类型
  • struct 确保它是值类型
  • new() 确保它具备不含参数的公共构造函数
  • 类名 就代表这个类或通过多态表示T代表从中衍生的任意类
  • 接口名 表示T已实现这个接口
 public T GenericMethod<T>(T param) where T : class

为了使用泛型,必须知道希望它使用的具体类型,假如想使用刚刚创建的泛型方法

 public class B05_SomeOtherClass : MonoBehaviour
 {
     void Start()
    {
         B05_SomeClass myClass = new B05_SomeClass();
         //为了使用此方法,必须告诉此方法用什么类型替换“T”。
         myClass.GenericMethod<int>(5);
    }
 }

这个类使用泛型类型T,意味着在使用时,在类中作用类型的类型T的每个实例将替换实际类型,这个类又一个成员变量名为item类型为T,还有一个名为UpdateItem的方法,这个方法有一个类型为T的参数,并将其分配给成员item

 public class B05_GenericClass<T> : MonoBehaviour
 {
     T item;
     public void UpdateItem(T newItem)
    {
         item = newItem;
    }
 }

为了实例化这个类的对象,必须为为T指定一个类型,方法是输入类的名称加尖括号和所需类型,在输入构造函数的名称之后,并在构造函数的参数列表之前也必须执行这个操作

 public class B05_GenericClassExample : MonoBehaviour
 {
     void Start(){
         B05_GenericClass<int> myclass = new B05_GenericClass<int>();
         myclass.UpdateItem(5);
    }
 }

泛型的一种常用方法是用于字典和列表等集合

06.Inheritance 继承

一个类继承自另一个类时,它会获得被继承类的特征,父类及子类。继承结果是父类中存在的项 ,也将出现在子类中。

假设父类中有两个方法,子类中也同时有这两个方法,无需在子类中创建。处理继承时需要注意三个访问修饰符:public、private、protected

  • public公开的类特征将存在于子类中,并且可供访问
  • private私有的类存在于子类中,但不可访问
  • protected相当于两者混合:与公开的特征一样,受保护的父类的所有特征将在子类中且可供访问;但在父类或子类之外将不可访问,就像私有的特征一样
 public class 类名 : 继承的父类名{}

在子类继承的项中,构造函数是一个例外,因为他们对类是唯一的不会共享。但是在子类中调用构造函数时,其父类的构造函数会立刻被调用。

由于类可能有多个不同的构造函数,因此我们可能想控制调用那个基类的构造函数,为此可以使用关键字base,通过在子类构造函数的参数列表后面加一个冒号,可以使用关键字base在构造函数的参数列表中,显式调用基类的具体构造函数。

 public class Apple : Furit{
   public Apple() :base("apple"){
     //Constructor Code...
  }
 }

如果不显示调用基类的构造函数,则会隐式调用默认构造函数

除了调用基类构造函数,base关键字还可用来访问基类的其他成员

Fruit 类:

 using UnityEngine;
 using System.Collections;
 ​
 //这是基类,也称为父类。
 public class Fruit
 {
     public string color;
 ​
     //这是 Fruit 类的第一个构造函数,不会被任何派生类继承。
     public Fruit()
    {
         color = "orange";
         Debug.Log("1st Fruit Constructor Called");
    }
 ​
     //这是 Fruit 类的第二个构造函数,不会被任何派生类继承。
     public Fruit(string newColor)
    {
         color = newColor;
         Debug.Log("2nd Fruit Constructor Called");
    }
 ​
     public void Chop()
    {
         Debug.Log("The " + color + " fruit has been chopped.");        
    }
 ​
     public void SayHello()
    {
         Debug.Log("Hello, I am a fruit.");
    }
 }

Apple 类:

 using UnityEngine;
 using System.Collections;
 ​
 //这是派生类,也称为子类。
 public class Apple : Fruit
 {
     //这是 Apple 类的第一个构造函数。它立即调用父构造函数,甚至在它运行之前调用。
     public Apple()
    {
         //注意 Apple 如何访问公共变量 color,该变量是父 Fruit 类的一部分。
         color = "red";
         Debug.Log("1st Apple Constructor Called");
    }
 ​
     //这是 Apple 类的第二个构造函数。它使用“base”关键字指定要调用哪个父构造函数。
     public Apple(string newColor) : base(newColor)
    {
         //请注意,该构造函数不会设置 color,因为基构造函数会设置作为参数传递的 color。
         Debug.Log("2nd Apple Constructor Called");
    }
 }

FruitSalad 类:

 using UnityEngine;
 using System.Collections;
 ​
 public class FruitSalad : MonoBehaviour
 {
     void Start ()
    {
         //让我们用默认构造函数来说明继承。
         Debug.Log("Creating the fruit");
         Fruit myFruit = new Fruit();
         Debug.Log("Creating the apple");
         Apple myApple = new Apple();
 ​
         //调用 Fruit 类的方法。
         myFruit.SayHello();
         myFruit.Chop();
 ​
         //调用 Apple 类的方法。注意 Apple 类如何访问Fruit 类的所有公共方法。
         myApple.SayHello();
         myApple.Chop();
 ​
         //现在,让我们用读取字符串的构造函数来说明继承。
         Debug.Log("Creating the fruit");
         myFruit = new Fruit("yellow");
         Debug.Log("Creating the apple");
         myApple = new Apple("green");
 ​
         //调用 Fruit 类的方法。
         myFruit.SayHello();
         myFruit.Chop();
 ​
         //调用 Apple 类的方法。注意 Apple 类如何访问Fruit 类的所有公共方法。
         myApple.SayHello();
         myApple.Chop();
    }
 }

07.Polymorphism多态

多态是继承的一个特征,它允许一个类具有多种类型。

在继承层次结构中,任何子类都可以作为父类使用。这意味着在需要基类的地方,可以使用派生类来代替它。

在调用 onTrigger 函数时,我们不知道会使用什么类型的 Collider。每个对象的特定 Collider 都会传入函数,由于它们都继承自 Collider 父类,因此它们都可以被正确处理。

 public class SomeScript : MonoBehaviour{
   void onTriggerEnter(Collider other){
     //Do Something Fun!
  }
 }

多态的一种明智用法涉及构造函数和对象引用。你可以声明基类类型的对象,然后调用其中一个派生类的构造函数,因为变量需要的是基类的类型。子类的构造函数会创建衍生类型的对象。由于子类是父类的一种,因此这种转换是有效的,这个过程被称为向上转型。当对象向上转型时,它只能被视为其父类的一个对象。

在本实例中,子类向上转型时,它只能被视作父类,这表示只能使用父类中可用的变量和方法,在使用时会把他们视作位于父类对象中。

虚拟函数是一个例外,虚拟函数将调用最新覆盖版本

 public class SomeScript : MonoBehaviour{
   void Start(){
     ParentsClass myClass =new ChildClass();
     myClass.ParentMethod();
  }
 }

为了将这个子类视作子类,我们需要向下转型子类变量,使其恢复为子类类型,具体方法是将类型名称括在括号内并将其置于变量前面。我们可以再用一组括号括起来,并使用点运算符来访问成员,也可以创建对这个新版本的引用

 public class SomeScript : MonoBehaviour{
   void Start(){
     ParentsClass myClass =new ChildClass();
     myClass.ParentMethod();
     ChildClass myClass=(ChildClass)myClass;
     myChild.ChildMethod();
  }
 }

Fruit 类

 using UnityEngine;
 using System.Collections;
 ​
 public class Fruit
 {
     public Fruit()
    {
         Debug.Log("1st Fruit Constructor Called");
    }
 ​
     public void Chop()
    {
         Debug.Log("The fruit has been chopped.");        
    }
 ​
     public void SayHello()
    {
         Debug.Log("Hello, I am a fruit.");
    }
 }

Apple 类

 using UnityEngine;
 using System.Collections;
 ​
 public class Apple : Fruit
 {
     public Apple()
    {
         Debug.Log("1st Apple Constructor Called");
    }
 ​
     //Apple 有自己的 Chop() 和 SayHello() 版本。
     //运行脚本时,请注意何时调用Fruit 版本的这些方法以及何时调用Apple 版本的这些方法。
     //此示例使用“new”关键字禁止来自 Unity 的警告,同时不覆盖Apple 类中的方法。
     public new void Chop()
    {
         Debug.Log("The apple has been chopped.");        
    }
 ​
     public new void SayHello()
    {
         Debug.Log("Hello, I am an apple.");
    }
 }

FruitSalad 类

 using UnityEngine;
 using System.Collections;
 ​
 public class FruitSalad : MonoBehaviour
 {
     void Start ()
    {
         //请注意,这里的变量“myFruit”的类型是Fruit,但是被分配了对 Apple 的引用。这是由于多态而起作用的。由于 Apple 是 Fruit,因此这样是可行的。虽然 Apple 引用存储在 Fruit 变量中,但只能像 Fruit 一样使用
         Fruit myFruit = new Apple();
         myFruit.SayHello();
         myFruit.Chop();
 ​
         //这称为向下转换。Fruit 类型的变量“myFruit”实际上包含对 Apple 的引用。因此,可以安全地将它转换回 Apple 变量。这使得它可以像 Apple 一样使用,而在以前只能像 Fruit
 一样使用。
         Apple myApple = (Apple)myFruit;
         myApple.SayHello();
         myApple.Chop();    
    }
 }

08.Member Hiding 成员隐藏

在子类中重新创建,即重新声类成员的过程被称为成员隐藏

为了隐藏基类的成员,应在成员的类型前面使用new声明子类成员。

如果我们对这个集合中的所有对象,则它们都将调用Humanoid版本的Yell。这是因为我们将orc和Enemy,对像声明为Humanoid,并且们已隐式向外转型为Humanoid

Humanoid 类

 using UnityEngine;
 using System.Collections;
 ​
 public class Humanoid
 {
     //Yell 方法的基版本
     public void Yell()
    {
         Debug.Log ("Humanoid version of the Yell() method");
    }
 }

Enemy 类

 using UnityEngine;
 using System.Collections;
 ​
 public class Enemy : Humanoid
 {
     //这会隐藏 Humanoid 版本。
     new public void Yell()
    {
         Debug.Log ("Enemy version of the Yell() method");
    }
 }

Orc 类

 using UnityEngine;
 using System.Collections;
 ​
 public class Orc : Enemy
 {
     //这会隐藏 Enemy 版本。
     new public void Yell()
    {
         Debug.Log ("Orc version of the Yell() method");
    }
 }

WarBand 类

 using UnityEngine;
 using System.Collections;
 ​
 public class WarBand : MonoBehaviour
 {
     void Start ()
    {
         Humanoid human = new Humanoid();
         Humanoid enemy = new Enemy();
         Humanoid orc = new Orc();
 ​
         //注意每个 Humanoid 变量如何包含
         //对继承层级视图中
         //不同类的引用,但每个变量都
         //调用 Humanoid Yell() 方法。
         human.Yell();
         enemy.Yell();
         orc.Yell();
    }
 }

09.Overriding 覆盖

覆盖是指更改子类中的父类方法,结果是当我们调用方法时将调用最新版本的方法或最新覆盖的版本

使用virtual和override关键字,这些关键字位于方法的返回类型之前,父类中的方法定义为virtual,而所有子类中的方法定义为override。声明为virtual的任何方法可被任何子类覆盖,

可以使用base关键字来同时调用方法的父版本

Fruit 类

 using UnityEngine;
 using System.Collections;
 ​
 public class Fruit
 {
     public Fruit ()
    {
         Debug.Log("1st Fruit Constructor Called");
    }
 ​
     //这些方法是虚方法,因此可以在子类中
     //将它们覆盖
     public virtual void Chop ()
    {
         Debug.Log("The fruit has been chopped.");        
    }
 ​
     public virtual void SayHello ()
    {
         Debug.Log("Hello, I am a fruit.");
    }
 }

Apple 类

 using UnityEngine;
 using System.Collections;
 ​
 public class Apple : Fruit
 {
     public Apple ()
    {
         Debug.Log("1st Apple Constructor Called");
    }
 ​
     //这些方法是覆盖方法,因此
     //可以覆盖父类中的任何
     //虚方法。
     public override void Chop ()
    {
         base.Chop();
         Debug.Log("The apple has been chopped.");        
    }
 ​
     public override void SayHello ()
    {
         base.SayHello();
         Debug.Log("Hello, I am an apple.");
    }
 }

FruitSalad 类

 using UnityEngine;
 using System.Collections;
 ​
 public class FruitSalad : MonoBehaviour
 {    
     void Start ()
    {
         Apple myApple = new Apple();
 ​
         //请注意,Apple 版本的方法
         //将覆盖 Fruit 版本。另外请注意,
         //由于 Apple 版本使用“base”关键字
         //来调用 Fruit 版本,因此两者都被调用。
         myApple.SayHello();
         myApple.Chop();    
 ​
         //“覆盖”在多态情况下也很有用。
         //由于 Fruit 类的方法是“虚”的,
         //而 Apple 类的方法是“覆盖”的,因此
         //当我们将 Apple 向上转换为 Fruit 时,
         //将使用 Apple 版本的方法。
         Fruit myFruit = new Apple();
         myFruit.SayHello();
         myFruit.Chop();
    }
 }

10.Interfaces 接口

实现接口的任何类,必须拥有其所有方法和属性。通过使用多态,其他类可将实现类视作接口。需要注意的是接口不是类,不能有首己的实例

  • 继承是is a关系,即一个类继承自另一个类
  • 而接口使用实现关系,即一个类实现一个接口

接口通常在类外部声明,声明接口时通常对每个接口使用一个脚本。由于接口通常描述实现类将具备的某种功能,因止此许多接口以后缀able结尾,不过这是非强制的

 public interface IKillable
 {
     void Kill();
 }
 ​
 //这是一个通用接口,其中 T 是将由实现类提供的数据类型的占位符。
 public interface IDamageable<T>
 {
     void Damage(T damageTaken);
 }

要实现接口只需在类具有的任何继承之后添加一个逗号,后跟接口的名称;如果类不是从其他类继承而来,则不需要逗号;如果接口具有型类型,则名称应后跟尖括号在里面输入类型

Avatar 类

 using UnityEngine;
 using System.Collections;
 ​
 public class Avatar : MonoBehaviour, IKillable, IDamageable<float>
 {
     //IKillable 接口的必需方法
     public void Kill()
    {
         //执行一些有趣操作
    }
 ​
     //IDamageable 接口的必需方法
     public void Damage(float damageTaken)
    {
         //执行一些有趣操作
    }
 }

因为实现接口,可以使两个完全不相于的类具有同一个功能,或者同一件事情,这样两个类有了共同点,一因为这样使得程序结构设计更加有逻辑,看到接口就知道功能大致的效果了

11.Extension Methods 拓展方法

通过护展方法可以尚类型添加功能而不必创建DriveType或更改原始类型,适用于向类添加功能但不能编辑类的情况。简单来说就是为一个修改不了的类添加更多自定义的静态方法

扩展方法必须放在泛型静态类中,常见做法是专门创建一个类来包含它们。要使函数成为拓展方法而非静态方法,需要在参数中使用this关键字,this关键字后跟Transform类和任意参数名称,

第一个参数将是调用对象,因此当我们调用这个数时无需提供这个参数,此外该第一个参数规定了,这个方法属于哪个类,如果我们想要更多参数,可以在此输入而不使用this关键字

现在我们可以编写代码来重置Transform

ExtensionMethods

 using UnityEngine;
 using System.Collections;
 ​
 //创建一个包含所有扩展方法的类是很常见的做法。此类必须是静态类。
 public static class ExtensionMethods
 {
     //扩展方法即使像普通方法一样使用,也必须声明为静态。请注意,第一个参数具有“this”关键字,后跟一个 Transform变量。此变量表示扩展方法会成为哪个类的一部分。
     public static void ResetTransformation(this Transform trans)
    {
         trans.position = Vector3.zero;
         trans.localRotation = Quaternion.identity;
         trans.localScale = new Vector3(1, 1, 1);
    }
 }

SomeClass

 using UnityEngine;
 using System.Collections;
 ​
 public class SomeClass : MonoBehaviour
 {
     void Start () {
         //请注意,即使方法声明中有一个参数,也不会将任何参数传递给此扩展方法。调用此方法的Transform 对象会自动作为第一个参数传入。
         transform.ResetTransformation();
    }
 }

12.Namespaces 命名空间

命名空间就像是类的容器,其目的是帮助组织脚本,避兔脚本之间发生冲突,脚本的前两行 using UnityEngine; using System.Collections; 都是命名空间

 using UnityEngine;
 using System.Collections;
 ​
 namespace SampleNamespace
 {
     public class SomeClass : MonoBehaviour
    {
         void Start ()
        {
 ​
        }
    }
 }

通过三种方法使用来自特定命名空间的类:

  1. 脚本顶部使用 using  using SampleNamespace
  2. 使用点运算符:每次要引l用来自SampleNamespace的类时,都可以输入SampleNamespace点类 SampleNamespace Someclass myclass=new SampleNamespace.Someclass():
  3. 最后一种选择是将你编写的类,放入需要访问的命名空间中

只要类位于不同的命名空间中,它们就可以使用相同名称

命名空间可嵌套,要嵌套命名空间,只需将一个命名空间声明括在另一个命名空间声明内即可

13.Lists and Dictionaries 列表和字典

列表就像是大小动态变化的数组。创建列表需要添加System.Collections.Generic名称空间

 using System.Collections.Generic;

首先comparable位于系统名称空间,所以需要声明我们正在使用它,接着需要声明这个类正在实现这个接口,这看起来类以于继承,我们将使用泛型lcomparable其泛型类型必须是这个类,最后为了完成comparable接口的协定我们需要声公开承数compareTo,这个承数返回整数并以我们的泛型类型BadGuy作为参数。CompareTo方法的思路是:

  • 如果从中调用这个方法的对象大于被视作参数的对象,则函数返回正数
  • 如果从中调用这个方法的对象小于被视作参数的对象,则函数返回负数
  • 如果二者等,则返回零

对于我们的承数我们先要检查传递函数的BadGuy是否存在,如果不存在则这个BadGuy较大,函数应返回数,否则函数返回两个BadGuy的幂值差

 using UnityEngine;
 using System.Collections;
 using System; //这允许 IComparable 接口
 ​
 //这是您将存储在不同集合中的类。为了使用集合的 Sort() 方法,此类需要实现 IComparable 接口。
 public class BadGuy : IComparable<BadGuy>
 {
     public string name;
     public int power;
 ​
     public BadGuy(string newName, int newPower)
    {
         name = newName;
         power = newPower;
    }
 ​
     //IComparable 接口需要此方法。
     public int CompareTo(BadGuy other)
    {
         if(other == null)
        {
             return 1;
        }
 ​
         //返回力量差异。
         return power - other.power;
    }
 }

SomeClass

 using UnityEngine;
 using System.Collections;
 using System.Collections.Generic;
 ​
 public class SomeClass : MonoBehaviour
 {
     void Start ()
    {
         //这是创建列表的方式。注意如何在
         //尖括号 (< >) 中指定类型。
         List<BadGuy> badguys = new List<BadGuy>();
 ​
         //这里将 3 个 BadGuy 添加到列表
         badguys.Add( new BadGuy("Harvey", 50));
         badguys.Add( new BadGuy("Magneto", 100));
         badguys.Add( new BadGuy("Pip", 5));
 ​
         badguys.Sort();
 ​
         foreach(BadGuy guy in badguys)
        {
             print (guy.name + " " + guy.power);
        }
 ​
         //这会清除列表,使其
         //为空。
         badguys.Clear();
    }
 }

字典的工作原理与列表类似,但它有两种类型,这表示每个元素组成一个键值对有时简称为KVP,字典的预期用途也与列表不同,列表通常用于替代需要更多灵活性或功能的数组。字典用作可通过一个或多个键访问的值的集合

首先添加System.Collections.Generic名称空间,然后像之前一样声明变量,但具有两个泛型类型,第一个类型是键,这是为了访问第二个类型,而引用的类型第二个类型是值,

 using UnityEngine;
 using System.Collections;
 using System.Collections.Generic;
 ​
 public class SomeOtherClass : MonoBehaviour
 {
     void Start ()
    {
         //这是创建字典的方式。注意这是如何采用
         //两个通用术语的。在此情况中,您将使用字符串和
         //BadGuy 作为两个值。
         Dictionary<string, BadGuy> badguys = new Dictionary<string, BadGuy>();
 ​
         BadGuy bg1 = new BadGuy("Harvey", 50);
         BadGuy bg2 = new BadGuy("Magneto", 100);
 ​
         //可以使用 Add() 方法将变量
         //放入字典中。
         badguys.Add("gangster", bg1);
         badguys.Add("mutant", bg2);
 ​
         BadGuy magneto = badguys["mutant"];
 ​
         BadGuy temp = null;
 ​
         //这是一种访问字典中值的更安全
         //但缓慢的方法。
         if(badguys.TryGetValue("birds", out temp))
        {
             //成功!
        }
         else
        {
             //失败!
        }
    }
 }

14.Coroutines 协程

协同程序可被视为按时间间隔执行的函数

Coroutine(协程)是一种轻量级的线程管理方式,它允许你编写异步代码,而不必使用多线程或多线程相关的复杂机制。协程主要用于处理那些需要在一定时间间隔后执行的任务,或者需要等待某些事件发生才能继续执行的任务。

使用Unity的协程功能来使某个游戏对象平滑地移动到目标位置

 using UnityEngine;
 using System.Collections;
 ​
 public class CoroutinesExample : MonoBehaviour
 {
   public float smoothing = 1f;
   public Transform target;
 ​
 ​
   void Start ()
  {
     //启动了一个协程,名为MyCoroutine,传入target作为参数。
     StartCoroutine(MyCoroutine(target));
  }
 ​
  //返回IEnumerator类型的协程方法
   IEnumerator MyCoroutine (Transform target)
  {
     //使用Vector3.Distance方法计算当前游戏对象和目标之间的距离,当距离大于0.05时,循环继续。
     while(Vector3.Distance(transform.position, target.position) > 0.05f)
    {
       //使用Vector3.Lerp方法使当前游戏对象的位置平滑地向目标位置移动
       transform.position = Vector3.Lerp(transform.position, target.position, smoothing * Time.deltaTime);
  //等待下一个帧,这是协程中常用的等待方法,以便每次循环结束后都能重新开始下一次循环。
       yield return null;
    }
     print("Reached the target.");
     yield return new WaitForSeconds(3f);
     print("MyCoroutine is now finished.");
  }
 }

PropertiesAndCoroutines

 using UnityEngine;
 using System.Collections;
 ​
 public class PropertiesAndCoroutines : MonoBehaviour
 {
   public float smoothing = 7f;
 ​
   // 目标位置的属性,使用自动实现的属性来简化访问
   public Vector3 Target
  {
     get { return target; }
     set
    {
       // 更新目标位置,并重启移动协程
       target = value;
       StopCoroutine("Movement");
       StartCoroutine("Movement", target);
    }
  }
 ​
   // 实际的目标位置变量
   private Vector3 target;
 ​
   // 移动协程
   IEnumerator Movement(Vector3 target)
  {
     // Distance方法计算当前游戏对象和目标之间的距离
     while (Vector3.Distance(transform.position, target) > 0.05f)
    {
       // 使用Lerp函数实现平滑移动
       transform.position = Vector3.Lerp(transform.position, target, smoothing * Time.deltaTime);
       yield return null; // 等待下一帧
    }
  }
 }

ClickSetPosition

 using UnityEngine;
 using System.Collections;
 ​
 /// ClickSetPosition 类负责在鼠标点击时,设置一个对象的位置。
 /// 它依赖于另一个脚本中的协程来实现特定的行为。
 public class ClickSetPosition : MonoBehaviour
 {
   /// coroutineScript 是一个公开的属性,它引用了另一个脚本实例,
   /// 该脚本中含有必要的协程,用于处理位置的变化。
   public PropertiesAndCoroutines coroutineScript;
 ​
   void OnMouseDown()
  {
     // 创建一个从摄像机到鼠标位置的射线
     Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
     // 用于存储射线投射的碰撞信息
     RaycastHit hit;
     // 使用射线投射来检测是否有碰撞
     Physics.Raycast(ray, out hit);
     // 检查碰撞的物体是否是当前游戏对象
     if (hit.collider.gameObject == gameObject)
    {
       // 计算新的目标位置,稍微高于点击的位置
       Vector3 newTarget = hit.point + new Vector3(0, 0.5f, 0);
       // 将新的目标位置设置到协程脚本的Target属性中
       coroutineScript.Target = newTarget;
    }
  }
 }

15.Quaternions 四元数

选择存储为四元数,类似于向量但有四个组件:X、Y、Z、W,

Unity将旋转存储为四元数,但像是Inspector要将四元数转换为欧拉角,欧拉角要遵从万向节锁,而万向节锁会妨碍增量旋转正常工作,四元数不受万向节锁的影响。

LookAtScript:

使对象始终面向另一个对象。

 using UnityEngine;
 using System.Collections;
 ​
 public class LookAtScript : MonoBehaviour
 {
     public Transform target;
 ​
 ​
     void Update ()
    {
      //计算目标Transform的位置与当前游戏对象Transform的位置之间的相对位置,
         Vector3 relativePos = target.position - transform.position;
      //然后使用Quaternion.LookRotation()函数将该相对位置转换为朝向目标的旋转。将当前游戏对象的Transform的旋转设置为该值,从而使游戏对象始终面向目标。
         transform.rotation = Quaternion.LookRotation(relativePos);
    }
 }

GravityScript:

 using UnityEngine;
 using System.Collections;
 ​
 public class GravityScript : MonoBehaviour
 {
   public Transform target;
 ​
 ​
   void Update ()
  {
  //计算目标对象与当前游戏对象的相对位置,将目标对象的位置向上偏移1.5单位。
     Vector3 relativePos = (target.position + new Vector3(0, 1.5f, 0)) - transform.position;
     //根据相对位置,计算出当前游戏对象应该朝向的旋转四元数。
     Quaternion rotation = Quaternion.LookRotation(relativePos);
  //获取当前游戏对象的局部旋转四元数。
     Quaternion current = transform.localRotation;
  //使用四元数插值方法"Slerp",在一定时间内平滑地从当前旋转向目标旋转过渡,以达到朝向目标的效果。
     transform.localRotation = Quaternion.Slerp(current, rotation, Time.deltaTime);
     //根据时间增量,沿着当前游戏对象的局部Z轴正方向移动一段距离,实现重力下落的效果。
     transform.Translate(0, 0, 3 * Time.deltaTime);
  }
 }

16.Delegates 委托

在Unity中,Delegate(委托)是一种引用方法的类型。它们可以让你在运行时动态地选择要执行的方法,这对于事件处理、回调机制等非常有用。委托可以被简单视为函数的容器,可以进行传达或像变量一样使用,区别在于变量包含数据而委托包含函数。

  • 动态绑定:可以在运行时动态地改变委托指向的方法。
  • 回调机制:可以用来创建回调函数,例如响应用户输入或其他系统事件。
  • 事件处理:Unity中的事件系统大量使用了委托,允许不同的脚本响应相同的事件。
  • 解耦:可以让代码更加模块化,减少代码间的耦合度。

在Unity中,委托通常与事件一起使用,以便多个方法可以订阅同一个事件,当事件触发时,所有订阅的方法都会被调用。这种方式在处理游戏逻辑、UI交互等方面非常有用。

DelegateScript

 using UnityEngine;
 using System.Collections;
 ​
 public class DelegateScript : MonoBehaviour
 {
     // 定义一个委托类型MyDelegate,该委托类型的方法有一个整数参数
     delegate void MyDelegate(int num);
     // 声明一个MyDelegate类型的实例变量
     MyDelegate myDelegate;
 ​
     void Start(){
         // 将PrintNum方法赋给myDelegate实例,调用myDelegate实例,传入参数50
         myDelegate = PrintNum;
         myDelegate(50);
 ​
         // 将DoubleNum方法赋给myDelegate实例调,用myDelegate实例,传入参数50
         myDelegate = DoubleNum;
         myDelegate(50);
    }
 ​
     void PrintNum(int num){
         print("Print Num: " + num);
    }
 ​
     void DoubleNum(int num){
         print("Double Num: " + num * 2);
    }
 }

MulticastScript

 using UnityEngine;
 using System.Collections;
 ​
 public class MulticastScript : MonoBehaviour
 {
     // 定义一个委托类型MultiDelegate,用于引用无参数的void方法
     delegate void MultiDelegate();
     // 声明一个MultiDelegate类型的实例变量
     MultiDelegate myMultiDelegate;
 ​
     void Start(){
         // 将PowerUp和TurnRed方法添加到myMultiDelegate的调用列表中
         myMultiDelegate += PowerUp;
         myMultiDelegate += TurnRed;
 ​
         // 检查myMultiDelegate是否不为空
         if (myMultiDelegate != null)
        {
             // 调用myMultiDelegate所引用的所有方法
             myMultiDelegate();
        }
    }
 ​
     void PowerUp(){
         print("Orb is powering up!");
    }
 ​
     void TurnRed(){
         // 修改渲染器的材质颜色为红色
         renderer.material.color = Color.red;
    }
 }

17.Attributes 属性

使用属性可以将其他行为附加到所创建的方法和变量。

 using UnityEngine;
 using System.Collections;
 ​
 public class SpinScript : MonoBehaviour
 {
    [Range(-100, 100)] public int speed = 0;
 ​
     void Update ()
    {
         transform.Rotate(new Vector3(0, speed * Time.deltaTime, 0));
    }
 }
 ​

ColorScript

 using UnityEngine;
 using System.Collections;
 ​
 [ExecuteInEditMode]
 public class ColorScript : MonoBehaviour
 {
     void Start()
    {
         renderer.sharedMaterial.color = Color.red;
    }
 }

18.Events 事件

在Unity中,事件(Event)主要用于实现不同游戏对象或组件之间的通信。事件允许一个对象向其他对象发送消息,这样就可以解耦各个部分的代码,使得它们不需要直接相互依赖就能进行交互。

在Unity中,事件通常与委托一起使用。委托定义了一个方法签名,而事件则是公开的委托字段,其他类可以通过+=操作符向事件添加方法,通过-=操作符移除方法。

例如,在Unity UI中,Button 组件有一个 onClick 事件,这是一个多播委托,你可以添加多个方法作为事件处理器。当按钮被点击时,所有注册的方法都会被调用。需要注意的是,Unity还提供了一些内置的事件系统,如 EventSystemPhysics 中的碰撞事件 (Collision, Trigger),这些都可以用来处理特定的游戏逻辑。

EventManager

 using UnityEngine;
 using System.Collections;
 ​
 public class EventManager : MonoBehaviour
 {
   // 定义一个委托类型,用于点击事件的监听器
   public delegate void ClickAction();
   // 定义一个静态事件,当点击事件被触发时,会调用所有注册了此事件的监听器
   public static event ClickAction OnClicked;
 ​
   // 在屏幕上绘制GUI,并监听点击事件
   void OnGUI()
  {
     // 如果用户点击了屏幕上的按钮
     if (GUI.Button(new Rect(Screen.width / 2 - 50, 5, 100, 30), "Click"))
    {
       // 检查是否有任何监听器注册了OnClicked事件,如果有,则调用该事件
       if (OnClicked != null)
         OnClicked();
    }
  }
 }

TeleportScript

 using UnityEngine;
 using System.Collections;
 ​
 public class TeleportScript : MonoBehaviour
 {
     // 当脚本启用时,订阅OnClicked事件
     void OnEnable()
    {
         EventManager.OnClicked += Teleport;
    }
 ​
     // 当脚本禁用时,取消订阅OnClicked事件
     void OnDisable()
    {
         EventManager.OnClicked -= Teleport;
    }
 ​
     // 当点击事件触发时,执行Teleport方法
     void Teleport()
    {
         // 获取当前物体的位置
         Vector3 pos = transform.position;
         // 随机生成一个1到3之间的数,并将其赋值给y坐标
         pos.y = Random.Range(1.0f, 3.0f);
         // 将新的位置赋值给物体的位置
         transform.position = pos;
    }
 }

TurnColorScript

 using UnityEngine;
 using System.Collections;
 ​
 public class TurnColorScript : MonoBehaviour
 {
   void OnEnable()
  {
     // 当此脚本启用时,将TurnColor方法注册为事件响应函数。当特定事件触发时,会调用TurnColor方法。
     EventManager.OnClicked += TurnColor;
  }
   void OnDisable()
  {
     // 当此脚本禁用时,从事件监听器中移除TurnColor方法。这是为了避免在脚本禁用后仍然响应事件,从而引发错误。
     EventManager.OnClicked -= TurnColor;
  }
   void TurnColor()
  {
     // 通过生成一个随机的Color值,并将其应用到对象的渲染材质上,以实现颜色的改变。
     Color col = new Color(Random.value, Random.value, Random.value);
     renderer.material.color = col;
  }
 }

THE END