Unity官方初级中级教程 (图片问题已修复)

本文详细讲解了Unity中脚本Awake和Start的执行顺序,内存泄漏的概念与防治,以及高级特性如继承、多态、接口、委托和事件的使用。深入理解如何控制Awake函数的执行顺序,以及如何通过各种编程技术提升代码质量和性能。
摘要由CSDN通过智能技术生成

Unity官方初级教程视频

07 Awake和Start

加载脚本时调用这两个函数
首先调用Awake,即使脚本组件还未启用not enabled也没关系,适合在脚本和初始化之间设置任何引用
IMG_1423.jpeg
不启用脚本也运行Awake
Start在Awake后执行,并且在首次update前,前提是启用了脚本组件,用来启动任何所需操作,从而将初始化代码的任何部分延迟到延迟到真正需要的时候在运行

例如:敌人进入游戏,首先awake赋予弹药,当敌人想要射击,需要enable射击脚本组件,使用start在定义时间实现射击

只运行一次,不能通过禁用和重新启用来重复执行start

附加:事件执行顺序

事件函数的执行顺序 - Unity 手册
image.pngimage.png

8.Update 和 FixedUpdate

Update不按固定时间调用
任何对刚体的物理计算都应该放到FixedUpdate当中
在Visual Studio中光标的位置按下Ctrl + Shift + M可以直接创建函数方法创建的向导,直接创建对应的回调函数。
image.png

9.矢量数学

Unity采用左手系
image.png
点乘用来确定是否垂直,,等于0意味着两个向量垂直。可以用来创建飞行模拟器,如果方向和y轴垂直无阻力,上升点乘为正,爬升增大阻力。有下降夹角,俯冲。
image.png

叉乘通过两个向量计算出与他们垂直的第三个向量,AB计算C,,,A^B = C,也是左手系
image.pngimage.pngVector3.Cross(VectorA, VectorB)
例如确定坦克的转动方向,,,,B目标方向,A坦克朝向,C扭矩转轴方向
image.png

11.激活游戏对象

如果子物体激活,但是父物体没激活,,对于子物体运行时就会显示第一条True,第二条False

public class CheckState : MonoBehaviour
{
    public GameObject myObject;
    
    
    void Start ()
    {
        Debug.Log("Active Self: " + myObject.activeSelf);
        Debug.Log("Active in Hierarchy" + myObject.activeInHierarchy);
    }
}

12.Translate 和 Rotate

transform.Translate(new Vector3(0,0,1));
每帧移动1m
但是如果改为transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
就变成了每秒移动1m

14.线性插值

使用 Color.Lerp 时适用同样的原理。在 Color 结构中,颜色由代表红色、蓝色、绿色和 Alpha 的 4 个 float 参数表示。使用 Lerp 时,与 Mathf.Lerp 和 Vector3.Lerp 一样,这些 float 数值将进行插值。
在某些情况下,可使用 Lerp 函数使值随时间平滑。请考虑以下代码段:

void Update ()
{
    light.intensity = Mathf.Lerp(light.intensity, 8f, 0.5f);
}

如果光的强度从 0 开始,则在第一次更新后,其值将设置为 4。下一帧会将其设置为 6,然后设置为 7,再然后设置为 7.5,依此类推。因此,经过几帧后,光强度将趋向于 8,但随着接近目标,其变化速率将减慢。请注意,这是在若干个帧的过程中发生的。如果我们不希望与帧率有关,则可以使用以下代码:

void Update ()
{
    light.intensity = Mathf.Lerp(light.intensity, 8f, 0.5f * Time.deltaTime);
}

这意味着强度变化将按每秒而不是每帧发生。
请注意,在对值进行平滑时,通常情况下最好使用 SmoothDamp 函数。仅当您确定想要的效果时,才应使用 Lerp 进行平滑。

15.Destroy

删除特定组件。

DestroyComponent
using UnityEngine;
using System.Collections;

public class DestroyComponent : MonoBehaviour
{
    void Update ()
    {
        if(Input.GetKey(KeyCode.Space))
        {
            Destroy(GetComponent<MeshRenderer>());
        }
    }
}

16.GetButton 和 GetKey

GetKey只能输入KeyCode.xxxx,,,,因此可以在Input中定义输入名称,然后使用GetButton调用相应的string名称。

bool down = Input.GetButtonDown("Jump");

image.png

17.GetAxis

GetAxis与上面两个input有点类似,,,,但是前两者返回bool,GetAxis返回浮点值介于-1和1,,因为介于-1~1所以是一个滑尺,,,Gravity影响滑尺在按钮松开后归零的速度,Gravity越高,归零速度越快Sensitivity影响滑尺按下按钮后达到-1或1的速度,Sensitivity越高,反应速度越快。越小越流畅。如果我们使用操纵杆的话,就不希望操纵杆轻微移动的作用,为了避免这种情况使用盲区Dead,Dead越大,盲区越大。Snap按钮允许你同时按下正负按钮时归零。
Input.GetAxis(“Raw”)进返回整数不返回非整数。这是分适合需要精确控制的二维游戏。而不适用于需要平滑值的游戏,它不需要Gravity和Sensitivity
image.png

using UnityEngine;
using System.Collections;

public class AxisExample : MonoBehaviour
{
    public float range;
    public GUIText textOutput;
    
    
    void Update () 
    {
        float h = Input.GetAxis("Horizontal");
        float xPos = h * range;
        
        transform.position = new Vector3(xPos, 2f, 0);
        textOutput.text = "Value Returned: "+h.ToString("F2");  
    }
}

18.OnMouseDown

该函数及其相关函数可以检测碰撞体或GUI文本元素的点击
只有包含了OnMouseDown的脚本挂到对象身上,并且该对象添加了碰撞体,才会执行OnMouseDown
image.png

19.GetComponent

image.png存放对其他脚本的引用(对其他脚本的引用,也就是一脚本名称为类型的变量,其实我们引用的是这个脚本中定义的类的实例)。

using UnityEngine;
using System.Collections;

public class UsingOtherComponents : MonoBehaviour
{
    public GameObject otherGameObject;
    
    
    private AnotherScript anotherScript;
    private YetAnotherScript yetAnotherScript;
    private BoxCollider boxCol;
    
    
    void Awake ()
    {
        anotherScript = GetComponent<AnotherScript>();
        yetAnotherScript = otherGameObject.GetComponent<YetAnotherScript>();
        boxCol = otherGameObject.GetComponent<BoxCollider>();
    }
    
    
    void Start ()
    {
        boxCol.size = new Vector3(3,3,3);
        Debug.Log("The player's score is " + anotherScript.playerScore);
        Debug.Log("The player has died " + yetAnotherScript.numberOfPlayerDeaths + " times");
    }
}

GetComponent会占用大量的处理能力。最好是在Awake和Start中调用。或尽在首次需要时调用一次。

20.DeltaTime

基本上是指两次更新或固定更新函数调的间隔时长,,,该值可以让其他增量计算的值变得平滑。帧之间的时间不一样,所以如果用update移动不适用Time.deltaTime会时流畅时不流畅,使用Time.deltaTime相当于匀速。
这样改变的是每秒的值,而不是每帧的值。

21.数据类型

基本上属于类对象的变量都叫做引用类型。
值类型变量其实包含某个值。。。。。引用类型变量其实值存储位置的存储地址。
image.png

using UnityEngine;
using System.Collections;

public class DatatypeScript : MonoBehaviour 
{
    void Start () 
    {
        //值类型变量
        Vector3 pos = transform.position;
        pos = new Vector3(0, 2, 0);
        
        //引用类型变量
        Transform tran = transform;
        tran.position = new Vector3(0, 2, 0);
    }
}

22.类

类是一个容器,用来存储变量和函数,具备多种功能,包括将配合工作的要素结合起来。他们是有组织结构的工具,属于面向对象编程,简称OOP。核心思想是将程序拆分成多个脚本,每个脚本承担一个角色或职责。
类非常适合用来完成一项单独的工作。
使用Rigidbody类型的变量来引用prefab身上rigidbody实例化对象的地址,,,不管前面使用什么类型的变量引用,后面只要prefab上有这个类型,就可以引用prefab身上对应组件类的实例化的地址,从而进行操作。不管使不使用as xxx类型,都已经转化好了,,相当于Instantiate(T orgin,xxxx)

using UnityEngine;
using System.Collections;

public class SingleCharacterScript : MonoBehaviour
{
    public class Stuff
    {
        public int bullets;
        public int grenades;
        public int rockets;
        
        public Stuff(int bul, int gre, int roc)
        {
            bullets = bul;
            grenades = gre;
            rockets = roc;
        }
    }
      
    public Stuff myStuff = new Stuff(10, 7, 25);
    public float speed;
    public float turnSpeed;
    public Rigidbody bulletPrefab;
    public Transform firePosition;
    public float bulletSpeed;
      
    void Update ()
    {
        Movement();
        Shoot();
    }
    
    void Movement ()
    {
        float forwardMovement = Input.GetAxis("Vertical") * speed * Time.deltaTime;
        float turnMovement = Input.GetAxis("Horizontal") * turnSpeed * Time.deltaTime;
        
        transform.Translate(Vector3.forward * forwardMovement);
        transform.Rotate(Vector3.up * turnMovement);
    }
       
    void Shoot ()
    {
        if(Input.GetButtonDown("Fire1") && myStuff.bullets > 0)
        {
            Rigidbody bulletInstance = Instantiate(bulletPrefab, firePosition.position, firePosition.rotation) as Rigidbody;
            bulletInstance.AddForce(firePosition.forward * bulletSpeed);
            myStuff.bullets--;
        }
    }
}

23.Instantiate

一般而言,instantiate会返回一个名为object的类型。但为了打出子弹并给他添加作用力,需要将其强制转换为rigidbody,需要在Instantiate后加上as Rigidbody,然后将返回值放到Rigidbody类型的引用变量中
过一段时间删除物体,只需要Destroy()加个时间

using UnityEngine;
using System.Collections;

public class RocketDestruction : MonoBehaviour
{
    void Start()
    {
        Destroy (gameObject, 1.5f);
    }
}

24.数组

数组的声明需要在变量后面加上括号。数组不是变量类型,而是特定类型的变量的集合。
需要在数组后面用new输入方括号中的元素数量。
或者直接用大括号生成。

public class Arrays : MonoBehaviour
{
	int[] myIntArray1 = new int [5];
	int[] myIntArray2 = {12,76,8,937,903};
	public GameObject[] players;

	void Start()
	{
    	players = GameObject.FindGameObjectsWithTag("Player");
    }
}

25.Invoke

Invoke函数的作用是:将函数调用安排在指定延时后发生,我们可以借此构建对时间敏感的有效的方法调用系统,以秒为单位。
Invoke后面是要调用的方法名和延迟时间。,,,2s后调用SpawnObject
只有不包含参数,切换返回值为void的方法才可以用invoke调用。

using UnityEngine;
using System.Collections;

public class InvokeScript : MonoBehaviour 
{
    public GameObject target;
    
    
    void Start()
    {
        Invoke ("SpawnObject", 2);
    }
    
    void SpawnObject()
    {
        Instantiate(target, new Vector3(0, 2, 0), Quaternion.identity);
    }
}

InvokeRepeating最后一个参数表明2s后运行SpawnObject方法后,隔1s再次运行。
使用CancelInvoke停止所有Invoke,,或者特定的方法。


using UnityEngine;
using System.Collections;

public class InvokeRepeating : MonoBehaviour 
{
    public GameObject target;
    
    
    void Start()
    {
        InvokeRepeating("SpawnObject", 2, 1);
//全部停止
		CancelInvoke();
//停止特定需要停止的invoke,因为其内部传入了方法。
		CancelInvoke("SpawnObject");
    }
    
    void SpawnObject()
    {
        float x = Random.Range(-2.0f, 2.0f);
        float z = Random.Range(-2.0f, 2.0f);
        Instantiate(target, new Vector3(x, 2, z), Quaternion.identity);
    }
}

26.枚举

enum默认从0开始往上递增,,,但是可以指定数据,,例如North=1,就从1开始递增,,或者直接指定所有值。。下面East默认2.
也可以指定enum的类型不是int是short等

using UnityEngine;
using System.Collections;

public class EnumScript : MonoBehaviour 
{
    enum Directionshort {North=1, East, South=20, West=28};

        void Start () 
    {
        Direction myDirection;
        
        myDirection = Direction.North;
    }
    
    Direction ReverseDirection (Direction dir)
    {
        if(dir == Direction.North)
            dir = Direction.South;
        else if(dir == Direction.South)
            dir = Direction.North;
        else if(dir == Direction.East)
            dir = Direction.West;
        else if(dir == Direction.West)
            dir = Direction.East;
        
        return dir;     
    }
}

C#中级编程

1.创建属性

从位于类外的代码访问这个类的成员变量。使用属性的方法比较好,,,属性本身可以当作变量。并且可以封装成员变量,也称之为字段。可以控制字段的访问时间和访问方式
形式:访问修饰符 类型 变量名称(首字母相对于成员变量大写)输入花括号。输入访问器,get set关键字和花括号

using UnityEngine;
using System.Collections;

public class Player
{
    //成员变量可以称为
    //字段。
    private int experience;

    //Experience 是一个基本属性
    public int Experience
    {
        get
        {
            //其他一些代码
            return experience;
        }
        set
        {
            //其他一些代码
            experience = value;
        }
    }

    //Level 是一个将经验值自动转换为
    //玩家等级的属性
    public int Level
    {
        get
        {
            return experience / 1000;
        }
        set
        {
            experience = value * 1000;
        }
    }

    //这是一个自动实现的属性的
    //示例
    public int Health{ get; set;}
}

为什么命名使用public就可以完成的事情,必须要用属性解决?
因为:使用属性可以执行两项公共变量无法实现的操作。第一通过省略get或set将字段设置为只读或只写。如果字段是私有的没有set就无法写入,没有get就无法读取。第二可以将get或set视为函数,可以在访问其中运行其他代码或调用其他函数。例如Level没有创建对应属性,可以直接根据experience转换。
属性的另一个特点是可以自动实现。通过写一个属性public int Health{ get; set; }get set只写一个分号。
visual studio输入prop按下tab自动生成一个属性。

3.静态

静态成员是跨类的所有案例共享的成员,静态成员可直接通过类访问,而无需对类的对象进行实例化。
通常来说类内成员变量对每个类来说都是唯一的。尽管类的每个对象具有相同的变量。但值不同。
静态变量类的每个对象具有相同的变量和相同的值。一处改变其他也改变。
静态方法、成员属于类,非静态方法、成员属于类的实例。

using UnityEngine;
using System.Collections;

public class Enemy
{
    //静态变量是在类的所有实例之间
    //共享的变量。
    public static int enemyCount = 0;

    public Enemy()
    {
        //通过递增静态变量了解
        //已创建此类的多少个对象。
        enemyCount++;
    }
}
using UnityEngine;
using System.Collections;

public static class Utilities 
{
    //可以在没有类对象的情况下调用
    //静态方法。请注意,静态方法无法访问
    //非静态成员变量。
    public static int Add(int num1, int num2)
    {
        return num1 + num2;
    }
}

例如Input.GetAsix等都是静态方法。不需要实例化Input
注意:不能再静态方法内部,使用非静态成员变量。

using UnityEngine;
using System.Collections;

public class UtilitiesExample : MonoBehaviour 
{
    void Start()
    {
        //可以使用类名和点运算符
        //来访问静态方法。
        int x = Utilities.Add (5, 6);
    }
}

Static放在Class前面,整个类变成静态,并且不能创建实例化。如果想要类完全由静态成员变量和方法组成,如Input类,这样非常有用

4.方法重载

每个方法都有签名(signature),签名由方法的名称和参数列表组成。在同一个作用域内,每个方法的签名都是唯一的。
重载让方法有不同的签名。只要参数列表不同就行。
传入的参数会:1、精准匹配;2、如果不匹配进行最小转化量的方法匹配;3、没有匹配或者转化量相同抛出异常

using UnityEngine;
using System.Collections;

public class SomeClass
{
    //第一个 Add 方法的签名为
    //“Add(int, int)”。该签名必须具有唯一性。
    public int Add(int num1, int num2)
    {
        return num1 + num2;
    }

    //第二个 Add 方法的签名为
    //“Add(string, string)”。同样,该签名必须具有唯一性。
    public string Add(string str1, string str2)
    {
        return str1 + str2;
    }
}

image.png

5.通用(泛型)

泛型是一种特征,通过该特征类型可以作为参数传递给类和方法等。允许在不了解处理的数据类型是,进行一般编程。
泛型不仅限于三个参数,但是很少见使用超过三个参数,,,T只是一个占位符
image.png
不是所有类型都能处理,因此我们需要对T进行限制。使用where:进行限制,用逗号分割具体的限制
限制通常分为:1、class限制T为引用类型;2、struct限制T为值类型;3、new()确保它具有不含参数的公共构造函数;4、使用类名称表示T就代表这个类或通过多态表示T代表从中衍生的任意类;5、接口名称来表示T已实现这个接口。
这些特征适用于GenericClass,接口和方法,通过类指定泛型类型,可以影响其中的字段、属性和方法的类型。

using UnityEngine;
using System.Collections;

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

using UnityEngine;
using System.Collections;

public class SomeOtherClass : MonoBehaviour 
{
    void Start () 
    {
        SomeClass myClass = new SomeClass();

        //为了使用此方法,必须
        //告诉此方法用什么类型替换
        //“T”。
        myClass.GenericMethod<int>(5);
    }
}
using UnityEngine;
using System.Collections;

//这是一个通用类。注意通用类型“T”。
//“T”将被替换为实际类型,同样,
//该类中使用的“T”类型实例也将被替换。
public class GenericClass <T>
{
    T item;

    public void UpdateItem(T newItem)
    {
        item = newItem;
    }
}

using UnityEngine;
using System.Collections;

public class GenericClassExample : MonoBehaviour 
{
    void Start () 
    {        
        //为了创建通用类的对象,必须
        //指定希望该类具有的类型。
        GenericClass<int> myClass = new GenericClass<int>();

        myClass.UpdateItem(5);
    }
}

如何控制脚本之间的Awake执行的先后顺序

【Unity小技巧】如何控制脚本间Awake执行的先后顺序?(三种方式)_哔哩哔哩_bilibili
三种方法:
1、在Script Execution Order手动添加需要控制执行顺序的脚本,数值越小越先启动。顺序可以拖动image.png这里可以打开,也可以去Player Settings找image.png
2、直接修改meta文件的order,修改启动顺序数值,越小越靠前,和前面一样,修改完了能在面板中看到数值和顺序image.png
3、给脚本文件直接添加字段修饰启动顺序,,面板不会显示,但是会比高于1000order的先执行
image.png

内存泄漏

内存泄漏(原因、分类、危害)_Verdure的博客-CSDN博客_内存泄漏
内存泄漏:也称“存储泄漏”,用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果一直占据该内存单元,直到程序结束。
即该内存空间使用完毕之后未回收
内存泄漏形象的比喻是“操作系统可提供给所有的进程的存储空间正被某个进程榨干”,最终结果是程序运行时间越长,占用存储空间越来越多,最终用尽全部存储空间,整个系统崩溃。
“内存泄漏”是从操作系统的角度来看的。这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。由程序申请的一块内存,如果没有任何一个指针指向它,那么这块内存就泄漏了。

深入浅出再谈Unity内存泄漏
undefined - 腾讯WeTest

6.继承

protected父类有的,子类也可以访问,但外界不能访问。
为什么对象要继承MonoBehaviour,因为GameObject、Transform、Start方法、Update方法等均来源于Mono,继承Mono让我们能够访问这些特征。
继承是一种Is-A关系,,,BoxCollider是Collider
image.png
在子类继承的项中,构造函数是一个例外,因为他们对类是唯一的,不会共享。但是子类调用构造函数时,其父类的构造函数会被立刻调用,由于类可能有多个不同的构造函数,因此需要控制调用哪个基类构造函数,为此可以使用关键字base,通过在子类构造函数的参数列表后加一个冒号,显式调用基类的具体构造函数。如果不显式调用基类的构造函数,则扔会隐式调用默认构造函数。除了调用基类的构造函数,base关键字还可用来访问基类的其他成员,可以用来访问基类版本的任何内容,因为它不同于派生的版本,覆盖函数时通常会有这样的需要。
public Apple() : base(“apple”)image.png

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.");
    }
}
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");
    }
}
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();
    }
}

image.png

7.多态

多态是继承的一个特征,允许类拥有多个类型。在继承层次结构中。任何子类都可以称为父类。这表示在需要基类的时候,可用派生类来代替它。
当在游戏中需要创建一个集会包含所有群体时,不需要创建多个类型,一个包含Orc一个包含Goblin。而可以创建一个集合,让他包含所有Enemy对象,既包含Orc又包含Goblin
image.png
所有物体身上都没有Collider,但是它能检测出Box Collider,因为他们均继承于Collider
image.png
不能为需要子类的某个项提供父类。
因此使用多态,对于设计构造函数和对象引用非常有用。设计构造函数和对象引用。

ParentClass myClass = new ChildClass();
myClass.ParentMethod();

声明基类类型的对象,然后调用其中一个派生类的构造函数。这事因为变量引用需要的是基类的类型,子类的构造函数,会创建衍生类型的项。**向上转型(子类被视作父类的一个对象)**只能使用父类的可用的变量和方法,会把他们视作于父类对象中。
虚函数例外。

ChildClass myChild = (ChildClass)myClass;
myChild.ChildMethod();

为了将子类视作子类,,需要向下转型子类变量恢复为子类类型,,,再强制转换。

8.成员隐藏

通过继承,父类的成员在子类中自动可用或继承到子类中。在子类中重新创建即重新声明父类成员的过程被称作成员隐藏。
隐藏成员使用关键字new的方式略有不同。为了隐藏基类的成员,应在成员的类型前面使用new声明子类成员,一般情况下,这不会影响以这种方式声明的成员的使用。但是当子类向上转型为父类和使用了成员时,他将是来自父类的成员。尽管实例为子类。例如所有Orc Goblin都用向上转型,他们调用Yell都是Humanoid

public class ChildClass : ParentClass
{
	new float SomeValue = 5f;
	void SayHello()
	{

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

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

public class Orc : Enemy
{
    //这会隐藏 Enemy 版本。
    new public void Yell()
    {
        Debug.Log ("Orc version of the Yell() method");
    }
}
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();
    }
}

1:方法重写:就是在基类中的方法用virtual关键字来标识,然后在继承类中对该类进行重写(override),这样基类中的方法已经被重写了,已经失去了功能了。当让基类的对象的引用直接指向继承类的对象时(多态性),调用该方法则是调用的继承类的方法。
2:方法隐藏:无论基类中的方法是否用了virtual关键字,继承类中都可以用new关键字(如果不用new的话,不会产生错误,但会生成一个编译警告)将基类中的方法隐藏,所谓隐藏就是隐藏,不像重写,重写就是原来的(基类中)已经不存在了,而隐藏是原来的还存在。所以当让基类的对象的引用直接指向继承类的对象时(多态性),调用该方法则是调用的基类的方法。

9.覆盖\重写

更改子类中的父类方法,结果是当我们调用方法时,将调用最新版本的方法或最新覆盖的方法,使用继承层次结构时,通常想要使用与基类不同的函数版本。
使用virtual和override关键字。父类写virutal,子类写override,,声明为virtual的任何方法可以被任何子类覆盖。
如果希望能够为方法添加特定的功能,但又不失去基类的原始功能,为此,需要使用base关键字。来同时调用父类的版本

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.");
    }
}
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.");
    }
}

10.接口

实现接口的类必须实现其所有方法和属性。作为交换,通过使用多态其他类可将实现类视作接口。
接口不是类,不能有自己的实例。
image.png接口通常在类外部声明。声明接口时,通常对每个接口使用一个脚本,通常接口以大写字母I开头。接口通常描述实现类具备的某种功能,因此通常接口后缀为able
实现IKillable的接口的任何类,必须有一个与这个签名匹配的公共函数,IDamageable接口具有泛型类型T,表示这个接口中的任意内容都可以具有泛型类型。类实现具有泛型类型的接口时必须选中这个类型。
为了实现这个接口,类必须公开声明这个接口的存在的所有方法属性和索引器
接口的优势:允许跨多个类定义通用功能(例如不同的敌人、player功能不同,,但是都可以继承Ikillable,,,,,,Wall和Car类型完全不同,但都可以实现IDamageable功能的接口,但是不能继承于同一个类),因此可以根据类实现的接口安全地对类的用途做出假设。
类实现多个接口加逗号就行。
为什么要这么做,因为可以实现多个接口,但是不饿能继承多个类

using UnityEngine;
using System.Collections;

//这是只有一个必需方法的基本
//接口。
public interface IKillable
{
    void Kill();
}

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

public class Avatar : MonoBehaviour, IKillable, IDamageable<float>
{
    //IKillable 接口的必需方法
    public void Kill()
    {
        //执行一些有趣操作
    }

    //IDamageable 接口的必需方法
    public void Damage(float damageTaken)
    {
        //执行一些有趣操作
    }
}

11.扩展方法

通过扩展方法,可以向类型添加功能而不必创建DriveType或改变原始类型,他们非常适用于向类添加功能,但不能编辑类的情况,,,例如transform添加新的方法,我们无法访问它的源码
扩展方法必须放在非泛型静态类中。常见做法是专门创建一个类来包含他们,扩展方法的用法与实例方法类似,他们也声明为静态方法。要使函数成为扩展方法而非静态方法,需要在第一个参数中使用this关键字,第一个参数将是调用对象(因为static类没有实例,所以只能是调用这个函数的类的实例),因此当我们调用这个函数时无需提供这个参数,此外第一个参数规定了这个方法属于哪个类。如果需要更多参数,可以直接输入,而不使用this
为了使用这个方法,只需要把他视为所扩展类的成员,也就是transform的默认成员。
image.png

public static class ExtensionMethods
{
	public static void ResetTransformation(this Transform trans)
    {
        trans.position = Vector3.zero;
        trans.localRotation  = Quaternion.identity;
        trans.localScale = new Vector3(1,1,1);
    }
}

调用此方法的Transform对象会自动作为第一个参数传入,因此后面的多参数不需要再写this关键字了。

using UnityEngine;
using System.Collections;

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

12.命名空间

命名空间是类的容器。其目的是组织脚本,避免脚本之间发生冲突。
使用using表示其后面的命名空间中的任何内容都可在脚本中使用。
如果类位于不同命名空间中,可以名字相同,但是文件夹不能相同。
命名空间可以嵌套。。。using System.Collections;

using UnityEngine;
using System.Collections;

namespace SampleNamespace
{
    public class SomeClass : MonoBehaviour 
    {
        void Start () 
        {

        }
    }
}

13.列表和字典

列表类似大小动态调整的数组
列表的最强大的函数之一是Sort,可用于按给定类型的任何变量对这个类型的列表进行排序它依赖于类型来实现IComparable接口(需要使用system命名空间),T的泛型类型必须是这个类
CompareTo方法的思路:如果从中调用这个方法的对象大于被视作参数的对象,则函数返回正数,如果从中调用这个方法的对象小于被视作参数的对象,则函数返回负数。如果二者相等则返回零。定义一个对象是否大于另一个对象由程序员决决定

using System;
public class BadGuy : IComparable<BadGuy>
{
    public string name;
    public int power;

    public BadGuy(string newName, int newPower)
	{
    	name = newName;
    	power = newPower;
	}

	//实现接口的函数
	public int CompareTo(BadGuy other)
	{
    	if(other == null)
    		return 1;

    	//返回力量差异
    	return power - other.power;
	}
}

如果不确定字典中是否有某个值,就是用TryGetValue,尽快更安全,但速度相对于直接引用具体键更慢

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.协程

协程可以认为是按时间间隔执行的函数,需要与yield语句搭配使用。yield语句从函数中返回代码执行,然后当函数执行时,将从上次停止的地方开始执行。协程返回类型IEnumerator,这表示函数可以返回实现IEnumerator接口的任意内容,
yield return null表示在执行这行代码时,将产生函数执行并返回空的IEnumerator,代码将在返回值指示的时间从这个点继续执行。由于返回值null,这表示为协程将在下一个更新后继续。。。由于协程将在循环结束时继续,因此将重新评估循环条件。

using UnityEngine;
using System.Collections;

public class CoroutinesExample : MonoBehaviour
{
    public float smoothing = 1f;
    public Transform target;


    void Start ()
    {
        StartCoroutine(MyCoroutine(target));
    }


    IEnumerator MyCoroutine (Transform target)
    {
        while(Vector3.Distance(transform.position, target.position) > 0.05f)
        {
            transform.position = Vector3.Lerp(transform.position, target.position, smoothing * Time.deltaTime);

            yield return null;
        }

        print("Reached the target.");
    	//返回WaitForSeconds类的实例,在给定数秒之后代码将继续执行。
        yield return new WaitForSeconds(3f);

        print("MyCoroutine is now finished.");
    }
}

协同程序真正的优势是与属性结合时发挥的作用。

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)
    {
        while(Vector3.Distance(transform.position, target) > 0.05f)
        {
            transform.position = Vector3.Lerp(transform.position, target, smoothing * Time.deltaTime);

            yield return null;
        }
    }
}
using UnityEngine;
using System.Collections;

public class ClickSetPosition : MonoBehaviour
{
    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);
            coroutineScript.Target = newTarget;
        }
    }
}

image.png
Target是环境挂载ClickSetPosition脚本。。。
效果就是当点击到了环境物体的目标位置,机器人自己移动过去,而不需要update

15.四元数

不用欧拉角是因为万向锁,image.png
但是如果我们的飞机俯仰角到达±90°时,你会发现此时绿色代表的滚转运动和蓝色代表的偏航运动他们的旋转轴重合了,这时候你必须要改变最里面自转轴的角度才能够达到你需要的空间位置,而这是违背陀螺定轴性规律的,所以下图陀螺仪中运动的那个方向其实是被锁住了的,你在俯仰角达到±90°时就不可能有这个方向的运动,这是因为当你俯仰角达到±90°时,你改变了第三个要旋转的轴的方向,它与你第一次旋转的Z轴重合了,所以在空间中失去了一个自由度:
image.png
image.png
https://zhuanlan.zhihu.com/p/346718090
自己拿一个手机来做一下试验,有助于理解万象锁:
可以拿出手机放在桌面上,屏幕朝上,手机的最长边垂直与桌子的边缘设置为X轴,这个时候屏幕的短边平行于桌子的边缘设置为Y轴,因此垂直与屏幕的向量为Z轴。我们先绕手机的最长边X轴顺时针旋转30度,这个时候手机离开桌面,留下一个长边与桌子接触;然后再绕Y轴,也就是手机的短边旋转90度,让屏幕面与桌子的边缘平行;再绕Z轴旋转10度,也就是绕垂直于屏幕的轴旋转10度,这个时候你会发现,绕Z轴旋转时,屏幕面还是平行桌子的边缘,而此时绕Z轴旋转的角度给手机姿态带来的影响和最开始旋转X轴给手机姿态带来的影响是一样的——都是使手机最终的姿态(已经绕Y轴旋转了90度使得手机屏幕与桌子边缘平行)为绕着垂直于屏幕的轴旋转一定的角度。你完全可以不用绕Z轴旋转,通过调节绕X轴旋转的角度数,使得最终手机的姿态和上述旋转过程达到的姿态一样。此时就是造成了万向锁。
“你完全可以不用绕Z轴旋转,通过调节绕X轴旋转的角度数,使得最终手机的姿态和上述旋转过程达到的姿态一样。此时就是造成了万向锁。”,我觉得这才是“丢失一个自由度”的合适理解。

using UnityEngine;
using System.Collections;

public class LookAtScript : MonoBehaviour 
{
    public Transform target;


    void Update () 
    {
        Vector3 relativePos = target.position - transform.position;
    	//让物体z轴朝向LookRotation看向的目标位置,有一个重载,输入vector3指定哪个方向朝上
        transform.rotation = Quaternion.LookRotation(relativePos);
    }
}

slerp函数是球形插值的简称,与lerp相似,lerp是线性插值,lerp在两个四元数之间均匀插值,slerp在曲线上插值,slerp中间块然后越来越慢

using UnityEngine;
using System.Collections;

public class GravityScript : MonoBehaviour 
{
    public Transform target;


    void Update () 
    {
        Vector3 relativePos = (target.position + new Vector3(0, 1.5f, 0)) - transform.position;
        //只计算了面朝target的rotation,并没有朝向他   
		Quaternion rotation = Quaternion.LookRotation(relativePos);

        Quaternion current = transform.localRotation;
    	//平滑调整面朝目标
        transform.localRotation = Quaternion.Slerp(current, rotation, Time.deltaTime);
    	//相当于每次转向一点后,都沿着转向后的自身的z轴平移一点,然后计算rotation转向,再平移
    	//实际效果等价于围着一个点转,形成重力的感觉,轨道效果
        transform.Translate(0, 0, 3 * Time.deltaTime);
    }
}

image.png
identity是无旋转或者欧拉角000的状态。
绝对不能单独改变四元数的某一个值。

16.委托

委托可以看作函数的容器,可以进行传递或像变量一样使用,与变量一样可以向委托分配值,这些值可以在运行时更改,区别在于变量包含数据而委托包含函数
脚本中,首先要做的是清除委托模板,这个模板将明确指示可以分配给委托哪些类型的方法。
用delegate关键字创建委托,后面是委托的签名。委托有返回类型、名称和参数列表。
这可以让我们在程序中更好的动态控制函数的调用

//要将方法分配给MyDelegate,它的返回值必须为void,并接受单个整型参数
delegate void MyDelegate(int num);
using UnityEngine;
using System.Collections;


public class DelegateScript : MonoBehaviour 
{    
    delegate void MyDelegate(int num);
    //创建委托类型后声明成员变量myDelegate
    //这个成员变量拥有刚刚创建的委托的类型
    MyDelegate myDelegate;
    
    
    void Start () 
    {
        //将方法分配给myDelegate变量
        myDelegate = PrintNum;
        //myDelegate直接当做函数用了
        myDelegate(50);
        //myDelegate重新指向DoubleNum
        myDelegate = DoubleNum;
        //myDelegate就相当于DoubleNum的引用了
        myDelegate(50);
        
        myDelegate = PrintNum;
        myDelegate += DoubleNum;//多播委托
    }
    //脚本底部的两个方法都是void和单整型参数
    void PrintNum(int num)
    {
    	print ("Print Num: " + num);
    }
    
    void DoubleNum(int num)
    {
    	print ("Double Num: " + num * 2);
    }
}

C#中的委托类似于C或C++中的函数指针。使用委托使程序员可以将方法引用封装在委托对象内。然后可以将该委托对象传递给可调用所引用方法的代码,而不必在编译时知道将调用哪个方法。与C或C++中的函数指针不同,委托是面向对象、类型安全的,并且是安全的。
_委托还允许多播,多播允许单个委托变量同时代表多个方法,_可以用来叠加方法,使用+=添加方法,使用-=移除方法
不要在分配函数前调用委托,未分配方法前myDelegate值为null,最好确定myDelegate != null再运行

using UnityEngine;
using System.Collections;

public class MulticastScript : MonoBehaviour 
{
    delegate void MultiDelegate();
    MultiDelegate myMultiDelegate;


    void Start () 
    {
        myMultiDelegate += PowerUp;
    	myMultiDelegate += TurnRed;
    
        if(myMultiDelegate != null)
        {
            //一次调用多播委托,同时执行两个方法
            //可以用来叠加方法
            myMultiDelegate();
        }
    }

    void PowerUp()
    {
        print ("Orb is powering up!");
    }

	void TurnRed()
    {
        renderer.material.color = Color.red;
    }
}

17.属性

通过属性可以在声明方法、变量或类时为其附加信息
例如不想时刻检测变量值,就给变量添加范围属性[Range(1, 20)]在要添加属性的变量前或者上面,通常不会影响其他数据,会使变量变成滑块
[ExecuteInEditMode]属性使关键脚本运行即使场景未处于运行模式,放在class前将应用于脚本的将应用于脚本中的所有代码,,,需要注意这样不安全,因为在非运行时运行脚本删除了物体是无法重生的,修改的数据也无法恢复,永久的。。下面的脚本如果执行,那么将意味着挂在脚本的物体的材质被永久改成了红色。

using UnityEngine;
using System.Collections;

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

18.事件

event是一种特殊的委托,非常适用于提醒其他类发生了某件事,与多播委托很相似
时间可以被看作是广播系统,对事件感兴趣的任何类都可以订阅到事件
事件和委托就是函数引用,和变量引用类型一样,当一个事情发生时,通常对变量直接修改产生变化,同样可以让一个函数引用指向一些方法让方法执行产生效果,我们只需要让这个函数引用执行即可,而不需要操心指向的方法具体是什么,这个过程在订阅时就处理好了。
event只能+= -=
首先创建delegate委托类型,接受void的无参方法订阅,然后使用委托创建事件变量(使用event关键字)这也是个static静态变量,因此可以在累外部使用而无需实例化这个类的对象,OnClicked是上面的委托类型。这个事件变量只负责在发生相应情况下调用事件。
不使用的函数必须退订,否则可能造成内存泄漏或游戏出错,OnEnable和OnDisable一起写,用来订阅退订

using UnityEngine;
using System.Collections;

public class EventManager : MonoBehaviour 
{
    public delegate void ClickAction();
    //为什么要使用静态事件event变量?而非公开委托delegate变量?
    //可以用公开委托变量实现相同的事件功能,事件只是特殊的委托
    //原因是事件具有内在安全性只能+= -=,而委托变量没有
    //通过事件其他类只能订阅或退订。
    //而公开委托变量其他类可能会调用或覆盖委托变量来执行不合理操作
    public static event ClickAction OnClicked;
    
    
    void OnGUI()
    {
        if(GUI.Button(new Rect(Screen.width / 2 - 50, 5, 100, 30), "Click"))
        {
            //如果没有订阅者将报错
            if(OnClicked != null)
            	OnClicked();
        }
    }
}
using UnityEngine;
using System.Collections;

public class TeleportScript : MonoBehaviour 
{
    //启用或创建这个脚本关联的物体时调用
    void OnEnable()
    {
        //订阅
        EventManager.OnClicked += Teleport;
    }
    
    //场景中某个对象禁用或被销毁时调用
    void OnDisable()
    {
        //退订
        EventManager.OnClicked -= Teleport;
    }
    
    //无参数且void返回与委托一样
    void Teleport()
    {
        Vector3 pos = transform.position;
        pos.y = Random.Range(1.0f, 3.0f);
        transform.position = pos;
    }
}
using UnityEngine;
using System.Collections;

public class TurnColorScript : MonoBehaviour 
{
    void OnEnable()
    {
        EventManager.OnClicked += TurnColor;
    }


    void OnDisable()
    {
        EventManager.OnClicked -= TurnColor;
    }


    void TurnColor()
    {
        Color col = new Color(Random.value, Random.value, Random.value);
        renderer.material.color = col;
    }
}

EventManager只需要留意事件本身和事件触发器,它不需要了解订阅函数本身
同样订阅函数之间也无需互相了解其内容
从而创建一个可靠且灵活的广播系统

19. Action

相当于封装了delegate并且是泛型T
因此直接public static event Action 事件名。
让需要订阅的类订阅事件名,广播的类当某一下情况下广播(即调用函数Callxxxxxxx),如果Callxxxx内部判断事件不为空就传入参数执行。

//用于确认物品是否被选中
public static event Action<ItemDetails, bool> ItemSelectedEvent;
public static void CallItemSelectedEvent(ItemDetails itemDetails, bool isSelected)
{
    ItemSelectedEvent?.Invoke(itemDetails, isSelected);
}

附加:Unity ref 和out、 params的使用

Unity ref 和out、 params的使用_长风颇浪的博客-CSDN博客_unity out

ref是C#的参数引用传递, 不需要执行完这个方法就可以改变参数的值,或者想要改变的参数值比较多的时候使用ref,ref需要初始化

public void UpdateScore(ref int  score)
{
    score = UnityEngine.Random.Range(80, 100);
}

void Start()
{
    int myscore = 60;
    UpdateScore(ref myscore);
    print(myscore);
}

image.png
**out也是参数传递(ref有进有出,out只出不进),在没有返回值的函数就能返回值,例如在返回值是void的函数就能返回,还有就是我们经常使用的都是返回一个值的,那么我们如果想在一个函数中返回多个值呢?那么out就起作用了,我们可以使用out返回多个值 **

public void GetScore(out int score)
{
    score = UnityEngine.Random.Range(80, 100);
}

void Start()
{
    int score;
    GetScore(out score);
    print(score);
}

image.png
params:我们以往传递数组的时候,是不是不是这样传递的,params是为动态数组而准备的,我们直接输入数组的元素就行了,如果不加params,我们只能向后面那样调用,new数组放进去

public void Getdd(params int[] pp)
{
    foreach (var p in pp)
    {
        print(p);
    }
}

void Start()
{
   Getdd(1,2,3);
}

image.png

public void Getdd(int[] pp)
{
    foreach (var p in pp)
    {
        print(p);
    }
}

void Start()
{
   Getdd(new []{1,2,3});
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值