ITPub博客

首页 > Linux操作系统 > Linux操作系统 > RacingGame学习笔记2——基础图形部分1

RacingGame学习笔记2——基础图形部分1

原创 Linux操作系统 作者:思月行云 时间:2010-10-13 17:38:17 0 删除 编辑

第二节:基础图形部分

飞车游戏的图形效果在这样一个不大的工程中而言,算是格外出色的了。游戏中,让我印象深刻的是界面时而抖动,变色的效果,渲染了很不错的游戏气氛;游戏场景中画面的各个细节也都十分优秀。只是大多数涉及这些激动人心的内容的代码还要在以后的章节中才能与大家相见。这一节,仅仅介绍Graphics目录中的基础图形类,也是为以后的内容加以铺垫。

Graphics中的文件中,包括了对2d贴图的封装类;包括了对模型、材质、网格的封装类;包括了对镜头光晕效果的绘制类;包括一个绘制界面的辅助类;也包括了一些为了方便调试而支持的线、面的绘制类。

在目录中看上去有些与众不同的是BaseGame类,他从XnaFramework的Game类继承,提供游戏的一些基础绘制功能,(所以放在Graphics文件夹中也是有道理的。)并从他继承出管理整个游戏内容的RacingGa**nager类。

闲话不说了,让我们开始在Graphics目录中的旅途吧。

 

文件一:Texture.cs

Texture类提供了对XnaFramework中的Texture2D类的封装。封装是面向对象编程中的语言。为了通俗一些,可以把将其理解为形象包装。打娘胎里出来,一直都是的自然、淳朴的形象。突然间要找工作了。为了要吸引到更多的目光。决定改善自己的形象。于是就花了点钱,改得面目一新,精神抖擞。看上去已经不像原来的你了。换句话说,你被封装了。封装的结果不错,你顺利地进入某外企,月薪过万。可接下来,在时间的消磨下,你的本质又逐渐在人前人后暴露出来。实际上,面向对象中封装与此十分相似。目的,就是改善一个类对外的接口,使接口对使用接口的人更友善,也能够屏蔽掉不需要让其知道内容。仔细对比下Texture类与XnaFramework中的Texture2D类。能发现在绘制的接口上,Texture隐藏了SpriteBatch这个Texture2D绘制中必需的对象,而是在Texture类中包含了两个静态的SpriteBatch:alphaSprite和additveSprite。(前面的章节提到过SpriteBatch有两种混合模式,在SpriteBatch.Begin()函数中制定。所以在这个类中这两个SpriteBatch仍只是名称上的区别。)不仅如此,Texture类的绘制函数中还包括一些在不同屏幕分辨率的情况下绘制自己的函数。这样就为Texture类的调用者节省了繁琐的计算,也就是说绘制接口对调用方更友善了。

我们也可以顺便注意到该类中在不同屏幕分辨率下的绘制函数中,都会调用BaseGame.CalcRectangle系列函数。对于不同分辨率的方法的区分便在不同的CalcRectangle函数中了。定位到系列函数中的任意一个。(也就是BaseGame.cs文件中)可能会发现如下两点:

其一,在将浮点运算的结果装换为整形的过程中使用了Math.Round函数,该函数的作用是四舍五入。(这么说其实并不严密,因为函数有一个参数可以指定小数位恰好达到0.5的时候是进位还是舍去)之前我对类型转换中的这个细节也不在意,后来发现在我的程序中,对两个贴图传进去的绘制坐标是紧挨着的,但实际上画出来相距了一个像素的间隔。所以说像Benjamin这样写还是有一定优点的。

其二,作为在绘制循环中被调用的高频函数,这些函数在效率上有个小小的瑕疵。每次调用CalcRectangle函数都要重新计算两个只会在Device Reset时改变的准常量,而且计算中使用了除法。在加减乘除的基本运算中,除法的效率是最低的,高频运算表达式也往往需要面对除法进行优化。(去除除法,或减少除法运算次数。)比如在这几段代码中,完全可以把每一个winthFactor,heightFactor提升为BaseGame的静态成员,只在Device Reset的时候更新他们的值,而在CalcRectangle函数中直接使用。这样的话,这些值的计算成本就可以忽略不计了。

接下来还是回过头来看看Texture类。需要注意还有以下几点:

其一,注意下Constructor区间中的构造函数。首先能够看到一个被声明为protected的空构造函数。这个构造函数的作用跟上一节中Directories类中的私有空构造函数的作用是类似的:覆盖默认构造函数。但这里被声明为protected,区别就在于该类的继承类可以调用一个空的构造函数了。另外可以看到在另两个公有构造函数中。开头都会判断两个SpriteBatch是否未被初始化,如果是的话就在此处创建新实例。这种初始化的方法一般被称为惰性初始化。与在类中加入一个静态Initialize函数的方法相比,这种方法显得比较灵活。但像我这样有对程序结构追求完美的癖好的人,还是倾向于写Initialize函数了。

其二,注意下Disposing区间。Texture类继承了接口IDisposable。表示其实现了释放它拥有的非托管资源的能力。如果查找对Dispose函数的引用,就会发现引用处又是高一级别对象的Dispose函数。资源释放就是这样的一个自上往下的传递过程。而Dispose函数中调用GC.SuppressFinalize函数目的是告诉垃圾回收器不要调用该对象的Finalize函数(终结器)。这一行基本上在Dispose函数中都应包含。原因是调用终结器的成本比较高。关于这方面的内容可以参加MSDN中的“实现 Dispose 方法”词条。

另外,可以看到Texture类重载了ToString方法。ToString方法作用在将对象向string进行类型转换的时候。此处的重载方法中返回当前Texture的基本信息。好处是方面调试时显示对象内部的状态。例如有一个Texture对象tex,我们只需要使用Console.WriteLine(tex)就可以在控制台中输出tex的内部信息。或者我们也可以使用我们的Log类,调用Log.Write(tex.ToString())来将tex的信息记录到日志中。

 

文件二:TextureFont.cs

虽然Xna Game studio Express 1.0 Refresh已经对英语字符的绘制有内建的支持了。如今已经不需要自己编写像这样的类来支持字符的绘制。但这个文件中提供的一些方法,在很多地方(如动画切帧中)有一定的参考价值。

先来介绍一下Xna中在1.0Refresh之前绘制字符的方式(也就是这个类的工作机制)。

首先要理解的一点是,无论是WinForm中绘制使用的GDI,还是在Xna中使用的2D或3D的绘制方式。本质上都是封装了显示硬件设备的接口。我们之所以在Windows编程中能够很方便地绘制字符,并不是因为硬件设备本身支持Asc或者Unicode中的字符的绘制,而是因为windows操作系统中的GDI对绘制字符有良好的支持。也就是说,Xna在1.0Refresh之前不支持字符绘制,我们也就没办法直接绘制字符,而只能通过贴图的方式把字符贴到相应的位置上去。

这便是1.0Refresh之前在Xna中写字的方法了。第一步是准备一张包含了26个字母以及一些标点符号的贴图。贴图应该是具有透明通道的格式,并且贴图的背景应该是透明的。为了游戏中能根据需要显示不同颜色的字符,贴图上字符的颜色应该是白色的。(也可以带一个黑色的边界。)飞车代码中Content\Textures中的GameFont.png就是飞车中使用的字符贴图了。

第二步则是将字符贴图中各个字符在贴图中的位置记录下来。只要有了每个字符的位置信息,我们就可以使用SpriteBatch.Draw函数中的sourceRectangle参数指定需要绘制的贴图区域。也就可以实现字符的随意组合了。

看到这里,你可能会觉得以上两步中涉及的工作量可不是一般的大。“绘制一个字符贴图看上去是专业美工的活,我只是想做个很小的游戏而已,能上哪里去寻找这样的专业美工呢?”其实程序员也有程序员绘图的方法。基于一定的算法,程序员能画出专业美工都望尘莫及的图案来。如果你在网上搜索下“分形”,看看一些分形学的创造物,或许你也会赞同我的观点。但我的意思并不是说分形学能够产生这样的字符贴图。我的意思是说,只要拥有了储存字符模样的资源文件(一般是Windows字体文件),理论上就可以通过一定的转换算法画出一张我们需要的字符贴图。实际上,以这种方式生成字符贴图和相应字符位置信息的小工具可以很容易在网上搜索到。TextureFont.cs文件的开头也推荐了几个关于在Xna中处理字符的链接。

让我们来看看TextureFont类中的代码。首先看一下TextureFont类中的一个内部类——FontToRender。这个类的作用只是储存将要绘制的字符的信息。接着看看WriteText系列函数和WriteAll函数。就能发现TextureFont类的运作机制:任何调用WriteText函数的人会在remTexts中注册一个FontToRender,而当WriteAll被调用的时候,所有remTexts中的FontToRender会被绘制到屏幕上。接着remTexts被清空,完成一次绘制。对于使用这种机制的原因,个人认为是为了提高整体绘制的效率。因为在绘制过程中相对复杂,而且需要使用的一个比较大的包含每个字符位置信息的Rectangle数组CharRects。如果将所有的字符串的绘制集中起来,就能够较好的利用机器中速度很快的缓存,而并不需要每次零散的绘制过程都从内存中获得数据。加快了绘制的速度。

现在可以将视线集中到WriteAll函数中了,这里是整个类的运作核心。函数中,第一层for循环遍历remTexts中的所有字符串。循环中对每个字符串进行的操作步骤这是我们应该关注的。首先,定义两个整形的x、y记录当前的绘制位置,他们首先被初始化为当前FontToRender的位置。接着进入遍历当期字符串中每一个字符的循环。先获得当前字符的ASCII码:

int charNum = (int)chars[num];

现在我们可以找来一张ASCII码表来对照一下我们的字符贴图(或者代码中定义CharRects处的定义)。将会发现,字符贴图中字符的排列(同时也是CharRects中的对应的矩形的排列)恰是从ASCII码的32号(空格)开始,一直延续到126号(‘~’)结束。所以在WriteAll函数中只需要将当前字符的ASCII减去32便是对应矩形的数组下标:

Rectangle rect = CharRects[charNum - 32];

这个rect将会作为sourceRectangle传入绘制函数中,我们就称之为源矩形。接下来将rect的Y坐标加1,Benjamin的注释翻译过来是说,“降低高度以防止像素的重合。”我想可能是由于生成贴图的工具有些不完美,直接使用计算出的矩形数据会让绘制的字符的下边缘显示不完整,所以才将源矩形往下方移动一个像素。像这样的问题只可能在测试这个类的时候被发现,也只是一个修补性的代码,我们也就并不需要过多的注意了。

接下来将rect的高度设置为统一的字符高度。然后计算将作为destinationRectangle传入绘制函数的目标矩形。计算过程中使用了BaseGame的辅助函数。然后有按照指定的字符缩放率缩放了目标矩形。于是sourceRectangle和destinationRectangle都已经准备好,便可以在屏幕中绘制这个字符了。

我们需要注意的是接下来的两行:

int charWidth = CharRects[charNum - 32].Height;

x += BaseGame.XToRes1400((int)Math.Round( charWidth * fontText.scale ) );

第一行是将当前字符的高度存储到一个叫charWidth的变量中去?这在逻辑上仿佛是不可理喻的。那么让我们看看CharRects的注释。注释的第二行写到:Height没有被使用(始终是一样的),我们用实际使用的宽度作为height的值。仔细看看下面Rectangle的width和height位置的值,发现height位置的值始终比width位置的值小1或2个单位。这也就是说,width中存储的值在绘制函数中作为源矩形的宽度。而height位置储存的值用作字符的实际宽度,决定了下一个字符的实际绘制x坐标。这样,就避免了在CharRects中对一致的字符高度的重复储存,节约了储存空间。

第二行将实际屏幕上的x坐标增量加到x上,作为下一个字符的绘制位置。这样,结束循环时一个完整的字符串就在屏幕上绘制出来了。

在WriteAll函数的最后,将remTexts中的元素清除,为下一次绘制做准备。

看完这里,我们终于可以送一口气了。TextureFont文件中还有一个获得字符串总宽度的函数GetTextWidth。相信已经不会对你造成任何障碍了。

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/21146222/viewspace-675855/,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2009-03-18

  • 博文量
    49
  • 访问量
    67011