委托

delegate

学完之后我对delegate的理解是:一个限制了参数和返回值的函数指针
做个比喻的话,delegate就像一个火车头,规定了后续车厢的要求,只有符合要求的车厢才能接上火车头,而且不会拉空车

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Player : MonoBehaviour {
// 定义了一个火车头,这个火车头后面只能接:“返回空值”+“接收一个字符串”的车厢
public delegate void HuoCheTou(string goods);
public static HuoCheTou train; // 定义一个全局唯一的火车头
// 比如下面三个函数(车厢),只有A是符合规则的,可以挂在HuoCheTou上
void CheXiangA(string goods){
Debug.Log("我是车厢A");
}
bool CheXiangB(string goods){
Debug.Log("我是车厢B");
return true;
}
void CheXiangC(int num){
Debug.Log("我是车厢C");
}
private void Awake() {
train += CheXiangA; // 初始化时在火车头后面接上A车厢
}
private void Start() {
// 先判断是否有事件订阅,有才执行(没有车厢挂着此次航程无意义)
// 执行时,会依次调用所有挂载在火车头后面的车厢
// 但是如果是有返回值的delegate,只会返回最后挂载的函数的值(只会返回最后一节车厢的值)
train?.Invoke("goods");

}
}

无返回值、无传参(广播)

无返回值,无传参,几乎没有限制要求,所以可以挂载任意函数(任意车厢)
在这个使用场景下,用“广播”去做比喻更加合适
例如:玩家死亡时,显示游戏结束界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Player : MonoBehaviour {
public delegate void OnPlayerDead();
public static event OnPlayerDead OnDeath; // 定义全局唯一的标志玩家死亡的事件

// 再编写一个函数,当玩家死亡时,调用所有订阅了此事件的函数
void Die() {
OnDeath?.Invoke(); // 检测是否非空,非空再执行,等价于下面注释内的写法
/*
if(OnDeath != null)
OnDeath();
*/
}
}
// 收音机
public class UIManager : MonoBehaviour {
void OnEnable() => Player.OnDeath += ShowGameOverScreen; // 当UI组件启用时,订阅玩家死亡事件
void OnDisable() => Player.OnDeath -= ShowGameOverScreen; // 取消订阅(防止内存泄漏)

// 只要玩家死亡,OnDeath就会执行下方的函数
void ShowGameOverScreen() => Debug.Log("游戏结束界面显示");
}

无返回值、有传参

使用场景例如:玩家掉血时,在玩家身边显示掉血数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Player : MonoBehaviour {
public delegate void OnPlayerDamage(int damage); // 定义一个委托,参数为int类型
public static event OnPlayerDamage OnDamage;
void TakeDamege(int damage){
if(damage <= 0) return; // 如果掉血量小于等于0,直接返回
OnDamage?.Invoke(damage); // 检测是否非空,非空再执行,等价于下面注释内的写法
}
}
public class UIManager : MonoBehaviour {
void OnEnable() => Player.OnDamage += ShowDamageNumber;
void OnDisable() => Player.OnDamage -= ShowDamageNumber;

// 只要玩家掉血,OnDamage就会执行下方的函数
void ShowDamageNumber(int damage) => Debug.Log($"玩家掉血了{damage}点");
}

有返回值

之前有提到,有返回值的delegate,只会返回最后挂载的函数的值,所以并不适合“广播”,这时候更倾向于他本身的翻译“委托”

1
2
3
4
5
6
7
8
9
10
public class ShopSystem : MonoBehaviour {
public delegate int CalFinalPrice(bool isVIP,int originPrice);
public static event CalFinalPrice OnCalFinalPrice;
// 商店想要在所有物品下面显示价格,根据是否是VIP用户,显示不同的价格
void ShowItem(){
int finalPrice = OnCalFinalPrice(isVIP,originPrice);
Debug.Log($"物品价格:{finalPrice}");
}
// 可以看到有返回值时,delegate的作用就有点像普通的函数调用
}

Action(封装无返回值的delegate)

Action是一个泛型委托,它可以表示没有返回值的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Player : MonoBehaviour {
/*
namespace System
{
// 微软提前帮你写好的无参委托
public delegate void Action();

// 微软利用泛型,帮你写好的 1个参数 的委托(T可以是int,也可以是string)
public delegate void Action<T>(T obj);

// 2个参数的组合
public delegate void Action<T1, T2>(T1 arg1, T2 arg2);

// 3个参数的组合
public delegate void Action<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3);

// ... 微软事先替我们写好了,所以只需要用就可以了
}
*/

// 1. 定义全局唯一的标志玩家死亡的事件
public static event Action OnDeath;
// 2. 带一个 int 参数:用于显示玩家掉血
public static event Action<int> takeDamageEvent;
// 3. 带一个 string 参数:例如用于播放对应名称的音效
public static event Action<string> playSoundEvent;
}

Func(封装有返回值的delegate)

Func是一个泛型委托,它可以表示有返回值的方法。

1
2
3
4
5
6
7
8
9
10
11
12
public class ShopSystem : MonoBehaviour {
// public delegate int CalFinalPrice(bool isVIP,int originPrice);
// public static event CalFinalPrice OnCalFinalPrice;
// 使用Func的话写法如下
public static event Func<bool,int,int> OnCalFinalPrice; // 最后一个参数为返回值

// 商店想要在所有物品下面显示价格,根据是否是VIP用户,显示不同的价格
void ShowItem(){
int finalPrice = OnCalFinalPrice(isVIP,originPrice);
Debug.Log($"物品价格:{finalPrice}");
}
}

Unity生命周期

Unity生命周期主要的有AwakeStartUpdateFixedUpdateLateUpdateOnEnableOnDisableOnDestroy

启动顺序

当物体勾选,开始渲染时,会按照以下顺序执行:

  1. Awake:始终在任何 Start 函数之前并在实例化预制件之后调用此函数。(如果游戏对象在启动期间处于非活动状态,也就是在Unity中未勾选,则在激活之后才会调用 Awake。)
  2. OnEnable:(仅在对象处于激活状态时调用,之前在GraphicsPanel中有应用)在启用对象后立即调用此函数。在创建 MonoBehaviour 实例时(例如加载关卡或实例化具有脚本组件的游戏对象时)会执行此调用。
  3. Start:仅当启用脚本实例后,才会在第一次帧更新之前调用 Start
  4. FixedUpdate:调用 FixedUpdate 的频度常常超过 Update。如果帧率很低,可以每帧调用该函数多次;如果帧率很高,可能在帧之间完全不调用该函数。在 FixedUpdate 之后将立即进行所有物理计算和更新。在 FixedUpdate 内应用运动计算时,无需将值乘以 Time.deltaTime。这是因为 FixedUpdate 的调用基于可靠的计时器(独立于帧率)。
  5. Update:每帧调用一次 Update。这是用于帧更新的主要函数。
  6. LateUpdate:每帧调用一次 LateUpdate__(在 Update__ 完成后)。LateUpdate 开始时,在 Update 中执行的所有计算便已完成。LateUpdate 的常见用途是跟随第三人称摄像机。如果在 Update 内让角色移动和转向,可以在 LateUpdate 中执行所有摄像机移动和旋转计算。这样可以确保角色在摄像机跟踪其位置之前已完全移动。

  1. 动画更新循环(暂不需要深究)
  2. 渲染Rendering(暂不需要深究)

  1. OnDisable:行为被禁用或处于非活动状态时,调用此函数(往往和OnEnable配对使用)
  2. OnDestroy:对象存在的最后一帧完成所有帧更新之后,调用此函数
  3. OnApplicationQuit:在退出应用程序之前在所有游戏对象上调用此函数。在编辑器中,用户停止播放模式时,调用函数

一些注意事项

  • Start永远运行在所有Awake之后运行,所以在Awake当中获取当前物体组件(GetComponent),在Start中跨物体交互,
  • AwakeStart只会运行一次,而OnEnableOnDisable会根据对象的状态变化而运行多次(所以刷新UI的内容要写在OnEnable中,先前GraphicsPanel中下拉选项的更新就是如此)
  • SetActive(false)不是销毁物体,再次启用不会运行AwakeStart,所以这是节省性能的一个途径,例如UI的隐藏子弹模型的复用