RPG游戏中的设计模式之策略模式

来源:互联网 发布:python dom解析xml 编辑:程序博客网 时间:2024/05/16 17:57

上篇博客分析了游戏中的单例模式,这篇博客分析一下游戏中使用到的策略模式。

看这篇博客的读者,如果没有看过上篇博客对游戏和单例模式的分析,建议看一下上一篇博客,最好将游戏下载下来运行一下,看一下源码和类图,对游戏的整体结构有个大概了解,这样博客读起来会比较轻松,也比较容易理解其中的模式.

上篇博客链接:RPG游戏中的设计模式之单例模式

策略模式

定义

定义了算法族,分别封装起来,让他们之间可以相互替换,此模式的算法可以独立于使用它的客户。[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原则

单例模式中,由于只有一个类,体现不出什么OO设计原则,在策略模式中,就可以体现出一些重要的OO设计原则。

OO原则:针对接口编程,而不是针对实现编程

[1]P11
其实整个设计模式的一个核心思想就是针对接口编程,而不是针对实现编程。这里的接口,并不是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]P12

针对实现编程

Dog d=new Dog();d.bark();

针对接口编程

Animal animal=new Dog();animal.bark();

当我们针对接口编程时,就可以利用多态调用,而不用管实际的类型到底是什么,我们只需要关心如何正确地进行makeSound()就可以了。这样就增加了系统的弹性。

参考文献

[1] 《Head First设计模式(中文版)》 ,中国电力出版社
[2] 《设计模式:可复用面向对象软件的基础》(著名的GOF设计模式),机械工业出版社

0 0
原创粉丝点击