委托是事件的基础,欲了解事件,请先阅读拙作——【C#】委托基础
【概述】
事件
源于委托,可以理解为委托的一种形式。使用事件,当事件触发时,所有注册该事件的订阅者均会收到消息并发生改变。这里有两个新名词,一个是注册,一个是订阅者。注册事件与委托关联方法其实差不多,可以用理解委托关联方法的方式来理解注册事件。与注册相对应的便是注销,在委托的语法中相当于移除对方法的关联。
注册和注销分别用“+=”和“-=”操作符。这里有一点与委托不同的是,委托在关联多个方法时,需要对其赋值null或者方法,其后才可用“+=”和“-=”操作符;而事件创建实例后,可以直接注册或者注销方法。产生这种差异与事件这个类型本身内部机制有关,这里不做赘述。
观察者模式
观察者模式是软件设计众多模式中的一种,是一种解决问题的方案。在此种模式中:当一个对象的状态发生改变时,会通知所有与此对象相关联的对象(观察者),并使用观察者提供(注册)的方法改变观察者的状态。简单地说,就是定义的方法自己不去调用,而是当引发某个事件后(由其他对象引发)此方法自动被响应,这个方法是跟事件紧密相关的。
举个例子,上学时老师都会留作业,那么“布置作业”就是一个事件,老师就是发起这个事件的对象,班级的每个同学都是不同的对象,他们此时便是观察者,他们都具有方法“写作业”,他们将“写作业”这个方法注册到“布置作业”这个事件上,当老师留家庭作业时,就会引发“布置作业”事件,从而每个同学都开始写作业,即执行“写作业”方法。总的来说就是“写作业”这一行为由“布置作业”来决定,而布置作业的对象是老师,并不是学生本身。当学生毕业了,不需要在写作业了,那么此时在“布置作业”事件中注销“写作业”方法就可以了。
【案例】
为了更好地理解事件机制,我们在Unity中构建一个案例。
策划
对象:
敌人,哨兵,守城军队,居民。
流程:
当敌人军队被哨兵发现时,守城军队会出城抵御敌人,而居民会向后撤退。
方法设计:
为了简化流程,用点击按钮的形式表示敌人接近,所以,敌人类只包含Name属性和构造方法即可。当敌人接近时,此时触发哨兵的方法“发现敌人”(DiscoverEnemy,并触发事件“敌人来了”(EnemyComing),此事件包含两个参数,一是敌人对象,二是敌人的位置;该事件被守城军队和居民注册,注册的方法分别为“御敌”(DefenceEnemy)和“撤退”(Retreat),当事件触发时会调用这两个方法。
实现
敌人类(Enemy):
public class Enemy
{
public readonly string Name;
public Enemy(string name)
{
Name = name;
}
}
哨兵类(Sentry):
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
public class Sentry : MonoBehaviour {
public delegate void EnemyComingHandler(Enemy enemy, Vector3 position);
public event EnemyComingHandler EnemyComing;
public void DiscoverEnemy()
{
//如果有注册事件,才会触发事件
if (EnemyComing != null)
//触发事件,通知所有订阅者
EnemyComing(new Enemy("Make"), new Vector3(1f, 2f, 3f));
}
}
守卫队类(Guards):
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
public class Guards : MonoBehaviour {
public Sentry MySentry;
void Start ()
{
AddEvent();
}
//注册
void AddEvent()
{
MySentry.EnemyComing += new Sentry.EnemyComingHandler(DefenceEnemy);
}
//注销
void RemoveEvent()
{
MySentry.EnemyComing -= new Sentry.EnemyComingHandler(DefenceEnemy);
}
void DefenceEnemy(Enemy enemy, Vector3 enemyPosition)
{
Debug.Log("We are guard, we know the enemy named " + enemy.Name + " at the " + enemyPosition.ToString());
}
}
居民类(Inhabitants):
public class Inhabitants : MonoBehaviour {
public Sentry MySentry;
void Start()
{
AddEvent();
}
//注册
void AddEvent()
{
MySentry.EnemyComing += new Sentry.EnemyComingHandler(Retreat);
}
//注销
void RemoveEvent()
{
MySentry.EnemyComing -= new Sentry.EnemyComingHandler(Retreat);
}
void Retreat(Enemy enemy, Vector3 enemyPosition)
{
Debug.Log("We are inhabitants, we know the enemy named " + enemy.Name + " at the " + enemyPosition.ToString());
}
}
场景设置:
1.在场景中创建三个空物体,分别表示哨兵,守卫,居民对象,并将脚本分别指定为给对应对象;
2.将哨兵对象指定给守卫与居民的Sentry属性;
3.创建Button,并关联哨兵的DiscoverEnemy方法。
4.点击Button,即可调用Retreat方法与DefenceEnemy方法。
【总结】
在面向对象的程序设计中,模块化是基本,而模块之间往往需要“通信”,事件机制便是最常用的方式之一。事件的特点是“1拖n”牵引效应,它往往应用于模块之间因果关系明显的情况。通过事件机制,模块之间的耦合度降低,我不需要关心你的事件是如何发生的,你也不需要关系事件发生之后我会做些什么,我与你之间的联系不过是一个信号,而这个信号就是事件触发。事件机制也十分利用项目拓展,当有有新的模块的方法需要主程的某个事件时,只需注册这个事件,而不需要改变模块方法的访问级与主程代码,避免未知风险的发生。
【参考资料】
[1] 《Unity3D脚本编程》 陈嘉栋 著。
[2] 维基百科-观察者模式