§14.3最后的单元测试和调整

现在您拥有了游戏的所有类,但还没完。我们已经谈到了几次Player类,但你从来没有见过它的调用。原因是XNA分隔了更新和渲染代码。如果你看一下RacingGame类的Update方法,你终于可以看到对Player类Update方法的调用:

/// <summary>

/// Update racing game

/// </summary>

protected override void Update(GameTime time)

{

// Update game engine

base.Update(time);

// Update player and game logic

player.Update();

} // Update()

如果你看一下Player类的内容,您可能会问,为什么它是如此简单。Update方法在这里并没有做太多工作,它只是处理一些额外的游戏逻辑。在Rocket Commander游戏中Player类处理了几乎整个游戏的逻辑和输入。借助于分布在四个不同类中但相互联系的游戏逻辑代码,赛车游戏的游戏逻辑是非常简单的(见图14-12)。

图14-12

图14-12

/// <summary>

/// Update game logic, called every frame. In Rocket Commander we did

/// all the game logic in one big method inside the player class, but it

/// was hard to add new game logic and many small things were also in

/// the GameAsteroidManager. For this game we split everything up into

/// many more classes and every class handles only its own variables.

/// For example this class just handles the game time and zoom in time,

/// for the car speed and physics just go into the CarController class.

/// </summary>

public virtual void Update()

{

// Handle zoomInTime at the beginning of a game

if (zoomInTime > 0)

{

// Handle start traffic light object (red, yellow, green!)

RacingGame.Landscape.ReplaceStartLightObject( 2-(int)(zoomInTime/1000));

zoomInTime -= BaseGame.ElapsedTimeThisFrameInMs;

if (zoomInTime < 0) zoomInTime = 0;

} // if (zoomInTime)

// Don't handle any more game logic if game is over or still zooming

// in.

if (CanControlCar == false)

return;

// Increase game time

currentGameTimeMs += BaseGame.ElapsedTimeThisFrameInMs;

} // Update()

为了测试物理效果你应使用这个类中的TestCarPhysicsOnPlane和TestCarPhysicsOnPlaneWithGuardRails单元测试,特别是如果你想改变一些在这个类中定义的一些常量,如汽车质量,最大速度,最高转速和加速度。在第13章中您可以找到更多有关这个类的信息。

BaseGame类的观察矩阵在这里更新,如果您需要获得摄像机的位置,旋转矩阵或旋转轴,可以看这里。您可能不会频繁地使用这个类,因为游戏的大多数重要信息,如汽车的当前位置或游戏时间能从BasePlayer和CarPhysics类的属性中获得。

调整赛道

您现在已经知道如何改变一般的游戏逻辑规则,但大多数的关卡是从level数据直接定义的。正如你在第12章中所见,创建赛道并不容易,将赛道数据导入到游戏中也不简单,因为你需要做以下的事情(见图14-13):

您需要3D Studio Max打开一个赛道并修改它。可能使用其他3D建模程序也行,但尚未测试过。作为一个游戏程序员你可能没有这些工具。

接下来,您必须在3D Studio Max中使用一个COLLADA导出插件,得到一个对应最新版本3D Studio Max的导出插件并不容易。例如,在编写本书时,没有对应3D Studio Max 9的Collada格式导出插件,你不得不使用3D Studio Max 8去导出赛道,而这又不对应储存为3D Studio Max 9格式的.max文件。你可以看到这将变得更加复杂。

现在,你需要在TrackImporter类中单元测试的帮助下将Collada格式的赛道数据导入到游戏中,如果发生错误会告知你,但不会给你可视的反馈。

最后你必须自己测试赛道,或者通过开始并玩游戏,或者通过使用Track和TrackLine的单元测试。

图14-13

图14-13

是的,这个不是很理想,我会在以后制作一个赛道编辑器来解决这一问题。请查看官方网站http://www.xnaracinggame.com,获取游戏的更新和修改赛道更好的方法。

现在您可以创建赛道,它们通过定义一些3D点的方式在TrackLine类的单元测试中被使用。要导入一个赛道,你只需在一个3D点数组中写下赛道要用到的所有点,并用这个数组替代导入的赛道的二进制数据。您还可以创建隧道,道路宽度辅助类,并设置场景模型。

TestRenderingTrack单元测试显示了如何使用自定义的向量数组创建赛道并初始化TrackLine类。如果您只想尽快测试一些赛道的构思,我建议首先使用这个单元测试。

TrackLine testTrack = new TrackLine(

new Vector3[]

{

new Vector3(20, 0, 0),

new Vector3(20, 10, 5),

new Vector3(20, 20, 10),

new Vector3(10, 25, 10),

new Vector3(5, 30, 10),

new Vector3(-5, 30, 10),

new Vector3(-10, 25, 10),

new Vector3(-20, 20, 10),

new Vector3(-20, 0, 0),

new Vector3(-10, 0, 0),

new Vector3(-5, 0, 0),

new Vector3(7, 0, 3),

new Vector3(10, 0, 10),

new Vector3(7, 0, 17),

new Vector3(0, 0, 20),

new Vector3(-7, 0, 17),

new Vector3(-10, -2, 10),

new Vector3(-7, -4, 3),

new Vector3(5, -6, 0),

new Vector3(10, -6, 0),

});

TestGame.Start( delegate

{

ShowGroundGrid();

ShowTrackLines(testTrack);

ShowUpVectors(testTrack);

});

} // TestRenderingTrack()

阴影映射

阴影映射类在本章的前面已经讨论过了,它是调整的主要地方。不仅有许多设置和参数,还包括一些shader,优化必须兼顾性能和视觉质量。

贯穿阴影映射技术整个过程的主要单元测试是ShadowMapShader类中的TestShadowMapping方法(见图14-14)。如果您按下Shift键(或手柄的A键)你可以看到阴影在本章的前面你看到了GameScreen类是如何使用ShadowMapShader类的。首先,你对所用使用阴影映射的物体调用GenerateShadows和RenderShadows方法。请注意,这两种方法绘制三维数据,你应该只在必要时才绘制。这些数据应该可以从虚拟的阴影映射光线中看到,如果对象是如图14-14所示的只接受阴影的平板,你就不需要把它列入到GenerateShadows方法调用。只需使用RenderShadows方法让阴影投射到它上面!

图14-14

图14-14

在本章的前面你看到了GameScreen类是如何使用ShadowMapShader类的。首先,你对所用使用阴影映射的物体调用GenerateShadows和RenderShadows方法。请注意,这两种方法绘制三维数据,你应该只在必要时才绘制。这些数据应该可以从虚拟的阴影映射光线中看到,如果对象是如图14-14所示的只接受阴影的平板,你就不需要把它列入到GenerateShadows方法调用。只需使用RenderShadows方法让阴影投射到它上面!

if (Input.Keyboard.IsKeyUp(Keys.LeftAlt) && Input.GamePadXPressed == false)

{

// Generate shadows

ShaderEffect.shadowMapping.GenerateShadows(

delegate

{

RacingGame.CarModel.GenerateShadow(Matrix.CreateRotationZ(0.85f));

});

// Render shadows

ShaderEffect.shadowMapping.RenderShadows(

delegate

{

RacingGame.CarSelectionPlate.UseShadow(Matrix.CreateScale(1.5f));

RacingGame.CarModel.UseShadow(Matrix.CreateRotationZ(0.85f));

});

} // if

在阴影映射产生后您就可以开始渲染真正的3D内容。单元测试使用RenderShadows委托方法,这和在sky cube shader帮助下绘制背景天空盒的代码类似。通过这种方式可以优化游戏,绘制哪个对象,哪些会产生阴影以及哪些会接受阴影。如果您只是生成、渲染并将阴影提供给场景中的每个物体,帧速率将大大下降。在游戏代码中只有大约10% -20%的可见物体将被列入阴影映射中,但在这个单元测试中你只测试汽车和汽车选择平台的阴影映射的。

if (Input.Keyboard.IsKeyUp(Keys.LeftAlt) && Input.GamePadXPressed == false)

哪些会产生阴影以及哪些会接受阴影。如果您只是生成、渲染并将阴影提供给场景中的每个物体,帧速率将大大下降。在游戏代码中只有大约10% -20%的可见物体将被列入阴影映射中,但在这个单元测试中你只测试汽车和汽车选择平台的阴影映射的。

if (Input.Keyboard.IsKeyUp(Keys.LeftAlt) && Input.GamePadXPressed == false)

{

ShaderEffect.shadowMapping.ShowShadows();

} // if

通过单元测试您现在就可以调整阴影映射的代码。大多数调整变量可以在ShadowMapShader类中直接找到,但其中一些如阴影颜色只在ShadowMap.fx着色文件中定义,一旦shader被加载就不会改变。

要查看阴影映射的结果可通过按下Shift键或手柄上的A键。最重要的渲染目标是第一个,它显示了从虚拟阴影光线位置而来的实际阴影贴图。它显示为青色,因为您使用的渲染目标表面格式是本章早些时候讨论过的R32F格式,其中仅包含红色通道。其他颜色通道没用用到,将使用默认值1.0。如果阴影贴图的值是1.0(最不可能的值),你最后将获得白色,如果接近0.0则产生青色。往往很难看到阴影贴图的差异。为了提高数值,您可以将它乘以一个常量或将虚拟阴影光线的位置更靠近目标。

下面的变量和常量是最重要的调整量,您可以通过改变ShadowMap.fx和PostScreenShadowBlur.fx中的顶点和像素shader调整更多的事情。请注意,由于pixel shader1.1的限制,大多数针对shader 1.1的代码是固定的,不会受到大多数变量的影响。如果您仍需支持shader model 1.1并改变某些参数,请确保shader model 1.1仍能然工作。可以通过在shader类中强制使用shader model 1.1技术代替2.0结尾的技术(这是用于shader model 2.0的)来进行测试。

例如,在XNA Shooter使用一个非常遥远的虚拟光线位置和一个相对较小的虚拟可视范围,导致的结果是阴影映射矩阵在一个很小的视场范围内,几乎正交?。通过这种方式,阴影始终朝向同一方向,但这样会导致难以调整的阴影映射的光线。在赛车游戏你不用考虑太多更近的虚拟光距离,因为驾车时你不太会注意到阴影,而且因为阴影映射中的车位置总是相同,所以车的阴影也始终是相同的。

另一方面,对于阴影映射,你只需看看重叠的深度值,因为你需要测试场景中的每个像素是否使用阴影映射。使用一个不好的阴影映射深度缓冲精度将使整个阴影映射变得很糟糕。这也是有许多其他可用的阴影算法的主要原因之一,尤其是stencil阴影,它是阴影映射的主要竞争对手。使用stencil阴影会解决很多问题,但它往往难处理得多,而且还会使用更多的几何体,这样会拖慢游戏。

赛车游戏的主要使用farPlane值,这个值很低(30到50),然后在shader代码中自动生成nearPlane。老版本使用nearPlane值,它约是farPlane值的一半超过以提高深度的精度,但靠近虚拟光线的物体会在阴影映射生成过程中被忽略。为了更好地调整nearPlane值可参见XNA Shooter游戏,它为虚拟光线距离和范围值采用了更好的代码。

// Use farPlane/10 for the internal near plane, we don't have any

// objects near the light, use this to get much better percision!

float internalNearPlane = farPlane / 10;

// Linear depth calculation instead of normal depth calculation.

Out.depth = float2( (Out.pos.z - internalNearPlane/// <summary>

/// Calculate the texScaleBiasMatrix for converting proj screen

/// coordinates in the -1..1 range to the shadow depth map

/// texture coordinates.

/// </summary>

internal void CalcShadowMapBiasMatrix()

{

texelWidth = 1.0f / (float)shadowMapTexture.Width;

texelHeight = 1.0f / (float)shadowMapTexture.Height;

texOffsetX = 0.5f + (0.5f / (float)shadowMapTexture.Width);

texOffsetY = 0.5f + (0.5f / (float)shadowMapTexture.Height);

texScaleBiasMatrix = new Matrix( 0.5f * texExtraScale, 0.0f, 0.0f, 0.0f, 0.0f, -0.5f * texExtraScale, 0.0f, 0.0f, 0.0f, 0.0f, texExtraScale, 0.0f, texOffsetX, texOffsetY, 0.0f, 1.0f);

} // CalcShadowMapBiasMatrix()

// Color for shadowed areas, should be black too, but need

// some alpha value (e.g. 0.5) for blending the color to black.

float4 ShadowColor = { 0.25f, 0.26f, 0.27f, 1.0f };

// Depth bias, controls how much we remove from the depth

// to fix depth checking artifacts. For ps_1_1 this should

// be a very high value (0.01f), for ps_2_0 it can be very low.

float depthBias = 0.0025f;

// Substract a very low value from shadow map depth to

// move everything a little closer to the camera.

// This is done when the shadow map is rendered before any

// of the depth checking happens, should be a very small value.

float shadowMapDepthBias = -0.0005f;

shadowMapDepthBias被添加到阴影贴图生成代码中使深度值更接近与观察者。

// Pixel shader function

float4 PS_GenerateShadowMap20(VB_GenerateShadowMap20 In) : COLOR

{

// Just set the interpolated depth value.

float ret = (In.depth.x/In.depth.y) + shadowMapDepthBias;

return ret;

} // PS_GenerateShadowMap20(.)

depthBias值更重要,它用在UseShadowMap20技术的阴影深度比较代码中。没有depthBias大多数阴影映射像素不会被施加阴影,只是有来产生和接收阴影,由于贴图精度的误差导致类似的值往往会pop in或out阴影映射(见图14-15)。请注意,阴影映射模糊效果隐藏了错误,但它们越强,模糊效果越明显,即使有良好的模糊代码应用到阴影映射,在游戏中仍会出错,特别是当移动相机时。

图14-15

图14-15

// Advanced pixel shader for shadow depth calculations in ps 2.0.

// However this shader looks blocky like PCF3x3 and should be smoothend

// out by a good post screen blur filter. This advanced shader does a

// good job faking the penumbra and can look very good when adjusted

// carefully.

float4 PS_UseShadowMap20(VB_UseShadowMap20 In) : COLOR

{

float depth = (In.depth.x/In.depth.y) - depthBias;

float2 shadowTex = (In.shadowTexCoord.xy / In.shadowTexCoord.w)

shadowMapTexelSize / 2.0f;

float resultDepth = 0;

for (int i=0; i<10; i++)

resultDepth += depth > tex2D(ShadowMapSampler20, shadowTex+FilterTaps[i]*shadowMapTexelSize) ? 1.0f/10.0f : 0.0f;

// Multiply the result by the shadowDistanceFadeoutTexture, which

// fades shadows in and out at the max. shadow distances

resultDepth *= tex2D(shadowDistanceFadeoutTextureSampler, shadowTex).r;

// We can skip this if its too far away anway (else very far away

// landscape parts will be darkenend)

if (depth > 1) return 0;

else

// And apply shadow color

return lerp(1, ShadowColor, resultDepth);

} // PS_UseShadowMap20(VB_UseShadowMap20 In)

关于阴影映射技术可能还有更多的东西可以讨论,也可以使用更好的代码来构建虚拟光线矩阵和调整某些参数使阴影表现得更好。你看到了阴影映射的最重要的代码,但还有更多的技巧。现今还有这么多的阴影映射技术和技巧,可能需要另一本书才能讲完。

今天两个最激动人心的阴影映射技术是perspective shader mapping lighting generation技术(有许多不同的变化,我在一年前写过一些这个技术的代码,但它真的很难调整和优化,尤其是如果您的游戏允许不同的perspective时)。另一个技术是variance shadow mapping,它使用两个阴影贴图替代一个(或两个通道),并允许存储精度更高的值。我没有太多时间研究variance shadow mapping,它是一个相当新的技术,但早期试验表明,通过小得多的阴影映射尺寸(512×512的阴影贴图看起来和传统的2048×2048一样好)你可以极大的提升速度和节省内存带宽,它修正了不少的阴影映射的问题。但是,这种阴影映射总是有问题,一些程序员如著名的id Software的约翰卡马克不太喜欢这个技术,他宁可实现更加复杂的stencil shadow而不是使用阴影映射。 或两个通道),并允许存储精度更高的值。我没有太多时间研究variance shadow mapping,它是一个相当新的技术,但早期试验表明,通过小得多的阴影映射尺寸(512×512的阴影贴图看起来和传统的2048×2048一样好)你可以极大的提升速度和节省内存带宽,它修正了不少的阴影映射的问题。但是,这种阴影映射总是有问题,一些程序员如著名的id Software的约翰卡马克不太喜欢这个技术,他宁可实现更加复杂的stencil shadow而不是使用阴影映射。

Windows上的最后测试

好了,有了这么多代码,现在可以进行游戏测试了。在您启动游戏并在赛道上驾驶挑战最高分前,您应该确保已经查看了游戏引擎中的大部分单元测试(见图14-16)。

图14-16

图14-16

如果您在测试游戏前进行单元测试,你就不必测试阴影映射,物理效果,菜单等直接在游戏中的东西。你要在单元测试中解决所有问题,而不是直到他们工作正常。然后无需测试游我在最后阶段一直使用的一个技巧是改变游戏屏幕初始化代码。这样,我可以直接进入游戏而不是现进入主菜单,那样的话我还要先设置一些选项,然后选择一个关卡,然后才并开始游戏。如果你只想测试游戏本身中的一些问题,一遍又一遍地进行上述操作是没有意义的。

// Create main menu at our main entry point

gameScreens.Push(new MainMenu());

// But start with splash screen, if user clicks or presses Start,

// we are back in the main menu.

gameScreens.Push(new SplashScreen());

#if DEBUG

//tst: Directly start the game and load a certain level, this code is

// only used in the debug mode. Remove it when you are done with

// testing! If you press Esc you will also quit the game and not

// just end up in the menu again.

gameScreens.Clear();

gameScreens.Push(new GameScreen());

#endif

因为所有的单元测试只工作在调试模式下,你也无法在release模式中添加NUnitFramework.dll,您应该确保游戏在Release模式下也运行良好。有时在Release模式可以获得更好的性能,但由于大多数性能的关键代码发生在XNA框架内部,如果您的代码优化得足够好的话,两种模式下性能区别不大。

图14-17显示了正在运行的赛车游戏。仍有东西需要调整,但游戏运行得已经很不错了,在Xbox 360的最高分辨率下(1080p,是1920×1050)也具有良好的帧速率。最后的微调,关卡的设计和游戏测试要花额外一周的工作时间,但很有趣。把游戏给一些你认识的人(或许那些人对你的这个游戏类型并不喜欢)试玩是个不错的主意。还要确保游戏安装容易。没有人愿意自己编译游戏并找出哪些组件被使用。安装文件应包含已编译的游戏,并应检查框架是否已经安装在目标计算机上。您的游戏可能需要.NET Framework 2.0 (约30MB),最新的DirectX版本(约50MB),以及XNA Framkwork Redistributables(只有2MB)。在Xbox 360上目前还没有可用的部署方法。我选择NSIS(Nullsoft Install Script)制作安装文件,如果用户没有安装以上这些框架,它能自动下载并安装。之后你就可以享受游戏了。 件被使用。安装文件应包含已编译的游戏,并应检查框架是否已经安装在目标计算机上。您的游戏可能需要.NET Framework 2.0 (约30MB),最新的DirectX版本(约50MB),以及XNA Framkwork Redistributables(只有2MB)。在Xbox 360上目前还没有可用的部署方法。我选择NSIS(Nullsoft Install Script)制作安装文件,如果用户没有安装以上这些框架,它能自动下载并安装。之后你就可以享受游戏了。

图14-17

图14-17

Xbox 360上的最后测试

显然你也希望你的游戏在Xbox 360上也运行良好,赛车游戏主要是针对Xbox 360平台开发的,让赛车游戏运行在游戏机平台上也是很有意义的,特别是如果你像我一样有一个方向盘控制器。

正如我之前多次提到的,你应在Windows和Xbox 360平台同时进行所有的单元测试,但你可能会不时忘记这样做,然后当项目结束时,例如,在Xbox 360上的最后测试时你会发现阴影映射工作的和在电脑上的不一样好了,现在到了再次使用这些单元测试(见图14-16)的时候了,在Xbox 360上一个接一个进行单元测试直到找出问题所在。

由于赛车游戏是我为Xbox 360开发的第一个项目,我犯了好几个错误(XNA的第一个测试版本还不支持Xbox 360平台,所以我只能在PC上测试XNA)。两个主要的问题一个是游戏和菜单中的布局在某些电视上并不匹配(这本书前面已经谈到过这个问题),另一个问题是Xbox 360上某些特定的渲染目标表现与PC有很大不同。

最难的部分是在Xbox 360上实现正确的阴影映射。有几个解决渲染目标的方法,我也用了一些技巧,但这些技巧不允许也不可能在Xbox 360上使用,包括同时使用几个渲染目标和在渲染目标后重用后备缓冲区的信息。

以下是一些文字来自与2006年11月我的博客(http://abi.exdream.com)上,当时我在访问美国的XNA团队时谈到了这些问题:

// More distance to the screen borders on the Xbox 360 to fit better

// into the safe region. Calculate all rectangles for each platform,

// then they will be used the same way on both platforms. #if XBOX360

// Draw all boxes and background stuff Rectangle

lapsRect = BaseGame.CalcRectangle1600( 60, 46, LapsGfxRect.Width, LapsGfxRect.Height);

ingame.RenderOnScreen(lapsRect, LapsGfxRect, baseUIColor);

Rectangle timesRect = BaseGame.CalcRectangle1600( 60, 46, CurrentAndBestGfxRect.Width, CurrentAndBestGfxRect.Height);

timesRect.Y = BaseGame.Height-timesRect.Bottom;

ingame.RenderOnScreen(timesRect, CurrentAndBestGfxRect, baseUIColor);

// [etc.]

图14-18

图14-18


发布时间:2008/11/19 上午9:30:06  阅读次数:6559

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

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号