在图10-2你已经看到了一个简单的主菜单的例子,但您的游戏包含的不仅仅是游戏屏幕和菜单。您通常还需要一个credit屏幕让你出名,有一个Option屏幕让用户轻松设置所有设置和更改屏幕分辨率,最好还有一个Help屏幕解释游戏的基本规则(见图10-3)。
大多数游戏屏使用一个特殊的背景纹理把大多数的信息显示在屏幕上。在任务选择画面,您可以选择四项任务中的其中一个,然后启动游戏。其他屏幕只是显示出一些信息和只有在Option屏幕上允许用户可以一些东西。所有的游戏画面在按下Back键后返回主菜单。
你使用游戏屏幕堆栈自动处理所有游戏屏幕上(见图10-4)。这可以让你使用更复杂的多层次的菜单系统,并可以随时返回以前的画面。如果你想返回到主菜单,只需移除堆栈中除最后一个以外的所有项。另一个我经常使用的技巧是在主菜单前添加另一个屏幕,当用户退出游戏时显示“购买这个游戏”屏幕。这个类本身只有几行代码,你只需在主类中添加一行额外的代码。大多数游戏屏幕类也非常简单。
为什么游戏屏幕堆栈很好?因为您需要做的就是从IgameScree接口继承得到到所有的游戏屏幕类(见图10-5),然后您可以使用下面的代码自动渲染和处理屏幕了。每个游戏屏幕类的Render方法会在推出此屏幕时返回true,这样将返回到前一个屏幕。当所有游戏屏幕都被移除则退出游戏。
// No more game screens? if (gameScreens.Count == 0) { // Then quit Exit(); return; } // if (gameScreens.Count) // Handle current screen if (gameScreens.Peek().Render()) { // Play sound for screen back Sound.Play(Sound.Sounds.ScreenBack); gameScreens.Pop(); } // if (gameScreens.Peek)
作为一个非常简单的例子,下面是GameScreens命名空间下的Help类的完整代码。其他游戏屏幕类也不复杂,除了Mission类,它要负责处理整个游戏。
/// <summary> /// Help /// </summary> class Help : IGameScreen { #region Properties /// <summary> /// Name of this game screen /// </summary> /// <returns>String</returns> public string Name { get { return "Help"; } // get } // Name #endregion #region Run /// <summary> /// Run game screen. Called each frame. /// </summary> /// <param name="game">Form for access to asteroid manager</param> public bool Run(RocketCommanderGame game) { // Render background game.RenderMenuBackground(); // Show helper screen texture game.helpScreenTexture.RenderOnScreen( new Rectangle(0, 174 * BaseGame.Height / 768, BaseGame.Width, 510 * BaseGame.Height / 768), new Rectangle(0, 0, 1024, 510)); if (game.RenderMenuButton(MenuButton.Back, new Point(1024 - 210, 768 - 140)) || Input.KeyboardEscapeJustPressed) return true; return true; } // Run(game) } // class Help
就是这样。看起来很简单,不是吗?如果你想了解得更多,请自己看一下游戏屏幕类。有些类包含单元测试表明行为方式和使用情况。
在这一节我要你跟随以下步骤制作XNA Shooter的游戏用户界面。用户界面不是很复杂,但你会遇到几个问题。以下内容被显示在这个游戏中:
图10-6显示使用下列纹理显示这些内容:
一个新的类通过NumbersFont.png纹理去显示数字,归功于Texture类和RenderOnScreen方法,这不难。
NumbersFont.png纹理在NumbersFont类中处理,它与第4章的TextureFont类很像,但简单得多,因为你只需要11个矩形,0至9的10个数字和一个冒号显示时间。
private void RenderHud() { // Render top hud part hudTopTexture.RenderOnScreenRelative4To3(0, 0, hudTopTexture.GfxRectangle); // Time BaseGame.NumbersFont.WriteTime( BaseGame.XToRes(73), BaseGame.YToRes(8), (int)Player.gameTimeMs); // Score BaseGame.NumbersFont.WriteNumberCentered( BaseGame.XToRes(485), BaseGame.YToRes(8), Player.score); // Highscore BaseGame.NumbersFont.WriteNumberCentered( BaseGame.XToRes(920), BaseGame.YToRes(8), Highscores.TopHighscore); // Render bottom hud part Rectangle bottomHudGfxRect = new Rectangle(0, 24, 1024, 40); hudBottomTexture.RenderOnScreenRelative4To3(0, 768 - 40, bottomHudGfxRect); // Health Rectangle healthGfxRect = new Rectangle(50, 0, 361, 24); hudBottomTexture.RenderOnScreenRelative4To3(50, 768 - 31, new Rectangle(healthGfxRect.X, healthGfxRect.Y, (int)(healthGfxRect.Width * Player.health), healthGfxRect.Height)); // Weapon and Emps! Rectangle weaponMgGfxRect = new Rectangle(876, 0, 31, 24); Rectangle weaponGattlingGfxRect = new Rectangle(909, 0, 27, 24); Rectangle weaponPlasmaGfxRect = new Rectangle(939, 0, 33, 24); Rectangle weaponRocketsGfxRect = new Rectangle(975, 0, 24, 24); Rectangle weaponEmpGfxRect = new Rectangle(1001, 0, 23, 24); TextureFont.WriteText(BaseGame.XToRes(606), BaseGame.YToRes(768 - 20) - TextureFont.Height / 3, "Weapon: "); // Show weapon icon! Rectangle weaponRect = Player.currentWeapon == Player.WeaponTypes.MG ? weaponMgGfxRect : Player.currentWeapon == Player.WeaponTypes.Gattling ? weaponGattlingGfxRect : Player.currentWeapon == Player.WeaponTypes.Plasma ? weaponPlasmaGfxRect : weaponRocketsGfxRect; hudBottomTexture.RenderOnScreenRelative4To3( 715, 768 - 31, weaponRect); // And weapon name TextureFont.WriteText(BaseGame.XToRes(717+weaponRect.Width), BaseGame.YToRes(768 - 20) - TextureFont.Height / 3, Player.currentWeapon.ToString()); TextureFont.WriteText(BaseGame.XToRes(864), BaseGame.YToRes(768 - 20) - TextureFont.Height / 3, "EMPs: "); // Show emp icons if we have any for (int num = 0; num < Player.empBombs; num++) hudBottomTexture.RenderOnScreenRelative4To3( 938 + num * 23, 768 - 31, weaponEmpGfxRect); } // RenderHud()
这种解决办法在PC很好,但一旦你运行在Xbox 360上并在电视屏幕显示(在写RenderHud方法之前我就写了TestHud单元测试)上你会看到,HUD不是完全可见的,或者更糟,几乎看不到。
如果你从来没在一个需要连接到电视机的游戏平台上编写游戏,这可能是一个新的问题,因为在电脑显示器,您可以使用100%的可视面积,没有安全区域的概念。但对大多数电视屏幕,你看不到100%的屏幕,更多的是90%,这意味着约10%屏幕边界宽度和高度都是不可见的(见图10-7)。
你可能会问,为什么XNA不把所有东西都自动放到安全区域中去呢。因为这不是那么容易。电视机接收全部信号,并根据输入电缆和电视机的模式,你会看到不同的结果。下面是我已经遇到的情况:
如你所见,在Xbox 360上不是正好是90%,不过不用担心。在不同的情况下结果可有很大差异,XNA框架和你的游戏也没法帮你自动检查。只有一点可以肯定的是,在电脑上100%的屏幕像素都是可见,这也就是为什么许多电脑游戏使用的边界显示用户界面元素和其他信息。如果你看一下Xbox 360游戏,你会发现,它们往往只有一个较简单的界面,也不在屏幕边缘放置信息。
你不能只是将HUD显示在90%的安全区域,因为如果使用者能看到更多区域就会发现画面有错误,因为90%区域外没有画面,如果用户看到得更少仍有以前同样的问题。在继续工作之前你应该停在这里,并重新考虑这一问题。如图10-6那样显示用户界面元素对Xbox 360游戏机来说不是个好主意。最好的解决办法是改变用户界面的图片,把他们放在安全区域里。图10-8显示了如何改变HUD的图片使之能适用两种显示情况:在PC上,您把他们在屏幕边界上;在Xbox 360,放在内部92%的区域 (在90%的安全区域仍可见,但如果只有85%或更少就很难看到,,但我从来没有见过这种最坏的情况,大多数电视机都能达到90%~95%)。
图10-9显示了最XNA Shooter游戏的最终屏幕布局。请查看Misson类中的RenderHud方法获取更多细节。它更好地适应了宽屏分辨率,在Xbox 360看上去很好,即使较小分辨率的电脑上看起来也不错。为了测试菜单和游戏屏幕可运行游戏项目示例,这个例子仍基于开始于第5章的XnaGraphicEngine项目,但你有一些新的升级过的类。
以我的经验,在Xbox 360开发XNA游戏你必须记住几件事。在PC上这不是个大问题,但需要花额外一点时间让它们正常工作。一个主要的问题是,如果你只在PC编写游戏而最后才在Xbox 360上测试,很多事情可能出差错(.NET Compact Framework上性能不佳、UI元素屏幕边界上导致在一些电视上不可见或对Xbox 360手柄支持差,而手柄是Xbox 360游戏机最主要的输入设备等等)。如果你有兴趣,请到我的博客仔细阅读更多关于这些问题的讨论。
重要的是让重要的用户界面元素在此90%(或93%,如果你想更接近边缘)矩形之内。这意味着在1920×1080分辨率下只使用90%(1728×945),或在屏幕边缘5%左右的地方(x坐标:96,y坐标:54)绘制用户界面元素。这些像素的位置取决于屏幕分辨率,要在你的游戏主类中计算好,并利用它们绘制用户界面。
有关.NET Compact Framework、如何在Xbox 360上更好地使用XNA框架的更多知识,请阅读.NET Compact Framework小组写的以下文章,网址在http://blogs.msdn.com/netcfteam/archive/2006/12/22/managed-code-performance-on-xbox-360-for-the-xna-framework-1-0.aspx。