返回

14.观察者模式

在对象之间定义一个一对多的链接方法,当一个对象变换状态时,其他关联的对象都会自动收到通知。

成就系统

成就系统,是早期单机游戏就出现的一种系统,例如:收集到多少颗星星就能开启特定关卡、全装备收集完成就能额外获得另一组套装等等。这些收集的项目并不会影游戏主线的进行,也不与游戏主要的玩法相关。但增加这些成就项目,有助于游戏的可玩性,并提升玩家对游戏的挑战和目标的追求。

观察者模式与命令模式,两者都是希望“事件发生”与“功能执行”之间不要有太多的依赖性,不过,还是可以按照系统的使用需求,分析出应该运用哪个模式。

观察者模式

在对象之间定义一个一对多的链接方法,当一个对象变换状态时,其他关联的对象都会自动收到通知。

观察者模式的定义

社交网站就是最佳的观察者模式实现返利。当我们在社交网站上,与另一位用户成为好友、加入一个粉丝团的关注另一位用户的状态,那么当这些好友、粉丝团、用户有任何的新动态或状态变动时,就会在我们动态页面上”主动“看到这些对象更新的情况,而不必到每一位好友或粉丝团中查看。

观察者模式实现上可以分为以下几点:

  • 主题者、订阅者的角色
  • 如何建立订阅者与主题者的关系
  • 主题者发布信息时,如何通知所有订阅者

参与者说明如下:

  • Subject
    • 定义主题的接口
    • 让观察者通过接口方法,来订阅、解除订阅主题。这些观察者在主题内部可以使用泛型容器加以管理
    • 在主题更新时,通知所有观察者
  • ConcreteSubject
    • 实现主题接口
    • 设置主题的内容及更新,当主题变化时,使用父类的通知方法告知所有的观察者。
  • Observer
    • 定义观察者的接口
    • 提供更新通知的方法,让主题可以通知更新
  • ConcreteObserver
    • 实现观察者接口
    • 针对主题的更新,按需求向主题获取更新状态

具体实现

    // 觀察者介面
    public abstract class Observer
    {
        public abstract void Update();
    }

    // 主題介面
    public abstract class Subject
    {
        List<Observer> m_Observers = new List<Observer>();

        // 加入觀察者
        public void Attach(Observer theObserver)
        {
            m_Observers.Add(theObserver);
        }

        // 移除觀察者
        public void Detach(Observer theObserver)
        {
            m_Observers.Remove(theObserver);
        }

        // 通知所有觀察者
        public void Notify()
        {
            foreach (Observer theObserver in m_Observers)
                theObserver.Update();
        }
    }

    // 主題實作 
    public class ConcreteSubject : Subject
    {
        string m_SubjectState;

        public void SetState(string State)
        {
            m_SubjectState = State;
            Notify();
        }

        public string GetState()
        {
            return m_SubjectState;
        }
    }

    // 實作的Observer1
    public class ConcreteObserver1 : Observer
    {
        string m_ObjectState;

        ConcreteSubject m_Subject = null;

        public ConcreteObserver1(ConcreteSubject theSubject)
        {
            m_Subject = theSubject;
        }

        // 通知Subject更新
        public override void Update()
        {
            Debug.Log("ConcreteObserver1.Update");
            // 取得Subject狀態
            m_ObjectState = m_Subject.GetState();
        }

        public void ShowState()
        {
            Debug.Log("ConcreteObserver1:Subject目前的主題:" + m_ObjectState);
        }
    }

    // 實作的Observer2
    public class ConcreteObserver2 : Observer
    {
        ConcreteSubject m_Subject = null;

        public ConcreteObserver2(ConcreteSubject theSubject)
        {
            m_Subject = theSubject;
        }

        // 通知Subject更新
        public override void Update()
        {
            Debug.Log("ConcreteObserver2.Update");
            // 取得Subject狀態
            Debug.Log("ConcreteObserver2:Subject目前的主題:" + m_Subject.GetState());
        }
    }

信息的推与拉

主题(Subject)改变时,改变的内容要如何让观察者(Observer)得知,运行方式可分为推(Push)信息与拉(Pull)信息两种模式。

  • 推信息:主题(Subject)将变动的内容主动“推”给观察者(Observer)。一般会在调用观察者(Observer)的通知(Update)方法时,同时将更新的内容当成参数传给观察者(Observer)。例如传统的报社,杂志社的模式,每一次的发行都会将所有的内容一次发送给订阅者,所有的订阅者街道的信息都是一致的,然后订阅者再从中获取需要的信息来进行处理:
    • 优点:所有内容都一次传给观察者(Observer),省去观察者(Observer)再次向主题(Subject)查询的操作,主题(Subject)类也不需要定义太多的查询方法供观察者(Observer)来查询
    • 缺点:如果推送的内容过多,容易是观察者(Observer)收到不必要的信息或造成查询上的困难,不当的信息设置也可能造成系统性能降低。
  • 拉信息:主题(Subject)内容变动时,只是先通知观察者当前内容有变动,而观察者(Observer)则是按照系统的需求,再向主题(Subject)查询(拉)所需的信息
    • 优点:主题(Subject)只通知当前内容有更新,再由观察者(Observer)自己去获取所需信息,因为观察者(Observer)自己更知道需要哪些信息,所以不太会去获取不必要的信息。
    • 缺点:因为观察者(Observer)需要向主题(Subject)查询新的内容,所以主题(Subject)必须提供查询方式,这样依赖,就容易造成主题(Subject)类的接口过多。

使用观察者模式实现成就系统

重构成就系统,可按照下面的步骤来进行

  1. 实现游戏事件系统(GameEventSystem)
  2. 完成各个游戏事件的主题及其观察者
  3. 实现成就系统(AchievementSystem)及订阅游戏事件
  4. 重构游戏事件触发点。

参与者说明

  • GameEventSystem:游戏事件斯通,用来管理游戏中发生的事件。针对每一个游戏事件产生一个“游戏事件主题”,并提供接口方法让其他系统能订阅指定的游戏事件。
  • IGameEventSubject:游戏事件主题接口,负责定义游戏中“游戏事件”内容的接口,并延申出下列游戏事件主题
    • EnemyKilledSubject:敌方角色阵亡
    • SoldierKilledSubject:玩家角色阵亡
    • SoldierUpgateSubject:玩家角色升级
    • NewStageSubject:新关卡
  • IGameEventObserver:游戏事件观察者接口,负责游戏中事件触发时被通知的操作接口
  • EnemyKilledObserver:观察者们:订阅“敌方角色阵亡”主题(EnemyKilledSubject)的观察者类,共有:
    • EnemyKilledObserverUI:将敌方角色阵亡信息显示在界面上
    • EnemyKilledObserverStageScore:将敌方角色阵亡信息提供给关卡系统(StageSystem)
    • EnemyKilledObserverAchievement:将敌方橘色提供给成就系统(AchievementSystem)

// 遊戲事件
public enum ENUM_GameEvent
{
    Null = 0,
    EnemyKilled = 1, // 敵方單位陣亡
    SoldierKilled = 2, // 玩家單位陣亡
    SoldierUpgate = 3, // 玩家單位升級
    NewStage = 4, // 新關卡
}

// 遊戲事件系統
public class GameEventSystem : IGameSystem
{
    private Dictionary<ENUM_GameEvent, IGameEventSubject> m_GameEvents =
        new Dictionary<ENUM_GameEvent, IGameEventSubject>();

    public GameEventSystem(PBaseDefenseGame PBDGame) : base(PBDGame)
    {
        Initialize();
    }

    // 釋放
    public override void Release()
    {
        m_GameEvents.Clear();
    }

    // 替某一主題註冊一個觀測者
    public void RegisterObserver(ENUM_GameEvent emGameEvnet, IGameEventObserver Observer)
    {
        // 取得事件
        IGameEventSubject Subject = GetGameEventSubject(emGameEvnet);
        if (Subject != null)
        {
            Subject.Attach(Observer);
            Observer.SetSubject(Subject);
        }
    }

    // 註冊一個事件
    private IGameEventSubject GetGameEventSubject(ENUM_GameEvent emGameEvnet)
    {
        // 是否已經存在
        if (m_GameEvents.ContainsKey(emGameEvnet))
            return m_GameEvents[emGameEvnet];

        // 產生對映的GameEvent
        IGameEventSubject pSujbect = null;
        switch (emGameEvnet)
        {
            case ENUM_GameEvent.EnemyKilled:
                pSujbect = new EnemyKilledSubject();
                break;
            case ENUM_GameEvent.SoldierKilled:
                pSujbect = new SoldierKilledSubject();
                break;
            case ENUM_GameEvent.SoldierUpgate:
                pSujbect = new SoldierUpgateSubject();
                break;
            case ENUM_GameEvent.NewStage:
                pSujbect = new NewStageSubject();
                break;
            default:
                Debug.LogWarning("還沒有針對[" + emGameEvnet + "]指定要產生的Subject類別");
                return null;
        }

        // 加入後並回傳
        m_GameEvents.Add(emGameEvnet, pSujbect);
        return pSujbect;
    }

    // 通知一個GameEvent更新
    public void NotifySubject(ENUM_GameEvent emGameEvnet, System.Object Param)
    {
        // 是否存在
        if (m_GameEvents.ContainsKey(emGameEvnet) == false)
            return;
        //Debug.Log("SubjectAddCount["+emGameEvnet+"]");
        m_GameEvents[emGameEvnet].SetParam(Param);
    }
}
// 遊戲事件主題
public class IGameEventSubject 
{
	private List<IGameEventObserver> m_Observers = new List<IGameEventObserver>(); // 觀測者
	private System.Object m_Param = null;	// 發生事件時附加的參數

	// 加入
	public void Attach(IGameEventObserver theObserver)
	{
		m_Observers.Add( theObserver );
	}

	// 取消
	public void Detach(IGameEventObserver theObserver)
	{
		m_Observers.Remove( theObserver );
	}

	// 通知
	public void Notify()
	{
		foreach( IGameEventObserver theObserver  in m_Observers)
			theObserver.Update();
	}

	// 設定參數
	public virtual void SetParam( System.Object Param )
	{
		m_Param = Param;
	}
}

// 敵人單位陣亡
public class EnemyKilledSubject : IGameEventSubject
{
    private int m_KilledCount = 0;
    private IEnemy m_Enemy = null;

    public EnemyKilledSubject()
    {
    }

    // 取得對像
    public IEnemy GetEnemy()
    {
        return m_Enemy;
    }

    // 目前敵人單位陣亡數
    public int GetKilledCount()
    {
        return m_KilledCount;
    }

    // 通知敵人單位陣亡
    public override void SetParam(System.Object Param)
    {
        base.SetParam(Param);
        m_Enemy = Param as IEnemy;
        m_KilledCount++;

        // 通知
        Notify();
    }
}

// Soldier單位陣亡
public class SoldierKilledSubject : IGameEventSubject
{
    private int m_KilledCount = 0;
    private ISoldier m_Soldier = null;

    public SoldierKilledSubject()
    {
    }

    // 取得對像
    public ISoldier GetSoldier()
    {
        return m_Soldier;
    }

    // 目前我方單位陣亡數
    public int GetKilledCount()
    {
        return m_KilledCount;
    }

    // 通知我方單位陣亡
    public override void SetParam(System.Object Param)
    {
        base.SetParam(Param);
        m_Soldier = Param as ISoldier;
        m_KilledCount++;

        // 通知
        Notify();
    }
}

// Soldier升級
public class SoldierUpgateSubject : IGameEventSubject
{
	private	int	m_UpgateCount = 0;
	private ISoldier m_Soldier = null;

	public SoldierUpgateSubject()
	{}

	// 目前升級次數
	public int GetUpgateCount()
	{
		return m_UpgateCount;
	}

	// 通知Soldier單位升級
	public override void SetParam( System.Object Param )
	{
		base.SetParam( Param);
		m_Soldier = Param as ISoldier;
		m_UpgateCount++;

		// 通知
		Notify();
	}

	public ISoldier GetSoldier()
	{
		return m_Soldier;
	}
}

// 新的關卡
public class NewStageSubject : IGameEventSubject
{
    private int m_StageCount = 1;

    public NewStageSubject()
    {
    }

    // 目前關卡數
    public int GetStageCount()
    {
        return m_StageCount;
    }

    // 通知
    public override void SetParam(System.Object Param)
    {
        base.SetParam(Param);
        m_StageCount = (int) Param;

        // 通知
        Notify();
    }
}

使用观察者模式的优点

成就系统以“游戏事件”为基础,记录每个游戏事件发生的次数及事件点,作为成就项目的判断依据。但是当同一个游戏事件被触发后,可能不只是只有一个成就系统会被触发,系统中也可能存在者其他系统需要使用同一个游戏事件。因此,加入了以观察者模式为基础的事件系统,就可以有效地接触“游戏事件的发生”与有关的“系统功能调用”之间的绑定。这样在游戏事件发生时,不必理会后续的处理工作,而是交给游戏事件主题负责调用观察者/订阅者。此外,也能同时调用多个系统同时处理这个事件引发的后续操作。

观察者模式的应用

在游戏场景中,设计者通常会摆放一些所谓的“事件触发点”,这些事件触发点会在玩家角色进入时,触发对应的游戏功能,例如突然会出现一群怪物NPC来攻击玩家角色;或是进入剧情模式演出一段游戏故事剧情等等。而且游戏通常不会限制一个事件触发点只能执行一个操作,因此实现时可以将每一个事件触发点当成一个“主题”,而每一个要执行的功能,都能成为“观察者”,当事件被触动发布时,所有的观察者都能立即反应。