概念
观察者模式(Observer Pattern)属于行为型模式。当对象间存在一对多关系时,则使用观察者模式。比如,当一个对象被修改时,则会自动通知依赖它的对象。
观察者模式中有两个概念:观察者(Observer)和对象(Subject),后者即“被观察者”。
观察者关注某些对象,或对象的某些变化,因而要在对象上进行注册;
对象(被观察者)则会在发生变化时通知这些注册的对象。
举个例子,你(对象)在上自习,老师(观察者)告诉你“如果遇到不会的题就告诉我”(注册),于是你在之后遇到不会做的题时就去问了老师(通知)。
如此一来,老师不需要一直待在你身边看着你有没有不会的题。观察者程序也不需要一直检测对象程序的变化,对象程序会在出现变化是主动告知观察者。
示例程序
class Observer{
public void OnNotify(Object src, Event e){
// 处理通知
}
}
class Subject{
private Observer[] ObserverList;
public void AddListener(Observer obs){
// 将 obs 添加到 ObserverList
}
public void RemoveListener(Observer obs){
// 自 ObserverList 中移除 obs
}
public void Notify(Event e){
foreach (Observer obs in ObserverList){
Obs.OnNotify(this, e);
}
}
}
Observer通过调用Subject.AddListener来将自己注册到Subject实例,随后Subject发生特定变化时便通过Subject.Notify通知注册的观察者。
在Unity中你可以主动公开一个Action,让其观察者将方法绑定至该Action,对象则在满足某一条件时调用这些方法。
常见应用
观察者模式是应用最广泛的GoF模式,在游戏中也有很多应用之处,比如成就系统和UI。
成就系统
击杀100个敌人、达到100级、收集所有收藏品、偷看10次2B的秘密...游戏对效率的要求使得成就系统不可能每一帧都检查一次成就是否完成,因而观察者模式是一个很好的解决方案。
UI
UI系统与之类似,UI也不必时刻关注角色的等级、血量、道具数,只需要在发生变化时根据这些变化来调整或者播放动画即可。
实例代码
玩家
public class Player_Observer : MonoBehaviour
{
public Action JumpAction;
void Start()
{
JumpAction += Achievements_Observer.Instance.JumpOnce;
}
void Update()
{
// 省略其他控制逻辑
GravityAndJump();
}
private void GravityAndJump()
{
// 省略其他逻辑
if (Input.GetButtonDown("Jump"))
{
// 跳跃逻辑
JumpAction();
}
}
}
成就系统
public class Achievements_Observer : MonoBehaviour
{
public static Achievements_Observer Instance { get; private set; }
[SerializeField] private int JumpCount = 0;
[SerializeField] private bool FallInAbyss = false;
private void Awake()
{
if (!Instance) Instance = this;
}
public void JumpOnce()
{
JumpCount++;
JumpTipAction?.Invoke(JumpCount);
if (JumpCount == 10)
{
UI_Observer.Instance.AddAchievement("Jump, jump, jump!", "Jump for 10 times.");
}
}
}
action?.Invoke()
// ↑
// 等效
// ↓
if (action != null) action();
上述两种写法是一致的,对于没有任何绑定的Action,直接调用会报错。
UI系统
public class UI_Observer : MonoBehaviour
{
public static UI_Observer Instance;
private void Awake()
{
if(!Instance) Instance = this;
}
public void AddAchievement(string title, string description)
{
// 获得成就
}
}
效果如下:
完整示例代码见结尾
值得注意的问题及可能的解决方案
快速返回
对象要在通知过所有观察者之后才会继续执行后面的代码。因此为了执行效率,观察者应该迅速返回,开其他线程或协程处理复杂逻辑。
动态内存分配
实例程序使用数组来保存所有观察者。这意味着加入新的观察者时,可能会涉及动态内存分配的问题。因而可以用链表来解决这一问题,以下给出两种方案:
链表观察者
对象中保存一个指向观察者的指针,观察者中也保存一个指向其他观察者的指针,如此一来即可常量时间添加、删除观察者。如下图:
注意:此方法一个显而易见的问题在于,其默认一个观察者只能关注一个对象。如果想让观察者可以关注多个对象,可以使用链表节点池。
链表节点池
与前种方法类似,也是用链表来保存观察者,不同的是,链表上不是观察者,而是保存指向观察者的指针的节点,见下图:
销毁对象和观察者
销毁一个对象或观察者前,一定要解绑与之相关的对象和观察者。保持一个好习惯,防止对象通知到一个不存在的观察者,或者让一个观察者等着并不存在对象通知它。
可维护性
这个点有点抽象,简单来说就是,观察者模式看起来很好用,但并不是所有程序都适合用观察者模式。观察者模式适用于两个相关性不高的系统之间,比如玩家运动和成就,背包和UI等,如果在关系密切的系统内使用观察者模式,那如果有Bug出现,那调试低耦合的观察者模式会比较困难,这种情况不如耦合度较高的写法。
碎碎念
本来下一章应该是享元模式(Flyweight)(这个翻译真妙啊),但享元模式的应用场景更接近于引擎底层,图形渲染当中,我想不到其他合适的应用,就略过了,以后再说吧。
参考资料
"观察者模式 | 菜鸟教程." https://www.runoob.com/design-pattern/observer-pattern.html
"Observer · Design Patterns Revisited · Game Programming Patterns." http://gameprogrammingpatterns.com/observer.html
UI及成就系统示例代码