成就系统
成就系统,是早期单机游戏就出现的一种系统,例如:收集到多少颗星星就能开启特定关卡、全装备收集完成就能额外获得另一组套装等等。这些收集的项目并不会影游戏主线的进行,也不与游戏主要的玩法相关。但增加这些成就项目,有助于游戏的可玩性,并提升玩家对游戏的挑战和目标的追求。
观察者模式与命令模式,两者都是希望“事件发生”与“功能执行”之间不要有太多的依赖性,不过,还是可以按照系统的使用需求,分析出应该运用哪个模式。
观察者模式
在对象之间定义一个一对多的链接方法,当一个对象变换状态时,其他关联的对象都会自动收到通知。
观察者模式的定义
社交网站就是最佳的观察者模式实现返利。当我们在社交网站上,与另一位用户成为好友、加入一个粉丝团的关注另一位用户的状态,那么当这些好友、粉丝团、用户有任何的新动态或状态变动时,就会在我们动态页面上”主动“看到这些对象更新的情况,而不必到每一位好友或粉丝团中查看。
观察者模式实现上可以分为以下几点:
- 主题者、订阅者的角色
- 如何建立订阅者与主题者的关系
- 主题者发布信息时,如何通知所有订阅者
参与者说明如下:
- 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)类的接口过多。
使用观察者模式实现成就系统
重构成就系统,可按照下面的步骤来进行
- 实现游戏事件系统(GameEventSystem)
- 完成各个游戏事件的主题及其观察者
- 实现成就系统(AchievementSystem)及订阅游戏事件
- 重构游戏事件触发点。
参与者说明
- 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来攻击玩家角色;或是进入剧情模式演出一段游戏故事剧情等等。而且游戏通常不会限制一个事件触发点只能执行一个操作,因此实现时可以将每一个事件触发点当成一个“主题”,而每一个要执行的功能,都能成为“观察者”,当事件被触动发布时,所有的观察者都能立即反应。