译者:林公子
出处:木木的二进制人生
转载请注明作者和出处,谢谢!
第四章 应用面向对象设计
恭喜!在上一章的末尾您已经初步建立了一个基本的游戏。不过到目前为止我们编码的方式用作教学演示不错,从设计的角度来说是非常低效的。一个好的设计总是能提升开发效率的。
您可能注意到要添加一个新的精灵到项目中是件多么棘手的事情。特别是当您需要添加一个动画物体,移动并使用碰撞检测,有许多变量和代码需要复制粘贴,结果您的代码很快开始变得一团糟。如果您继续这样走下去,事情很快会失控。因此,让我们花几分钟的时间应用一些好的面向对象设计到您的游戏中——这会使您将来的路好走得多。
开始设计您的类
如果您在进行真正的游戏开发,您应该对本章中的设计进行进一步补充和调整来达到您的需求。我们没有时间让您的设计如您所期望的成为拥有商业品质的应用程序,但是您可以做一些改进来大幅改善到目前为止所做的。
当务之急,让我们看看您的对象。2D游戏中的一个基本的虚拟对象就是一个精灵。在您之前章节中建立的代码里,您有两类不同的对象:由玩家控制的对象和非玩家控制的对象。除此之外,您到目前为止用到的动画精灵的其他特征是一样的。
想想这两种对象共享了哪种特征。它们都会被绘制到屏幕上,它们都用一个精灵图来进行动画。更深入的考虑,您应该会赞同所有的动画精灵都会有这两个特征元素。基于这种想法,创建一个基类来表示拥有绘制自己和通过精灵图进行动画的能力的标准精灵是合理的。如果需要自定义的特征,您可以从基类派生一个类来创造拥有新行为的类。基于之前的例子,您可以预料到您的类层次最后将看起来图4-1中那样。
图4-1 预期的精灵类层次
创建一个精灵类
现在您可以开始创建您的Sprite基类了。您应该在这个类中包含些什么呢?表4-1列出了成员变量,表4-2列出了成员方法。
表4-1 Sprite类的成员变量
成员变量 类型 描述
textureImage Texture2D 要绘制的精灵或精灵图。
Position Vector2 精灵被绘制的位置。
frameSize Point 精灵图中单独帧的尺寸。
collisionOffset int 偏移量,用来修改精灵碰撞检测中用
到的包围矩形(详见上一章末尾代码
和相关解释)。
currentFrame Point 当前帧在精灵图中的索引。
sheetSize Point 精灵图中的行/列数。
timeSinceLastFrame int 自上一帧到当前帧经过的时间(毫秒)。
millisecondsPerFrame int 帧间等待的时间(毫秒)。
speed Vector2 精灵在X,Y方向移动的速度。
_______________________________________________________________________________
表4-2 Sprite类的方法
方法 返回值 描述
Sprite(...) 构造方法 Constructor Sprite类的构造方法。
Update(GameTime, Rectangle) void 处理所有碰撞检测,移动,用户输入等。
Draw(GameTime, SpriteBatch) void 绘制精灵。
_______________________________________________________________________________
这一章以第三章中建立的代码为基础。打开代码,在项目管理的项目节点上点击右键->添加->类。命名新类文件名为Sprite.cs。
因为没有实例化Sprite类的理由,因此合适的做法是把它作为抽象类,强制您使用派生类来实例化对象。在Sprite类的定义之前添加abstract关键字使其成为抽象类:
为您的新类添加两个XNA命名空间,确保您可以使用XNA对象:
然后,添加下列变量,如同表4-1所示。确保您适当为变量标记protected,否则接下来您创建的子类将不能正确工作。
除了表4-1中列出的变量之外,您定义了一个表示默认动画速度的常量,如果没有动画速度被指定将会用到这个默认值。
接下来添加以下两个构造方法:
两个构造方法唯一不同的地方是第二个方法需要一个millisecondsPerFrame变量,用来计算动画速度。因此,第一个构造方法只是调用第二个构造方法(使用this关键字)并且将所有参数传递给第二个构造方法,包括用来表示默认动画速度的常量。
最低限度,所有的动画精灵干两件事情:通过在精灵图中移动一个当前帧索引来进行动画,并且绘制动画中的当前帧到屏幕上。除此之外,您应该想要为这个类添加一个额外的功能,或者您更倾向于将这个功能放到派生类去创建一个更特殊化得类。但至少您想要精灵动画和绘制所有的动画精灵,因此让我们将这个功能添加到基类Sprite中。
在之前的章节中您已经编写了动画和绘制代码。现在您所需要做得就是使用同样的代码,然后应用基类Sprite中定义的变量。再说一次,为了产生动画,您只需要在精灵图中移动一个当前帧索引,确保当索引超过整张图的范围时将它重置。
下面的代码经过之前的章节应该已经很熟悉了,按以下这样编写Sprite类的Update方法:
您可能注意到方法声明中的virtual关键字。这个关键字标记方法为虚方法,使您能够根据需要在子类中覆写这个方法来改变方法的功能。
同样您可能注意到了Rectangle参数。这个参数代表了游戏窗口客户区矩形,用来检测物体何时越过了游戏窗口边缘。
正如您之前编写了动画代码,您同样也编写了从动画精灵中绘制单独帧的代码。现在您只需要将那些代码加入到Sprite类中,然后使用Sprite类中定义的变量。
和之前章节中的绘制代码不同的一点是在Sprite类中无法访问SpriteBatch对象(但愿您还记得,需要它来绘制一个Texture2D对象)。为避免这一点,您需要Sprite类的Draw方法接受一个GameTime参数并增加一个SpriteBatch参数。
Sprite类的Draw方法看起来应该像这样:
除了Update和Draw方法之外,您要为Sprite类添加一个用来表示精灵移动方向的属性。
方向总是用一个Vector2来表示,表示X,Y方向的移动,但是它通常是在子类中定义(例如:自动精灵和受控精灵的移动方式不同)。所以这个属性需要存在于基类中,但是应该是抽象的,意味着在基类中它没有实现并且必须在所有的子类中定义。
如下那样加入抽象的Direction属性到基类中:
还有一个要加入到Sprite类中的东西:一个返回矩形值的属性,用来进行碰撞检测。添加下面的属性到Sprite类中:
您的基类现在已经完成得不错了。它可以用Draw方法来绘制自己并且通过Update方法来遍历精灵图。因此,让我们把注意力转移到用户控制精灵上一会。
创建用户控制精灵类
现在您要创建一个从Sprite基类派生的类,添加一个用户控制的功能。添加一个新类到您的项目中,在解决方案的项目节点上点击鼠标右键,选择添加->类。命名新类文件名为UserControlledSprite.cs。建好类后,将它标记为Sprite类的派生类:
接下来您需要添加一些XNA using语句。使用和Sprite类中一样的语句,另外添加一条using语句(Microsoft.Xna.Framework.Input)让您能从输入设备读取数据:
然后为UserControlledSprite类添加构造方法。这些构造方法基本上和Sprite的一样,仅仅将参数传递给基类:
然后,您需要添加代码来实现Direction属性。Direction属性将在Update方法中用来改变精灵的位置(或者换句话说,让精灵往这个属性指示的方向移动)。就UserControlledSprite类来说,Direction属性将被定义为结果由基类的speed成员和玩家按下的方向共同决定。
用户也能用鼠标来控制精灵,但是鼠标输入会有一些不同的处理。当用鼠标来移动精灵的时候,您移动精灵到鼠标所在的位置。因此,实际上在处理鼠标移动的时候不需要Direction属性。这个属性将只会反映玩家通过键盘或Xbox360手柄进行的输入。为了用从摇杆和键盘读取的数据构建Direction属性,如下这样编写代码:
这个属性将会返回一个Vector2值来指示移动方向(在X和Y平面)。请注意键盘和游戏手柄输入结合在一起了,允许玩家用两种输入设备来控制精灵。
为了处理鼠标移动,您本质上需要在每帧检测鼠标是否移动。如果移动了,您就假设用户想要用鼠标控制精灵,然后移动精灵到鼠标光标所在的地方。如果鼠标没有移动,键盘和手柄输入将会影响精灵的移动。
为了检测帧与帧之间鼠标是否移动,添加下面这个成员变量到UserControlledSprite类中:
您需要覆写继承自基类的Update方法,并且添加基于Direction属性来移动精灵的代码,包括鼠标移动(如果鼠标被移动的话)。另外,您要添加一些逻辑到方法中来保持用户控制的精灵不会移动到屏幕外。您的Update方法看起来应该像这样:
现在行啦——您的UserControlledSprite类就绪了!您不需要对这个类中的Draw方法做什么因为您的基类将会处理精灵单独帧的绘制,干的漂亮!
创建一个自动精灵
现在您有了一个允许用户控制精灵的类,现在是时候添加一个能产生动画精灵并自主运动的类了。添加一个新类到您的项目中,在解决方案的项目上点击鼠标右键,选择添加->类。命名类文件名为AutomatedSprite.cs。文件建好后将新类标记成Sprite类的子类:
像以前那样加入XNA命名空间,但是不用输入命名空间,因为您不会从这个类中获得任何设备输入:
接下来,为AutomatedSprite添加两个构造方法,这些方法将和UserControlledSprite类用到的一样:
您的自动精灵将会使用基类speed成员的速度值在屏幕上移动。这个可以通过覆写Direction属性来做到,因为这个属性是抽象的,所以必须在子类中定义。如下这样定义Direction属性:
现在您要添加让精灵动起来的代码。因为Direction属性由一个Vector2值来表示,这个属性表示了自动精灵移动的速度和方向。2D空间中的任何方向都可以用一个Vector2(二维向量)值来表示,并且向量的大小(或长度)指示着物体的速度:向量越长,精灵移动的速度越快。
您所需要做得就是将Direction属性值和精灵的位置position相加,精灵就会往那个向量的方向并以其长度所指示的速度移动。
添加一个覆写的Update方法到AutomatedSprite类中,让精灵基于Direction属性移动:
搞定了!您现在有了一个自动精灵类可以绘制自己并基于一个2D向量来更新自己的位置。
人工智能?
好了,这个精灵正在自己移动——但是这是我们所说的人工智能吗?那么,什么是人工智能呢?这是一个困难的问题。人工智能指的是使计算机的行为表现出智能的科学技术。这个定义的问题是实际上智能一词本身并没有清晰的定义。因此,定义人工智能的含义相当的困难。
您刚刚编写了一个类让精灵可以自己移动,所以,您可以主张创建了一个人工智能算法— 一个很简单且不怎么吸引人的,但是仍然是一个人工智能算法。在另一方面,您可以主张您的精灵不比一个划过天空的子弹更具备智能,因为它所做的只是朝一个方向以不变的速度移动。并且不会有多少人主张子弹是智能的!
在将来的章节中,我们从不同的角度使您的自动精灵和3D物体表现得更加智能,但是与此同时,无论智能与否,您有了一个好的开始。
|
迄今为止您有了两类精灵,都从一个基类派生。在之前的章节中,当您想要增加一个新的精灵您必须添加许多不同的变量和设置来实现新的精灵。用这种方法,您可以在添加精灵到程序中时增加新的变量(AutomatedSprite或UserControlledSprite)。
然而,想想更好的办法,让我们看看一个更加模块化的方案。XNA为我们提供了一个强大的工具可以让逻辑部分的代码分离到不同的模块中并且让它们很容易加入游戏中并良好共存。
在下一部分,您将学习游戏组件,并且您会创建一个管理游戏中所有精灵的组件。
游戏组件
XNA有一个相当棒的方法去将不同的逻辑代码片段整合进您的程序(例如您即将创建的SpriteManager类)。GameComponent类允许您将代码模块化得插入到程序中并且自动的将这个部件加入到游戏循环的Update调用中(例如,在您游戏的Update方法调用之后,所有相关联的GameComponent类的Update方法都会被调用)。
要创建一个新的游戏组件,右键点击解决方案中的项目节点,选择添加->新建项。在模版列表中选择Game Component,然后命名游戏组件文件名为SpriteManager.cs。
看看您的新游戏组件类生成的代码,您会注意到它包含构造方法,Initialize和Update方法。并且从GameComponent类派生。
如果您想要创建一个游戏组件还能和游戏循环的Draw方法一起工作使您的游戏组件有能力绘制东西,您可以从DrawableGameComponent类派生来代替。
您要使用精灵管理类来调用它管理的所有的精灵的Draw方法,所以您需要这个游戏组件和游戏的Draw方法一起工作。修改游戏组件的基类为DrawableGameComponent来启用绘制功能:
修改基类之后,您要为您的游戏组件创建一个覆写的Draw方法:
要将新创建的组件添加到游戏中并让组件的Update和Draw方法在游戏循环中开始工作,您还要将组件添加到Game1类使用的组件列表中。要这样做您需要添加一个SpriteManager类型的成员变量到Game1类中:
然后,在Game1类的Initialize方法中,您需要实例化SpriteManager对象,传递一个Game1类的引用(this)给构造方法。最后,将这个对象添加到Game1类的组件列表中:
哈!您已经一切就绪。当您游戏的Update和Draw方法被调用时,游戏组件中同样的方法也会被调用。
您可以看到添加一个GameComponent到游戏中是多么容易。想象一下这种工具的使用,例如,您创建了一个组件用来绘制帧速率和其他性能相关的调试信息到屏幕上,您可以用两行代码将这个组件添加到任何游戏中!非常酷的东西。
编写SpriteManager
虽然您的SpriteManager类准备好了并能够使用了,但是它还没有干任何事。您可以在SpriteManager的Draw方法中进行绘制,就像在Game1类中的那样。事实上,要将游戏中剩下的逻辑清楚的分开,您应该让SpriteManager类来控制所有的精灵绘制。为了做到这一点,您得添加一些代码来让SpriteManager绘制精灵。
您首先需要一个SpriteBatch。虽然Game1类中已经有了一个SpriteBatch对象,但是这里创建自己的SpriteBatch比重用Game1类中的要合理。只有这样您才能真正使游戏组件独立于游戏。游戏和游戏组件之间过多的数据传递会破坏这种设计。
除了增加一个SpriteBatch变量外,您需要添加一些其他的变量:一组Sprite对象用来保持所有的自动精灵,一个代表玩家UserControlledSprite, 添加这些变量到SpriteManager类中:
就像SpriteManager的Update和Draw方法会在Game1类的Update和Draw方法调用之后被调用,Initialize和LoadContent方法也会在Game1类对应方法调用之后被调用。您需要添加一些代码来加载纹理,初始化SpriteBatch,初始化玩家对象,并且为了测试,添加一些精灵到精灵管理类的精灵列表中。用以下代码添加一个覆写的LoadContent来完成所有这些工作:
这里做了些什么呢?首先,您初始化了SpriteBatch对象;然后,您初始化了玩家对象并且添加了4个自动精灵到精灵列表中。这些精灵只是作为测试目的使用,这样您完成精灵管理类的时候就可以看到效果。
接下来,您需要在每次调用精灵管理类的Update方法时调用玩家对象和精灵列表中所有精灵的Update方法。在精灵管理类的Update方法中,添加以下代码:
现在,您要对绘制做同样的事情。Sprite基类有一个Draw方法,所以您需要在SpriteManager类的Draw方法中调用所有精灵的Draw方法。精灵必须总是在Sprite.Begin和SpriteBatch.End调用对中绘制,因此确定你添加了Sprite.Begin和End方法来包含精灵绘制方法调用:
您的SpriteManager类中只剩一件事要处理:碰撞检测。您将在精灵管理类中处理碰撞检测而不是在独立的精灵或游戏对象中。
在这个特定的游戏中,您不关心自动精灵是否互相碰撞——您只需检测玩家精灵和自动精灵的碰撞。修改Update调用来检测玩家和AutomatedSprite们的碰撞:
现在,每当游戏的Update方法被调用时,SpriteManager中的Update方法也会被调用。SpriteManger会依次调用所有精灵的Update方法并且检测和玩家精灵的碰撞。很不错,对吧?
清理
Wow,看起来做了不少工作,但是我保证您会很高兴做到这些。您的精灵管理类已经完成并和Game1类联系在一起了。然而您的Game1类中仍然有您在之前章节中添加的代码。您现在可以到Game1类中删除除了SpriteManager相关和IDE生成的代码之外的其他代码。您的Game1类看起来应该像这样:
编译并运行程序,您会看到旋转的圆环对象和4个头骨。您可以通过键盘,鼠标和游戏手柄控制圆环对象,并且和任意一个头骨碰撞时,游戏将结束。
图4-2 投入运行的纯面向对象设计。
这个例子中有一件需要注意的事是当游戏开始时,圆环会出现在鼠标光标所在的地方。如果这个位置恰好在一个自动精灵上,碰撞检测在游戏一开始就成立并且游戏结束。如果您有这个问题,将鼠标光标移动到屏幕的角落里再开始游戏。这个小问题接下来不会产生不良结果因为之前提到过了,现在绘制的这些精灵只是用来测试精灵管理类的功能。
不错!如果您想知道这一章的要点是什么,看看Game1类的代码。您的程序现在有了非常纯粹的面向对象设计,和之前您做的那些比较一下。看看Draw方法和Game1类其余部分,几乎什么都没有。更棒的是,看看增加一个全新的动画精灵需要做什么:只是一行代码而已!还记得之前章节中要添加一个新精灵是多么痛苦吗?您需要添加许多变量和代码,做很多复制,粘贴,修改变量名,等等。想想这些,您就可以看到使用诸如XNA GameComponent之类的强大工具的模块化方法和设计良好的类层次所带来的益处。
让它们动起来
您可能会觉得您的动画精灵出了什么问题。您记得添加了让它们自己移动的代码,但是它们什么都没干,只是待在那儿不停的旋转。您的动画精灵不会动的原因是因为您在如图4-2所示创建动画精灵时使用的速度值为0:也就是说,在SpriteManager的LoadContent方法中,您传递了Vector2.Zero作为每个动画精灵对象构造方法的最后一个参数。
为了让您的动画精灵在屏幕上移动,试着修改您传递给它们的速度参数。要注意到您除了让这些精灵移动,没有编写其他任何逻辑。结果就是您的精灵会向前移动甚至移动到屏幕之外。在接下来的章节中,您会添加一些逻辑来动态创建精灵并且使它们从屏幕的一边飞向另一边。这一章和您创建的Sprite类体系将会是将来开发的基础。
您刚刚做了些什么
在这里停下来并好好表扬一下自己吧。要熟悉可靠的软件设计很难。太多的开发者不经思考就一头扎入代码中,结果就是意大利面条般混乱的代码,并且将很快失去控制。让我回顾一下您做了些什么:
•您为精灵创建了一个继承体系,包括一个用来处理动画的基类和两个处理用户输入和自主移动的派生类。
•您学习了GameComponent,可以用来进行可替换部件的模块化设计。
•您创建了一个SpriteManager类来处理精灵的更新,绘制和碰撞检测。
•您清理了Game1类便于将来的开发。
摘要
•可靠的设计和正确的代码同等重要。游戏开发项目中花在设计上的时间对于加速开发进程,提高可维护性和提升性能来说非常重要。
•应用可靠的层次设计减少了冗余代码并且提升了系统整体的可维护性。
•GameComponent是一个让开发者可以分离某些功能到独立的模块中并易于应用到不同项目中的强大工具。
•这世界已经改变,我从水中触摸到XNA,我从泥土里感觉到它,我从空气中嗅到它。过去的一切,都已经失落,现在没有人记得了。
译注:这段话是作者根据电影魔戒首部曲的经典开篇台词改编的,原文是:
The world is changed.
I feel it in the water.
I feel it in the earth.
I smell it in the air.
Much that once was is lost.
For none now live who remember it.
======================================================================
知识测试:问答
1.游戏组件从哪个类派生?
2.如果您想要使用您的游戏组件绘制,您需要从哪个类进行派生?
3.真还是假:花费时间建立可靠的面向对象设计不如编码有价值因为它是不必要和多余的。
4.美国的哪个州禁止打鼾,除非卧室的所有窗户都已关闭并牢固的锁好。(马萨诸塞州)。
知识测试:练习
1.修改这章创建的代码,生成4个精灵在屏幕上移动并在触及屏幕四边时反弹。要实现这个,创造一个名为BouncingSprite的从AutomatedSprite派生的新类。BouncingSprite和AutomatedSprite类做同样的事情除了在Update方法中检测精灵是否越过屏幕边缘了。如果是,将speed变量乘以-1反转精灵的移动方向。
生成两个使用头骨图像的反弹精灵,另外两个使用"+"号图像(位于本章代码的AnimatedSprites\AnimatedSprites\Content\Images目录下)。
请注意当应用这些修改后运行游戏,屏幕上会有4个自动精灵在屏幕上移动,任何一个碰到了用户控制的精灵游戏将会结束。测试游戏的时候可能会导致一些问题,因为精灵可能在游戏刚开始的时候就碰撞了。在运行游戏前试着把您的鼠标光标移动屏幕角落使您的用户控制精灵在游戏开始时离自动精灵比较远。