上篇博客分析了游戏中的单例模式,这篇博客分析一下游戏中使用到的策略模式。
策略模式
定义
定义了算法族,分别封装起来,让他们之间可以相互替换,此模式的算法可以独立于使用它的客户。[1]P24
可以将算法族理解为同一个算法的不同变体,典型的策略模式其实就是客户可以使用同一个算法的不同变体,使用的过程中可以更换。
适用性
以下情况可以使用策略模式
1. 需要使用一个算法的不同变体,如本文的游戏,Hero会使用不同的行为。
2. 许多相关的类仅仅行为有异,策略模式提供了一种用多个行为中的一个行为配置一个类的方法
上面对策略模式的描述说的可能比较抽象,下面我们还是结合游戏分析一下策略模式。
游戏中的客户与算法族
游戏中的客户就是角色(Roles),而算法族(同一个算法的不同变体)就是发射子弹的行为。
打开VS工程中的类图ClassDiagram1.cd,我们可以看到。
客户(所有角色)
这些角色,包括英雄,怪兽。双击Roles和Hero可以看到源码:
public abstract class Roles : RoAndMi
{
//拥有发射子弹的行为
protected FireBehavior fireBehavior;
//更换发射子弹的行为
public void SetFireBehavior(FireBehavior fireBehavior)
{
this.fireBehavior = fireBehavior;
}
/// <summary>
/// 调用fireBehavior.Fire()实现真正的发射
/// </summary>
public void Fire()
{
fireBehavior.Fire();
}
...
}
public class Hero : Roles
{
public Hero(int x, int y, int xspeed, int yspeed, int life, bool good)
: base(x, y, myImage.Width, myImage.Height, xspeed, yspeed, life, good)
{
blb = new BloodBar(x,y, life);
//Hero拥有FireOneMissilesByHero行为
SetFireBehavior(new FireOneMissilesByHero(this));
}
}
算法族(角色的行为)
public abstract class FireBehavior
{
protected Roles role;//哪个角色的射击
public abstract void Fire();
}
//Hero拥有的行为
class FireOneMissilesByHero : FireBehavior
{
public FireOneMissilesByHero(Roles r)
{
this.role = r;
}
//工厂方法,生产子弹
public override void Fire()
{
if (!role.Live)
{
return;
}
HitCheck.GetInstance().AddElement(new MissileHero(HitCheck.GetInstance().MyHero, 20, 20, HitCheck.GetInstance().MyHero.Good, MissileDirection.U, 10));
}
}
类图
看一下他们的类图,会更加清晰的看到他们之间的关系
分析类图
游戏中所有角色都有个发射子弹的行为FireBehavior(继承自Roles),但是每个角色拥有的行为又不一样,如EnemyOne拥有FireMissileOneByEnemy,Hero拥有FireOneMissilesByHero,并且客户在程序运行过程中可以更换行为,如Hero在程序运行过程中可以通过调用SetFireBehavior方法更换发射子弹的行为为FireThreeMissilesByHero,也就是说Hero使用了算法FireBehavior的不同变体,同时也可以更换行为,实现了同一算法不同变体的互换。
Hero更换发射行为
Hero更换发射行为的代码在HitCheck中,游戏中,当英雄的经验值>100后,Hero装备升级,能够同时发射3个子弹,通过SetFireBehavior方法更换行为(同一算法的不同变体)
//更换行为,升级装备
if (myHero.score > 100)
myHero.SetFireBehavior(new FireThreeMissilesByHero(myHero));//策略模式
这里为什么没有使用接口,而使用了抽象类
在游戏的实现过程中,发射子弹的行为与角色相关,所以在发射的时候,需要先判断是哪个角色,所以FireBehavior里面需要成员变量Roles,而接口是不能包含成员变量的,所以需要使用抽象类。仔细分析一下游戏的源码就会非常清楚为什么用抽象类了。
关于在设计过程中优先采用抽象类还是接口,这个问题要考虑很多东西,我不敢妄作评论,在具体设计过程中,我个人更加倾向于优先采用抽象类,而不是接口。
游戏中为什么要用策略模式
游戏中,英雄和敌人有个发射子弹的行为FireBehavior(继承自Roles),但是每个角色的行为都不一样,而且同一个角色在游戏中的发射子弹的行为也会发生变化,如英雄一开始每次只能发射一颗子弹,后来由于经验值增加,每次可以发射多个子弹,发射行为在游戏中会发生变化,在程序中需要更换行为,如果以后游戏要升级,角色会拥有更多的行为,如果使用策略模式,将不同的发射子弹的行为看成是FireBehavior的不同变体,让他们可以相互替换,那么以后升级游戏将会变得很容易。
在最初的版本中,是没有用策略模式的,也没有一种在程序中使用设计模式的意识,但是后来由于对设计模式的理解逐渐加深,自己也在思考,能否在游戏中加入设计模式,而且这个游戏非常适合加入策略模式,就这样,后面就把游戏给修改了。如果看过游戏的源码,就可以发现,以前的角色的行为都是作为角色类的一个方法,直接写在角色类里面的,如Hero源码中:
//public override void fire()//实际发射
//{
// if (!live)
// {
// return;
// }
// HitCheck.GetInstance().AddElement(new MissileHero(this, 20, 20, this.good, MissileDirection.LUU, 10));
// HitCheck.GetInstance().AddElement(new MissileHero(this, 20, 20, this.good, MissileDirection.U, 10));
// HitCheck.GetInstance().AddElement(new MissileHero(this, 20, 20, this.good, MissileDirection.RUU, 10));
//}
用了策略模式后,将这些都注释掉了,将所有角色的行为都抽取出来了,封装成算法族了。使用策略模式,能够降低客户和算法族之间的耦合性,能够使系统具有良好的扩展性和维护性。
OO原则
针对接口编程,而不是针对实现编程
其实整个设计模式的一个核心思想就是针对接口编程。这里的接口,并不是java或者C#中的interface,这里的接口其实指的是超类型,包括接口和抽象类,核心思想就是多态。
看一下上面的类图
Roles的成员变量fireBehavior
//使用发射子弹的行为
protected FireBehavior fireBehavior;
和SetFireBehavior方法:
public void SetFireBehavior(FireBehavior fireBehavior)
{
this.fireBehavior = fireBehavior;
}
我们可以发现,fireBehavior是抽象类型,SetFireBehavior参数也是抽象类型,这样做的一个好处就是,调用SetFireBehavior方法的时候,可以将任何一个FireBehavior的子类作为参数,如英雄初始行为为SetFireBehavior(new FireOneMissilesByHero(this))
当经验值>100,更换发射子弹行为myHero.SetFireBehavior(new FireThreeMissilesByHero(myHero))
,最后统一通过FireBehavior中的Fire()方法实现多态调用,而不用知道具体是什么类型,这样可以实现在程序运行过程中更换行为,增加系统的扩展性。
参考文献
[1] 《Head First设计模式(中文版)》 ,中国电力出版社
[2] 《设计模式:可复用面向对象软件的基础》(著名的GOF设计模式),机械工业出版社
非常感谢您的阅读,如果您觉得这篇文章对您有帮助,欢迎扫码进行赞赏。