游戏屏幕

赛车游戏中有很多不同的游戏屏幕,这些都是由RacingGame类中的gameScreens堆栈管理的。本节介绍游戏中使用的大部分屏幕和对应的功能。大多数游戏屏幕相当简单,但其他的有点复杂并实现了一个单元测试,通过单元测试能更好地了解这个类。例如,Credits屏幕是相当简单的,它只显示一个背景纹理,但主菜单很复杂并具有所有能进入另一个屏幕的按钮。Options屏幕介绍了许多新的控制选项,这些都要进行测试,这个类里有一个单元测试能帮你完成这个过程。

所有的游戏屏幕类都继承于IgameScreen接口(见图14-6),您也许还记得在第8章的Rocket Commander中也使用过。几乎没有改变,但更简单了。您现在只有一个叫做Render的方法,它没有参数并返回一个布尔值。该代码和你在XNA Shooter中使用的几乎一样。如果Render方法返回true意味着您可以返回到以前的游戏屏幕,一旦推出最后一个游戏屏幕则游戏推出。通常游戏屏幕返回false,因为玩家再次进入游戏屏幕后不会立即退出。

1
图 14-6

由于这个简单的接口,所以所有的游戏屏幕的类视图几乎是一样的,它们都只有一个Render方法。有些类还会有一些私有的辅助方法,但它们大多是非常简单的。一些更复杂的游戏屏幕还有单元测试用以测试是否所有内容和功能都能正确实行。处理整个游戏的GameScreen类是复杂的。不像XNA Shooeter或Rocket Commander,所有游戏代码的的处理和渲染是在Landscape和model类中的。游戏逻辑在Player类中被处理,这个有点复杂,但你已经在上一章学习了所有的CarPhysics类的基本物理知识和ChaseCamera类。

看看图14-7了解一下游戏屏幕的基本概况。最复杂的类显然是Mission类,它处理游戏过程和游戏逻辑。它不包含所有的游戏代码,有些是在Player类和RacingGame类中被处理的。

2
图 14-7

启动屏幕

启动屏幕(见图14-8)是较容易的一个类,它只是等着玩家按下手柄上的Start。如果没有手柄,Space或Esc键或鼠标左键也可让玩家继续。这个类唯一令人感兴趣的地方就是让“Press Start to continue”文字闪烁的代码。

3
图 14-8

/// <summary>
/// Render splash screen
/// </summary>
public bool Render()
{
  // This starts both menu and in game post screen shader!
  BaseGame.UI.PostScreenMenuShader.Start();

  // Render background and black bar
  BaseGame.UI.RenderMenuBackground();
  BaseGame.UI.RenderBlackBar(352, 61);

  // Show Press Start to continue.
  if ((int)(BaseGame.TotalTime / 0.375f) % 3 != 0)
    BaseGame.UI.Logos.RenderOnScreen(
      BaseGame.CalcRectangleCenteredWithGivenHeight(
        512, 352 + 61 / 2, 26, UIRenderer.PressStartGfxRect),
        UIRenderer.PressStartGfxRect);

  // Show logos
  BaseGame.UI.RenderLogos();

  // Clicking or pressing start will go to the menu
  return Input.MouseLeftButtonJustPressed ||
    Input.KeyboardSpaceJustPressed ||
    Input.KeyboardEscapeJustPressed ||
    Input.GamePadStartPressed;
} // Render()

RenderMenuBackground方法有点复杂。它用来显示菜单背景,即显示出汽车通过一条赛道。汽车是由计算机控制的而相机只是跟随它。代码不是很复杂:

// [From RenderMenuTrackBackground(), which is called by
// RenderMenuBackground(), both located in the UIRenderer class]

// [Some code to calculate carPos, carMatrix, etc.]

// Put camera behind car
RacingGame.Player.SetCameraPosition(
  carPos + carMatrix.Forward * 9 - carMatrix.Up * 2.3f);

// For rendering rotate car to stay correctly on the road
carMatrix =
  Matrix.CreateRotationX(MathHelper.Pi / 2.0f) *
  Matrix.CreateRotationZ(MathHelper.Pi) *
  carMatrix;

RacingGame.Landscape.Render();
RacingGame.CarModel.RenderCar(
  randomCarNumber, randomCarColor, carMatrix);

借助于Landscape类的Render方法和Model类的RenderCar方法您不必担心渲染场景、赛道或其他东西。相机矩阵可以确保你在正确的位置上观看,而赛车通过汽车矩阵在赛道上行驶。.

所有其他游戏屏幕菜单也使用RenderMenuBackground方法显示屏幕背景,但在一些游戏屏幕上不太明显,因为你把一个较暗的纹理放在前面(例如,在Credits屏幕上很难看到背景)。这只是一个背景效果,当你正式开始玩游戏时你可以看到更多的屏幕。

主菜单

主菜单(见图14-9)比其他菜单屏幕复杂点,但即使这样这个类也只有约250行代码。除了启动屏幕,其他屏幕都是从这里开始的。最重要的选项是开始游戏和观看highscores (头两个按钮)。

4
图 14-9

这个类最酷的功能是菜单按钮的动画。每个按钮获得一个介于0和1之间的浮点值,其中0代表按钮尺寸的最小可能值而1代表最大。当鼠标悬浮或用手柄、键盘选择按钮时,它将慢慢变大直到达到1.0。当你离开按钮,它又缓慢变小。

第一个按钮初始设为1,其他按钮设为最小尺寸(0)。

/// <summary>
/// Current button sizes for scaling up/down smooth effect.
/// </summary>
float[] currentButtonSizes =
  new float[NumberOfButtons] { 1, 0, 0, 0, 0, 0 };

然后在Rnnder方法中处理按钮选择和缩放。为了确保你会在同时悬浮在一个以上的按钮,这里为鼠标使用了一个辅助变量。如果不使用鼠标选取菜单按钮,这个变量将不会被使用。

// Little helper to keep track if mouse is actually over a button.
// Required because buttons are selected even when not hovering over
// them for GamePad support, but we still want the mouse only to
// be applied when we are actually over the button.
int mouseIsOverButton = -1;

// [a little later in the code ...]
for (int num = 0; num < NumberOfButtons; num++)
{
  // Is this button currently selected?
  bool selected = num == selectedButton;

  // Increase size if selected, decrease otherwise
  currentButtonSizes[num] +=
    (selected ? 1 : -1) * BaseGame.MoveFactorPerSecond * 2;
  if (currentButtonSizes[num] < 0)
    currentButtonSizes[num] = 0;
  if (currentButtonSizes[num] > 1)
    currentButtonSizes[num] = 1;

  // Use this size to build rect
  Rectangle thisRect =
    InterpolateRect(activeRect, inactiveRect, currentButtonSizes[num]);
  Rectangle renderRect = new Rectangle(
    xPos, yPos - (thisRect.Height - inactiveRect.Height) / 2,
    thisRect.Width, thisRect.Height);
  BaseGame.UI.Buttons.RenderOnScreen(renderRect, ButtonRects[num],
    // Make button gray if not selected
    selected ? Color.White : new Color(192, 192, 192, 192));

  // Add border effect if selected
  if (selected)
    BaseGame.UI.Buttons.RenderOnScreen(renderRect,
      UIRenderer.MenuButtonSelectionGfxRect);

  // Also check if the user hovers with the mouse over this button
  if (Input.MouseInBox(renderRect))
    mouseIsOverButton = num;

  // [etc.]
} // for (num)

if (mouseIsOverButton >= 0)
  selectedButton = mouseIsOverButton;

Game Screen

GameScreen类(见图14-10)是最重要的游戏屏幕,因为它处理整个游戏逻辑。游戏变量不储存在这个类,但所有的重要组成部分(场景,赛道,汽车,对象,HUD等)从这里被渲染和调用。

5
图 14-10

大多数玩家变量都存储在Player类,所有的输入和物理在Player的基类中(CarPhysics和ChaseCamera)处理。Player类还使用了所有的游戏变量比赛。大多数变量显示在用户界面中,比如在HUD上的目前的游戏时间,它们在Player类的HandleGameLogic方法中被更新。

所有Render方法,包括HUD,都在UIRenderer辅助类中处理,其余的在Landscape类和model类中处理和渲染,阴影映射也在那里进行。所有的渲染和游戏处理是从这里调用,所以这个类为您提供了游戏中发生了什么的一个很好的概括。如果你编写一个改编版本,那么从这开始修改代码。在这或调用方法中注释掉代码,能很快地看到游戏的哪一部分受到了影响。

Render方法的第一部分处理所有的阴影映射,这不是很复杂,因为大多数已在Landscape类中被处理了。你只需将数据提供给阴影映射类,这个类渲染所有阴影映射,这些阴影映射能直接被使用。

/// <summary>
/// Render game screen. Called each frame.
/// </summary>
public bool Render()
{
if (BaseGame.AllowShadowMapping)
{
  // Generate shadows
  ShaderEffect.shadowMapping.GenerateShadows(
    delegate
    {
      RacingGame.Landscape.GenerateShadow();
      RacingGame.CarModel.GenerateShadow(
        RacingGame.Player.CarRenderMatrix);
    });

  // Render shadows
  ShaderEffect.shadowMapping.RenderShadows(
    delegate
    {
      RacingGame.Landscape.UseShadow();
      RacingGame.CarModel.UseShadow(
      RacingGame.Player.CarRenderMatrix);
    });
} // if (BaseGame.AllowShadowMapping)

然后开始post-screen glow shader并渲染所有的3D内容。这包括天空盒,带有赛道的场景和所有三维模型,最后是汽车。

// This starts both menu and in game post screen shader!
BaseGame.UI.PostScreenGlowShader.Start();

// Render background sky and lensflare.
BaseGame.UI.RenderGameBackground();

// Render landscape with track and all objects
RacingGame.Landscape.Render();

// Render car with matrix we got from CarPhysics
RacingGame.CarModel.RenderCar(
  RacingGame.currentCarNumber, RacingGame.CarColor,
  RacingGame.Player.CarRenderMatrix);

// And flush all models to be rendered
BaseGame.MeshRenderManager.Render();

在MeshRenderManager渲染所有的三维模型后您可以添加阴影映射效果。这里的调用顺序是重要的,因为如果在显示阴影前还没有渲染三维模型,阴影将不正确或不工作。

// Show shadows we calculated above
if (BaseGame.AllowShadowMapping)
{
  ShaderEffect.shadowMapping.ShowShadows();
} // if (BaseGame.AllowShadowMapping)

// Apply post screen shader here before doing the UI
BaseGame.UI.PostScreenGlowShader.Show();

代码的最后是游戏的用户界面,如果你想去除或改变HUD,在这里做这件事。

// Play motor sound
Sound.UpdateGearSound(RacingGame.Player.Speed,
  RacingGame.Player.Acceleration);

// Show on screen UI for the game.
BaseGame.UI.RenderGameUI(
  (int)RacingGame.Player.GameTimeMiliseconds,
  // Best time and current lap
  (int)RacingGame.Player.BestTimeMs,
  RacingGame.Player.CurrentLap+1,
  RacingGame.Player.Speed * CarPhysics.MeterPerSecToMph,
  // Gear logic with sound (could be improved ^^)
  1+(int)(5*RacingGame.Player.Speed/CarPhysics.MaxSpeed),
  // Motormeter
  0.5f*RacingGame.Player.Speed/CarPhysics.MaxSpeed +
  // This could be improved
  0.5f*RacingGame.Player.Acceleration,
  RacingGame.Landscape.CurrentTrackName,
  Highscore.GetTop5Highscores());

if (Input.KeyboardEscapeJustPressed ||
  Input.GamePadBackJustPressed)
{
  // Stop motor sound
  Sound.StopGearSound();

  // Play menu music again
  Sound.Play(Sound.Sounds.MenuMusic);

  // Return to menu
  return true;
} // if (Input.KeyboardEscapeJustPressed)

  return false;
} // Render()
Highscores

Highscores屏幕(见图14-11)非常相似与Rocket Commander的,但所有在线highscores被移除,因为没有实现网络代码或Web服务。原因仍是XNA缺乏网络支持,但在PC版本中仍有可能实现。

6
图 14-11

在Highscores类中有几个辅助方法,例如,帮助您确定当前在游戏中的排名,但大多数方法已经这本书的前面几个游戏中用到过了。

下面的代码是用来显示排行榜前10名的玩家。你会发现,借助于UIRenderer类中的辅助方法代码是很简单的。如果您只想测试Highscores游戏屏幕,请使用类内部的单元测试,这是用来定位所有的用户界面元素的,此游戏的其他屏幕也以同样的方式做这件事。我也在Visual Studio 2005使用了TestDriven.NET,它能通过热键重新运行测试。这样我可以测试代码,按下热键,说:“哦,不,”,按下Escape,并修复代码,直到类工作正常。大多数的UI代码都是通过这种方式进行测试的。

// Go through all highscores
for (int num = 0; num < NumOfHighscores; num++)
{
  // Show player in white if mouse is over line or else use gray color
  Rectangle lineRect = new Rectangle(
    0, yPos, BaseGame.Width, lineHeight);
  Color col = Input.MouseInBox(lineRect) ?
    Color.White : new Color(200, 200, 200);

  // Fill in text for this line
  BaseGame.UI.WriteText(xPos1, yPos, (1 + num) + ".", col);
  BaseGame.UI.WriteText(xPos2, yPos,
    highscores[selectedLevel, num].name, col);
  BaseGame.UI.WriteGameTime(xPos3, yPos,
    highscores[selectedLevel, num].timeMs, Color.Yellow);
  yPos += lineHeight;
} // for (num)

剩下的其他游戏屏幕类是Option,Help和Credit。它们和highscores类非常相似,并不令人兴奋。Option有一些不错的UI功能,可以让你在Input类的帮助下输入文字,选取其中一个或多个滑块并拖动它们。使用Option类的单元测试更多地了解这些功能。Help和Credit类只是在屏幕上显示一个纹理,非常类似于你先前看到的SplashScreen类。

最后,点击退出按钮可以退出游戏,因为主菜单关闭后将不会有其他游戏屏幕了。而所有其他游戏屏幕总是返回到主菜单(包括SplashScreen类)。