恭喜!在上一章的末尾您已经初步建立了一个基本的游戏。不过,虽然到目前为止我们编码的方式用作教学演示不错,但从设计的角度来说却是非常低效的。一个好的设计总是能够提升开发效率的。
您可能注意到要添加一个新的精灵到项目中是件多么棘手的事情。特别是当您需要添加一个动画对象,移动并使用碰撞检测时,有许多变量和代码需要复制粘贴,结果您的代码很快开始变得一团糟。如果您继续这样走下去,事情很快会失控。因此,让我们花几分钟的时间在游戏中应用一些好的面向对象设计——这会使您将来的路好走得多。
如果您在进行真正的游戏开发,您应该对本章中的设计进行进一步补充和调整来达到您的需求。我们没有时间让您的设计如您所期望的成为拥有商业品质的应用程序,但是您可以做一些改进来大幅改善目前的设计。
当务之急,让我们来看看您的对象。2D游戏中的基本的虚拟对象就是精灵。在您之前章节中建立的代码里,您有两类不同的对象:由玩家控制的对象和非玩家控制的对象。除此之外,您到目前为止用到的动画精灵的其它特征都是一样的。
想想这两种对象共享了哪种特征。它们都会被绘制到屏幕上,它们都用一个精灵位图来进行动画。更深入地考虑,您应该会赞同所有的动画精灵都会有这两个特征元素。基于这种想法,创建一个基类来表示标准精灵是合理的,这个标准精灵应该拥有绘制自己和通过精灵位图进行动画的方法。如果需要自定义的特征,您可以从基类派生一个拥有新行为的类。基于之前的例子,您可以预料到您的类层次最后将看起来图5-1中那样。
图5-1 预期的精灵类层次
现在您可以开始创建您的Sprite基类了。您应该在这个类中包含些什么呢?表5-1列出了成员变量,表5-2列出了成员方法。
表5-1 Sprite类的成员变量
属性 | 类型 | 描述 |
---|---|---|
textureImage | Texture2D | 要绘制的精灵或精灵位图 |
Position | Vector2 | 精灵被绘制的位置 |
frameSize | Point | 精灵位图中单帧的尺寸 |
collisionOffset | int | 偏移量用来修改精灵碰撞检测中用到的包围矩形 |
currentFrame | Point | 当前帧在精灵位图中的索引 |
sheetSize | Point | 精灵位图中的行/列数 |
timeSinceLastFrame | int | 自上一帧到当前帧经过的时间(毫秒数) |
millisecondsPerFrame | int | 帧间等待的时间(毫秒数) |
speed | Vector2 | 精灵在X,Y方向移动的速度 |
表5-2 Sprite类的方法
方法 | 返回值 | 描述 |
---|---|---|
Sprite(…)(multiple constructors) | Constructor | Sprite类的构造方法 |
Update(GameTime, Rectangle) | void | 处理所有碰撞检测,移动和用户输入等 |
Draw(GameTime, SpriteBatch) | void | 绘制精灵 |
这一章以第四章中建立的代码为基础。打开代码,在解决方案资源管理器的项目节点上点击右键选择Add→Class。命名新类文件名为Sprite.cs。
因为没有实例化Sprite类的理由,因此合适的做法是把它作为抽象类,强制您使用派生类来实例化对象。在Sprite类的定义之前添加abstract关键字使其成为抽象类:
abstract class Sprite
为您的新类添加两个XNA命名空间,确保您可以使用XNA对象:
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics;
然后,添加下列变量,如表5-1所示。确保您适当地为变量标记protected,否则接下来您创建的子类将不能正确工作。
Texture2D textureImage; protected Point frameSize; Point currentFrame; Point sheetSize; int collisionOffset; int timeSinceLastFrame = 0; int millisecondsPerFrame; const int defaultMillisecondsPerFrame = 16; protected Vector2 speed; protected Vector2 position;
除了表5-1中列出的变量之外,您定义了一个表示默认动画速度的常量,如果没有动画速度被指定将会用到这个默认值。
接下来添加以下两个构造方法:
public Sprite(Texture2D textureImage,Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed) : this(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, defaultMillisecondsPerFrame) { } public Sprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, int millisecondsPerFrame) { this.textureImage = textureImage; this.position = position; this.frameSize = frameSize; this.collisionOffset = collisionOffset; this.currentFrame = currentFrame; this.sheetSize = sheetSize; this.speed = speed; this.millisecondsPerFrame = millisecondsPerFrame; }
两个构造方法唯一不同的地方是第二个方法需要一个用来计算动画速度的变量millisecondsPerFrame。因此,第一个构造方法仅仅是简单地调用第二个构造方法(使用this关键字)并且将所有参数传递给第二个构造方法,包括用来表示默认动画速度的常量。
所有的精灵最少需要干两件事情:通过在精灵位图中移动当前帧索引来实现动画,并且在屏幕上绘制动画中的当前帧。除此之外,您应该还想为这个类添加一个额外的功能,或者您更倾向于将这个功能放到派生类去创建一个更特殊化的类。但至少您想要让精灵实现动画并在屏幕上进行绘制,因此让我们将这个功能添加到基类Sprite中。
在之前的章节中您已经编写了动画和绘制代码。现在您所需要做的就是使用同样的代码,然后应用基类Sprite中定义的变量。再说一次,为了产生动画,您只需要在精灵位图中移动当前帧索引,并确保当索引超过整张图的范围时将它重置。
下面的代码经过之前的章节应该已经很熟悉了,按以下这样编写Sprite类的Update方法:
public virtual void Update(GameTime gameTime, Rectangle clientBounds) { timeSinceLastFrame += gameTime.ElapsedGameTime.Milliseconds; if (timeSinceLastFrame > millisecondsPerFrame) { timeSinceLastFrame = 0; ++currentFrame.X; if (currentFrame.X >= sheetSize.X) { currentFrame.X = 0; ++currentFrame.Y; if (currentFrame.Y >= sheetSize.Y) currentFrame.Y = 0; } } }
您可能注意到方法声明中的virtual关键字。这个关键字标记方法为虚方法,使您能够根据需要在子类中覆写这个方法来改变方法的功能。
同样您可能注意到了Rectangle参数。这个参数代表了游戏窗口客户区矩形,用来检测物体何时越过了游戏窗口边缘。
正如您之前编写了动画代码,您同样也编写了从动画精灵中绘制单独帧的代码。现在您只需要将那些代码加入到Sprite类中,然后使用Sprite类中定义的变量。
和之前章节中的绘制代码不同的一点,是在Sprite类中无法访问SpriteBatch对象(但愿您还记得,需要它来绘制一个Texture2D对象)。为避免这一点,您需要Sprite类的Draw方法接受一个GameTime参数并增加一个SpriteBatch参数。
Sprite类的Draw方法看起来应该像这样:
public virtual void Draw(GameTime gameTime, SpriteBatch spriteBatch) { spriteBatch.Draw(textureImage, position, new Rectangle(currentFrame.X * frameSize.X, currentFrame.Y * frameSize.Y, frameSize.X, frameSize.Y), Color.White, 0, Vector2.Zero, 1f, SpriteEffects.None, 0); }
除了Update和Draw方法之外,您要为Sprite类添加一个用来表示精灵移动方向的属性。
方向总是用一个Vector2来表示,表示X、Y方向的移动,但是它通常是在子类中定义(例如:自动精灵和用户控制精灵的移动方式不同)。所以这个属性需要存在于基类中,但是应该是抽象的,意味着在基类中它没有实现并且必须在所有的子类中定义。
如下那样加入抽象的Direction属性到基类中:
public abstract Vector2 direction { get; }
还有一个要加入到Sprite类中的东西:一个返回矩形值的属性,用来进行碰撞检测。添加下面的属性到Sprite类中:
public Rectangle collisionRect { get { return new Rectangle( (int)position.X + collisionOffset, (int)position.Y + collisionOffset, frameSize.X - (collisionOffset * 2), frameSize.Y - (collisionOffset * 2)); } }
您的基类现在已经完成得不错了。它可以用Draw方法来绘制自己并且通过Update方法来对精灵位图进行循环遍历。因此,让我们将焦点转移一下,看看用户控制精灵。
现在您要创建一个从Sprite基类派生的类,添加一个用户控制的功能。首先需要添加一个新类到您的项目中。在解决方案的项目节点上点击鼠标右键,选择Add→Class。命名新类文件名为UserControlledSprite.cs。建好类后,将它标记为Sprite类的派生类:
class UserControlledSprite : Sprite
接下来您需要添加一些XNA using语句。使用和Sprite类中一样的语句,另外添加一条using语句(Microsoft.Xna.Framework.Input)让您能从输入设备读取数据:
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input;
然后为UserControlledSprite类添加构造方法。这些构造方法基本上和Sprite的一样,仅仅将参数传递给基类:
public UserControlledSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed) { } public UserControlledSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, int millisecondsPerFrame) : base(textureImage, position, frameSize,collisionOffset, currentFrame, sheetSize, speed, millisecondsPerFrame) { }
然后,您需要添加代码来实现Direction属性。Direction属性将在Update方法中用来改变精灵的位置(或者换句话说,让精灵往这个属性指示的方向移动)。就UserControlledSprite类来说,Direction属性将被定义为结果由基类的speed成员和玩家按下的方向共同决定。
用户也能用鼠标来控制精灵,但是鼠标输入会有一些不同的处理。当用鼠标来移动精灵的时候,您移动精灵到鼠标所在的位置。因此,实际上在处理鼠标移动的时候不需要Direction属性。这个属性将只会反映玩家通过键盘或Xbox 360手柄进行的输入。为了用从摇杆和键盘读取的数据构建Direction属性,编写如下代码:
public override Vector2 direction { get { Vector2 inputDirection = Vector2.Zero; if (Keyboard.GetState( ).IsKeyDown(Keys.Left)) inputDirection.X -= 1; if (Keyboard.GetState( ).IsKeyDown(Keys.Right)) inputDirection.X += 1; if (Keyboard.GetState( ).IsKeyDown(Keys.Up)) inputDirection.Y -= 1; if (Keyboard.GetState( ).IsKeyDown(Keys.Down)) inputDirection.Y += 1; GamePadState gamepadState = GamePad.GetState(PlayerIndex.One); if(gamepadState.ThumbSticks.Left.X != 0) inputDirection.X += gamepadState.ThumbSticks.Left.X; if(gamepadState.ThumbSticks.Left.Y != 0) inputDirection.Y += gamepadState.ThumbSticks.Left.Y; return inputDirection * speed; } }
这个属性将会返回一个Vector2值来指示移动方向(在X和Y平面)。请注意键盘和游戏手柄输入结合在一起了,允许玩家用两种输入设备来控制精灵。
为了处理鼠标移动,您本质上需要在每帧检测鼠标是否移动。如果移动了,您就假设用户想要用鼠标控制精灵,然后移动精灵到鼠标光标所在的地方。如果鼠标没有移动,键盘和手柄输入将会影响精灵的移动。
为了检测帧与帧之间鼠标是否移动,添加下面这个成员变量到UserControlledSprite类中:
private MouseState prevMouseState;
您需要覆写继承自基类的Update方法,并且添加基于Direction属性来移动精灵的代码,包括鼠标移动(如果鼠标被移动的话)。另外,您要添加一些逻辑到方法中来保持用户控制的精灵不会移动到屏幕外。您的Update方法看起来应该像这样:
public override void Update(GameTime gameTime, Rectangle clientBounds) { // Move the sprite based on direction position += direction; // If sprite is off the screen, move it back within the game window MouseState currMouseState = Mouse.GetState( ); if (currMouseState.X != prevMouseState.X || currMouseState.Y != prevMouseState.Y) { position = new Vector2(currMouseState.X, currMouseState.Y); } prevMouseState = currMouseState; // If sprite is off the screen, move it back within the game window if (position.X < 0) position.X = 0; if (position.Y < 0) position.Y = 0; if (position.X > clientBounds.Width - frameSize.X){ position.X = clientBounds.Width - frameSize.X;} if (position.Y > clientBounds.Height - frameSize.Y){ position.Y = clientBounds.Height - frameSize.Y;} base.Update(gameTime, clientBounds); }
现在行啦。您的UserControlledSprite类就绪了!您不需要对这个类中的Draw方法做什么,因为您的基类将会处理精灵单独帧的绘制,干得漂亮!
现在您有了一个允许用户控制精灵的类,现在是时候添加一个能产生动画精灵并自主运动的类了。添加一个新类到您的项目中,在解决方案的项目上点击鼠标右键,选择Add→Class。命名类文件名为AutomatedSprite.cs。文件建好后将新类标记成Sprite类的子类:
像以前那样加入XNA命名空间,但是不需要处理输入的命名空间,因为您不会从这个类中获得任何设备输入:
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics;
接下来,为AutomatedSprite添加两个构造方法,这些方法将和UserControlled
Sprite类用到的一样:
public AutomatedSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed) { } public AutomatedSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, int millisecondsPerFrame) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, millisecondsPerFrame) { }
您的自动精灵将会使用基类speed成员的速度值在屏幕上移动。这可以通过覆写Direction属性来实现,因为这个属性是抽象的,所以必须在子类中定义。如下这样定义Direction属性:
public override Vector2 direction { get { return speed; } }
现在您要添加让精灵动起来的代码。因为Direction属性由一个Vector2值来表示,这个属性表示了自动精灵移动的速度和方向。2D空间中的任何方向都可以用一个Vector2(二维向量)值来表示,并且向量的大小(或长度)指示着物体的速度:向量越长,精灵移动的速度越快。
您需要做的就是将Direction属性值和精灵的位置position相加,精灵就会往那个向量的方向并以其长度所指示的速度移动。
添加一个覆写的Update方法到AutomatedSprite类中,让精灵基于Direction属性移动:
public override void Update(GameTime gameTime, Rectangle clientBounds) { position += direction; base.Update(gameTime, clientBounds); }
就这么多了!您现在有了一个可以绘制自己,并基于一个2D向量来更新自己的位置的自动精灵类。
图5-2 位置 + 速度 = 新的位置
到目前为止,您有了两类精灵,都是从代表了普通的移动精灵的基类派生出来的。在之前的章节中,当您想要增加一个新的精灵是,您必须添加许多不同的变量和设置来实现新的精灵。现在利用这种模式,只需要加上一个新的变量(AutomatedSprite或UserControlledSprite),就可以将新的精灵添加到应用程序中。
然而,除了针对不同的精灵在各处增加变量的做法之外,让我们看看更为模块化的处理手法。XNA为我们提供了一个强大的工具,可以让逻辑部分的代码分离到不同的模块中,并且让它们能够很容易地加入游戏中并良好地共存。这个工具就是GameComponent(游戏组件)类。
在下一部分,您将学习游戏组件,并且您会创建一个管理游戏中所有精灵的组件。
XNA有一个相当棒的方法,可以把不同的逻辑代码片段整合进您的程序(例如您即将创建的SpriteManager类)。GameComponent类允许您将代码以模块化的反思插入到程序中,并且自动地将这个部件加入到游戏循环的Update调用中(也就是说,在您的游戏的Update方法被调用之后,所有相关联的GameComponent类的Update方法都会被调用)。
要创建一个新的游戏组件,请用鼠标右键点击解决方案资源管理器中的项目节点,选择Add→New Item。在窗口右侧的模版列表中选择Game Component,然后命名游戏组件文件名为SpriteManager.cs。
看看您的新游戏组件类生成的代码,您会注意到它包含构造方法,以及Initialize和Update方法,并且继承了GameComponent类。
如果您想创建一个还能和游戏循环的Draw方法一起工作的游戏组件,使您的游戏组件有能力绘制东西,您也可以通过继承DrawableGameComponent类来实现。
由于您需要使用精灵管理类来调用它管理的所有精灵的Draw方法,所以您需要让这个游戏组件和游戏的Draw方法一起工作。修改游戏组件的基类为DrawableGameComponent,以启用绘制功能:
public class SpriteManager : Microsoft.Xna.Framework.DrawableGameComponent
修改基类之后,您要为您的游戏组件创建一个覆写的Draw方法:
public override void Draw(GameTime gameTime) { base.Draw(gameTime); }
要将新创建的组件添加到游戏中,并让组件的Update和Draw方法在游戏循环中开始工作,您还要将组件添加到Game1类使用的组件列表中。要实现这一点,您需要添加一个SpriteManager类型的成员变量到Game1类中:
SpriteManager spriteManger;
然后,在Game1类的Initialize方法中,您需要实例化SpriteManager对象,传递一个Game1类的引用(this)给构造方法。最后,将这个对象添加到Game1类的组件列表中:
spriteManger = new SpriteManager(this); Components.Add(spriteManger);
好!一切准备就绪。当游戏的Update和Draw方法被调用时,游戏组件中同样的方法也会被调用。
您可以看到添加一个GameComponent到游戏中是多么地容易。想象一下这种工具的灵活运用,例如,如果您创建了一个组件用来绘制帧率和其它性能相关的调试信息到屏幕上,您可以用两行代码将这个组件添加到任何游戏中!这真是非常酷的事情。
虽然您的SpriteManager类已经准备好并能够使用了,但是它还没有干任何事。您可以在SpriteManager的Draw方法中进行绘制,就像在Game1类中那样。事实上,为了将精灵逻辑部分与游戏里的其它部分清楚地区分开来,您应该让SpriteManager类控制所有精灵的绘制。为了做到这一点,您需要添加一些代码来让SpriteManager绘制精灵。
首先您需要一个SpriteBatch类的对象。虽然Game1类中已经有了一个SpriteBatch对象,但是这里创建自己的SpriteBatch比重用Game1类中的要合理。只有这样您才能真正使游戏组件独立于游戏。游戏和游戏组件之间过多的数据传递会破坏这种设计。
除了增加一个SpriteBatch变量外,您需要添加一些其它的变量:一组用来代表所有的自动精灵的Sprite对象,以及一个用来代表玩家控制精灵的UserControlledSprite对象,添加这些变量到SpriteManager类中:
SpriteBatch spriteBatch; UserControlledSprite player; List<Sprite> spriteList = new List<Sprite>( );
就像SpriteManager的Update与Draw方法会和Game1类的Update与Draw方法一起被调用那样,Initialize与LoadContent方法也会和Game1类的相应方法一起被调用。您需要添加一些代码来加载纹理、初始化SpriteBatch对象、初始化玩家对象,以及针对测试的需要,在精灵管理类的精灵列表中添加一些精灵。用以下代码添加一个覆写的LoadContent来完成所有这些工作:
protected override void LoadContent( ) { spriteBatch = new SpriteBatch(Game.GraphicsDevice); player = new UserControlledSprite( Game.Content.Load<texture2d>(@"Images/threerings"), Vector2.Zero, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), new Vector2(6, 6)); spriteList.Add(new AutomatedSprite( Game.Content.Load<texture2d>(@"Images/skullball"), new Vector2(150, 150), new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), Vector2.Zero)); spriteList.Add(new AutomatedSprite( Game.Content.Load<texture2d>(@"Images/skullball"), new Vector2(300, 150), new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), Vector2.Zero)); spriteList.Add(new AutomatedSprite( Game.Content.Load<texture2d>(@"Images/skullball"), new Vector2(150, 300), new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), Vector2.Zero)); spriteList.Add(new AutomatedSprite( Game.Content.Load<texture2d>(@"Images/skullball"), new Vector2(600, 400), new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), Vector2.Zero)); base.LoadContent( ); }
这里做了些什么呢?首先,您初始化了SpriteBatch对象;然后,您初始化了玩家对象并且添加了4个自动精灵到精灵列表中。这些精灵只是作为测试目的使用,这样您完成精灵管理类的时候就可以看到效果。
接下来,您需要在每次调用精灵管理类的Update方法时调用玩家对象和精灵列表中所有精灵的Update方法。在精灵管理类的Update方法中,添加以下代码:
public override void Update(GameTime gameTime) { // Update player player.Update(gameTime, Game.Window.ClientBounds); // Update all sprites foreach (Sprite s in spriteList) { s.Update(gameTime, Game.Window.ClientBounds); } base.Update(gameTime); }
现在,您需要对绘制做同样的事情。Sprite基类有一个Draw方法,所以您需要在SpriteManager类的Draw方法中调用所有精灵的Draw方法。精灵必须总是在Sprite.Begin和SpriteBatch.End调用对中绘制,因此请确保您添加了Sprite.Begin和End方法来包含精灵绘制方法的调用,进而绘制出精灵:
public override void Draw(GameTime gameTime) { spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.FrontToBack, SaveStateMode.None); // Draw the player player.Draw(gameTime, spriteBatch); // Draw all sprites foreach (Sprite s in spriteList) s.Draw(gameTime, spriteBatch); spriteBatch.End( ); base.Draw(gameTime); }
您的SpriteManager类中只剩一件事要处理:碰撞检测。您将在精灵管理类中处理碰撞检测而不是在独立的精灵或游戏对象中。
在这个特定的游戏中,您不必关心自动精灵是否互相碰撞——您只需要检测玩家精灵和自动精灵的碰撞。修改Update方法来检测玩家和自动精灵的碰撞:
public override void Update(GameTime gameTime) { // Update player player.Update(gameTime, Game.Window.ClientBounds); // Update all sprites foreach (Sprite s in spriteList) { s.Update(gameTime, Game.Window.ClientBounds); // Check for collisions and exit game if there is one if (s.collisionRect.Intersects(player.collisionRect)) Game.Exit(); } base.Update(gameTime); }
现在,每当游戏的Update方法被调用时,SpriteManager中的Update方法也会被调用。SpriteManger会依次调用所有精灵的Update方法并且检测和玩家控制精灵的碰撞。这是很棒的处理方式,对吧?
哇!看起来似乎很费功夫,但是我保证您会很满意这些程序代码所做的一切。您的SpriteManager类已经完成并和Game1类联系在一起了。然而您的Game1类中仍然有您在之前章节中添加的代码。您现在可以到Game1类中删除除了SpriteManager相关和IDE生成的代码之外的其它代码。现在Game1类看起来应该像这样:
using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Media; using Microsoft.Xna.Framework.Net; using Microsoft.Xna.Framework.Storage; namespace AnimatedSprites { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; SpriteManager spriteManager; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void Initialize() { spriteManager = new SpriteManager(this); Components.Add(spriteManager); base.Initialize(); } protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); } protected override void UnloadContent() { // TODO: Unload any non-ContentManager content here } protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.White); base.Draw(gameTime); } } }
编译并运行程序,您会看到旋转的三环精灵和4个骷髅球精灵。您可以通过键盘、鼠标和游戏手柄控制三环精灵,同时如果它和任意一个骷髅球精灵发生碰撞的话,游戏就会结束。
这个例子中有一件需要注意的事,就是当游戏开始时,三环精灵会出现在鼠标光标所在的地方。如果这个位置恰好在一个自动精灵上,碰撞检测在会游戏一开始就成立并导致游戏提前结束。如果您遇到了这个问题,请将鼠标光标移动到屏幕的角落里再开始游戏。这个小问题接下来不会产生什么不良的后果,因为之前已经提到过了,现在绘制的这些精灵只是用来测试精灵管理类的功能。
不错!如果您想知道这一章的要点是什么,看看Game1类的代码。您的程序现在有了非常纯粹的面向对象设计,和之前您做的那些比较一下。看看Draw方法和Game1类其余部分,几乎什么都没有。更棒的是,看看增加一个全新的动画精灵需要做什么:只是一行代码而已!还记得之前章节中要添加一个新精灵是多么痛苦吗?您需要添加许多变量和代码,做很多复制、粘贴、修改变量名等等。想想这些,您就可以看到使用诸如XNA的GameComponent之类的强大工具的模块化方法和设计良好的类层次所带来的益处。
图5-3 投入运行的纯粹的面向对象设计
您可能会觉得您的自动精灵好像有点不对劲。您记得您明明添加了让它们自己移动的代码,但是它们什么都没干,只是待在那儿不停的旋转。您的动画精灵不会动的原因,是因为您在如图5-2所示创建动画精灵时使用的速度值为零;也就是说,在调用SpriteManager对象的LoadContent方法时,您传递了Vector2.Zero作为每个动画精灵对象的构造方法的最后一个参数。
为了让您的动画精灵在屏幕上移动,试着修改您传递给它们的速度参数。要注意到您除了让这些精灵移动,没有编写其它任何逻辑。结果就是您的精灵会一直向前移动,甚至移动到屏幕之外。在接下来的章节中,您会添加一些逻辑来动态创建精灵,并且使它们从屏幕的一边飞向另一边。这一章和您创建的Sprite类层次结构将会是将来开发的基础。
在这里停下来并好好表扬一下自己吧。要想精通所谓稳定而且可靠的软件设计是相当不容易的。太多的开发者不经思考就一头扎入代码中,结果就是意大利面条般混乱的代码,并且将很快失去控制。让我回顾一下您做了些什么:
• 您为精灵创建了一个继承体系,包括一个用来处理动画的基类和两个处理用户输入和自主移动的派生类。
• 您学习了GameComponent类,可以用来进行可替换部件的模块化设计。
• 您创建了一个SpriteManager类来处理精灵的更新、绘制和碰撞检测。
• 您清理了Game1类,以便于将来的开发。
• 可靠的设计和正确的代码同等重要。游戏开发项目中花在设计上的时间对于加速开发进程、提高可维护性和提升性能来说非常重要。
• 应用可靠的类层次设计减少了冗余代码并且提升了系统整体的可维护性。
• GameComponent是一个让开发者可以分离某些功能到独立的模块中并易于应用到不同项目中的强大工具。
1. 游戏组件从哪个类派生?
2. 如果您想要使用您的游戏组件绘制,您需要从哪个类进行派生?
3. 真还是假:花费时间建立可靠的面向对象设计不如编码有价值,因为它是不必要和多余的。
修改这章创建的代码,生成4个精灵在屏幕上移动并在触及屏幕四边时反弹。为了完成这个目标,需要创造一个继承自AutomatedSprite类的新类,可以将它命名为BouncingSprite。BouncingSprite和AutomatedSprite类做同样的事情,除了在Update方法中检测精灵是否越过屏幕边缘。如果是,则将speed变量乘以-1,反转精灵的移动方向。
生成两个使用骷髅球精灵位图的反弹精灵,另外两个使用加号精灵位图(它们位于本章代码的AnimatedSprites\AnimatedSprites\AnimatedSpritesContent\Images文件夹下)。
请注意当应用这些修改后运行游戏,屏幕上会有4个自动精灵在屏幕上移动,任何一个碰到了用户控制的精灵游戏将会结束。测试游戏的时候可能会导致一些问题,因为精灵可能在游戏刚开始的时候就发生碰撞了。在运行游戏前试着把您的鼠标光标移到屏幕角落,使用户控制精灵在游戏开始时远离自动精灵。