译者:林公子
出处:木木的二进制人生
转载请注明作者和出处,谢谢!
第二章 精灵的乐趣(1)
译注:精灵,原文为Sprite,本文译作精灵,Sprite指场景中用来表示角色或其他物体的2D或3D图像。
具体的论述可以看风海迷沙的文章以便得到关于Sprite一词准更准确的认识:
感谢风海迷沙的专业精神,让我受益良多。
在上一章中,我提到您创建的项目的简单蓝色背景下实际上有很多事正在发生。让我们更深入了解代码看到底发生了些什么。请打开您上一章创建的游戏项目。
幕后一览
Program.cs文件内容很简单。您的Main函数创建了一个类型为Game1的新对象game,并且执行了game的Run()方法。
您游戏的真正部分在Game1.cs文件中,文件内容看起来应该像这样:
2 using System.Collections.Generic;
3 using System.Linq;
4 using Microsoft.Xna.Framework;
5 using Microsoft.Xna.Framework.Audio;
6 using Microsoft.Xna.Framework.Content;
7 using Microsoft.Xna.Framework.GamerServices;
8 using Microsoft.Xna.Framework.Graphics;
9 using Microsoft.Xna.Framework.Input;
10 using Microsoft.Xna.Framework.Media;
11 using Microsoft.Xna.Framework.Net;
12 using Microsoft.Xna.Framework.Storage;
13 namespace Collision
14 {
15 /// <summary>
16 /// This is the main type for your game
17 /// </summary>
18 public class Game1 : Microsoft.Xna.Framework.Game
19 {
20 GraphicsDeviceManager graphics;
21 SpriteBatch spriteBatch;
22 A Look Behind the Scenes | 9
23 public Game1()
24 {
25 graphics = new GraphicsDeviceManager(this);
26 Content.RootDirectory = "Content";
27 }
28 protected override void Initialize()
29 {
30 // TODO: Add your initialization logic here
31 base.Initialize();
32 }
33 protected override void LoadContent()
34 {
35 // Create a new SpriteBatch, which can be used to draw textures.
36 spriteBatch = new SpriteBatch(GraphicsDevice);
37 // TODO: use this.Content to load your game content here
38 }
39 protected override void UnloadContent()
40 {
41 // TODO: Unload any non ContentManager content here
42 }
43 protected override void Update(GameTime gameTime)
44 {
45 // Allows the game to exit
46 if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
47 ButtonState.Pressed)
48 this.Exit();
49 // TODO: Add your update logic here
50 base.Update(gameTime);
51 }
52 protected override void Draw(GameTime gameTime)
53 {
54 GraphicsDevice.Clear(Color.CornflowerBlue);
55 // TODO: Add your drawing code here
56 base.Draw(gameTime);
57 }
58 }
59 }
在这段代码中您会注意到自动提供了几个类成员变量,还有Game1的构造函数和其他五个方法。第1个类成员变量是GraphicsDeviceManager.这是一个非常重要的对象,因为它为开发者提供了一种途径去访问PC,Xbox360或Zune上的图形设备。GraphicsDeviceManager有一个属性GraphicDevice代表了您机器上实际的图形设备。因为图形设备对象在XNA游戏和您的显卡之间一个中介作用(或更准确的说,显卡上的GPU),您的XNA游戏在屏幕上做的任何事情都要通过这个对象。
第2个成员变量是SpriteBatch类的实例,这是一个您将用来绘制精灵(Sprite)的核心对象。在计算机图形学术语中,一个精灵被定义为场景中的一个2D或3D图像。2D游戏由场景中各种各样的精灵组成(角色精灵,敌人精灵,背景精灵,等等)。在这一章中您将用到这个概念并且绘制自己的精灵。
Initialize方法用来初始化变量和Game1对象相关的其他对象。您的图形设备对象将在这时被实例化,然后可以在Initialize被用来帮助您基于设备对象的设置来初始化其他的对象。在将来的章节中您将用这个方法来初始化分数值和其他类似项目。
LoadContent方法在Initialize方法之后被调用,另外在任何需要重新载入游戏图形内容的时候也会被调用(例如,因为玩家改变显示设置而重置了图形设备,诸如此类)。LoadContent是您加载游戏需要的所有图形或其他内容的地方,包括图像,模型,声音等等。
因为目前的项目不会做什么令人激动的事,所以这个方法中也不会有什么。
当LoadContent方法调用完成后,Game1对象将进入游戏循环。几乎所有的游戏都使用某种形式的游戏循环,不管它们是否用XNA写成。这是游戏开发和常规应用程序开发不同的一个方面。对一些开发者来说需要一点时间来适应。
本质上来说,一个游戏循环由一系统方法组成,这些方法反复被调用直到游戏结束。在XNA中,游戏循环只包含两个函数:Update和Draw。眼下您可以这样理解游戏循环:所有影响游戏的逻辑都将在Update或Draw方法中完成。您应该设法在Draw方法中做最少的事情。游戏运行需要的所有东西(最终涉及到移动物体,碰撞检测,更新分数,游戏结束检测逻辑,等等)应该放到Update方法中。
游戏开发和轮询
另一个游戏开发和典型应用程序开发之间的关键差异是轮询和事件注册的概念。许多非游戏应用程序都仅仅编写成通过用户进行事件驱动。举个例子,如果你要为某些系统写一个窗体组件命名模块,你可能会创建一个窗口然后要求用户输入想要的名字,窗口有一个确定和取消按钮。不管这个程序用哪种编程语言写成,直到用户点击确定或取消按钮之前都不会做任何事。当用户点击其中一个按钮时,系统将产生一个程序能够捕获的事件。换句话说,应用程序只有在用户给它发送一个某个按钮被按下的事件标识后才会被唤醒并做一些处理。
相比之下,游戏程序由事件轮询(polling for events)驱动,而不是等待并监听是否有事件被激发。取而代之,游戏程序会主动询问系统鼠标是否被移动,同时程序会一直运行,不管有没有用户输入。
假设您开发的一个游戏中名叫Jimmy的男巫(哦,男巫Jimmy游戏拥有很大的市场)试图从恶魔珈蓝鸟军团的掌握中逃离。您必须要解析用户事件比如玩家移动Jimmy到左边或发动了一个破坏珈蓝鸟翼的咒语。但是并不是由XNA来告诉您玩家进行了这些动作,而是由您自己来轮询输入设备(鼠标,键盘,手柄,等等)来获得输入的改变。
在同一时间,不管用户是否与系统进行交互,游戏中的一切仍在进行。举个例子,可能珈蓝鸟军正在追逐Jimmy,而不管用户有没有激发任何事件,这个都会发生,并且游戏负责不断移动敌人的位置而不需要注册任何事件。这是需要游戏循环的主要原因:它提供了一种途径让游戏始终运行着而不管玩家在做什么。
当然,除了让敌人在屏幕上移动,还可以做更多的事情。要是珈蓝鸟军能够在空中抛出抗魔法的炸弹怎么样?或许会有1,2,5,50甚至更多的炸弹在天空飞过而需要不断的被移动。你还必须去不断检测看那些炸弹是否击中了什么而起作用。要是玩家一直不移动Jimmy让珈蓝鸟军抓住了Jimmy呢?在那种情况下或许要发生点什么。也许你需要设置一个计时器,Jimmy必须在3分钟之内逃脱--现在你又得去追踪某些类型的计时器,并且在计时器归零或Jimmy逃脱的时候会执行一些逻辑。
在游戏开发中,程序会不断运作,并且您要持续不断的更新动画,移动物体,进行碰撞检测,更新分数,检测游戏结束逻辑,等等。
在假设的窗体组件命名程序中,要做到持续检测非用户事件这一点有些困难,但在XNA开发中,这一点用游戏循环的形式整合进了应用程序框架。所有这些任务都在游戏循环的Update方法中被处理,接着场景绘制在游戏循环的Draw方法中完成。
事实上,所有的应用程序都有和游戏循环功能相似的循环。Windows本身使用一个消息和事件系统,不断循环告知应用程序何时需要重绘和完成其他功能。对这些循环的访问通常是隐藏起来的,不过,大多数应用程序不需要访问这些非用户驱动事件。
好了,让我们回到之前看到的代码中。您会注意到在Update方法中有一些代码行在玩家按下手柄上的“返回”按钮时让游戏退出:
2this.Exit( );
这是在使用Xbox360控制器时如何结束Xbox360或Windows游戏的方法(否则,您可以在Windows中点击游戏窗口红色的X按钮或按Alt + F4组合键去结束它)。
如早先提到的,Update方法是您更新和游戏有关的一切东西的地方。您能更新物体在屏幕中的位置,分数,动画序列等等。您也能检查用户输入,进行碰撞检测,并且调整AI算法。
在Update方法中对游戏中变化的检测和依照这些变化的行动通常都和游戏状态有关。游戏状态是一个非常重要的概念:这是一种让游戏知道当前游戏状况的方法。游戏一般有多种完全不同的状态,比如启动画面,实际游戏画面,游戏结束画面。还会有一些细微的状态改变,比如玩家得到某种宝物使角色无敌一段时间或其他一些游戏行为的改变。通常您需要在Update方法中改变游戏状态然后在Draw方法中使用这些状态来绘制不同的图像,场景或其他和特定状态相关的信息。
Draw方法是把游戏中所有的物体绘制到屏幕上的地方,使用之前提到的图形设备对象。在目前的程序中,Draw方法做的唯一件事就是清除屏幕并设置背景色为CornFlowerBlue(待会我们会深入讨论)。
图2-1展示了一个XNA游戏的生命周期,包括Update和Draw方法形成一个游戏循环。
图2-1 一个XNA游戏的生命周期。
请注意Update方法的执行有两个可能的结果:要么持续执行然后Draw方法被调用,要么游戏结束,退出游戏循环并且调用UnloadContent方法。当您调用Game类的Exit方法时,游戏循环将结束,就像您按下Xbox360控制器上的“返回”按钮一样。游戏循环也会在您按下Alt + F4组合键或点击红色的X按钮关闭游戏窗口时退出。
在您的游戏退出循环和结束游戏之间应该存在一些过程。比如说,如果恶魔珈蓝鸟捉住了男巫Jimmy,然后游戏直接退出并且游戏窗口就这么消失,将使玩家觉得很郁闷。实际上大多数玩家将会把这种行为看作某种Bug。相应的,您通常使用某种游戏状态逻辑使Draw方法调用去渲染游戏结束画面代替游戏进行画面。然后过了固定时间后或您检测到玩家按下某些键,游戏才会真正的退出。这些工作现在好像有些复杂不容易理解,但是请不要过分担心。因为本书至始至终都会接触到这些,很快您就能明白怎么做到。
一旦游戏退出循环,UnloadContent会被调用。这个方法用来卸载所有在LoadContent方法里加载的内容。就想.NET一样,XNA会进行自动垃圾回收,但如果你在某些对象中进行需要特别处理的内存操作,UnloadContent方法就可以让您进行这些处理。
修饰您的游戏
好,讲得足够多了,您一定心痒痒想要开始进行开发然后准备放一些很酷的东西到您的游戏中了。让我们开始吧。
看一看Draw方法,现在方法中包含以下代码:
2 {
3 GraphicsDevice.Clear(Color.CornflowerBlue);
4
5 // TODO: 在这儿添加您的绘制代码。
6
7 base.Draw(gameTime);
8 }
这里需要注意的第一件事是Draw方法接收的参数。这个参数是GameTime类型的,用来表示游戏中经过的时间。为什么您需要一个追踪时间的的变量呢?因为并不是所有的计算机都以相同的速度运行。这个变量帮助您使用真实游戏时间而不是处理器速度来确定动画和其他事件发生的时机。gameTime将在整本书中用来度量诸如帧率,动画,声音和其他效果。同样这个参数也传入Update方法,因为很多控制那些效果的函数需要在Update而不是Draw方法中执行。
在这个方法的末尾,调用了Game1基类的Draw方法。这是为了在GameComponents和其他对象中级联调用Draw方法所必不可少的。现在您可能不理解,但是您需要在代码中调用base.Draw方法,并且请不要移除它。
最后,让我们看看用图形对象的GraphicsDevice属性来调用Clear方法。再次说明,这个属性代表您的PC,Xbox360或Zune上的真实图形设备并且允许您绘制各种物体到屏幕中。
Clear方法擦除屏幕上所有的东西然后用指定的颜色填充背景(在这里是CornFlowerBlue)。把颜色改变成如Color.Red然后再运行您的游戏,您将看到和之前同样的窗口,不过现在背景被填充成了红色。
记得我曾说过看起来无趣的蓝色背景下其实隐藏着许多细节吗?这就是指我刚才谈到的。当您看到一个乏味的蓝色背景(或现在的红色),XNA做了许多工作来将它呈现给您。它将游戏循环每秒种运行60次,擦除屏幕上所有的东西然后填充为红。另外还每秒调用Update方法60次,并检测Xbox360控制器上的“返回”按钮是否被按下。
XNA最棒的一点就是游戏的框架已经搭好,让您很容易进行定制和扩展。
那么,如果游戏循环每秒运行60次并且调用Update和Draw,为什么每一帧都需要清屏?尽管您可能觉得每一帧都清除屏幕然后重画整个场景和所有物体会效率低下,但是这远比试图追踪场景中所有的东西,在新位置画出它们,然后画出之前被它们挡住的物体这样高效得多。如果您移除了Clear调用,XNA在每帧绘制前将不会擦除屏幕,并会产生意外的绘制结果。
帧和帧速率 什么是帧?像以前提到的一样,默认情况下XNA会在每次Draw调用的时候清除屏幕并且重绘场景。这样一次Draw调用所产生的场景就称为一帧。您可以把XNA中的2D游戏想象成活页卡通书,您在一页中绘制一个角色,在下一页稍稍移动一点的位置绘制同样的角色,以此类推,当您快速翻动书页的时候,您就有角色在动的假象。XNA也做了同样的事情。每16微秒(或60次/秒),屏幕被清除然后新场景被绘制,当新场景中角色的位置和之前稍有不同的时候,就会产生角色活动的假象。 多帧就形成了游戏中的动画,每秒绘制的帧数就被称为游戏的帧速率(例如,60fps=60帧/秒。 |
在您的项目中增加一个精灵
OK,我说过不再多说,这一次我是认真的。让我们开始。您的项目到目前为止是令人乏味的。现在,让我们绘制一副图像到屏幕上。
XNA中所有的图形,声音,特效和其他东西都要通过一个称为内容管线的东西加载。本质上内容管线将诸如.Jpg文件,.Bmp文件,.Png文件和其他格式的文件在编译过程中转换成一种XNA很容易使用的内部格式,对于其他类型的资源也是如此。如声音文件,3D模型,字体等,后来的章节会深入探讨。XNA框架很大的一点好处就是文件类型对于XNA是透明的,如果您在游戏中添加了一个图像文件,内容管线在编译过程中能够识别文件格式。您不需要为图像格式担心。(稍后的章节会更深入讨论内容管线)。
下载本章的源码到您的硬盘上。这样您就可以得到本章剩下部分需要用到的图像文件,并把它们加入到您的项目中。
打开Visual Studio的解决方案管理器(Solution Explorer)看看您的项目,您会看到一个Content节点,这就是您将要往项目中添加资源(图形,声音,模型等)的地方。因为我喜欢一切都井井有条,所以我建议在Content节点下为每种内容类型创建一个子文件夹,把资源分类存放。要做到这一点右键点击Content节点,选添加(Add)->新建文件夹(New Folder)。将新建的文件夹命名为Images.然后在Images文件夹上右键选泽添加(Add)->现有项目(如图2-2)。
图2-2 添加一张图片到您的解决方案中
在打开的文件查找对话框中,导航到您存放刚才下载的源码的文件夹,定位到BasicSprite\Collision\Conten\Images目录下。选择logov.png文件,然后点击添加(Add)按钮关闭对话框,您选择的文件就会出现在解决方案浏览器的Content\Images文件夹下,同时文件也拷贝到您项目的Content\Images\文件夹中。
内容管线使用一个资源名称(asset name)来访问内容资源。另一种确认您的图像文件可以被内容管线识别的方法是查看新添加项目的属性,右键点击解决方案管理器中刚才添加的图像文件,选择属性,如图2-3.
图2-3 图像文件的属性
如图所示,在图2-3中,您添加的logo.png文件默认的资源名是logo或者说就是是不包括扩展名的图像文件名,默认情况下资源名都是这样命名的。
如果您可以在属性窗口中看到Asset Name属性,就说明内容管线可以识别您的图像文件。当然您可以改变资源名,项目中的资源名要求是唯一的,不过只有在同一个文件夹下才做这样的要求。这是在Content节点中使用子文件夹组织的另一个好处——您可以让多个资源拥有同样的资源名,只要它们存在于Content节点下不同的文件夹中。这个似乎是个只会把事情搞复杂的坏主意,但这实际上很常见而且很有用。举例来说,您有一个字体文件,一个特效文件,和一个图形文件,用作一个爆炸效果,那么如果把它们都命名为"Eexplosion"并分别放到所属类型的文件夹下会让事情变得容易,
您也许还注意到图2-3中Asset Name属性下面的两个属性:Content Importer和Conten Processor,它们被设置成Texture-XNA Framework,表明内容管线验证过您添加到项目中的图像文件:并且它们准备被内容管线作为纹理对象来使用。在计算机图形学中纹理指应用到3D物体表面的2D图像,在本书的3D部分我们会做这些。不过现在我们直接将这些纹理绘制到屏幕上。
加载并绘制精灵
现在您的解决方案中加载了一副图像并且能够被内容管道所识别,已经准备好将它绘制到屏幕上。不过在您能够在代码中访问它之前,您需要将资源从内容管道加载到变量中以便操作它们。
用来存储图像的缺省对象是Texture2D。在Game1.cs代码文件中的GraphicDeviceManager和SpriteBatch变量声明下面添加一个Texture2D变量:
现在,您需要将实际的图像文件加载到Texture2D变量中。为了访问内容管道中的数据,使用Game类的Content属性。Content属性是ContentManager类型,可以用来访问所有加载到内容管道中的对象。ContentManager类有一个Load方法可以让您将内容加载到不同类型的XNA对象中。
就像之前所说,所有内容资源的加载都在LoadContent方法中完成,所以添加以下代码到LoadContent方法中:
传入Content.Load方法的参数是图像文件的路径,根目录是解决方案中的Content节点。字符串前面加上@符号表示逐字字符串,忽略字符串中的转义序列,所以以下两行代码产生相同的字符串:
2string str2 = "images\\logo";
同样请注意参数中使用的资源名而不是文件名。
ContentManager类的Load方法是一个泛型方法,需要一个类型参数来指定您想要访问哪种类型的变量。目前的情况下,您正在处理一个图像文件并且期望返回一个Texture2D对象。
现在图像文件已经加载到texture变量中并可以使用了。XNA中所有的绘制工作都要在
2spriteBatch.Draw(texture, Vector2.Zero, Color.White);
3spriteBatch.End();
这三行代码将图像绘制到屏幕的右上角。选择调试(Debug)->运行(Run),您将会看到和图2-4类似的画面。
图2-4 XNA logo图像出现在屏幕的左上角
让我们看看这三行代码,注意到的第一件事是三行代码都用到了一个叫spriteBatch的SpriteBatch类型对象。这个变量在您创建项目的时候被声明,然后在LoadContent方法中被初始化。
本质上这里发生的事情就是XNA用SpriteBatch对象的Begin和End调用告诉图形设备将要向它发送一个精灵(或2D图像)。在XNA游戏过程中图形设备会接收大量的数据,而且数据会有不同的格式和类型。无论何时您向图形设备输送数据,都要让它知道数据类型以便它正确进行处理。因为,您不能只是随意调用SpriteBatch.Draw多次,您首先要调用SpriteBatch.Begin告诉显卡精灵数据已经发送。
Draw方法有三个参数,如表格2-1所述。
表2-1 Draw方法参数
参数 类型 描述
Texture Texture2D 持有您想要绘制的图像的Texture2D对象。
Position Vector2 您想要开始绘制图像的位置(2D坐标)。通常图像从左上角 开始绘制。
Color Color 染色颜色。指定为White将不会为图像染色,否则将图像染色为 指定颜色。
______________________________________________________________________________
尝试改变一下Draw调用的参数--具体的说是位置和染色颜色参数。在2D中,XNA使用Vector2结构体来定义坐标。Vector2.Zero是将Vector2的X,Y坐标置零的简化方式(和new Vector2(0, 0)是一样的作用)。
在2D XNA游戏中,X,Y屏幕坐标(0,0)是屏幕的左上角,X轴正方向向右,Y轴正方向向下。
如果您想要使图像在游戏窗口中居中,您需要找到窗口的中心点并且适当的偏移左上角坐标。您可以通过访问Game类的Window.ClientBounds属性来获得窗口的尺寸。当游戏运行于窗口模式时,Window.ClientBounds.X和Window.ClientBounds.Y相当于游戏窗口的左上角坐标,而Window.ClientBounds的Width和Height值总是等于窗口的宽度和高度,在窗口或全屏模式下都是如此。在Xbox360和Zune上Window.ClientBounds.X和Y总是为0,而Width和Height属性总是等于画面的宽和高(因为Zune和Xbox360游戏总是运行于全屏模式)。
将Window.ClientBounds.Width和Window.ClientBounds.Height值除以2将得到屏幕中心点的坐标。要准确的将图像居中,需要将屏幕中心点坐标偏移图像宽和高的一半。因为传入Draw方法的位置参数并不代表绘制图像的中心,而是左上角。您可以通过Texture2D变量(texture)的Width和Height属性来获得图像的尺寸。将您的Draw调用用以下的代码替换来将图像居中:
2 new Vector2(
3 (Window.ClientBounds.Width / 2) - (texture.Width / 2),
4 (Window.ClientBounds.Height / 2) - (texture.Height / 2)),
5 Color.White);