关卡设计--责任链模式

思考并回答以下问题:

  • 责任链模式的官方定义是什么?
  • 责任链模式属于什么类型的模式?应用场景有哪些?
  • 让每一关都是对象并加以管理。怎么理解和实现?

本章涵盖:

  • 关卡设计
  • 责任链模式
    • 责任链模式的定义
    • 责任链模式的说明
    • 责任链模式的实现范例
  • 使用责任链模式实现关卡系统
    • 关卡系统的设计
    • 实现说明
    • 使用责任链模式的优点
    • 实现责任链模式时的注意事项
  • 责任链模式面对变化时
  • 结论

关卡设计

经过兵营训练单位-命令模式一章的功能实现后,现在可以通过兵营界面(CampInfoUI)来产生玩家角色,并迎战来袭的敌方角色。在游戏角色的产生-工厂方法模式时曾介绍,敌方角色也是从角色工厂(ICharacterFactory)中产生的,但是,玩家角色是由玩家自行决定产生的时间,而那些要占领玩家阵地的敌方角色,又是由谁负责下达产生的命令呢?在《P级阵地》中,是由关卡系统(StageSystem)负责这些工作。

《P级阵地》对于关卡功能的需求是这样的:

  • (1)每次关卡开始时,会同时出现n个敌方角色,这n个角色会不断地往玩家阵地移动。
  • (2)敌方角色到达阵地中心3次以上时,游戏结束,而玩家的最高闯关记录为当前这一个关卡。
  • (3)在兵营训练的玩家角色会守护阵地,如果将敌方角色全部击杀,代表通过这一关,游戏进入下一关。
  • (4)每一个关卡都设有通关分数,若未达关卡设置的分数,则重复这一关。
  • (5)重复1-4的流程,直到玩家无法成功守护阵地(即满足2的规则)为止。

分析上述功能需求后,《P级阵地》的关卡系统(StageSystem)需要完成下列相关实现:

  • (1)指定每一关出现敌方阵营角色的数量和等级。
  • (2)每一关需设置通关条件,条件满足后即开启下一关。
  • (3)每一关也必须知道要开启的下一关。
  • (4)如果阵地中央被占领次数超过3次,游戏结束。

关卡系统(StageSystem)实现时,将它列为游戏系统(IGameSystem )之一,因此和其他子系统一样继承IGameSystem接口并实现。另外,也在PBaseDefenseGame类中产生对象,作为类成员之一:

Listing1 关卡控制系统(StageSystem.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
public class StageSystem : IGameSystem
{
public const int MAX_HEART = 3; // 玩家最大生命

private int m_NowHeart = MAX_HEART; // 当前玩家生命
private int m_NowStageLv = 1; // 当前的关卡
private int m_EnemyKilledCount = 0; // 当前敌方单位阵亡数

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

// 初始化
public override void Initialize()
{
// 设置关卡
InitializeStageData();
// 指定第一个关卡
m_NowStageLv = 1;
}

// 释放
public override void Release ()
{
base.Release ();
m_SpawnPosition.Clear();
m_SpawnPosition = null;
m_NowHeart = MAX_HEART;
m_EnemyKilledCount = 0;
}

// 更新
public override void Update()
{
...
}

// 通知玩家损失
public void LoseHeart()
{
m_NowHeart --;
m_PBDGame.ShowHeart( m_NowHeart );
}

// 增加当前击杀数
public void AddEnemyKilledCount()
{
m_EnemyKilledCount++;
}

// 设置当前击杀数
public void SetEnemyKilledCount( int KilledCount)
{
// Debug.Log("StageSysem.SetEnemyKilledCount:"+KilledCount);
m_EnemyKilledCount = KilledCount;
}

// 获取当前击杀数
public int GetEnemyKilledCount()
{
return m_EnemyKilledCount;
}
...
}

关卡系统(StageSystem)成员包含了当前玩家阵营被攻击的情况(m_NowHeart)、当前击杀敌方阵营的角色数量(m_EnemyKilledCount)及当前进行的关卡(m_NowStageLv),并且提供相关的方法来操作相关成员。

如果在关卡系统(StageSystem)的定期更新(Update)方法中,以成员m_NowStageLv作为关卡前进的依据,那么可能会以下面的方式来实现:

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
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
public class StageSystem : IGameSystem
{
private int m_NowStageLv = 1; // 当前的关卡
private List<Vector3> m_SpawnPosition = null; // 敌方角色出生点
private Vector3 m_AttackPos = Vector3.zero; // 攻击点
private bool m_bCreateStage = false; // 是否需要产生关卡

// 定期更新
public override void Update()
{
// 是否要开启新关卡
if(m_bCreateStage)
{
CreateStage();
m_bCreateStage = false;
}

// 是否要切换下一个关卡
if(m_PBDGame.GetEnemyCount() == 0 )
{
if( CheckNextStage() )
m_NowStageLv++ ;
m_bCreateStage = true;
}
}

// 产生关卡
private void CreateStage()
{
// 一次产生一个单位
ICharacterFactory Factory = PBDFactory.GetCharacterFactory();
Vector3 AttackPosition = GetAttackPosition();

switch( m_NowStageLv)
{
case 1:
Debug.Log("产生第1关");
Factory.CreateEnemy( ENUM_Enemy.Elf, ENUM_Weapon.Gun, GetSpawnPosition(), AttackPosition);
Factory.CreateEnemy( ENUM_Enemy.Elf, ENUM_Weapon.Gun, GetSpawnPosition(), AttackPosition);
Factory.CreateEnemy( ENUM_Enemy.Elf, ENUM_Weapon.Gun, GetSpawnPosition(), AttackPosition);
break;

case 2:
Debug.Log("产生第2关");
Factory.CreateEnemy( ENUM_Enemy.Elf, ENUM_Weapon.Gun, GetSpawnPosition(), AttackPosition);
Factory.CreateEnemy( ENUM_Enemy.Elf, ENUM_Weapon.Rifle, GetSpawnPosition(), AttackPosition);
Factory.CreateEnemy( ENUM_Enemy.Troll, ENUM_Weapon.Gun, GetSpawnPosition(), AttackPosition);
break;

case 3:
Debug.Log("产生第3关");
Factory.CreateEnemy( ENUM_Enemy.Elf, ENUM_Weapon.Gun, GetSpawnPosition(), AttackPosition);
Factory.CreateEnemy( ENUM_Enemy.Troll, ENUM_Weapon.Gun, GetSpawnPosition(), AttackPosition);
Factory.CreateEnemy( ENUM_Enemy.Troll, ENUM_Weapon.Rifle, GetSpawnPosition(), AttackPosition);
break;
}
}

// 确认是否要切换到下一个关卡
private bool CheckNextStage()
{
switch( m_NowStageLv)
{
case 1:
if( GetEnemyKilledCount() >= 3)
return true;
break;

case 2:
if( GetEnemyKilledCount() >= 6)
return true;
break;

case 3:
if( GetEnemyKilledCount() >= 9)
return true;
break;
}
return false;
}

// 获取出生点
private Vector3 GetSpawnPosition()
{
if( m_SpawnPosition == null)
{
m_SpawnPosition = new List<Vector3>();

for(int i=1; i<=3; ++i)
{
string name = string.Format("EnemySpawnPosition{0}", i);
GameObject tempObj = UnityTool.FindGameObject( name );

if( tempObj==null)
continue;

tempObj.SetActive(false);
m_SpawnPosition.Add( tempObj.transform.position );
}
}

// 随机返回
int index = UnityEngine.Random.Range(0, m_SpawnPosition.Count -1 );
return m_SpawnPosition[index];
}

// 获取攻击点
private Vector3 GetAttackPosition()
{
if( m_AttackPos == Vector3.zero)
{
GameObject tempObj = UnityTool.FindGameObject("EnemyAttackPosition");

if( tempObj==null)
return Vector3.zero;

tempObj.SetActive(false);
m_AttackPos = tempObj.transform.position;
}
return m_AttackPos;
}
...
}

定期更新Update方法中,判断当前是否需要产生新的关卡,如果需要,则先调用产生关卡CreateStage方法。而关卡是否结束,则是直接判断当前敌方阵营的角色数量,如果为0,代表关卡结束可以进入下一个关卡。确认开始下一个关卡CheckNextStage方法中,会先判断当前得分来判断是否可以进入下一个关卡。

仔细分析两个与关卡产生有关的方法:CreateStage、CheckNextStage,其中都按照当前关卡(m_NowStageLv)的值,来决定接下来要产生哪些敌方角色以及是否切换到下一个关卡。上述的程序代码只产生了3个关卡而已,但《P级阵地》的目标是希望能设置数十个以上的关卡让玩家挑战,所以若以上述的写法来设计的话,程序代码将变得非常冗长,而且弹性不足,无法让策划人员快速设置和调整,所以我们需要使用新的设计来重新编写程序。

重构的目标是,希望能将关卡数据使用类加以封装。而封装的信息包含:要出场的敌方角色的设置、通关条件、下一关的记录等。也就是让每一关都是一个对象并加以管理。而关卡系统则是在这群对象中寻找“条件符合”的关卡,让玩家进入挑战。等到关卡完成后,再进入到下一个条件符合的关卡。

试着寻找GoF设计模式中可以使用的模式,责任链模式可以提供重构时的依据。

责任链模式

当有问题需要解决,而且可以解决问题的人还不止一个时,就有很多方式可以得到想要的答案。例如可以将问题同时交给可以解决问题的人,请他们都回答,但这个方式比较浪费资源,也会造成重复,也有可能回答的人有等级之分,不适合太简单和太复杂的问题。另一个方式就是将可以回答问题的人,按照等级或专业一个个串接起来。责任链模式就是提供了一个可以将这些回答问题的人,一个个链接起来的设计方法。

责任链模式的定义

GoF对责任链模式(Chain of Responsibility)的定义是:

1
让一群对象都有机会来处理一项请求,以减少请求发送者与接收者之间的耦合度(即依赖度),将所有的接收者对象串接起来,让请求沿着串接传递,直到有一个对象可以处理为止。

以下,笔者先以现实生活的亲身经历,来说明责任链模式在非软件设计领域中的呈现实例:

有家非常大的电信公司,拥有非常多的部门,每个部门都负责不同的业务,也是由于每个部门的业务都过于繁多,所以每个部门都有自己的客服部门。有一天,笔者一款上市运营中的游戏,接到玩家的反馈说,游戏出现无法正常连线的问题。经查明之后发现,这些玩家的计算机无法将游戏使用的域名(Domain Name)转换成IP地址(IP Address),恰好,这个问题也发生在笔者家中使用的计算机上。所以我先使用指令,确认游戏域名转换成IP地址时的情况,检查之后发现,转换后的IP地址会不定时地在两个IP之间切换,而我当时计算机的DNS服务器(DNS: Domain Name Server,即域名服务器)是设置为那一家电信公司提供的DNS。但当时服务公司的IT部门人员都已下班,无法获取公司DNS更新的情况,又着急要解决问题,所以当下的反应就是想直接打电话给电信公司询问:“为什么由你们DNS返回的网址,会不定时在两个IP之间切换?”

那么我该询问哪个部门呢?当下我也不是很清楚,所以就直接拿起电话拨打该电信公司的“通用客服专线”。接通后,我将遇到的问题很清楚地说明了一次。但很不幸,那位客服人员不好意思地说了声抱歉,因为他们负责的是“电信业务”不是“网络业务”,所以“电信业务的客服”就将我的电话转给了“网络业务的容服”。

“网络业务的客服”人员接通后,我一样将问题重新说明了一次(所以很明显地,他们的客服间为了快速转换服务,不会将客户遇到的问题一起移动,当然也可能是因为问题太复杂),而网络业务的客服人员,似乎是第一次遇到这样的问题,于是让我挂线等了一下。当网络业务的客服人员重新接回时却表示,他们部门也无法解决,所以再将我的电话转换到“网络机房的客服”。

同样地,在与“网络机房的客服”人员接通后,我再将问题重新说明了一次。而这次的客服人员,总算可以了解我的问题,并且再询问几个问题之后,留下我的联络方式并说明,待他们查明后会通知我。我在隔天早上收到了邮件通知,了解了问题发生的真正原因,进而解决了玩家无法正常连线的问题。

若运用责任链模式来说明的话,可以这样模拟:“为什么由你们的DNS返回的网址,会不定时在两个IP之间切换”是这次事件中的“请求”,客户(也就是我)就是“请求的发送者”,而那家电信公司的电信业务的客服、网络业务的客服、网络机房的客服,都是“接收者对象”,他们之间使用电话分机的方式彼此串联。当客户的“请求”无法在第一个部门(电信业务的容服)被解决时,客户的“请求”就通过他们内部串联(分机)的方式,转到下一个可能可以解决问题的部门,第二个部门无法解决时再转往下一个,直到“网络机房的客服”解决了问题为止,如图1所示。

图1 责任链模式的电信客服示意图

对于客户而言,当时对应的只有一个所谓的“客服人员”角色,而这个“客服人员”是属于电信公司的哪一个部门并不重要,客户(也就是我)还是使用与一般“客服人员”的对话方式,而对方也使用标准“客服流程”来应答。而这也是定义中所说的:“减少请求发送者与接收者之间的耦合度”,“客服人员”都使用一般的应答方式(电话沟通),不需要因为不同的部门,而有不同的沟通工具或互动方式。

所以,从上述举例中可以了解到,责任链模式描述的就是,将一群能够解决问题的部门(对象),使用电话分机通话后,一同来解决客户(请求发送者)问题的模式。而其中还要让客户(请求发送者)减少接口转换的负担(减少耦合度即依赖度),也就是都是使用电话沟通,不必中途转换去使用计算机或其他沟通工具

因此,让我们从现实的个案回到软件的开发上,在运用责任链模式解决问题时,只要将下列的几个重点列出来分别实现,即可满足模式的基本要求:

  • 可以解决请求的接收者对象:这些类对象能够了解“请求”信息的内容,并判断本身能否解决。
  • 接收者对象间的串接:利用一个串接机制,将每一个可能可以解决问题的接收者对象给串接起来。对于被串接的接收者对象来说,当本身无法解决这个问题时,就利用这个串接机制,让请求能不断地传递下去;或使用其他管理方式,让接收者对象得以链接。
  • 请求自动转移传递:发出请求后,请求会自动往下转移传递,过程之中,发送者不需特别转换接口。

责任链模式的说明

将责任链模式的各项重点结构化后,可用图2来表示。

图2 将责任链模式的各项重点结构化的结构图

GoF参与者的说明如下:

  • Handler(请求接收者接口)
    • 定义可以处理客户端请求事项的接口;
    • 可包含“可链接下一个同样能处理请求”的对象引用。
  • ConcreteHandler1、ConcreteHandler2(实现请求接收者接口)
    • 实现请求处理接口,并判断对象本身是否能处理这次的请求;
    • 不能完成请求的话,交由后继者(下一个)来处理。
  • Client(请求发送者)
    • 将请求发送给第一个接收者对象,并等待请求的回复。

责任链模式的实现范例</span>

从GoF定义的基本架构实现来看,我们首先必须定义Handler(请求接收者接口):

Listing3 处理信息的接口(ChainofResponsibility.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Handler
{
protected Handler m_NextHandler = null;

public Handler(Handler theNextHandler)
{
m_NextHandler = theNextHandler;
}

public virtual void HandleRequest(int Cost)
{
if(m_NextHandler != null)
m_NextHandler.HandleRequest(Cost);
}
}

类的构造方法说明了,当对象被产生时,就要提供一个可以链接到下一个请求接收者(Handler)的对象引用。而处理请求方法(HandleRequest)被声明为虚函数,让是否传递给后继者的任务交由子类重新定义,父类不针对传入的参数作判断,只是直接传给下一个对象。

接下来,定义3个子类来实现Handler接口,分别用来处理传入参数(Cost)所需要的判断:

Listing4 实现信息处理类(ChainofResponsibility.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
// 处理所负责的信息1
public class ConcreteHandler1 : Handler
{
private int m_CostCheck = 10;

public ConcreteHandler1( Handler theNextHandler ) : base( theNextHandler )
{}

public override void HandleRequest(int Cost)
{
if( Cost <= m_CostCheck)
Debug.Log("ConcreteHandler1.HandleRequest核准");
else
base.HandleRequest(Cost);
}
}

// 处理所负责的信息2
public class ConcreteHandler2 : Handler
{
private int m_CostCheck = 20;

public ConcreteHandler2( Handler theNextHandler ) : base( theNextHandler )
{}

public override void HandleRequest(int Cost)
{
if( Cost <= m_CostCheck)
Debug.Log("ConcreteHandler2.HandleRequest核准");
else
base.HandleRequest(Cost);
}
}

// 处理所负责的信息3
public class ConcreteHandler3 : Handler
{
public ConcreteHandler3( Handler theNextHandler ) : base( theNextHandler )
{}

public override void HandleRequest(int Cost)
{
Debug.Log("ConcreteHandler3.HandleRequest核准");
}
}

这个范例所要呈现的是,对于传入参数Cost的“核准权限”确认,而这3个类就是“可以解决请求的接收者对象”,使用父类中的成员m_NextHandler将它们串接起来。每一个类负责一定金额的核准权限,如果传入的Cost超过自己能够核准的权限,就将请求传给下一个对象,请下一个对象来核准,直到有某个对象完成核准为止。

在测试程序中,先将3个子类对象分别产生并串接,所以测试程序担任的就是Client端,最后再将不同的Cost参数带入:

Listing5 测试责任链模式(ChainofResponsibilityTest.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void UnitTest() 
{
// 产生Cost验证的链接方式
ConcreteHandler3 theHandle3 = new ConcreteHandler3(null);
ConcreteHandler2 theHandle2 = new ConcreteHandler2(theHandle3);
ConcreteHandler1 theHandle1 = new ConcreteHandler1(theHandle2);

// 确认
theHandle1.HandleRequest(10);
theHandle1.HandleRequest(15);
theHandle1.HandleRequest(20);
theHandle1.HandleRequest(30);
theHandle1.HandleRequest(100);
}

执行结果1显示出每一种金额负责核准的对象信息

1
2
3
4
5
ConcreteHandler1.HandleRequest核准
ConcreteHandler2.HandleRequest核准
ConcreteHandler2.HandleRequest核准
ConcreteHandler3.HandleRequest核准
ConcreteHandler3.HandleRequest核准

使用责任链模式实现关卡系统

游戏中的关卡都是一关关的串接,完成了这一关之后就进入下一关,所以在实现上使用责任链模式来串接每一个关卡是非常合适的。但是对于每一个关卡的通关判断规则,则要按照各个游戏的需求来设计,可能就不是如同前一节的范例那样,可以使用一个属性来作为开启下一个关卡的条件。

关卡系统的设计

对于《P级阵地》关卡系统(StageSystem)的修改需求上,关卡可能需要的信息包含要出场的敌方角色设置、通关条件及连接下一关卡对象的引用,封装成一个“接收者类”,并增加能够判断通关与否的方法,作为是否前进到下一关的判断依据。关卡串接的图解如图3所示。

图3 关卡串接的图解

每个关卡对象都会判断“当前的游戏状态”是否符合关卡的“通关条件”:

  • 如果符合通关条件,则将关卡通关与否的判断交由下一个关卡对象判断,直到有一个关卡对象负责接下来的“关卡开启”工作。
  • 如果不符合,则将“当前关卡”维持在这一个关卡对象上,继续让现在的关卡对象负责“关卡开启”工作。

运用责任链模式后的关卡系统结构如图4所示。

图4 运用责任链模式后的关卡系统结构

参与者的说明如下:

  • IStageHandler:定义可以处理“过关判断”和“关卡开启”的接口,也包含指向下一个关卡对象的引用。
  • NormalStageHandler:实现关卡接口,负责“常规”关卡的开启和过关条件判断。
  • IStageScore:定义判断通关与否的操作接口。
  • StageScoreEnemyKilledCount:使用当前的“击杀敌方角色数”,作为通关与否的判断。
  • IStageData:定义关卡内容的操作接口,在《P级阵地》中,关卡内容指的是:
    • 这一关会出现攻击玩家阵营的敌方角色数据之设置;
    • 关卡开启;
    • 关卡是否结束的判断。
  • NormalStageData:实现“常规”关卡内容,实际将产生的敌方角色放入战场上攻击玩家阵营,以及实现判断关卡是否结束的方法。

此外,IStageScore和IStageData这两个类也是应用策略模式(Strategy)的类,让关卡系统在“过关判断”和“产生敌方单位”这两个设计需求上,能更具备灵活性,不限制只有一种玩法。

实现说明

关卡接口IStageHandler中定义了关卡操作的方法:

Listing6 关卡接口(IStageHandler.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 关卡接口
public abstract class IStageHandler
{
protected IStageData m_StageData = null; // 关卡的内容(敌方角色)
protected IStageScore m_StageScore = null; // 关卡的分数(通关条件)
protected IStageHandler m_NextHandler = null; // 下一个关卡

// 设置下一个关卡
public IStageHandler SetNextHandler(IStageHandler NextHandler)
{
m_NextHandler = NextHandler;
return m_NextHandler;
}

public abstract IStageHandler CheckStage();
public abstract void Update();
public abstract void Reset();
public abstract bool IsFinished();
}

其中,CheckStage用来判断当前游戏所处的关卡是哪一个,所以会返回一个IStageHandler引用给关卡系统(StageSystem),当前《P级阵地》先实现了“常规关卡”的功能:

Listing7 常规关卡(NormalStageHandler.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 NormalStageHandler : IStageHandler 
{
// 设置分数和关卡数据
public NormalStageHandler(IStageScore StateScore, IStageData StageData )
{
m_StageScore = StateScore;
m_StageData = StageData;
}

// 设置下一个关卡
public IStageHandler SetNextHandler(IStageHandler NextHandler)
{
m_NextHandler = NextHandler;
return m_NextHandler;
}

// 确认关卡
public override IStageHandler CheckStage()
{
// 分数是否足够
if( m_StageScore.CheckScore()==false)
return this;

// 已经是最后一关了
if(m_NextHandler==null)
return this;

// 确认下一个关卡
return m_NextHandler.CheckStage();
}

public override void Update()
{
m_StageScore.Update();
}

public override void Reset()
{
m_StageScore.Reset();
}

public override bool IsFinished()
{
return m_StageScore.IsFinished();
}
}

在关卡初始化时(NormalStageHandler),会将关卡所使用的“过关条件”和“关卡内容”设置给关卡对象,并且利用SetNextHandler来设置连接的下一个关卡。在确认关卡CheckStage方法中,判断当前游戏状态是否符合关卡过关的条件判断。如果已满足,代表可前往下一个关卡,该方法最后会返回游戏当前可使用的关卡对象给关卡系统。

当前的关卡判定,是以“击杀敌方角色数”为通关的条件判断:

Listing8 关卡分数确认(IStageScore.cs)

1
2
3
4
public abstract class IStageScore
{
public abstract bool CheckScore();
}

Listing9 关卡分数确认:敌人阵亡数(StageScoreEnemyKilledCount)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class StageScoreEnemyKilledCount : IStageScore
{
private int m_EnemyKilledCount = 0;
private StageSystem m_StageSystem = null;

public StageScoreEnemyKilledCount( int KilledCount, StageSystem theStageSystem)
{
m_EnemyKilledCount = KilledCount;
m_StageSystem = theStageSystem;
}

// 确认关卡分数是否达到
public override bool CheckScore()
{
return ( m_StageSystem.GetEnemyKilledCount() >= m_EnemyKilledCount);
}
}

此外,在常规关卡(NormalStageHandler)类中,有个“关卡内容”对象需要被定期更新:

1
2
3
4
5
6
7
8
9
10
public class NormalStageHandler : IStageHandler
{
protected IStageData m_StageData = null; // 关卡的内容(敌方角色)
...
public override void Update()
{
m_StageData.Update();
}
...
}

至于关卡内容(IStageData)类主要负责的则是将关卡的“内容”呈现给玩家:

Listing10 关卡内容接口(IStageData.cs)

1
2
3
4
5
6
public abstract class IStageData
{
public abstract void Update();
public abstract bool IsFinished();
public abstract void Reset();
}

“关卡内容”一般指的就是玩家要挑战的项目,这些项目可能是出现3个敌方角色,让玩家击退;也可能是出现3个道具让玩家可以去搜索获取;或是设计特殊任务关卡让玩家去完成。而这些设置内容都会放进IStageData的子类中,并且通过Game Loop更新机制,让关卡内容可以顺利产生给玩家挑战。以下是当前实现的常规关卡内容(NormalStageData):

Listing11 常规关卡内容(NormalStageData.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
public class NormalStageData : IStageData 
{
private float m_CoolDown = 0; // 产生角色的间隔时间
private float m_MaxCoolDown = 0;
private Vector3 m_SpawnPosition = Vector3.zero; // 出生点
private Vector3 m_AttackPosition = Vector3.zero;// 攻击目标
private bool m_AllEnemyBorn = false;

// 关卡内要产生的敌人单位
private List<StageData> m_StageData = new List<StageData>();

// 常规关卡要产生的敌人单位
class StageData
{
public ENUM_Enemy emEnemy = ENUM_Enemy.Null;
public ENUM_Weapon emWeapon = ENUM_Weapon.Null;
public bool bBorn = false;
public StageData( ENUM_Enemy emEnemy, ENUM_Weapon emWeapon )
{
this.emEnemy = emEnemy;
this.emWeapon= emWeapon;
}
}

// 设置多久产生一个敌方单位
public NormalStageData(float CoolDown ,Vector3 SpawnPosition, Vector3 AttackPosition)
{
m_MaxCoolDown = CoolDown;
m_CoolDown = m_MaxCoolDown;
m_SpawnPosition = SpawnPosition;
m_AttackPosition = AttackPosition;
}

// 增加关卡的敌方单位
public void AddStageData( ENUM_Enemy emEnemy, ENUM_Weapon emWeapon,int Count)
{
for(int i=0;i<Count;++i)
m_StageData.Add ( new StageData(emEnemy, emWeapon));
}

// 重置
public override void Reset()
{
foreach( StageData pData in m_StageData)
pData.bBorn = false;
m_AllEnemyBorn = false;
}

// 更新
public override void Update()
{
if( m_StageData.Count == 0)
return ;

// 是否可以产生
m_CoolDown -= Time.deltaTime;
if( m_CoolDown > 0)
return ;
m_CoolDown = m_MaxCoolDown;

// 获取上场的角色
StageData theNewEnemy = GetEnemy();
if(theNewEnemy == null)
return;

// 一次产生一个单位
ICharacterFactory Factory = PBDFactory.GetCharacterFactory();
Factory.CreateEnemy( theNewEnemy.emEnemy, theNewEnemy.emWeapon, m_SpawnPosition, m_AttackPosition);
}

// 获取还没产生的关卡
private StageData GetEnemy()
{
foreach( StageData pData in m_StageData)
{
if(pData.bBorn == false)
{
pData.bBorn = true;
return pData;
}
}
m_AllEnemyBorn = true;
return null;
}

// 是否完成
public override bool IsFinished()
{
return m_AllEnemyBorn;
}
}

以当前实现的常规关卡内容(NormalStageData)来看,类内定义了数个与派送敌方角色上场有关的设置参数:

1
2
3
4
5
6
7
8
private float m_CoolDown = 0;  // 产生角色的间隔时间
private float m_MaxCoolDown = 0;
private Vector3 m_SpawnPosition = Vector3.zero; // 出生点
private Vector3 m_AttackPosition = Vector3.zero; // 攻击目标
private bool m_AllEnemyBorn = false;

// 关卡内要产生的敌人单位
private List<StageData> m_StageData = new List<StageData>();

而要上场的角色数据,则是利用AddStageData方法来设置。

1
2
3
4
5
6
7
8
// 增加关卡的故方单位
public void AddStageData (ENUM_Enemy emEnemy,
ENUM_Weapon emWeapon,
int count)
{
for (int i= 0; i<Count; ++i)
m_StageData.Add ( new StageData (emEnemy, emWeapon));
}

此外,还有一些其他信息会在更新方法(Update)中被使用到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 更新
public override void Update()
{
if( m_StageData.Count == 0)
return ;

// 是否可以产生
m_CoolDown -= Time.deltaTime;
if( m_CoolDown > 0)
return ;
m_CoolDown = m_MaxCoolDown;

// 获取上场的角色
StageData theNewEnemy = GetEnemy();
if(theNewEnemy == null)
return;

// 一次产生一个单位
ICharacterFactory Factory = PBDFactory.GetCharacterFactory();
Factory.CreateEnemy( theNewEnemy.emEnemy, theNewEnemy.emWeapon, m_SpawnPosition, m_AttackPosition);
}

当敌方角色可以产生时,就通过调用角色工厂(CharacterFactory)的方法,将对象产出并放入战场中。

关卡系统(StageSystem)也配合新的关卡类进行修正:

Listing12 关卡控制系统(StageSystem.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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
public class StageSystem : IGameSystem
{
public const int MAX_HEART = 3;

private int m_NowHeart = MAX_HEART; // 当前玩家阵地情況
private int m_EnemyKilledCount = 0; // 当前敌方单位阵亡数
private int m_NowStageLv = 1; // 当前的关卡

private IStageHandler m_NowStageHandler = null;
private IStageHandler m_RootStageHandler = null;
private List<Vector3> m_SpawnPosition = null; // 出生点
private Vector3 m_AttackPos = Vector3.zero; // 攻击点
private bool m_bCreateStage = false; // 是否需要产生关卡

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

public override void Initialize()
{
// 设置关卡
InitializeStageData();
// 指定第一个关卡
m_NowStageHandler = m_RootStageHandler;
m_NowStageLv = 1;
}

public override void Release ()
{
base.Release ();
m_SpawnPosition.Clear();
m_SpawnPosition = null;
m_NowHeart = MAX_HEART;
m_EnemyKilledCount = 0;
m_AttackPos = Vector3.zero;
}

// 更新
public override void Update()
{
// 更新当前的关卡
m_NowStageHandler.Update();

// 是否要切换下一个关卡
if(m_PBDGame.GetEnemyCount() == 0 )
{
// 是否结束
if( m_NowStageHandler.IsFinished()==false)
return ;

// 获取下一关
IStageHandler NewStageData = m_NowStageHandler.CheckStage();

// 是否为旧的关卡
if( m_NowStageHandler == NewStageData)
m_NowStageHandler.Reset();
else
m_NowStageHandler = NewStageData;

// 通知进入下一关
NotiyfNewStage();
}
}

// 通知损失
public void LoseHeart()
{
m_NowHeart --;
m_PBDGame.ShowHeart( m_NowHeart );
}

// 增加当前击杀数
public void AddEnemyKilledCount()
{
m_EnemyKilledCount++;
}

// 设置当前击杀数
public void SetEnemyKilledCount( int KilledCount)
{
m_EnemyKilledCount = KilledCount;
}

// 获取当前击杀数
public int GetEnemyKilledCount()
{
return m_EnemyKilledCount;
}

// 通知新的关卡
private void NotiyfNewStage()
{
m_PBDGame.ShowGameMsg("新的关卡");
m_NowStageLv++;

// 显示
m_PBDGame.ShowNowStageLv(m_NowStageLv);

// 通知Soldier升级
m_PBDGame.UpgradeSoldier();

// 事件
m_PBDGame.NotifyGameEvent( ENUM_GameEvent.NewStage , null );

}

// 初始化所有关卡
private void InitializeStageData()
{
if( m_RootStageHandler!=null)
return ;

// 引用点
Vector3 AttackPosition = GetAttackPosition();

NormalStageData StageData = null; // 关卡要产生的Enemy
IStageScore StageScore = null; // 关卡过关信息
IStageHandler NewStage = null;

// 第1关
StageData = new NormalStageData(3f, GetSpawnPosition(), AttackPosition );
StageData.AddStageData( ENUM_Enemy.Elf, ENUM_Weapon.Gun, 3);
StageScore = new StageScoreEnemyKilledCount(3, this);
NewStage = new NormalStageHandler(StageScore, StageData );

// 设置为起始关卡
m_RootStageHandler = NewStage;

// 第2关
StageData = new NormalStageData(3f, GetSpawnPosition(), AttackPosition);
StageData.AddStageData( ENUM_Enemy.Elf, ENUM_Weapon.Rifle,3);
StageScore = new StageScoreEnemyKilledCount(6, this);
NewStage = NewStage.SetNextHandler( new NormalStageHandler( StageScore, StageData) );

...

// 第10关
StageData = new NormalStageData(3f, GetSpawnPosition(), AttackPosition);
StageData.AddStageData( ENUM_Enemy.Elf, ENUM_Weapon.Rocket,3);
StageData.AddStageData( ENUM_Enemy.Troll, ENUM_Weapon.Rocket,3);
StageData.AddStageData( ENUM_Enemy.Ogre, ENUM_Weapon.Rocket,3);
StageScore = new StageScoreEnemyKilledCount(30, this);
NewStage = NewStage.SetNextHandler( new NormalStageHandler( StageScore, StageData) );
}

// 获取出生点
private Vector3 GetSpawnPosition()
{
if( m_SpawnPosition == null)
{
m_SpawnPosition = new List<Vector3>();

for(int i=1;i<=3;++i)
{
string name = string.Format("EnemySpawnPosition{0}",i);
GameObject tempObj = UnityTool.FindGameObject( name );
if( tempObj==null)
continue;
tempObj.SetActive(false);
m_SpawnPosition.Add( tempObj.transform.position );
}
}

// 随机返回
int index = UnityEngine.Random.Range(0, m_SpawnPosition.Count -1 );
return m_SpawnPosition[index];
}

// 获取攻击点
private Vector3 GetAttackPosition()
{
if( m_AttackPos == Vector3.zero)
{
GameObject tempObj = UnityTool.FindGameObject("EnemyAttackPosition");
if( tempObj==null)
return Vector3.zero;
tempObj.SetActive(false);
m_AttackPos = tempObj.transform.position;
}
return m_AttackPos;
}
}

修正的关键点在于

1、定期更新(Update)方法中,将切换关卡的判断交给一群关卡对象串接起来的链表来负责,所以需要切换关卡时,询问关卡对象链表,就可以获取当前可以进行的关卡:

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
 public override void Update()
{
// 更新当前的关卡
m_NowStageHandler.Update();

// 是否要切换下一个关卡
if(m_PBDGame.GetEnemyCount() == 0 )
{
// 是否结束
if( m_NowStageHandler.IsFinished()==false)
return ;

// 获取下一关
IStageHandler NewStageData = m_NowStageHandler.CheckStage();

// 是否为旧的关卡
if( m_NowStageHandler == NewStageData)
m_NowStageHandler.Reset();
else
m_NowStageHandler = NewStageData;

// 通知进入下一关
NotiyfNewStage();
}
}

2、在初始化关卡系统时,将所有关卡的数据一次设置完成,包含关卡要出现的敌方角色等级、数量、武器等级、过关的判断分数及连接的下一关:

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
private void InitializeStageData()
{
if( m_RootStageHandler!=null)
return ;

// 引用点
Vector3 AttackPosition = GetAttackPosition();

NormalStageData StageData = null; // 关卡要产生的Enemy
IStageScore StageScore = null; // 关卡过关信息
IStageHandler NewStage = null;

// 第1关
StageData = new NormalStageData(3f, GetSpawnPosition(), AttackPosition );
StageData.AddStageData( ENUM_Enemy.Elf, ENUM_Weapon.Gun, 3);
StageScore = new StageScoreEnemyKilledCount(3, this);
NewStage = new NormalStageHandler(StageScore, StageData );

// 设置为起始关卡
m_RootStageHandler = NewStage;

// 第2关
StageData = new NormalStageData(3f, GetSpawnPosition(), AttackPosition);
StageData.AddStageData( ENUM_Enemy.Elf, ENUM_Weapon.Rifle,3);
StageScore = new StageScoreEnemyKilledCount(6, this);
NewStage = NewStage.SetNextHandler( new NormalStageHandler( StageScore, StageData) );

...
}

正如同在游戏属性管理功能-享元模式提到的,将属性集中在角色属性工厂(IAttrFactory)中,有助于策划进行设置和调整。不过,更好的方式则是使用“策划设置工具”,让策划人员在使用工具程序设置后,输出成设置文件,再由关卡系统(StageSystem)读入。

使用责任链模式的优点

将旧方法中的CreateStage、CheckNextStage两个方法的内容,使用关卡对象来替代,这样一来,原本可能出现的冗长式写法就获得了改善。并且将关卡内容(IStageData)、过关条件(IStageScore)类化,可使得《P级阵地》中关卡的类型有多种形式的组合。而关卡设计的数据将来也可以搭配“策划工具”来设置,增加关卡设计人员的调整灵活度。

实现责任链模式时的注意事项

实现责任链模式并非一成不变的,在具体实现中,常常可按照实际的需求来微调实现方式:

不用从头判断

责任链模式的实现范例节的范例中,针对每一次的Score进行判断时,测试程序代码都要求从接收者链表中的第一个对象开始判断。但《P级阵地》在每次判断关卡推进时,并没有从第一个关卡开始,而是从当前的关卡对象(m_NowStageHandle)开始往下判断。存在这种实现上的差异,其原因在于设计需求上的不同,因为《P级阵地》的设计需求是一关一关往下推进的,并不会发生回头的情况,所以在判断上,可以直接从当前的关卡对象继续往下,不必再从第一关开始判断。但如果游戏的关卡设计存在“退回上一关卡”的需求时,那么就必须改写成“从第1关卡开始判断”的实现方式。

使用泛型容器来管理关卡对象

在责任链模式中的Handle类,通常都会定义一个引用指向下一个可以接收的对象。但如果接收对象间的关系如《P级阵地》中的关卡对象,那么还可以有另一种设计方式——也就是将所有关卡对象以泛型容器来管理,例如:

1
2
3
4
5
6
7
8
// 关卡控制系统
public class StageSystem : IGameSystem
{
private List<IStageHandler> m_StageHandlers;
private int m_NowStageLv = 1; // 当前的关卡

...
}

因为关卡的顺序是一关接着一关,没有其他树状分支的情况,所以在转换为下一个关卡时,只要获取List<IStageHandler>中的下一个成员即可。

责任链模式面对变化时

当《P级阵地》运用责任链模式并将关卡相关的“数据”及“操作方法”类化之后,只要通过继承类的方式就可以使关卡类型多样化。例如:某天的项目会议…

策划:“测试了这一阵子后,我发现玩家对于相同内容的关卡类型,可能会觉得无聊,不知大家有没有什么好意见?”
美术:“我认为可以增加Boss关卡,Boss关卡可能与其他关卡不一样,关卡内只会出现一个大Boss,虽然Boss的移动速度较慢,但攻击力强、生命力高,而且只要一攻击成功,玩家就会立即结束游戏。”
策划:“这个提案不错,小程你那边好调整吗?”

小程这时想了一下,已经运用了责任链模式的关卡系统中各类的实现情况…

小程:“我可以试着以增加关卡类型及扣除阵营生命力的方式来调整看看,给我一些时间试试。”
策划:“好的,如果没有问题的话,通知一下我们,然后就列入工作事项,美术那边也会给出Boss角色的需求。”

小程回到计算机前,开启项目研究了一下,发现需要将先前判定敌方角色占领玩家阵地成功后,原本要扣除的固定生命值1,改由关卡组件来决定要扣除多少生命力,所以关卡接口中要新增一个获取损失生命力的方法:

Listing13 关卡接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 关卡接口
public abstract class IStageHandler
{
protected IStageHandler m_NextHandler = null; // 下一個关卡
protected IStageData m_StageData = null;
protected IStageScore m_StageScore = null; // 关卡的分数

// 设置下一个关卡
public IStageHandler SetNextHandler(IStageHandler NextHandler)
{
m_NextHandler = NextHandler;
return m_NextHandler;
}

public abstract IStageHandler CheckStage();
public abstract void Update();
public abstract void Reset();
public abstract bool IsFinished();
public abstract int LoseHeart();
}

然后,在原有常规关卡类(NormalStageHandler)的类设置中重新实现新增的方法:

Listing14 常规关卡

1
2
3
4
5
6
7
8
9
public class NormalStageHandler : IStageHandler
{
...
// 损失的生命值
public override int LoseHeart()
{
return 1;
}
}

在原本的关卡系统(StageSystem)中,扣除阵营生命力的地方,也需要修正为:

Listing15 关卡控制系统

1
2
3
4
5
6
7
8
9
10
public class StageSystem : IGameSystem
{
// 通知损失
public void LoseHeart()
{
m_NowHeart -= m_NowStageHandler.LoseHeart();
m_PBDGame.ShowHeart( m_NowHeart);
}
...
}

将这些都修正好后,就可以着手进行Boss关卡的实现。因为Boss关卡与常规关卡的差异在于:在Boss关卡中,只要有敌方角色占领到玩家阵营之后,就会损失所有的阵营生命力,所以只要让Boss关卡在获取“损失的生命值”时,返回最大阵营生命力(MAX_HEART)就可以了。那么只要新增一个BossStageHandler类,其他的设置和操作就与常规关卡无异,因此让它继承常规关卡(NormalStageHandler),再重新实现LoseHeart()方法即可:

1
2
3
4
5
6
7
8
9
10
11
12
// Boss关卡
public class BossStageHandler : NormalStageHandler
{
public BossStageHandler(IStageScore StageScore, IStageData StageData) : base(StageScore, StageData)
{}

// 损失的生命值
public override int LoseHeart()
{
return StageSystem.MAX_HEART;
}
}

最后,在关卡设置时,将Boss关卡安插在常规关卡之间就可以了:

1
2
3
4
5
6
// 第5关
StageData = new NormalStageData(3f, GetSpawnPosition(), AttackPosition);
StageData.AddStageData(ENUM_Enemy.Ogre, ENUM_Weapon.Rocket, 3);

StageScore = new StageScoreEnemyKilledCount(13, this);
NewStage = NewStage.SetNextHandler(new BossStageHandler(StageScore, StageData));

结论

责任链模式让一群信息接收者能够一起被串联起来管理,让信息判断上能有一致的操作接口,不必因为不同的接收者而必须执行“类转换操作”,并且让所有的信息接收者都有机会可以判断是否提供服务或将需求移往下一个信息接收者,在后续的系统维护上,也可以轻易地增加接收者类。

与其他模式的合作

在通关判断上,可以配合策略模式(Strategy),让通关的规则具有其他的变化形式,而不只是单纯地击退所有进攻的敌方角色。

0%