成就系统--观察者模式

思考并回答以下问题:

  • 观察者模式的官方定义是什么?
  • 观察者模式的关键词是“一对多”怎么理解?
  • 观察者模式为什么属于行为型模式?
  • 理解行为型设计模式的关键是通信怎么理解?
  • “事件发生”与“功能执行”之间不要有太多的依赖性,所以在客户端控制怎么理解?
  • 动手画观察者模式设计图。
  • 观察者模式需要list、foreach怎么理解?
  • 主题通知观察者的是主题自身的状态。不是一个单纯的触发flag让观察者去执行观察者的方法。报纸订阅例子中报社(主题)告诉订阅者的是报纸上的信息。怎么理解?
  • 旧版的问题在哪里?新版如何优化?每个游戏都要有游戏事件系统吗?
  • 只允许客户端改动代码,处理关系,其他地方只准扩展,怎么理解?想增删订阅者怎么办?
  • 全部都在PBaseDefenseGame里处理怎么理解?
  • 比原架构多加一个观察者接口是构建观察者模式最重要的一步。使用设计模式时就要新建接口,比如工厂模式,观察者模式等,一听到使用设计模式,第一反应应该是新建接口。怎么理解?
  • 如果给你一个旧的架构,让你改成观察者模式,第一件事要做什么?
  • 主题接口管理观察者,主题的内容及更新由具体主题实现。也可以省略主题接口,但是观察者接口不能省略。怎么理解?
  • 信息的推与拉有什么区别?各有什么优点和缺点?
  • 为什么要先定义游戏事件枚举(ENUM_GameEvent)?

本章涵盖:

  • 成就系统
  • 观察者模式
    • 观察者模式的定义
    • 观察者模式的说明
    • 使用观察者模式的优点
    • 观察者模式的实现范例
  • 使用观察者模式实现成就系统
    • 成就系统的新架构
    • 实现说明
    • 使用观察者模式的优点
    • 实现观察者模式时的注意事项
  • 观察者模式面对变化时
  • 结论

成就系统

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

成就系统中的项目,都会和游戏本身有关,并且在玩家游玩的过程中,就能顺便收集,或是反复进行某项操作就能实现目标。一般可以先将成就项目分门别类,例如属于总数类的可能有累积击杀敌方角色达100次、训练我方单位达100个等;也有的是目标完成的项目,如完成训练一个等级3的玩家角色、成功打倒一个Boss等。所以,在实现成就系统之前,需要企划单位先将需要的成就事件列出来,并在项目完成到某个段落之后,才开始加入实现开发中。实现上,会先定义“游戏事件”(ENUM_GameEvent),如敌方角色阵亡、玩家角色阵亡、玩家角色升级等。当游戏进行过程中,有任何“游戏事件”被触发时,系统就要通知对应的“成就项目”,进行累积或条件判断,如果达到,则完成“成就项目”并通知玩家或直接给予奖励,如图1所示。

图1 成就系统与玩家奖励

一个简单的设计方式是,我们可以把通知成就系统的程序代码加入到“成就事件触发”的方法中。例如,击杀敌方角色,实现时就可以加在敌方角色阵亡的地方:

Listing1 Enemy角色接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class IEnemy : ICharacter
{
// 被武器攻击
public override void UnderAttack(ICharacter Attacker)
{
// 计算伤害值
m_Attribute.CalDmgValue(Attacker);

DoPlayHitSound(); // 音效
DoShowHitEffect(); // 特效

// 是否阵亡
if (m_Attribute.GetNowHP() <= 0)
{
Killed();

// 通知成就系统
AchievementSystem.NotifyGameEvent(ENUM_GameEvent.EnemyKilled, this, null);
}
}
}

事件触发后,调用成就系统(AchievementSystem)中的NotifyGameEvent方法,并将触发的游戏事件及触发时的敌方角色传入。上述范例中,使用枚举(ENUM)的方式来定义“游戏事件”,并将事件从参数行传入,而不是针对每一个游戏事件定义特定的调用方法,这样做可以避免成就系统定义过多的接口方法。而成就系统的NotifyGameEvent方法,可根据参数传入的“游戏事件”参数,来决定后续的处理流程:

Listing2 成就系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class AchievementSystem
{
// 记录的成就项目
private int m_EnemyKilledCount = 0;
private int m_SoldierKilledCount = 0;
private int m_StageLv = 0;
private bool m_KillOgreEquipRocket=false;

// 通知游戏事件发生
public void NotifyGameEvent(ENUM_GameEvent emGameEvent, System.Object Param1, System.Object Param2)
{
// 按照游戏事件
switch( emGameEvent )
{
case ENUM_GameEvent.EnemyKilled: // 敌方单位阵亡
Notify_EnemyKilled(Param1 as IEnemy );
break;

case ENUM_GameEvent.SoldierKilled: // 玩家单位阵亡
Notify_SoldierKilled( Param1 as ISoldier );
break;

case ENUM_GameEvent.SoldierUpgrade: // 玩家单位升级
Notify_SoldierUpgrade( Param1 as ISoldier );
break;

case ENUM_GameEvent.NewStage: // 新关卡
Notify_NewStage((int)Param1);
break;
}
}
...
}

因为“游戏事件”非常多,所以在NotifyGameEvent方法中先判断emGameEvent的参数属性,再分别调用对应的私有成员方法,而每个私有成员方法,再按照企划的需求,累积计算属性或判断单次成就是否实现。

Listing3 成就系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class AchievementSystem
{
...
// 敌方单位阵亡
private void Notify_EnemyKilled(IEnemy theEnemy )
{
// 阵亡数增加
m_EnemyKilledCount++;

// 击倒装备Rocket的Ogre
if( theEnemy.GetEnemyType() == ENUM_Enemy.Ogre && theEnemy.GetWeapon().GetWeaponType() == ENUM_Weapon.Rocket)
m_KillOgreEquipRocket = true;
}

// 玩家单位阵亡
private void Notify_SoldierKilled( ISoldier theSoldier)
{
...
}

// 玩家单位升级
private void Notify_SoldierUpgrade( ISoldier theSoldier)
{
...
}

// 新关卡
private void Notify_NewStage( int StageLv)
{
...
}
}

如果让成就系统(AchievementSystem)负责每一个游戏事件的方法,并针对每一个单独的游戏事件,去进行“成就项目的累积或判断”,会让成就系统的扩充被限制在每个游戏事件处理方法中。当以后需要针对某一个游戏事件增加成就项目时,就必须通过修改原有“游戏事件处理方法”中的程序代码才能达成。例如,想再增加一个成就项目“杀死装备武器为Rocket以上的敌人数”,那么就只能修改Notify_EnemyKilled方法,在其中追加程序代码来实现修改的目标(违反开闭原则)。

此外,“游戏事件”发生时可能不是只有成就系统会被影响,其他系统也可能需要追踪相关的游戏事件。因此,如果都是在“游戏事件”的触发点进行每个系统调用的话,那么触发点的程序代码将会变得非常复杂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Enemy角色接口
public abstract class IEnemy : ICharacter
{
...
// 被武器攻击
public override void UnderAttack(ICharacter Attacker)
{
// 是否阵亡
if(m_Attribute.GetNowHP() <= 0)
{
Killed();

// 通知成就系统
AchievementSystem.NotifyGameEvent(ENUM_GameEvent.EnemyKilled, this, null);

// 通知B系统
...
// 通知C系统
...
// 通知D系统
...
}
}
}

所以要将“游戏事件”与“成就系统”分开,让成就系统仅关注于某些游戏事件的发生;而游戏事件的发生,也不是只提供给成就系统使用(可以在客户端随意组装)。这样的设计才是适当的设计。

要如何完成这样的设计呢?如果能将“游戏事件的产生与通知”独立成为一个系统,并且让其他系统能通过“订阅”或“关注”的方式,来追踪游戏事件系统发生的事。也就是,当游戏事件系统发生事件时,会负责去通知所有“订阅”了游戏事件的系统,此时被通知的系统,再根据自己的系统逻辑自行决定后续的处理操作。如果能按照上述流程来进行设计,就是一个极为适当的设计。上述的流程,其实就是观察者模式所要表达的内容,如图2所示。

图2 由事件触发其他相关的系统

观察者模式

观察者模式与命令模式(Command)是很相似的模式,两者都是希望“事件发生”与“功能执行”之间不要有太多的依赖性,不过,还是可以按照系统的使用需求,分析出应该运用哪个模式。命令模式(Command)已经在兵营训练单位-命令模式中详细说明过了,接下来将说明观察者模式,另外,在实现观察者模式时的注意事项一节中也将说明,如何根据系统的需要在这两个模式中,选择合适的模式进行实现。

观察者模式的定义

GoF对观察者模式的定义为:

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

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

图3 社交网站上的关注对象的更新

在早期社交网站还没广泛流行之前,说明观察者模式常以“报社-订户”来做说明:多位订户向报社“订阅(Subscribe)”了一份报纸,而报社针对昨天的新闻整理编辑之后,在今天一早进行“发布(Publish)”的工作,接着送报生会主动按照订阅的信息,将每份报纸送到订户手上,如图4所示。

图4 向报社订阅后,报纸每日会送达到指定的订户手上

在上面的案例中,都存在“主题目标”与其他“订阅者/关注者”之间的关系(一对多),当主题有变化时,就会通过之前建立的“关系”,将更新的信息(报纸有内容)传送给“订阅者/关注者”。因此,实现上可分为以下几点:

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

观察者模式的说明

GoF定义的观察者模式的类结构图如图5所示。

图5 GOF定义的观察者模式的类结构图

GoF参与者的说明如下:

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

观察者模式的实现范例

实现观察者模式,首先定义Subject(主题接口):

Listing4 主题接口(Observer.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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();
}
}

在类定义中,使用了一个C#的List泛型容器(m_Observers)来管理所有的Observer(观察者),并实现了3个与Observer(观察者)相关的方法。当某一个Observer,对Subject(主题)感兴趣时,就利用Attach方法将自己加入主题的管理器中,通过这样的方式,Observer就能主动与Subject建立关系。当Subject被更改而需要通知Observer时,只要遍历m_Observers就能通知每一个在容器内的Observer现在有Subject发生了更改。

以下程序范例实现了一个主题:

Listing5 主题实现(Observer.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ConcreteSubject : Subject
{
string m_SubjectState;

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

public string GetState()
{
return m_SubjectState;
}
}

使用一个字符串m_SubjectState来表示主题的状态,并提供方法(SetState)让客户端(Program.cs)可以设置主题,而当主题一旦变动时,即调用父类的通知(Notify)方法来通知所有的Observer(观察者)。

Observer(观察者)的接口定义如下:

Listing6 观察者接口(Observer.cs)

1
2
3
4
public abstract class Observer
{
public abstract void Update();
}

接口内定义了一个更新(Update)方法,在主题通知更新时就会调用。范例内分别有两个子类实现了观察者接口:

Listing7 实现两个Observer(Observer.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 实现的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());
}
}

两个类在接收到通知之后的处理方式不太一样:ConcreteObserver1类先将信息保存之后,等待必要时刻(ShowState)才提示;而ConcreteObserver2类则是在收到主题的更新后,马上将更新的信息显示出来。两个类相同的地方在于,构造时都必须提供Subject(主题)的对象引用,让Observer(观察者)能够保存下来,这样做的主要原因是,因为上面范例实现的观察者模式属于“拉(pull)”模式,所以观察者类必须自己去向Subject(主题)获取信息(保存了主题的引用)。

在测试程序代码中,产生了主题(theSubject)之后,再分别将两个观察者(Observer)加入主题中,表示有两个观察者对主题感兴趣,此时可以增删订阅者,控制权完全在客户端这个本来就可以修改的文件这边

Listing8 测试观察者模式(ObserverTest.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void UnitTest () 
{
// 主题
ConcreteSubject theSubject = new ConcreteSubject();

// 加入观察者
ConcreteObserver1 theObserver1 = new ConcreteObserver1(theSubject);
theSubject.Attach( theObserver1 );
theSubject.Attach( new ConcreteObserver2(theSubject) );

// 设置Subject
theSubject.SetState("Subject状态1");

// 显示状态
theObserver1.ShowState();
}

测试程序代码的后半段,对主题(theSubject)进行设置(SetState):“Subject状态1”,信息栏上即可看到两个观察者被通知发生了更新:

执行结果

1
2
ConcreteObserver1.Update
ConcreteObserver2.Update

ConcreteObserver2在收到通知后会立即将信息显示出来:

1
ConcreteObserver2:Subject当前的主题:Subject状态1

而ConcreteObserver1则是在调用ShowState方法时,才将保留下来的主题状态显示出来:

1
ConcreteObserver1:Subject当前的主题:Subject状态1

**信息的推与拉**

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

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

而在实现设计上,必须根据系统所需要的最佳情况来判断,是要使用“推信息”还是“拉信息”的方式。

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

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

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

成就系统的新架构

对于解决《P级阵地》成就系统的需求,首先应该完成的是游戏事件系统(GameEventSystem)。在游戏事件系统中,会将每个游戏事件当成主题(Subject),让其他系统可针对感兴趣的游戏事件进行“订阅(Subscribe)”。当游戏事件被触发(Publish)时,游戏事件系统会去通知所有的系统,再让各个系统针对所需要的信息进行查询。

而成就系统将是游戏事件系统的一个订阅者/观察者。它将针对成就项目所需要的游戏事件进行订阅的操作,等到游戏事件系统发布游戏事件时,成就系统再去获取所需的信息来累积成就项目或判断成就项目是否达到。

图6显示了《P级阵地》中的游戏事件系统。

图6 游戏事件系统

参与者的说明如下:

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

同一个游戏事件可以提供给不同的系统一起订阅,并能同时接收到更新信息。

实现说明

接下来,按结构图和实现流程来完成成就系统的重构:

实现游戏事件系统

游戏事件系统(GameEventSystem)用来管理游戏当中发生的事件,并针对每一个游戏事件,产生一个“游戏事件主题(Subject)”:

Listing9 游戏事件系统的实现(GameEventSystem.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// 游戏事件
public enum ENUM_GameEvent
{
Null = 0,
EnemyKilled = 1, // 敌方单位阵亡
SoldierKilled = 2, // 玩家单位阵亡
SoldierUpgrade = 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();
}

/// <summary>
/// 为某一主题注册一个观察者
/// 通过事件得到主题,然后调用主题Attach方法注册观察者
/// 观察者持有主题的一个引用用于向主题查询信息
/// </summary>
/// <param name="emGameEvent">事件枚举</param>
/// <param name="Observer">要监听该事件的观察者类</param>
public void RegisterObserver(ENUM_GameEvent emGameEvent, IGameEventObserver Observer)
{
// 获取事件
IGameEventSubject Subject = GetGameEventSubject( emGameEvent );

if( Subject != null)
{
Subject.Attach( Observer );
Observer.SetSubject( Subject );
}
}

/// <summary>
/// 根据事件返回对应的具体主题类实例
/// 使用字典进行缓存
/// </summary>
/// <param name="emGameEvent">事件枚举</param>
/// <returns>具体主题类实例</returns>
private IGameEventSubject GetGameEventSubject( ENUM_GameEvent emGameEvent )
{
// 是否已经存在
if( m_GameEvents.ContainsKey( emGameEvent ))
return m_GameEvents[emGameEvent];

// 产生对应的GameEvent
IGameEventSubject pSujbect= null;

switch( emGameEvent )
{
case ENUM_GameEvent.EnemyKilled:
pSujbect = new EnemyKilledSubject();
break;

case ENUM_GameEvent.SoldierKilled:
pSujbect = new SoldierKilledSubject();
break;

case ENUM_GameEvent.SoldierUpgrade:
pSujbect = new SoldierUpgradeSubject();
break;

case ENUM_GameEvent.NewStage:
pSujbect = new NewStageSubject();
break;

default:
Debug.LogWarning("还没有针对["+emGameEvent+"]指定要产生的Subject类");
return null;
}

// 加入后并返回
m_GameEvents.Add (emGameEvent, pSujbect );
return pSujbect;
}

/// <summary>
/// 设置发生事件时,主题想传给观察者的参数
/// </summary>
/// <param name="emGameEvent">事件枚举</param>
/// <param name="Param">要传递给观察者的数据</param>
public void NotifySubject( ENUM_GameEvent emGameEvent, System.Object Param)
{
// 是否存在
if( m_GameEvents.ContainsKey( emGameEvent ) == false)
return ;

//Debug.Log("SubjectAddCount["+emGameEvent+"]");
m_GameEvents[emGameEvent].SetParam( Param );
}
}

类中使用了Dictionary泛型容器来管理所有的游戏事件主题,私有方法GetGameEventSubject负责管理这个Dictionary泛型容器。新增时,针对每一个游戏事件产生对应的主题(Subject)后加入容器内,并且保证一个游戏事件只存在一个主题对象

注册观察者(RegisterObserver)方法用于其他系统向游戏事件系统(GameEventSystem)订阅主题,调用时传入指定的游戏事件及观察者类对象。当某游戏事件触发时,通过通知主题更新(NotifySubject)方法,就能通知所有订阅该游戏事件主题的观察者类。

游戏事件主题(IGameEventSubject)负责定义《P级阵地》中“游戏事件”内容的接口:

Listing10 游戏事件主题(IGameEventSubject.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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;
}
}

类中定义了一个List泛型容器m_Observers,用来管理订阅主题的观察者,类提供了基本的新增、取消及通知方法来管理订阅者,并提供设置参数(SetParam)方法来设置每一个游戏事件所需提供的内容。

完成各个游戏事件主题及其观察者

以下是4个《P级阵地》定义的游戏事件主题。

①敌人角色阵亡

敌人角色阵亡时会发出通知,并将阵亡的敌人角色IEnemy使用SetParam方法传入,传入后再增加内部的计数器,提供给观察者查询:

Listing11 敌人单位阵亡(EnemyKilledSubject.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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();
}
}

②玩家角色阵亡

玩家角色阵亡时会发出通知,并将阵亡的玩家角色ISoldier使用SetParam方法传入,传入后再增加内部的计数器,提供给观察者查询:

Listing12 Soldier单位阵亡(SoldierKilledSubject.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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();
}
}

③玩家角色升级

玩家角色升级时会发出通知,升级的玩家角色ISoldier会使用SetParam方法传入,传入后再增加内部的计数器,提供给观察者查询:

Listing13 Soldier升级(SoldierUpgradeSubject.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class SoldierUpgradeSubject : IGameEventSubject
{
private int m_UpgradeCount = 0;
private ISoldier m_Soldier = null;

public SoldierUpgradeSubject()
{}

// 当前升级次数
public int GetUpgradeCount()
{
return m_UpgradeCount;
}

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

// 通知
Notify();
}

public ISoldier GetSoldier()
{
return m_Soldier;
}
}

④进入新关卡

玩家完成一个新关卡往下一个关卡推进时会收到通知,当前的关卡编号会使用SetParam方法传入,传入后再存储至内部成员中,提供给观察者查询:

Listing14 新的关卡(NewStageSubject.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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();
}
}

上面的4个主题分别都有对应的订阅者:

①“敌方角色阵亡”主题的观察者

“敌方角色阵亡”主题的观察者共有3个,而这些观察者最后都会将信息返回给注册它们的系统中,如图7所示。

图7 “敌方角色阵亡”主题的3个观察者

Listing15 实现Enemy阵亡事件的观察者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// UI观察Enemey阵亡事件(EnemyKilledObserverUI.cs)
public class EnemyKilledObserverUI : IGameEventObserver
{
private EnemyKilledSubject m_Subject = null;
private PBaseDefenseGame m_PBDGame = null;

public EnemyKilledObserverUI( PBaseDefenseGame PBDGame )
{
m_PBDGame = PBDGame;
}

// 设置观察的主题
public override void SetSubject( IGameEventSubject Subject )
{
m_Subject = Subject as EnemyKilledSubject;
}

// 被通知Subject的更新
public override void Update()
{
//Debug.Log("EnemyKilledObserverUI.Update: Count["+ m_Subject.GetKilledCount() +"]");
if(m_PBDGame != null)
m_PBDGame.ShowGameMsg("敌方单位阵亡");
}
}

// 成就观察Enemey阵亡事件(EnemyKilledObserverAchievement.cs)
public class EnemyKilledObserverAchievement : IGameEventObserver
{
private EnemyKilledSubject m_Subject = null;
private AchievementSystem m_AchievementSystem = null;

public EnemyKilledObserverAchievement(AchievementSystem AchievementSystem)
{
m_AchievementSystem = AchievementSystem;
}

// 设置观察的主题
public override void SetSubject( IGameEventSubject Subject )
{
m_Subject = Subject as EnemyKilledSubject;
}

// 被通知Subject的更新
public override void Update()
{
//Debug.Log("EnemyKilledObserverAchievement.Update: Count["+ m_Subject.GetKilledCount() +"]");
m_AchievementSystem.AddEnemyKilledCount();
}
}

// 关卡分数观察Enemey阵亡事件(EnemyKilledObserverStageScore.cs)
public class EnemyKilledObserverStageScore : IGameEventObserver
{
private EnemyKilledSubject m_Subject = null;
private StageSystem m_StageSystem = null;

public EnemyKilledObserverStageScore( StageSystem theStageSystem )
{
m_StageSystem = theStageSystem;
}

// 设置观察的主题
public override void SetSubject( IGameEventSubject Subject )
{
m_Subject = Subject as EnemyKilledSubject;
}

// 被通知Subject的更新
public override void Update()
{
//Debug.Log("EnemyKilledObserverUI.Update: Count["+ m_Subject.GetKilledCount() +"]");
m_StageSystem.SetEnemyKilledCount( m_Subject.GetKilledCount() );
}
}

// 兵营观察Enemy阵亡事件(EnemyKilledObserverCaptiveCamp.cs)
public class EnemyKilledObserverCaptiveCamp : IGameEventObserver
{
private EnemyKilledSubject m_Subject = null;
private CampSystem m_CampSystem = null;

public EnemyKilledObserverCaptiveCamp(CampSystem theCampSystem)
{
m_CampSystem = theCampSystem;
}

// 设置观察的主题
public override void SetSubject( IGameEventSubject Subject )
{
m_Subject = Subject as EnemyKilledSubject;
}

// 被通知Subject的更新
public override void Update()
{
// 累计阵亡10以上是即展示俘兵营
if( m_Subject.GetKilledCount() > 10 )
m_CampSystem.ShowCaptiveCamp();
}
}

而原本关卡系统对于敌方阵亡次数的获取,也因为新增了游戏事件系统(GameEventSystem),而改由观察者EnemyKilledObserverStageScore进行设置。

②“玩家角色阵亡”主题的观察者

“玩家角色阵亡”主题的观察者,最后会将信息反馈给成就系统(AchievementSystem)和玩家角色信息接口,如图8所示。

图8 “玩家角色阵亡”主题的两个观察者

Listing16 实现Soldier阵亡事件的观察者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 成就观察Soldier阵亡事件(SoldierKilledObserverAchievement.cs)
public class SoldierKilledObserverAchievement : IGameEventObserver
{
private SoldierKilledSubject m_Subject = null;
private AchievementSystem m_AchievementSystem = null;

public SoldierKilledObserverAchievement(AchievementSystem AchievementSystem)
{
m_AchievementSystem = AchievementSystem;
}

// 设置观察的主题
public override void SetSubject( IGameEventSubject Subject )
{
m_Subject = Subject as SoldierKilledSubject;
}

// 被通知Subject的更新
public override void Update()
{
m_AchievementSystem.AddSoldierKilledCount();
}
}

// UI观察Soldier阵亡事件(SoldierKilledObserverUI.cs)
public class SoldierKilledObserverUI : IGameEventObserver
{
private SoldierKilledSubject m_Subject = null; // 主题
private SoldierInfoUI m_InfoUI = null; // 要通知的界面

public SoldierKilledObserverUI( SoldierInfoUI InfoUI )
{
m_InfoUI = InfoUI;
}

// 设置观察的主题
public override void SetSubject( IGameEventSubject Subject )
{
m_Subject = Subject as SoldierKilledSubject;
}

// 被通知Subject的更新
public override void Update()
{
// 通知界面更新
m_InfoUI.RefreshSoldier( m_Subject.GetSoldier() );
}
}

③“玩家角色升级”主题的观察者

“玩家角色升级”主题的观察者,也会通知玩家角色信息接口,如图9所示。

图9 “玩家角色升级”主题的观察者

Listing17 实现UI观察Soldier升级事件(SoldierUpgradeObserverUI.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SoldierUpgradeObserverUI : IGameEventObserver 
{
private SoldierUpgradeSubject m_Subject = null; // 主题
private SoldierInfoUI m_InfoUI = null; // 要通知的界面

public SoldierUpgradeObserverUI( SoldierInfoUI InfoUI )
{
m_InfoUI = InfoUI;
}

// 设置观察的主题
public override void SetSubject( IGameEventSubject Subject )
{
m_Subject = Subject as SoldierUpgradeSubject;
}

// 被通知Subject的更新
public override void Update()
{
// 通知界面更新
m_InfoUI.RefreshSoldier( m_Subject.GetSoldier() );
}
}

④“进入新关卡”主题的观察者

“进入新关卡主题”的观察者,最后也是通知成就系统(AchievementSystem),如图10所示。

图10 “进入新关卡主题”的观察者

Listing18 成就观察新关卡(NewStageObserverAchievement.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class NewStageObserverAchievement : IGameEventObserver 
{
private NewStageSubject m_Subject = null;
private AchievementSystem m_AchievementSystem = null;

public NewStageObserverAchievement(AchievementSystem AchievementSystem)
{
m_AchievementSystem = AchievementSystem;
}

// 设置观察的主题
public override void SetSubject( IGameEventSubject Subject )
{
m_Subject = Subject as NewStageSubject;
}

// 被通知Subject的更新
public override void Update()
{
m_AchievementSystem.SetNowStageLevel( m_Subject.GetStageCount() );
}
}

到了这个阶段,游戏事件系统(GameEventSystem)算是构建完成,让我们再回到本章开始时讨论的成就系统,配合游戏事件系统的订阅机制,新的成就系统被重构为:只记录相关的成就事项,并提供相关的接口方法,让与成就事项相关的观察者们使用。其结构图如图11所示。

图11 重构后的新成就系统(AchievementSystem)

实现成就系统及订阅游戏事件

上图中,有许多的观察者们,这些观察者们在成就系统初始化时就会被加入到游戏事件系统中,而重构后的类也较为简单、清楚:

首先在PBaseDefenseGame中封装相关方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PBaseDefenseGame
{
...
// 注册游戏事件
public void RegisterGameEvent( ENUM_GameEvent emGameEvent, IGameEventObserver Observer)
{
m_GameEventSystem.RegisterObserver( emGameEvent , Observer );
}

// 通知游戏事件
public void NotifyGameEvent( ENUM_GameEvent emGameEvent, System.Object Param )
{
m_GameEventSystem.NotifySubject( emGameEvent, Param);
}
...
}

Listing19 成就系统(AchievementSystem.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class AchievementSystem : IGameSystem 
{
// 记录的成就项目
private int m_EnemyKilledCount = 0;
private int m_SoldierKilledCount = 0;
private int m_StageLv = 0;

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

public override void Initialize ()
{
base.Initialize ();

// 注册相关观察者
m_PBDGame.RegisterGameEvent( ENUM_GameEvent.EnemyKilled , new EnemyKilledObserverAchievement(this));
m_PBDGame.RegisterGameEvent( ENUM_GameEvent.SoldierKilled, new SoldierKilledObserverAchievement(this));
m_PBDGame.RegisterGameEvent( ENUM_GameEvent.NewStage , new NewStageObserverAchievement(this));
}

// 增加Enemy阵亡数
public void AddEnemyKilledCount()
{
//Debug.Log ("AddEnemyKilledCount");
m_EnemyKilledCount++;
}

// 增加Soldier阵亡数
public void AddSoldierKilledCount()
{
//Debug.Log ("AddSoldierKilledCount");
m_SoldierKilledCount++;
}

// 当前关卡
public void SetNowStageLevel( int NowStageLevel )
{
//Debug.Log ("SetNowStageLevel");
m_StageLv = NowStageLevel;
}
}

重构游戏事件触发点

对于重构完的游戏事件系统及成就系统来说,当游戏事件触发时,只要调用游戏事件系统中的信息通知(NotifySubject)方法,就能通过其中的观察者模式将信息广播给所有的相关系统:

Listing20 管理产生出来的角色(CharacterSystem.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class CharacterSystem : IGameSystem
{
// 删除角色
public void RemoveCharacter()
{
// 删除可以删除的角色
RemoveCharacter( m_Soldiers, m_Enemys, ENUM_GameEvent.SoldierKilled );
RemoveCharacter( m_Enemys, m_Soldiers, ENUM_GameEvent.EnemyKilled);
}

// 删除角色
public void RemoveCharacter(List<ICharacter> Characters, List<ICharacter> Opponents, ENUM_GameEvent emEvent)
{
// 分別获取可以删除及存活的角色
List<ICharacter> CanRemoves = new List<ICharacter>();
foreach( ICharacter Character in Characters)
{
// 是否阵亡
if( Character.IsKilled() == false)
continue;

// 是否确认过阵亡事件
if( Character.CheckKilledEvent()==false)
m_PBDGame.NotifyGameEvent( emEvent,Character );

// 是否可以删除
if( Character.CanRemove())
CanRemoves.Add (Character);
}
...
}
}

随着游戏事件系统的完成,相关的游戏事件也从原有的调用点,重构到合适的地点进行调用。通过图12的流程图,就能了解类对象之间的互动情况。

图12 成就系统及其相关系统的流程图

使用观察者模式的优点

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

实现观察者模式时的注意事项

双向与单向信息通知

社交网页上的“粉丝团”比较像是观察者模式:当粉丝团上发布了一则新的动态后,所有订阅的用户都可以看到新增的动态,而用户与用户之间则是同时扮演“主题”与“观察者”的角色,除了同时收到其他好友的动态信息,当自己有任何的动态消息时,也会同时广播给好友们(观察者)。

类过多的问题

“游戏事件”“游戏事件主题(IGameEventSubject)”会随着项目的开发不断地增加,与此同时,这些主题的观察者的数量也会随之上升。从当前的《P级阵地》内容来看,已经产生了7个游戏事件观察者类(IGameEventObserver),所以不难想象,在大型项目可能会产生非常多的观察者类。当然,在某些情况下类过多,反而是个缺点。因此,如果想要减少类的产生,可以考虑向游戏的主题注册时,不要使用“类对象”而是使用“回调函数”,之后再将功能相似的“回调函数”以同一个类来管理,就能减少过多类的问题。这一部分的解决方式与兵营训练单位-命令模式解决大量请求命令时的想法是一样的,读者可以回顾相关的内容。

比较命令模式(Command)与观察者模式

这两个模式都是着重在于将“发生”与“执行”这两个操作消除耦合(或减少依赖性)的模式。当观察者模式中的主题只存在一个观察者时,就非常像是命令模式的基本架构,但还是有一些差异可以分辨出两个模式应用的时机:

  • 命令模式:该模式的另一个重点是“命令的管理”,应用的系统对于发出的命令有新增、删除、记录、排序、撤销等等的需求。
  • 观察者模式:对于“观察者/订阅者”可进行管理,意思是观察者可以在系统运行时间决定订阅或退订等操作,让“执行者(观察者/订阅者)”可以被管理。

所以,两者在应用上还是有明确的目标。当然,如果有需要将两个模式整合应用并非不可能,像是让命令模式的执行者可以动态新增、删除;或是让观察者模式的“每一次发布”都可以被管理等等。而这也是本书所要呈现的重点——模式之间的交互合作,会产生出更大的效果。

观察者模式面对变化时

企划:“小程,我们想要在游戏过程中,增加兵营升级的诱因,让玩家认为高级单位有高生命值的好处,所以…”
小程:“所以想要加什么系统吗?”
企划:“可以记录当前连续成功击退敌人的数量吗?就是Combo。”
小程:“可以再定义清楚一些吗?”
企划:“就是玩家角色在没有阵亡的情况下,连续击退敌方角色的数量,但如果有我方单位阵亡的话,那么就从头开始计数。”
小程:“嗯…复杂了点,我试试看。”

小程分析,这一项记录连续击退(Combo Count)的功能,需要同时接收“玩家角色阵亡事件”以及“敌方角色阵亡事件”,若是新增一个游戏事件观察者(IGameEventObserver),而这个观察者可以同时订阅两个游戏事件主题,内部再加上主题判断的话,应该是可行的:

Listing21 我方连续击退事件(ComboObserver.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class ComboObserver : IGameEventObserver 
{
private SoldierKilledSubject m_SoldierKilledSubject = null;
private EnemyKilledSubject m_EnemyKilledSubject = null;
private PBaseDefenseGame m_PBDGame = null;

private int m_EnemyComboCount =0;
private int m_SoldierKilledCount = 0;
private int m_EnemyKilledCount = 0;

public ComboObserver(PBaseDefenseGame PBDGame)
{
m_PBDGame = PBDGame;
}

// 设置观察的主题
public override void SetSubject( IGameEventSubject theSubject )
{
if( theSubject is SoldierKilledSubject )
m_SoldierKilledSubject = theSubject as SoldierKilledSubject;

if( theSubject is EnemyKilledSubject)
m_EnemyKilledSubject = theSubject as EnemyKilledSubject;
}

// 通知Subject被更新
public override void Update()
{
int NowSoldierKilledCount = m_SoldierKilledSubject.GetKilledCount();
int NowEnemyKilledCount = m_EnemyKilledSubject.GetKilledCount();

// 玩家单位阵亡,重置计数器
if( NowSoldierKilledCount > m_SoldierKilledCount)
m_EnemyComboCount = 0;

// 增加计数器
if( NowEnemyKilledCount > m_EnemyKilledCount)
m_EnemyComboCount ++;

m_SoldierKilledCount = NowSoldierKilledCount;
m_EnemyKilledCount = NowEnemyKilledCount;

// 通知
m_PBDGame.ShowGameMsg("连续击退敌人数:"+m_EnemyComboCount.ToString());
}
}

因为要订阅两个游戏事件主题,所以SetSubject方法会被调用两次,每次调用时都先判断是由哪个主题调用的,然后分别记录在类的成员之中。而这两个主题对象也会在更新(Update)方法中,作为后续判断连续击退时,获取计数的来源。

最后,再将ComboObserver于系统开始时订阅这两个主题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PBaseDefenseGame
{
...
// 注册游戏事件系统
private void ResigerGameEvent()
{
// 事件注册
m_GameEventSystem.RegisterObserver( ENUM_GameEvent.EnemyKilled, new EnemyKilledObserverUI(this));

// Combo
ComboObserver theComboObserver = new ComboObserver(this);
m_GameEventSystem.RegisterObserver( ENUM_GameEvent.EnemyKilled, theComboObserver);
m_GameEventSystem.RegisterObserver( ENUM_GameEvent.SoldierKilled, theComboObserver);
}
...
}

这一次对于功能的增加,是利用现有游戏事件主题来实现的,所以只增加了一个ComboObserver类来完成功能。并且只修改了PBaseDefenseGame.cs来增加必要的订阅主题功能,对于系统的修改程度来说,并不算大(事件本来就已经触发)。由此可以证明运用观察者模式有助于系统的开发和维护。

结论

观察者模式的设计原理是,先设置一个主题(Subject),让这个主题发布时可同时通知关心这个主题的观察者/订阅者,并且主题不必理会观察者/订阅者接下来会执行那些操作。观察者模式的主要功能和优点,就是将“主题发生”与“功能执行”这两个操作解除绑定——即消除依赖性,而且对于“执行者(观察者/订阅者)”来说,还是可以动态决定是否要执行后续的功能。

观察者模式的缺点是可能造成过多的观察者类。不过利用注册“回调函数”来取代注册“类对象”可有效减少类的产生。

其他应用方式

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

0%