65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Silverlight 中重现 Frogger

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (76投票s)

2009年5月11日

CPOL

10分钟阅读

viewsIcon

130908

downloadIcon

1122

关于如何在 Silverlight 中重现经典街机游戏的教程。

引言

玩经典游戏非常有趣,而尝试重现它们则更有趣。几个月前,我决定在 Silverlight 中构建一个 Asteroids 克隆,以便熟悉这项技术。我在此项目中玩得很开心,因此这次我决定构建一个 Frogger 克隆。本文将引导您完成我用于重现这款街机经典的所有步骤。

游戏是如何构建的

由于我绝不是图形设计师,因此我需要为我的游戏找到一些现成的图像。我想我可以通过 Google 图片搜索找到一些旧 Frogger 游戏的截图。幸运的是,我找到了 Xbox Live Arcade 的截图,这些截图为我提供了所需的基准图像。现在我有了可以使用的东西,我在 GIMP 中打开了截图并开始裁剪图像。这个过程花费了很长时间,因为我需要裁剪出每个独特的图像并使背景透明。我最终得到了五种不同的车辆,三种不同尺寸的原木,三只不同的乌龟,一只青蛙,青蛙的头,以及最后一张纯背景图像。以下是一些裁剪出的图像的示例:

froghead.png 青蛙头,tractor.png 拖拉机,以及 frog.png 青蛙(我们的主要角色)。

现在图像已经处理完毕,是时候开始编码了。我的项目使用了 Visual Studio 2008 和 Silverlight Tools Add-on。这个插件允许您直接从 Visual Studio 创建 Silverlight 项目,这意味着您无需下载任何 Expression 产品。

new_silverlight_project.png

安装好插件后,就可以启动 Visual Studio 并创建一个使用 3.5 Framework 的新项目了。然后,选择 VB 或 C# 作为您的语言。最后,选择创建一个新的 Silverlight 应用程序。按照提示操作,当出现 **Add Silverlight Application** 对话框时,选择 **Automatically generate a test page to host Silverlight at build time** (在构建时自动生成一个托管 Silverlight 的测试页面) 选项。我喜欢这个选项,因为当您从 Visual Studio 运行项目时,会弹出一个 Internet Explorer 窗口,以便您可以测试您的应用程序。

现在您的项目已经创建,您可以导航到 Solution Explorer 并打开 Page.xaml 的设计界面。我的第一步是将我从原始图像创建的背景图像设置为页面的背景图像。这是通过将一个 Image 元素添加到 Canvas 来完成的。

<Image Source="media/background.png" Stretch="None" Canvas.Top="50"/>

frogger_board_mappings.png

我还想在页面顶部和底部留出一些空间,用于显示分数和其他与游戏相关的信息。因此,我将画布的宽度和高度更改为足够大,可以容纳背景图像,并且还有足够的边距用于页脚和页眉。现在基本元素已经放在页面上了,我需要考虑如何编写游戏程序。作为背景信息,游戏的整个想法是让青蛙安全地到达屏幕顶部的“家”。为了回家,青蛙必须穿过五车道的公路,避免与汽车发生碰撞。然后,青蛙必须跳上原木才能安全回家。因此,有了这些信息,我考虑我需要一种方法来定义“车道”。车道基本上会为精灵创建物理边界。我最终使用了 Rectangle(这些由左侧图像中的红色轮廓表示)来实现此效果。创建所有 Rectangle 后,我的屏幕看起来是这样的。另外,您会注意到顶部有一些额外的矩形,它们定义了您希望青蛙在穿越所有障碍物后最终到达的目标。

精灵

现在,是时候讨论游戏引擎的机制了。让我先给您 Sprite(精灵)的定义(来自 Wikipedia):

在计算机图形学中,Sprite 是一个二维/三维图像或动画,它被集成到一个更大的场景中。

Sprite 最初是作为一种利用专用硬件快速组合多个二维视频游戏图像的方法而发明的。随着计算机性能的提高,这种优化变得不再必要,该术语演变为特指集成到场景中的二维图像本身。也就是说,由自定义硬件或纯软件生成的图形都称为 Sprite。随着三维图形越来越普遍,该术语被用来描述一种将平面图像无缝集成到复杂三维场景中的技术。

更具体地说,在我的游戏中,原木、车辆和乌龟都被视为 Sprite。为了使我的编程更简单,我利用了一个名为 SpriteBase 的通用基类。使用通用基类可以实现代码重用,更重要的是实现多态。当您需要移动大量对象并对每个对象应用碰撞检测算法时,这是一个重要的因素。例如,如果您想在屏幕上移动一个 Sprite,您需要通过更改该对象的 X 和 Y 坐标来完成。因此,SpriteBase 类具有 XY 属性。

/// <summary>
/// The Y position of the sprite
/// </summary>
public virtual double Y
{
   get { return (double)this.GetValue(Canvas.TopProperty); }
   set { this.SetValue(Canvas.TopProperty, value); }
}

继续,我们已经确定 Sprite 是游戏的基本构建块。因此,第一步是将 Sprite 放到屏幕上。由于我们希望游戏具有挑战性,我们不希望硬编码每个 Sprite 的位置;因此,我们将不得不使用随机化器。随机化器将用于使汽车以不同的速度移动,确定物品是向左还是向右移动。此外,我们希望屏幕上有各种不同的汽车、原木和乌龟。

先前,我们讨论了我是如何使用 Rectangle 来创建各种 Sprite 在游戏内移动的逻辑边界的。基本上,我所做的是在背景图像上叠加 Rectangle 来为道路和水创建“车道”。每个 Rectangle 都在 XAML 文件中定义,并具有唯一的名称。我在代码中引用每个唯一名称,并将其添加到代码中的一个数组中。现在,我意识到我本来可以直接在代码中动态创建这些项,但我喜欢在设计模式下能够看到游戏的布局。下面是 XAML 文件中创建道路车道的节。您会注意到所有车道都有一个红色轮廓。这是为了让我在设计时可以看到线条。当应用程序加载时,我将循环遍历 Rectangle 并将 Stroke 设置为透明颜色。

<Rectangle x:Name="StreetLane5" Stroke="Red" 
  StrokeThickness="1" Canvas.Left="0" 
  Canvas.Top="440" Width="600" Height="50"/>

<Rectangle x:Name="StreetLane4" Stroke="Red" 
  StrokeThickness="1" Canvas.Left="0" 
  Canvas.Top="490" Width="600" Height="50"/>
    
<Rectangle x:Name="StreetLane3" Stroke="Red" 
  StrokeThickness="1" Canvas.Left="0" 
  Canvas.Top="540" Width="600" Height="50"/>
  
<Rectangle x:Name="StreetLane2" Stroke="Red" 
  StrokeThickness="1" Canvas.Left="0" Canvas.Top="590" 
  Width="600" Height="50"/>

<Rectangle x:Name="StreetLane1" Stroke="Red" 
  StrokeThickness="1" Canvas.Left="0" 
  Canvas.Top="640" Width="600" Height="50"/>

现在我有一个包含 Rectangle 的数组,我可以依靠这些 Rectangle 的 X 和 Y 坐标来控制每个 Sprite 的放置。现在,我创建一个嵌套的 for 循环,该循环遍历每个 Rectangle,并为每个“车道”添加随机数量的车辆。每条车道都被分配了随机的速度和方向供车辆移动。此外,每条车道中的汽车是随机分配的。这是代码:

private void CreateVehicles()
{
   //add some vehicles to each lane using some random logic
   for (int i = 1; i <= 5; i++)
   {
      Boolean rightToLeft = (_randomizer.Next(0, 11) % 2 == 0);
      Double startX = _randomizer.Next(0, 150);
      Double speed = (double)_randomizer.Next(5, 20) * .1;
      
      for (int j = 0; j < 4; j++)
      {
         VehicleType vType = (VehicleType)_randomizer.Next(0, 5);
         Vehicle vehicle = new Vehicle(vType, rightToLeft);
         
         if ((startX + vehicle.ActualWidth) >= this.Width) break;
         
         vehicle.Lane = i;
         vehicle.X = startX;
         vehicle.XSpeed = speed;
         vehicle.Y = GetNewYLocation(vehicle, i);
         this.LayoutRoot.Children.Add(vehicle);
         _sprites.Add(vehicle);
         
         int spacing = _randomizer.Next((int)(_frog.ActualWidth * 3), 
                                        (int)(_frog.ActualWidth * 4));
         startX += (vehicle.ActualWidth + spacing);
      }
   }
}

您会注意到汽车是随机间隔的。我使用青蛙的宽度作为基本度量单位。毕竟,如果青蛙想穿过马路,它需要在汽车之间穿行。此外,请注意每个新 Sprite 都被分配了一个车道。这很重要,因为它有助于我确定哪些物品在哪个车道。由于青蛙一次只能向前或向后移动一个车道,因此我可以轻松确定青蛙可能与之碰撞的对象。原木和乌龟的创建方式非常相似。新创建的 Sprite 都被添加到名为 _sprites 的私有列表中。将 Sprite 放在集合中可以方便地遍历它们进行动画和碰撞检测。

动画和碰撞检测

为了动画化 Sprite,我创建了一个新的 System.Windows.Threading.DispatcherTimer 类并实现了 Tick 事件。在 Tick 事件中,我调用了一个名为 MoveSprites() 的方法。此方法遍历添加到屏幕上的 Sprite 列表并更新它们的 **X** 和/或 **Y** 坐标。此外,它还会检测 Sprite 是否移出屏幕。当车辆、原木或乌龟移出屏幕时,它们会被一个新的随机 Sprite 替换。这使得游戏更加有趣。最后,如果青蛙恰好在河上跳跃,我将检测青蛙所在的物体,并以与该物体相同的速度移动青蛙。让我们看一下代码:

private void MoveSprites()
{
   for (int i = 0; i < _sprites.Count; i++)
   {
      Boolean remove = false;
      SpriteBase sprite = _sprites[i];
      double newX = sprite.X;
      
      //check the direction of the sprite and modify accordingly
      if (sprite.RightToLeft) {
         newX -= sprite.XSpeed;
         remove = (newX + sprite.ActualWidth < 0);
      }
      else {
         newX += sprite.XSpeed;
         remove = (newX > this.Width);
      }

  
      //when items go off the screen we replace them with a new random sprite
      if (remove == true){
         LayoutRoot.Children.Remove(sprite);
         SpriteBase replacement;

         if (sprite.GetType() == typeof(Vehicle))
            replacement = new Vehicle((VehicleType)_randomizer.Next(0, 5), 
                                       sprite.RightToLeft);
         else if (sprite.GetType() == typeof(Log))
            replacement = new Log((LogSize)_randomizer.Next(0, 3), sprite.RightToLeft);
         else
            replacement = new Turtle((TurtleType)_randomizer.Next(0, 3), 
                                      sprite.RightToLeft);
 
         //find the min or max X position of the sprite in the same lane
         var query = from x in _sprites
                     where
                        x.Lane == sprite.Lane
                     orderby
                        x.X ascending
                     select
                        x;
         SpriteBase lastSprite;
         //right to left means you want the max because 
         //when the item wraps around the screen 
         //it will appear in the higher range of X values
         if (sprite.RightToLeft){
            lastSprite = query.Last();
            if ((lastSprite.X + lastSprite.ActualWidth) >= this.Width)
               newX = (lastSprite.X + lastSprite.ActualWidth) + _randomizer.Next(50, 150);
            else
               newX = this.Width;
         }
         else{
            lastSprite = query.First();
            if (lastSprite.X <= 0)
               newX = (lastSprite.X) - _randomizer.Next(50, 150) - replacement.ActualWidth;
            else
               newX = 0 - replacement.ActualWidth;
         }
         replacement.XSpeed = sprite.XSpeed;
         replacement.Lane = sprite.Lane;
         replacement.Y = GetNewYLocation(replacement, sprite.Lane);

         _sprites[i] = replacement;
         sprite = replacement;
         LayoutRoot.Children.Add(replacement);
      }
      //when items start to move off the screen we clip part of the object so we do 
      //not see it hanging off the screen
      if ((newX + sprite.ActualWidth) >= this.Width){
         if (sprite.X < this.Width) {
            RectangleGeometry rg = new RectangleGeometry();
            rg.Rect = new Rect(0, 0, this.Width - sprite.X, sprite.ActualHeight);
            sprite.Clip = rg;
            sprite.Visibility = Visibility.Visible; //forces a repaint
         }
      }

      //if the frog is on a object in the river then move it at the same rate
      if (_frog.WaterObject == sprite){
         double frogX = _frog.X - (sprite.X - newX);
         Point p = new Point(frogX, _frog.Y);
         MoveFrog(p);                    
      }
      sprite.X = newX;
   }
}

由于对象实际上只沿水平方向移动,因此在 Sprite 放置在屏幕上后,我从不重新计算其 **Y** 位置。我只重新计算 **X** 位置。在函数中间,您会看到一些 LINQ 代码。LINQ 代码用于在某个对象移出屏幕时确定“替换”Sprite 的位置。由于 Sprite 的长度不同,因此在将对象添加到屏幕时,我必须动态确定它需要放置在左侧或右侧的距离。如果车道中的对象正在从右到左移动,我们需要找到最大的 X 值;如果从左到右移动,我们将寻找最小的 X 值。下图将有助于阐明此逻辑:

offscreen_diagram.png

那么,既然您已经了解了对象如何在屏幕上移动,让我们来谈谈碰撞检测。此游戏中的碰撞检测非常简单。由于青蛙一次只能在一个车道中,因此我一次只对特定对象组执行碰撞检测。同样,我使用 LINQ 来简化任务:

private bool CheckForCollisions()
{
   //check only the current lane to see if the frog is being hit by any vehicles.
   //rely on the X coordinates only since the frog basically sits in the middle of the lane.
   var query = from x in _sprites
               where
                  x.Lane == _currentRow &&
                  ((_frog.X >= x.X) &&
                  (_frog.X <= (x.X + x.ActualWidth)) ||
                  ((_frog.X + _frog.ActualWidth) >= x.X) &&
                  ((_frog.X + _frog.ActualWidth) <= (x.X + x.ActualWidth)))
               select
                  x;
   return (query.Count() > 0);
}

我的碰撞检测算法主要只关心 **X** 坐标。由于青蛙位于车道的中间,因此我可以依赖同一车道中的对象已经位于相同的 **Y** 值范围内这一事实。如前所述,当青蛙在公路上时,如果被车辆撞到,它就会死亡。因此,当青蛙在车道 1 到 5(公路)中且 CheckForCollisions() 方法的结果为 true 时,青蛙就被撞死了。但是,当青蛙在车道 6 到 10(水域)中发生碰撞时,青蛙是安全的,因为这意味着它正坐在原木或乌龟上。因此,当青蛙在水中移动时,我有一个名为 GetObjectUnderFrog() 的方法,该方法将返回青蛙所在的 Sprite 的引用。如果青蛙没有坐在任何东西上,该方法将返回 null,这意味着青蛙掉进了水里。如果返回了一个 Sprite,那么一个名为 WaterObject 的属性就会被设置,这样我就会保留对青蛙所在的对象的引用。此属性用于 MoveSprites() 方法(如上所示),以帮助以与青蛙所在的物体相同的速率移动青蛙。这会产生青蛙正在搭车的假象。这是代码:

void _mainLoop_Tick(object sender, EventArgs e)
{
   MoveSprites();
   
   //only check for collisions when the frog is on the road
   if (_currentRow > 0 && _currentRow < 6) {
     if (CheckForCollisions() == true){
        //frog is a pancake
        KillFrog();
     }
   }

  
   //you are in the water
   if (_currentRow > 6 && _currentRow < 12) {
      _frog.WaterObject = GetObjectUnderFrog();
      if (_frog.WaterObject == null) {
         KillFrog();
      }

      
      if (_frog.X > this.Width || _frog.X < 0)
         KillFrog();
   }
   else
   {
      _frog.WaterObject = null;
   }
}

以上就是我的关于**在 Silverlight 中重现 Frogger** 的文章。如果您喜欢本教程,请留下评论。我非常感谢您的反馈。

历史

  • 2009 年 5 月 11 日 - 初始修订。
© . All rights reserved.