using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public delegate void ChangeColor(Color newColor);
// delegate 可以理解为一种变量,用于保存 method
// 后面的 void 是对应 method 的类型,这里的 method 需要传入一个颜色参数
public ChangeColor onColorChange;
// 这里我们设定一个变量,这个变量的类型就是 ChangeColor
public delegate void MethodCompleted();
// 这个方法没有输入参数
public MethodCompleted methodCompleted;
// Start is called before the first frame update
void Start()
{
onColorChange = AlterColor;
// 形式匹配后,我们就可以给 delegate 的变量赋值了
// 形式不匹配会报错,比如 onColorChange = Task; 就不行
onColorChange(Color.black);
// 调用的方式和 method 相同
methodCompleted = Task;
// 同样,这里的形式也是匹配的,所以可以赋值
methodCompleted();
// 调用
}
// Update is called once per frame
void Update()
{
}
public void AlterColor(Color newColor) // 这里的形式要和前面统一,需要有同样类型的传入参数
{
Debug.Log("Change color to: " + newColor.ToString());
}
public void Task()
{
Debug.Log("Task completed!");
}
}
这种用法看起来不实用啊,直接调用 method 不好吗?
其实我们要用的是多重赋值,即 multicast:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public delegate void ChangeColor(Color newColor);
// delegate 可以理解为一种变量,用于保存 method
// 后面的 void 是对应 method 的类型,这里的 method 需要传入一个颜色参数
public ChangeColor onColorChange;
// 这里我们设定一个变量,这个变量的类型就是 ChangeColor
public delegate void MethodCompleted();
// 这个方法没有输入参数
public MethodCompleted methodCompleted;
// Start is called before the first frame update
void Start()
{
onColorChange = AlterColor;
// 形式匹配后,我们就可以给 delegate 的变量赋值了
// 形式不匹配会报错,比如 onColorChange = Task; 就不行
onColorChange(Color.black);
// 调用的方式和 method 相同
methodCompleted += Task1;
methodCompleted += Task2;
methodCompleted += Task3;
methodCompleted += Task1;
methodCompleted -= Task2; // 移除
if (methodCompleted != null)
{
methodCompleted();
}
// 调用 invoke
// 调用之前要记住先赋值,否则是 null 的话会报错
}
// Update is called once per frame
void Update()
{
}
public void AlterColor(Color newColor) // 这里的形式要和前面统一,需要有同样类型的传入参数
{
Debug.Log("Change color to: " + newColor.ToString());
}
public void Task1()
{
Debug.Log("Task1 completed!");
}
public void Task2()
{
Debug.Log("Task2 completed!");
}
public void Task3()
{
Debug.Log("Task3 completed!");
}
}
这样我们就做到了 method 的堆叠!
想象一下,比如在做图像数据增强处理,首先我们要把图像随机裁剪,然后随机转换颜色,最后要统一图像大小。
这样我们需要设计 3 个 method,以后调用的时候需要用一个大的 method (比如 imgProcession)包含这 3 个小的 method,然后进行统一调用。
这里 delegate 的方法就是做到了 method 的打包,而且小 method 的顺序调换也是非常便利的。
什么是 event 事件
任务说明:
创建 Main 脚本,挂载到 Main Camera 下面:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public delegate void ClickAction(); // 创建一个 delegate 类
public static event ClickAction click; // 创建一个 event 变量
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
public void ChangeColor()
{
if (click != null)
{
click();
}
}
}
在 Button 下面的 On Click() 下面点击 + 号。
把 Main Camera 拖拽到 Object 中,在 Function 中选择 Main.ChangeColor 方法,即表示点击了 Button 以后,运行 Main Camera 游戏对象下 Main 脚本中的 ChangeColor 方法。
创建一个脚本 Cube 挂载到 3 个 Cube 下面:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Cube : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Main.click += ChangeColor;
}
// Update is called once per frame
void Update()
{
}
public void ChangeColor()
{
this.gameObject.GetComponent().material.color = Color.red;
}
}
Cube 下面的脚本是独立的,不依赖于 Main,3 个之间也不相互依赖。Cube 的脚本会监听 Main 中的 broadcast 情况,完成对应的动作。
传统的做法是,Main 控制一切,所以需要载入 3 个 cube,然后控制颜色的改变,如果不是 3 个而是更多,这样的做法显然会有问题,比如 for 循环需要很长时间等。
这里我们让所有的游戏对象直接对按下按钮这个动作进行监听,只要监听到了,即自己执行对应动作(同样 cube 也不需要接入 Main Camera,两者是隔离的)。
这里注意:event 是一种特殊的 delegate,所以用 delegate 可以达到同样效果,但是 event 的好处在于,调用 method 仅允许通过 delegate 进行,而不能直接调用。
另外,堆叠到 event 的 method 在完成使命后记得退出,以避免报错:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Cube : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Main.click += ChangeColor;
}
// Update is called once per frame
void Update()
{
}
public void ChangeColor()
{
this.gameObject.GetComponent().material.color = Color.red;
}
private void OnDisable()
{
Main.click -= ChangeColor; // 这里设置退出,以避免程序报错问题
}
}
在 Main 脚本中,我们有一个 event 叫做 click,这个变量是基于 ClickAction 这个 delegate 所创建:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public delegate void ClickAction(); // 创建一个 delegate 类
public static event ClickAction click; // 创建一个 event 变量
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
public void OnClick() // 点击按钮以后,会自动执行 Main 脚本下的这个函数
{
if (click != null) // 如果不为空
{
click(); // 则运行该事件
}
}
}
Cube 下面的脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Cube : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Main.click += ChangeColor;
// 为事件添加元素
// 具体的元素内容其实与 Main 脚本无关,比如这里的 ChangeColor 方法是在本脚本中定义
}
// Update is called once per frame
void Update()
{
}
public void ChangeColor()
{
this.gameObject.GetComponent().material.color = Color.red;
}
private void OnDisable()
{
Main.click -= ChangeColor; // 这里设置退出,以避免程序报错问题
}
}
现在创建一个 Sphere,需要实现的是,点击按钮后,该球自由下落
首先需要为 Sphere 添加一个 Rigidbody,然后把 Use Gravity 的勾去除,添加同名脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Sphere : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Main.click += Fall;
// 为事件添加元素,该元素由本脚本中定义
// 这里我们不用接入 Main 脚本挂载的游戏对象去获取按钮的点击情况
// 只需要为 Main 脚本中的事件添加对应的元素即可!
}
// Update is called once per frame
void Update()
{
}
public void Fall() // 该方法用于激活重力选项,这个方法的形式需要和 event 定义的形式相同,即这里无需输入参数
{
this.gameObject.GetComponent().useGravity = true;
}
}
这里我们可以发现,球和方块都对应按钮进行各自的设定动作,但是却互不关联,也互不干扰。
建立 Main 脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public delegate void ClickAction(); // 创建一个 delegate 类
public static event ClickAction clickSpace;
// 创建一个 event 变量
// 设定为 static 后,不需要进行实例化
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.Space)) // 按下空格键
{
clickSpace(); // 执行事件
}
}
}
创建 cube 及其脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Cube : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Main.clickSpace += ChangePosition;
// 为事件添加元素
// 具体的元素内容其实与 Main 脚本无关,比如这里的 ChangeColor 方法是在本脚本中定义
}
// Update is called once per frame
void Update()
{
}
public void ChangePosition()
{
this.transform.position = new Vector3(2, 3, 1);
}
private void OnDisable()
{
Main.clickSpace -= ChangePosition; // 这里设置退出,以避免程序报错问题
}
}
创建 sphere 及其脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Sphere : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Main.clickSpace += ChangeColor;
// 为事件添加元素,该元素由本脚本中定义
// 这里我们不用接入 Main 脚本挂载的游戏对象去获取按钮的点击情况
// 只需要为 Main 脚本中的事件添加对应的元素即可!
}
// Update is called once per frame
void Update()
{
}
public void ChangeColor()
{
this.gameObject.GetComponent().material.color = Color.blue;
}
private void OnDisable()
{
Main.clickSpace -= ChangeColor; // 这里设置退出,以避免程序报错问题
}
}
实践由 event 驱动的编程
游戏说明:
创建一个 cube 命名为 Player,挂载同名脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Death(); // 按下空格键代表死亡,调用对应的方法
}
}
public void Death() // 死亡后执行
{
GameObject.FindObjectOfType().ResetPlayer();
// 接入 GameManager 并执行其下面的 ResetPlayer 方法
GameObject.FindObjectOfType().UpdateText();
// 接入 UIManager 并执行其下面的 UpdateText 方法
}
}
创建 UI,Text,命名为 DeathCountText,在 Canvas 上挂载 UIManager 脚本,并把 DeathCountText 游戏对象拖拽赋值到 UIManager 脚本下的 Death Count Text 格子中:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // 需要引入 UI 的库
public class UIManager : MonoBehaviour
{
public int deathCount;
public Text deathCountText;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
public void UpdateText() // 更新文本信息
{
deathCount++; // 死亡次数增加
deathCountText.text = "Death count: " + deathCount; //显示
}
}
创建空的游戏对象,命名为 GameManager,挂载同名脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
public void ResetPlayer()
{
Debug.Log("Rest Player!");
}
}
这种编程方法的问题在于,Player 脚本需要接入 GameManager 和 UIManager 的脚本,这对于模块化编程和代码复用来说不是好事。
所以更好的选择应该是 Player 应该是独立的,那就要用到 delegate 和 event:
首先修改 Player 的脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
public delegate void OnDeath(); // 设定委托
public static event OnDeath onDeath; // 建立事件
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Death(); // 按下空格键代表死亡,调用对应的方法
}
}
public void Death() // 死亡后执行
{
/*
GameObject.FindObjectOfType().ResetPlayer();
// 接入 GameManager 并执行其下面的 ResetPlayer 方法
GameObject.FindObjectOfType().UpdateText();
// 接入 UIManager 并执行其下面的 UpdateText 方法
*/
if (onDeath != null) // 如果这个事件中被添加了元素
{
onDeath(); // 执行该事件
}
}
}
然后是 GameManager:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
public void ResetPlayer()
{
Debug.Log("Rest Player!");
}
public void OnEnable() // 游戏对象启用并处于激活状态的时候调用该函数
{
Player.onDeath += ResetPlayer;
// 为事件添加元素
// 注意这里必须使用 +=,使用 = 会报错
// 在游戏启动的时候,为 Player 的死亡事件添加了一个 ResetPlayer 的元素
}
}
然后是 UIManager:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // 需要引入 UI 的库
public class UIManager : MonoBehaviour
{
public int deathCount;
public Text deathCountText;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
public void UpdateText() // 更新文本信息
{
deathCount++; // 死亡次数增加
deathCountText.text = "Death count: " + deathCount; //显示
}
public void OnEnable() // UI激活的时候
{
Player.onDeath += UpdateText;
// 为事件添加元素
// 这里表示游戏开始的时候,为 Player 死亡这个事件增加一个 UpdateText 的操作
}
}
建立 Player 脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
public delegate void OnDamageReceived(int currentHealth);
public static event OnDamageReceived onDamage;
public int Health { get; set; } // 设定一个血量的 property
// Start is called before the first frame update
void Start()
{
Health = 10; // 初始化为 10 格血
}
// Update is called once per frame
void Update()
{
}
void Damage() // 收到伤害
{
Health--; // 扣血
if (onDamage != null)
{
onDamage(Health);
}
}
}
建立 UIManager 脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // 需要引入 UI 的库
public class UIManager : MonoBehaviour
{
public void UpdateHealth(int health)
{
Debug.Log("Current Health: " + health);
}
public void OnEnable()
{
Player.onDamage += UpdateHealth;
}
}
以上是用 event 的方法,我们可以改用 action:
修改 Player 脚本,首先要添加 System 库:
using System; // 这个库让我们可以使用 action
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
/*
public delegate void OnDamageReceived(int currentHealth);
public static event OnDamageReceived onDamage;
*/
public static Action onDamage;
// 改成 action 后仅需 1 行代码
// 如果这里没有传入参数,则 <> 中空着就行
public int Health { get; set; } // 设定一个血量的 property
// Start is called before the first frame update
void Start()
{
Health = 10; // 初始化为 10 格血
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.Space)) // 假设按下空格键代表受到伤害
{
Damage();
}
}
void Damage() // 收到伤害
{
Health--; // 扣血
if (onDamage != null)
{
onDamage(Health);
}
}
}
使用 Action 和 event 是完全一样的效果,代码更简洁明了!
什么是 functional delegate
任务说明:
传统的方法是,创建 Main 脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
string s = "Good";
int l = GetLenth(s);
Debug.Log("Lenth: " + l);
}
// Update is called once per frame
void Update()
{
}
int GetLenth(string s)
{
return s.Length;
}
}
改用 delegate:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public delegate int WordLenth(string text);
// 输入和输出的类型要和下面的 GetLenth 方法匹配
WordLenth wl;
// 实例化一个 delegate
// Start is called before the first frame update
void Start()
{
wl = GetLenth; // 初始化赋值
Debug.Log("Lenth: " + wl("GOOOD")); // 使用起来和直接调用方法一样
}
// Update is called once per frame
void Update()
{
}
int GetLenth(string s)
{
return s.Length;
}
}
要使用 functional delegate,需要添加 system 库:
using System; // 使用 Func<> 需要这个库
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public Func WordLenth;
// 这里表示传入的是 string 类型,输出的是 int 类型
// 不再需要设定 delegate,也不需要进行实例化
// Start is called before the first frame update
void Start()
{
WordLenth = GetLenth; // 初始化赋值
Debug.Log("Lenth: " + WordLenth("GOOOD")); // 使用起来和直接调用方法一样
}
// Update is called once per frame
void Update()
{
}
int GetLenth(string s)
{
return s.Length;
}
}
看之前的案例:
using System; // 使用 Func<> 需要这个库
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public Func WordLenth;
// 这里表示传入的是 string 类型,输出的是 int 类型
// 不再需要设定 delegate,也不需要进行实例化
// Start is called before the first frame update
void Start()
{
WordLenth = GetLenth; // 初始化赋值
Debug.Log("Lenth: " + WordLenth("GOOOD")); // 使用起来和直接调用方法一样
}
// Update is called once per frame
void Update()
{
}
int GetLenth(string s)
{
return s.Length;
}
}
不需要返回值,那么我们可以使用 void 类型,对应的就是 Action(反之,如果需要返回值,那我们使用 Func):
using System; // 使用 Func<> 需要这个库
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public Action GetSum;
// 不需要返回值,则使用 Action
// 括号中的是我们输入元素的类型
// Start is called before the first frame update
void Start()
{
GetSum = DoSum; // 赋值
GetSum(5, 6); // 调用
}
// Update is called once per frame
void Update()
{
}
void DoSum(int a, int b) // 计算加法的函数
{
var result = a + b;
Debug.Log("The answer is: " + result);
}
}
如果该用 lambda 表达式:
using System; // 使用 Func<> 需要这个库
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public Action GetSum;
// 不需要返回值,则使用 Action
// 括号中的是我们输入元素的类型
// Start is called before the first frame update
void Start()
{
GetSum = (a, b) => Debug.Log("The answer is: " + (a + b)); // 赋值
GetSum(5, 6); // 调用
}
// Update is called once per frame
void Update()
{
}
/*
void DoSum(int a, int b) // 计算加法的函数
{
var result = a + b;
Debug.Log("The answer is: " + result);
}
*/
}
在 lambda 表达式中我们也可以设置多行代码:
using System; // 使用 Action<> 需要这个库
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public Action GetSum;
// 不需要返回值,则使用 Action
// 括号中的是我们输入元素的类型
// Start is called before the first frame update
void Start()
{
GetSum = (a, b) =>
{
var result = a + b;
if (result > 10)
{
Debug.Log("The result is greater than 10.");
}
Debug.Log("The answer is: " + result);
};
GetSum(5, 6); // 调用
}
// Update is called once per frame
void Update()
{
}
}
传统实现方法:
using System; // 使用 Action 需要这个库
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public Action onGetName;
// 这里不需要传入参数,所以没有 <>
// Start is called before the first frame update
void Start()
{
onGetName = GetName;
onGetName();
}
// Update is called once per frame
void Update()
{
}
void GetName()
{
Debug.Log("The name is: " + gameObject.name);
}
}
使用 lambda 表达式:
using System; // 使用 Action 需要这个库
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public Action onGetName;
// 这里不需要传入参数,所以没有 <>
// Start is called before the first frame update
void Start()
{
onGetName = () => Debug.Log("The name is: " + gameObject.name);
// 这里没有传入参数,所以括号内是空的
onGetName();
}
// Update is called once per frame
void Update()
{
}
/*
void GetName()
{
Debug.Log("The name is: " + gameObject.name);
}
*/
}
在 lambda 表达式中使用更复杂的代码,只需要用大括号即可。
练习使用带有返回值的 delegate
任务说明:
using System; // 使用 Func 需要这个库
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public Func onGetNameLenth;
// 这里返回的参数是 int
// Start is called before the first frame update
void Start()
{
onGetNameLenth = () => gameObject.name.Length; //赋值
Debug.Log("Name lenth: " + onGetNameLenth()); //调用
}
// Update is called once per frame
void Update()
{
}
}
using System; // 使用 Func 需要这个库
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public Func onGetSum;
// 前面两个是输入类型,最后一个是输出类型,都是 int
// Start is called before the first frame update
void Start()
{
onGetSum = (a, b) => a + b;
Debug.Log("The answer is: " + onGetSum(5, 6));
}
// Update is called once per frame
void Update()
{
}
}
using System; // 使用 Action 和 Func 需要这个库
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public Action onCallback;
// Start is called before the first frame update
void Start()
{
onCallback = () => Debug.Log("Passed 5 seconds");
StartCoroutine(CallbackTime(onCallback)); // 启动协程序
// 这里可以继续简化成 StartCoroutine(CallbackTime(() => Debug.Log("Passed 5 seconds")));
}
// Update is called once per frame
void Update()
{
}
public IEnumerator CallbackTime(Action OnComplete = null) // 设定一个协程,这里的 null 表示可以后续没有动作
{
yield return new WaitForSeconds(5); // 等待 5 秒
if (OnComplete != null) // 若果有需要在等待后执行的程序
{
OnComplete(); // 执行传入的 Action
}
}
}
这样设计的好处在于,我不需要在协程中定义具体的执行代码,协程仅仅用于计时。