本章开始,我们将设计一个模拟的战争游戏架构,这个项目当然是虚构的,如有雷同,纯属巧合。
现在,从游戏单位的设计开始;在游戏中,我们需要如下几种类型的游戏单位:
l 人类:士兵、医生、工程兵。
l 车辆:吉普、坦克。
l 飞机:战斗机、轰炸机、运输机。
l 舰艇:驱逐舰、运输船。
l 建筑物:工厂、船坞、要塞。
这些角色会陆续登场!
第一设计方案
作为一个面向对象(OO)的开发者,看到这些游戏单位,第一件事应该是什么?我想应该是对他们进行抽象化,以便找出他们的共同点,最大限度地简化代码。
现在,我们来看看他们有什么共同点:
l 这些游戏单位大部分可以移动,除了建筑物;不过话又说回来,在游戏中的建筑物如果真的能移动也可以做一个备选方案,这也是一个特点,如果有时空转换机器就很酷了,哈哈!这个主意不错!记下来,说不定哪天会实现这个功能。好了,移动(Move)算是一个共同点。
l 既然是战争游戏,每个角色都应该有手中的武器。我们让士兵使用冲锋枪、医生使用急救箱、工程兵使用工具箱;现在,你可以想像其他单位使用什么武器。可能你会想,有的角色没有武器,比如运输机、运输船、工厂和船坞,也许;不过,为了抽象的原则,我们可以让这些单位的武器为空。这样就找到了另一个共同点——他们都有武器(Weapon)。
l ……
也许还能够找出更多东西,不过现在只是在做演示,就让我们开始设计吧。
根据面向对象的设计原则,对于有共同成员(或称元素)的类,如共同的特性、行为等等;我们可以设计一个基类作为它们的基础,在这个项目中,这个基类起个什么名字呢?“CUnit”好了,下面就是使用VB.NET定义的CUnit类的伪代码(做做样子,不能真正执行的代码)。
Public Class CUnit Public Sub Move() Public Property Weapon As ??? End Class |
“移动(Move)是个方法,这个‘好办’;但武器应该是什么类型呢?”。好问题,不过要回答这个问题,我们还得先再回过头看看,这些游戏单位都使用些什么武器。
l 士兵、吉普车、战斗机,使用冲锋枪或机枪,它们都是可以自动连续射击的,在游戏中我们可以把它们当成一回事,毕竟我们只是在玩游戏。
l 坦克、驱逐舰、要塞,使用火炮。
l 医生使用急救箱。
l 工程兵做工具箱。
l 轰炸机使用炸弹。
l 运输机、运输船、工厂、船坞,没有武器。
你在想什么?创建武器类?但是这些武器真的是不太一样,在CUnit类中的Weapon属性怎么统一类型呢?接口!哈哈,被你猜到了。对,定义一个武器接口类型就解决了这个问题。
我们定义武器接口类型IWeapon,然后通过实现这个接口创建所需要的武器类,这样,我们就可以在CUnit类中随意“安装”武器,组合成不同类型的游戏单位。
现在,初步的伪代码就可以修改为:
Public Interface IWeapon Public Class CUnit Public Sub Move() Public Property Weapon As IWeapon End Class |
基类已经设计好了,一切看起来还不错。下面开始创建各种游戏单位类吧。
陆海空
现在,假设我们已经创建了很多武器类型,如CMachineGunWeapon(机枪)、CConnonWeapon(火炮)、……。
当然我们还应该创建了很多的游戏单位子类,比如:CSoldier(士兵)、CJeep(吉普车)、CFighter(战斗机)、CDistroyer(驱逐舰)、CFort(要塞)、……。这些游戏单位类都是继承于CUnit类,所以,他们都必须重写CUnit类中的Move()方法,以满足不同类型单位移动的需要。
但是,现在有一个问题,所有陆地单位的移动形式应该是一样的,只是速度有所不同,如果我们在每个单位类中重写Move()方法,就会发现我们重复了如此多的代码,这是很严重的问题,如果以后我们添加新的陆地单位,还要重复这些代码;如果移动的方式需要稍微修改一下代码,需要修改多少个类中的Move()方法,天知道!那样的日子可不怎么好过。
在重写Move()方法的过程中,我们还会发现在海上单位、空中单位中也同样会出现大量的重复代码;这肯定不是我们想要的设计方案。
既然,我们使用接口解决了武器的安装问题,移动的问题当然也可以照葫芦画瓢,我们可以将移动也设计成接口,然后只需要四种移动方式,即陆地移动、海洋移动、空中移动和没有移动。那么,我们的CUnit就应该改写,伪代码如下:
Public Interface IWeapon Public Interface IMove Public Class CUnit Public Property Speed As Integer Public Property Move As IMove Public Property Weapon As IWeapon End Class |
代码中,我们在CUnit类中添加了Speed属性,用于区分不同游戏单位的速度;虽然移动方法Move()不见了,不过不用担心,这个动作很快就会重新出现,我保证。
真正的设计方案
现在还有一个问题,既然移动和武器都成了接口,那么游戏单位呢?在游戏中,可能需要不同类型的游戏单位进行相同或相似的操作,或者他们作为某些行为的参数(比如作为攻击目标、或像运输机那样可以装上士兵等其他游戏单位等);那么,在这种情况下,游戏单位类型也应该定义为接口才能满足要求。
好吧,现在一切都成了接口。下面,我们给出真实的接口的定义:
''武器接口 Public Interface IWeapon Sub Attack(ByVal x As Integer, ByVal y As Integer) End Interface ''行为(移动)接口 Public Interface IBehavior Sub Move(ByVal x As Integer, ByVal y As Integer) End Interface ''游戏单位接口 Public Interface IUnit Property CurX As Integer Property CurY As Integer Property Speed As Integer Property Behavior As IBehavior Property Weapon As IWeapon End Interface |
(项目:StrategyPatternDemo 文件:GameInterface.vb)
在武器接口(IWeapon)我们定义了所有武器都会有的方法,即攻击(Attack)。
为了区分游戏单位中的Move()方法,我们将移动接口修改为行为接口(IBehavior),在这个接口中定义了移动方法(Move)。很快在游戏单位类中就会出现Move()方法了。
最后是游戏单位接口(IUnit),我们将Move属性同样改名为Behavior,另外还添加了CurX和CurY属性作为单位的当前坐标值。
现在,我们创建实现这几个接口的基类,首先是CUnit类,代码如下:
''游戏单位基类 Public Class CUnit Implements IUnit Protected myCurX, myCurY, mySpeed As Integer Protected myBehavior As IBehavior Protected myWeapon As IWeapon Public UnitId As Integer '游戏单位ID Public Name As String '游戏单位名字 '构造函数 Public Sub New(ByVal b As IBehavior, ByVal w As IWeapon) myBehavior = b myWeapon = w CurX = 0 CurY = 0 mySpeed = 0 UnitId = 0 Name = "" End Sub '属性,行为 Public Property Behavior As IBehavior Implements IUnit.Behavior Get Return myBehavior End Get Set(ByVal value As IBehavior) myBehavior = value End Set End Property '属性,当前X坐标 Public Property CurXAs Integer Implements IUnit.CurX Get Return myCurX End Get Set(ByVal value As Integer) myCurX = value End Set End Property '属性,当前Y坐标 Public Property CurY As Integer Implements IUnit.CurY Get Return myCurY End Get Set(ByVal value As Integer) myCurY = value End Set End Property '属性,速度 Public Property Speed As Integer Implements IUnit.Speed Get Return mySpeed End Get Set(ByVal value As Integer) mySpeed = value End Set End Property '属性,武器 Public Property Weapon As IWeapon Implements IUnit.Weapon Get Return myWeapon End Get Set(ByVal value As IWeapon) myWeapon = value End Set End Property '方法,移动 Public Sub Move(ByVal x As Integer, ByVal y As Integer) Me.myBehavior.Move(x, y) End Sub '方法,攻击 Public Sub Attack(ByVal x As Integer, ByVal y As Integer) Me.myWeapon.Attack(x, y) End Sub End Class |
(项目:StrategyPatternDemo 文件:CUnit.vb)
好吧,我承认这个类的代码有点多,但我也已经将关键的内容使用加粗和斜体了,这些东西应该不难理解。在CUnit类中,我们看到了移动(Move)和攻击(Attack)方法,这个诺言已经兑现了,我是不会食言的,哈哈!
也许你会问,Attack()方法的参数为什么是坐标,而不是游戏单位(IUnit),我们攻击的不是敌人的单位吗?这个问题很简单,如果攻击的是对方的移动单位,而这个单位在被击中前移动了,子弹和炮弹是没有跟踪攻击能力的;如果我们使用的是威力强大的武器攻击,如大炮和炸弹,它们的杀伤范围是很大的,不会只限于某个单位,所以,在游戏开发中,一般会将攻击目标设定为一个坐标位置,然后根据杀伤力确定周围的哪些单位被攻击。最后,如果真的需要一个攻击某个目标的方法,只需要重载一个Attack()方法就可以了。你可以这样定义它:
Public Sub Attack(ByVal target As IUnit) '想怎么玩都行 End Sub |
(项目:StrategyPatternDemo 文件:CUnit.vb)
到现在,我们还没有创建真正的行为和武器;不过,在创建这类具体类之前,我们还需要创建它们的基类CWeapon和CBehavior,请注意,它们都被定义为MustInherit,并且Attack()方法和Move()方法也都必须在子类中重写,代码如下:
''武器基类 Public MustInherit Class CWeapon Implements IWeapon '子类中告诉大家是什么武器在攻击 Public MustOverride Sub Attack(ByVal x As Integer, ByVal y As Integer) _ Implements IWeapon.Attack End Class |
(项目:StrategyPatternDemo 文件:CWeapon.vb)
''行为(移动方式)基类 Public MustInherit Class CBehavior Implements IBehavior '子类告诉大家是什么移动方式 Public MustOverride Sub Move(ByVal x As Integer, ByVal y As Integer) _ Implements IBehavior.Move End Class |
(项目:StrategyPatternDemo 文件:CBehavior.vb)
组合第一个作战单位
准备工作已做的差不多了,现在,我们要创建第一个作战单位——大兵瑞恩(Ryan)。首先,士兵是陆地上的行为,他的武器是机枪,我们还需要创建这两个类。
''陆地上的行为 Public Class CLandBehavior Inherits CBehavior Public Overrides Sub Move(ByVal x As Integer, ByVal y As Integer) Console.WriteLine("正在移动到位置({0}, {1})", x, y) End Sub End Class |
(项目:StrategyPatternDemo 文件:Behaviors.vb)
''机枪 Public Class CMachineGunWeapon Inherits CWeapon Public Overrides Sub Attack(ByVal x As Integer, ByVal y As Integer) Console.WriteLine("使用机枪攻击位置({0}, {1})", x, y) End Sub End Class |
(项目:StrategyPatternDemo 文件:Weapons.vb)
现在,你知道如何创建大兵了吗?代码如下:
Module Module1 Sub Main() Dim Ryan As NewCUnit(New CLandBehavior, New CMachineGunWeapon) Ryan.Name = "Ryan" Console.WriteLine("士兵" & Ryan.Name) Ryan.Move(100, 100) Ryan.Attack(130, 110) Console.ReadLine() End Sub End Module |
(项目:StrategyPatternDemo 文件:Module1.vb)
我们使用CUnit类的构造函数,通过一个陆地行为和一个机枪武器“组合”成了士兵“Ryan”,本代码运行的结果如下图:
在示例代码中,我还定义了一些其他的行为和武器,你完全可以让士兵背上大炮,然后飞来飞去,就像如下代码一样:
Module Module1 Sub Main() Dim superSoldier As NewCUnit(New CAirBehavior, New CCannonWeapon) superSoldier.Name = "Superman" Console.WriteLine("士兵" & superSoldier.Name) superSoldier.Move(100, 100) superSoldier.Attack(130, 110) Console.ReadLine() End Sub End Module |
(项目:StrategyPatternDemo 文件:Module1.vb)
这个代码运行的结果如下图:
汗!瀑布汗!!!
这一切是怎么发生的
“这一切是怎么发生的?它们和策略模式(Strategy Pattern)有什么关系?”
既然问到了,我只好说“我们正是在使用策略模式,才使得创建一个游戏单元变得如此方便、灵活。相信你不会否认这一点。
不过,我们还顺便使用了一下模板方法模式,不知道你发现没有?”如果没有发现在哪里使用了模板方法模式,请先看一看15.2中的图,然后想一想在本章中IWeapon接口、CWeapon类与CMachineGunWeapon等武器类的关系;还有IBehavior接口、CBehavior类和CLandBehavior等行为类的关系。现在你知道我在说什么了吧。
“有点跑题了,现在可是策略模式时间!”
好的,谢谢提醒,马上回到正题。
策略模式是这样定义的,我们定义了一系列的算法,将它们独立封装,并且可以相互替换使用;这些算法独立于使用它们的组件,可以在组件中通过组合,灵活地改变使用者中使用的算法类型。
要实现策略模式,我们必须首先使用接口,这里当然是说Interface语句创建的接口类型,只有这样才能让不同的行为、武器类型之间可以分别进行替换。
还有,就是我们多次提到的组合;在本例使用策略模式的过程中,我们将行为类、武器类组合使用,才能在游戏单位类(CUnit)中完成各种单位对象的创建,而不单单是依靠类的继承。
最后,当我们决定使用策略模式的时候,一定要认真分析,哪些内容是组件中经常变化的,哪些内容是不会变化的;我们将对象中经常变化的内容(比如游戏单位中的行为和武器)与不会变化的内容分离,对它们分别进行封装,方便代码维护,而不是将它们搅成了一锅粥。
小结
本章,我们主要使用了策略模式(Strategy Pattern)来组织游戏中的代码,这些代码主要包括了游戏单位、行为和武器;我们可以通过一系列的接口(Interface)方便、灵活地组合出各种游戏单位,同时,我们将行为和武器独立出来,可以随时修改或添加新的行为或武器类型,而无需修改游戏单位的代码,这也是我们使用策略模式最大的收获。
可能你也发现了,我们在使用策略模式时,会创建很多的行为、特别是武器的类型,这其实也是策略模式存在的一个问题——他会创建很多组件的小型类,不过有利就有弊,如何使用策略模式还需要看项目中的具体要求。
在本章最后的示例中,我们创建了一个超级士兵,他可以飞行,用着大炮,游戏中这可能很有意思;但是有一个问题,如果有过多的异形单位出现,就会破坏游戏的真实性和公正性;现在,我们必须让一切回到正轨,士兵就是士兵,他们是人类,他们不可能扛着大炮到处乱飞的。我们必须有一些约束机制来创建游戏单位,让使用游戏代码库的程序员不能随心所欲地创建游戏单位;其实,老板也是这么想的。
下一章,我们将实现这些约束性创建游戏单位的方法。
出自: http://www.caohuayu.com/books/B0003/B0003.aspx