§5.1你的游戏引擎能做什么?

通常你只是在这里写下游戏创意,然后决定你需要哪个类来使得游戏和所有组件运行。在Part II第八章的末尾,我们仍然想创建一款游戏,不过现在我们关注于创建一个可复用的游戏引擎来使得未来的游戏创作过程更加容易。本章和下两章仅仅关注于图像引擎编程,以及如何使得纹理、模型、字体、shader、effect特效等等为你的引擎工作。然后在第七章,高级的shader被介绍,在第八章我们学习更多关于post screen shaders,以及如何借助于Part II开发的图像引擎编写一款游戏。

归功于XNA Framework,它已经带给你许多辅助类和一种简单的方式来管理游戏窗体、游戏组件和内容文件(模型、纹理、shader、声音文件),你不必担心太多的自定义文件格式、编写自己的窗体管理类,或是如何把3D模型放到你的引擎和在屏幕上得到。如果你之前和DirectX 或者 OpenGL 打过交道,你可能知道范例和指南不是那么难,不过你一想把自己的纹理、3D模型、声音文件放到引擎中,就经常遇到问题,诸如无支持的文件格式、不支持3D模型,或者不能播放你的声音文件。然后你要么继续搜索,要么编写自己的类来支持这些特性。纹理和模型类是现成的,但不意味着它们是完美的。有时你想添加更多功能,或者以更简单的方式加载和访问纹理、模型。因此,你将为了纹理和模型的管理编写新类,它在内部仍然使用XNA的类,不过将使你更易于和纹理、模型打交道、以及和后面的材质和shader。

OK,首先你想要在屏幕上展示一个炫酷的3D模型。它不仅有一个纹理,而且在上面使用了一个令人振奋的shader特效。仅仅观察一个静态3D模型在屏幕中心不是太令人兴奋,也能通过展示一个屏幕快照来完成,所以你必须创建一个摄像机类,在你的3D世界四周移动一点儿。

为了在未来的单元测试中帮助你,并且对于UI (user interface) ,你需要渲染2D线条,也需要渲染3D线条的能力,来帮助你在单元测试和debug中查明3D 位置是否正确。你可能现在要说:“Hey,渲染线条没什么特殊,XNA难道不能做诸如OpenGL的glLine方法已经做到的事?”是的,XNA是很好,不过XNA的创造者删除了所有DirectX的固定管道功能,虽然你仍然可以借助于vertex buffers渲染某些线条,然后渲染line 图元,只是这种方式太复杂了。记住你将首先编写自己的单元测试,如何渲染线条的方式还不要考虑;你只是想绘制一条直线从(0,50)到(150,150),或者在3D中从(-100,0,0)到(100,0,0)。此外,如果你渲染每一条直线本身、为之创建一个新的 vertex buffer 或者 vertex 数组, 然后再一次消除它们,你的应用程序效能将实在糟糕,特别是在本书末尾的单元测试中每一帧有数以千计的线条被渲染的时候。

如果你想有前面几个游戏借助于TextureFont 类在屏幕上渲染文本的功能。你的引擎也应该能渲染精灵,但是你应该停止思考精灵,仅仅使用纹理。例如,如果我加载背景纹理,我不想考虑精灵类;我只想把背景纹理放到整个背景上,或者把一张UI纹理放到我想放的位置。这意味着我将从用户隐藏SpriteHelper类,让Texture 类为你管理所有在背景中的纹理,TextureFont 类也很类似,它也为你处理整个背景中的精灵渲染。

单元测试引擎

在你深入实现的细节和3D图像引擎的问题之前,首先为本章末尾编写你的单元测试,并且因此指定引擎应该具备的能力。 请注意单元测试的最初版本看上去有点儿不同;如果你忘了某件事,或者如果你有一些附件在单元测试中没被使用,要做一点儿修改,那绝对ok,不过在引擎中很有意义,并且应该被使用。通常你应该试着使得单元测试不修改就运行;在单元测试中我最初只写了 testModel.Render();不过,为了渲染测试模型,指定一个矩阵或者向量来告诉模型它必须被渲染在3D世界中的哪里,是更加有意义的。说得够多了;看看代码:

/// <summary>

/// Test render our new graphics engine

/// </summary>

public static void TestRenderOurNewGraphicEngine() {

 Texture backgroundTexture = null;

Model rocketModel = null;

 TestGame.Start("TestRenderOurNewGraphicEngine", delegate {

// Load background and rocket

backgroundTexture = new Texture("SpaceBackground");

rocketModel = new Model("Rocket");

}, delegate {

// Show background background

Texture.RenderOnScreen( BaseGame.ResolutionRect);

SpriteHelper.DrawSprites(width, height);

// Render model in center

BaseGame.Device.RenderState.DepthBufferEnable = true;

rocketModel.Render(Matrix.CreateScale(10));

 // Draw 3d line

BaseGame.DrawLine( new Vector3(-100, 0, 0), new Vector3(+100, 0, 0), Color.Red);

 // Draw safe region box for the Xbox 360, support for old monitors

Point upperLeft = new Point(width / 15, height / 15);

Point upperRight = new Point(width * 14 / 15, height / 15);

Point lowerRight = new Point(width * 14 / 15, height * 14 / 15);

Point lowerLeft = new Point(width / 15, height * 14 / 15);

BaseGame.DrawLine(upperLeft, upperRight);

BaseGame.DrawLine(upperRight, lowerRight);

BaseGame.DrawLine(lowerRight, lowerLeft);

BaseGame.DrawLine(lowerLeft, upperLeft);

 // And finally some text

TextureFont.WriteText(upperLeft.X + 15, upperLeft.Y + 15, "TestRenderOurNewGraphicEngine");

 }

);

} // TestRenderOurNewGraphicEngine()

在第一个委托中,经由初始化一个背景纹理和一个火箭模型,单元测试开始了,然后另一个委托中被用来渲染每一帧屏幕上的一切。在前面几章你仅仅用到非常简单的单元测试,它只带有一个渲染委托;现在你允许在特殊的初始化委托中创建数据了,委托之所以被需要是因为你需要在图像引擎被启动之前,你能从XNA的 content manager 加载任何内容。

你可能想知道为什么我调用Texture类的RenderOnScreen 方法,或者 Model 类的Render 方法,当时这些方法不存在。单元测试也使用了许多BaseGame 类中还不存在的属性和方法,不过这不意味着你不能用这种方法编写代码,并且以后让它们得以解决。举个例子,你可能会说,下列代码写起来太长太复杂:

BaseGame.Device.RenderState.DepthBufferEnable = true;

通过使用一个新的想象中的BaseGame.EnableDepthBuffer方法,就可以用一种较简单的方式去编写它,并且以后去担心其实现。我使用Texture类 、Model 类,以及在这个单元测试中使用诸如Render的一些方法的原因是相当简单的:那是我考虑这个问题的方式。当然,我知道 XNA 没有这些方法给我,但是从编写自己的Texture 类和Model 类到执行没有什么能阻止我。如果你喜欢简化单元测试,或者喜欢制作其他附件,自由的编辑单元测试,然后在你通读本章的时候实现这些功能。

作为你起步之前的一点儿注意事项:我喜欢提及几个BaseGame 类的附件 ,以及一些新的游戏组件,诸如摄像机类也在单元测试中被使用,不过因为它们被自动执行,所以在代码窗体中不可见。当你通读本章的时候,你将学到更多的附件、新的类以及改进。

 3D模型

在你能够开始渲染3D模型之前,首先你需要有一个3D模型应该看起来什么样子的想法,甚至更好,有一些加工的3D模型可用。如果你确实不是一个3D Studio Max、Maya、Softimage的专业人员,或者没有一个能快速为你提供一些模型的艺术家,你就不要亲自动手了。改为使用可以从范例、指南中免费获得的,或者来自于先前项目的文件。本章使用的Rocket 模型来自于我去年编写的Rocket Commander 游戏。(如图 5-1)

图5-1

Rocket 模型由3D几何数据(顶点、法线、切线、纹理坐标)、ParallaxMapping.fx 这个shader、该shader的一些纹理组成:

Rocket.dds 作为 rocket 纹理 (红头、灰色、所有的logo在中间等等).

RocketNormal.dds 作为normal mapping 特效,该特效在第七章讨论。

 RocketHeight.dds 作为 parallax mapping (视差映射)特效,该特效使用在Rocket Commander游戏中。

所有这些被3D模型艺术师创建,经由附加的shader设置、环境材质的设定、漫射、镜面反射的颜色,以及任何其他定义在shader中的参数,或能在3D Studio Max 中被设置的材质,艺术师也具体指定了火箭应该看上去如何。模型文件被保存为一个.max文件,它是3D Studio Max 中的一种自定义格式(无论你或你的艺术师使用什么建模程序)。为了把它放入你的游戏,你将需要一个.max的导入器,它不存在于DirectX或者XNA中,并且.max文件会变得非常大;如果你只需要低精度多边形的rocket 模型,在游戏中一个带有各种高精度多边形对象的100MB文件就是没有意义的,低精度火箭模型可能100KB就合适了。你需要一个导出器,来正好得到你游戏需要的模型数据。有许多的格式可获得,如此,为什么我还要这么久的谈论这个话题?好的,在3D Studio Max 中没有标准的导出器格式来支持你游戏中需要的shader设置。XNA也只能导入.x和.fbx文件。真是糟糕。

你很可能只是导出一个.x文件或者使用.fbx格式,并且忍受这个事实:没有shader 设置、切线数据,无论任何你需要执行的法线映射normal mapping、视差映射parallax mapping, 或者你想用在游戏中的自定义shader都没有。不过在你的游戏引擎中有大量的调整每一个单体模型的工作要做。或者,二选一的,你要编写自定义的3D Studio Max输出器,很多大型游戏工作室这样做了,但是他们比你作为一个个体拥有多得多的时间和金钱。另一方面,可能有一些自定义的XML文件为每个模型保存所有的shader设置,然后在你的游戏引擎中它们被导入、合并,这些会再次引起大量的工作,不仅仅是执行导入和合并,而且要维持这些XML设置文件是最新的。我见过所有这些被应用的方式,你可以按你喜欢的自由去做,不过我强烈建议你如果有可能使用一种不痛苦的处理过程。

在这个案例,借助于3D Studio Max 的community-created Panda-Exporter导出插件(如图5-2),Rocket模型被导出为一个自定义的.x文件,然后这个文件能被导入到XNA Framework。它不支持所有的特性(缺少切线数据),并且带有骨骼和皮肤数据的更复杂对象是一种痛苦,不过在你的游戏中还不需要它。一种好的替代是一种新的Collada 格式,它从许多3D模型程序中导出数据是用一个非常明了易用的XML数据文件,XML文件比起你编写自定义的模型数据的导入导出插件,能够更容易的被导入到你的游戏中。对于有皮肤的模型(那意味着你的模型使用了骨骼,并且每个顶点有几个皮肤因子来为之应用于任何骨架运动,在射击游戏中,以及无论何时有其他角色模型在四周运行的游戏中,经常用到),我建议.fxb格式(.X格式也工作在XNA)或者只要搜索一个.md3、.md5或者类似模型格式的导入插件,这些是被用在Quake和Doom系列游戏中的著名格式。

图5-2

 和你使用纹理(texture)或者其他内容文件(content file)同样的方式,这个被导出的 rocket.x 文件现在能被用在你的 XNA 引擎中。就像将内容文件放到项目中,它会自动获得所有被使用的shader和纹理,并且如果缺失它们会警告你。你不必亲自添加被应用的纹理和shader;XNA .x Model processor 会自动为你去做。你仅仅必须确保这些依赖文件在同一个文件夹中能被找到;在这个案例就是Rocket.dds、RocketNormal.dds、RocketHeight.dds、和ParallaxMapping.fx文件。Model 类如何实现,以及如何在屏幕上渲染Rocket模型在本章下一部份处理。

纹理渲染(Rendering of Textures)

直到现在,你使用XNA Framework的Texture 类来加载纹理,并且使用你在第三章借助于SpriteBatch 类编写的SpriteHelper 类在屏幕上渲染精灵。在你的第一个系列游戏中工作得很好,不过,在一个3D环境中使用纹理就不是非常有用了,它仍然过于复杂以至于不能使用所有这些sprite辅助类。

正如你在本章的单元测试中所见,应该有一个叫RenderOnScreen 的新方法直接在屏幕上渲染整个纹理,或者仅仅是纹理的一部分。在背景你仍然使用 SpriteHelper 类,以及你写的渲染精灵的所有代码,不过这使得纹理使用容易得多,你再也不必创建SpriteHelper 类的实例。此外,你后面可以改进精灵被渲染的方式,甚至可能不必改变任何 UI 代码或者单元测试,实现你自己的shader。

图5-3展示了新的Texture 类的层次布局;代码非常直观,并且前面几章已经覆盖了大多数功能。唯一重要的是记住你现在有两个纹理类,一个来自于XNA Framework,一个来自于你自己的引擎。当然,你能重命名你自己的类让一切疯狂,不过,在你只需写下Texture 的时候,谁想所有时候都写MyOwnTextureClass呢?取代你重命名XNA的Texture 类,在你的新Texture 类中再也不发生异常,在内部仍然使用XNA的texture纹理。要做到这个,你在using区域编写下列代码:

using Texture = XnaGraphicEngine.Graphics.Texture;

或者当你需要访问XNA 的texture纹理类的时候,写这个代码:

using XnaTexture = Microsoft.Xna.Framework.Graphics.Texture;

图5-3

一些属性和方法还不是重要的,我只是在这里包括它们,让整个texture 类立刻工作,以便于你不必重新实现任何缺失的特性(例如,渲染旋转的纹理精灵),就能在本书的后面使用它。

在你分析这个类的代码之前,你首先应该看看单元测试,它总是给出了一个关于如何使用类的概览。请注意我现在拆分静态单元测试到每一个类中,而不是把它们集中在main game类中。这种方式更有用,因为在你的引擎中有大量的类,而且这样易于你查明每个类的功能,并且快速测试每个类的功能。借助于texture 构造器,单元测试仅仅加载纹理,构造器传入content name资源名,然后你才能用RenderOnScreen 方法渲染:

/// <summary>

/// Test render textures

/// </summary>

public static void TestRenderTexture() {

Texture testTexture = null;

 TestGame.Start("TestTextures", delegate {

 testTexture = new Texture("SpaceBackground");

},

delegate {

testTexture.RenderOnScreen( new Rectangle(100, 100, 256, 256), testTexture.GfxRectangle);

 }

);

} // TestTextures()

此刻对你而言最重要的方法是RenderOnScreen 。Valid 属性指出是否已成功加载了纹理,以及你是否能使用内部的XnaTexture 属性。Width、Height 和GfxRectangle 提供给你texture的尺寸,其他属性此刻对你还不重要。后面当你用shader渲染细节的时候它们将被使用。

/// <summary>

/// Render on screen

/// </summary>

/// <param name="renderRect" />Render rectangle</param>

 public void RenderOnScreen(Rectangle renderRect)

 {

 SpriteHelper.AddSpriteToRender(this, renderRect, GfxRectangle);

} // RenderOnScreen(renderRect)

看上去非常简单,不是么?有几个重载全都是给SpriteHelper 类添加精灵,这个类使用的是你已经在第三章编写的代码。只有AddSpriteToRender 方法现在被改变为static,并且接受 GfxRectangles 而不是创建SpriteHelper类的新实例:

/// <summary>

 /// Add sprite to render

/// </summary>

/// <param name="texture" /> Texture

 /// <param name="rect" /> Rectangle

/// <param name="gfxRect" />

Gfx rectangle public static void AddSpriteToRender( Texture texture, Rectangle rect, Rectangle gfxRect)

{

sprites.Add(new SpriteToRender(texture, rect, gfxRect, Color.White));

} // AddSpriteToRender(texture, rect, gfxRect)

最后,你仍然使用TextureFont 类。它被移动到 graphics 命名空间,现在,使用新的Texture 类而不是 XNA的 Texture类,并且有新的文本渲染的单元测试。 除此之外,这个类保持不变;在静态的WriteText 方法帮助下(一些更多的重载被添加了)你直接使用它在屏幕上写文本:

 /// <summary>

 /// Write text on the screen

/// </summary>

/// <param name="pos" /> Position

/// <param name="text" /> Text

public static void WriteText(Point pos, string text)

{

remTexts.Add(new FontToRender(pos.X, pos.Y, text, Color.White));

} // WriteText(pos, text)

渲染线条(Line Rendering)

要在XNA中渲染线条,你不能仅仅使用一个嵌入的固定方法,你必须用自己的方式实现。就像我之前说过的,有几种方式,并且最有效率的一种就是创建一个大型的顶点列表,并且在你收集完一帧中的全部对象之后,一次性渲染所有的线条(如图5-4)。

图5-4

 此外,你使用了一个非常简单的shader绘制线条。你也能使用XNA的BasicEffect类,不过它比使用你自定义的shader快得多,BasicEffect类仅仅为每条线发出顶点色彩。因为渲染2D和3D线条基本上是相同的,只是 vertex shader 看上去有点不同;这个章节只谈论渲染2D线条。LineManager3D 类几乎以同样的方式工作(如图 5-5)。

图5-5

要学习如何使用这些类,只要看看TestRenderLines单元测试:

/// <summary>

/// Test render

/// </summary>

public static void TestRenderLines()

 {

TestGame.Start(delegate {

BaseGame.DrawLine(new Point(-100, 0), new Point(50, 100), Color.White);

BaseGame.DrawLine(new Point(0, 100), new Point(100, 0), Color.Gray);

BaseGame.DrawLine(new Point(400, 0), new Point(100, 0), Color.Red);

BaseGame.DrawLine(new Point(10, 50), new Point(100, +150), new Color(255, 0, 0, 64));

}

);

} // TestRenderLines()

因此,你所有必须要做的是调用BaseGame.DrawLine 方法,它接受 2D 点和 3D 向量来支持 2D 和3D 线条。两者的LineManager 类都如图5-6所示的工作, 只要调用几次 BaseGame 类的DrawLine 方法,让 LineManager 类处理剩下的。

 图5-6

有趣的代码是在Render方法中,它渲染了所有你在这一帧中收集到的线条。线条的收集和把它们添加到你的线条列表真不是复杂的代码;如果你想仔细看看,就亲自检查一下。Render 方法为所有的 vertex 元素使用了VertexPositionColor 结构体,vertex 数组则被创建在UpdateVertexBuffer 方法中。这是该方法的主体:

// Set all lines

for (int lineNum = 0; lineNum < numOfLines; lineNum++) {

Line line = (Line)lines[lineNum];

lineVertices[lineNum * 2 + 0] = new VertexPositionColor( new Vector3( -1.0f + 2.0f * line.startPoint.X / BaseGame.Width, -(-1.0f + 2.0f * line.startPoint.Y / BaseGame.Height), 0), line.color);

lineVertices[lineNum * 2 + 1] = new VertexPositionColor( new Vector3( -1.0f + 2.0f * line.endPoint.X / BaseGame.Width, -(-1.0f + 2.0f * line.endPoint.Y / BaseGame.Height), 0), line.color);

} // for (lineNum)

首先,你可能想知道为什么每条线的起始点和结束点要除以游戏的宽和高,然后乘以2,再减去1。这个公式覆盖了所有的2D点,从屏幕坐标系到你的view space,对于x 和 y公式都从-1变到+1的。在LineManager3D 类中,不真的需要这个公式,因为你已经有了正确的3D点。对于每条线,你储存了2个点(起始点和结束点),每个向量获得位置和颜色,因此XNA Framework里叫VertexPositionColor 结构体。

在Render方法中,你现在只要遍历所有加入的图元(那些线条;正好是你点数的一半),并且借助于LineRendering.fx 文件的shader渲染它们:

// Render lines if we got any lines to render

if (numOfPrimitives > 0)

{

BaseGame.AlphaBlending = true;

BaseGame.WorldMatrix = Matrix.Identity;

BaseGame.Device.VertexDeclaration = decl;

ShaderEffect.lineRendering.Render( "LineRendering2D",

 delegate {

BaseGame.Device.DrawUserPrimitives<VertexPositionColor>( PrimitiveType.LineList, lineVertices, 0, numOfPrimitives);

}

);

} // if

BaseGame 类的AlphaBlending 属性确定你是否允许使用alpha blending阿尔法混合渲染这些线条。WorldMatrix 被稍后讨论;因为现在这里只是确认 world matrix 被lineVertices 列表的初始位置数值重置了。这对于 LineManager3D特别重要。

然后vertex 声明被设置,它只是确定你现在能渲染 VertexPositionColor 向量(DirectX这么做的方式和 XNA 非常相似,不过你仍然能得益于使用generics 泛型和其他炫酷的 .NET 功能):

decl = new VertexDeclaration(BaseGame.Device, VertexPositionColor.VertexElements);

看上去很简单,难道不是么?好的,为什么声明这么麻烦;XNA不能为你自动去做么?是的,它能,不过记住如果你需要一种自定义格式,你也能创建自定义的vertex 声明,如果你编写自定义的shader它就很有意义。如何去做参见第七章,在TangentVertexFormat的讨论中。

你必须要做的最后一件事就是必须明白理解线条的渲染是为了用shader渲染数据。Yeah,我知道了,为了渲染一些线条这是大量的工作,不过无论如何对于3D中你做的一切,将需要shader的所有功能。在XNA中没有shader你就不能渲染任何物体;你可能现在说出“SpriteBatch 能干什么?你用它渲染2D图像,你从未谈论过shader?”那是对的;如果你只是想创建简单的2D游戏,你不必知道任何关于shader。不过这不意味着SpriteBatch 类不内在使用shader。另一个隐藏shader功能的类是BasicEffect ,它允许你渲染3D数据而不事先编写自定义的shader特效。它内在的使用了一个基础shader来模仿固定管道功能,不过它和固定功能管道相类似,和你编写的自定义shader不一样快,也不支持任何特别的。

这个已被讨论了一点的样板类也使用shader,不过XNA允许你隐藏所有的shader management。你只要给矩阵设置shader参数,然后使用Draw方法来让XNA渲染所有3D模型的mesh部分。

在第六章中shader被深入细节的讨论。在你看一看LineRendering.fx文件的shader之前,shader带来屏幕上的线条 ,你应该首先看看ShaderEffect 类,这个类允许shader借助于一个RenderDelegate委托渲染3D数据:

/// <summary>

/// Render

/// </summary>

/// <param name="techniqueName" /> Technique name

/// <param name="renderDelegate" /> Render

delegate public void Render(string techniqueName, BaseGame.RenderDelegate renderDelegate)

 {

SetParameters();

// Start shader

effect.CurrentTechnique = effect.Techniques[techniqueName];

effect.Begin(SaveStateMode.None);

// Render all passes (usually just one) //

foreach (EffectPass pass in effect.CurrentTechnique.Passes)

for (int num = 0; num < effect.CurrentTechnique.Passes.Count; num++)

{

EffectPass pass = effect.CurrentTechnique.Passes[num];

pass.Begin();

renderDelegate();

pass.End();

} // foreach (pass)

// End shader effect.End();

}

// Render(passName, renderDelegate)

ShaderEffect 类使用了effect 实例,从 content pipeline内容管道加载shader。然后你能使用 effect 来渲染带有特效的 3D数据。完成的经过是首先 通过 name 或者 index选择技术,然后shader 被启动,你必须对shader具有的每一个pass通道,调用 Begin 和 End。本书中大多数的shader,以及你将不断遭遇的shader将只有一个pass通道,那意味着你只须对第一个pass通道调用一次 Begin 和 End 。通常只有那些post screen shader更复杂,不过你能用很多通道编写 shader,这可能对于fur shaders 或者多层次特性的事物很有用。在Begin 和 End 之间,3D 数据在RenderDelegate 委托的帮助下被渲染,这个委托你早前为了调用带有lineVertices 数组的DrawUserPrimitives 方法时使用过。


发布时间:2008/6/30 下午12:10:21  阅读次数:9443

2006 - 2024,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号