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

如何构建类似小行星的 Silverlight 游戏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (15投票s)

2009年1月8日

CPOL

7分钟阅读

viewsIcon

51125

downloadIcon

1214

一个使用碰撞检测的 Silverlight 游戏的良好示例。

引言

最近,我听到了很多关于 Silverlight 的好评。然而,我没有任何项目需要使用它。为了熟悉它,我想我应该承担制作一个小游戏的任务。作为一个80年代的复古迷,我立刻想到了制作一个太空小行星游戏。

背景

为了开发这个游戏,我首先必须安装“Microsoft Silverlight Tools Alpha for Visual Studio 2008”。下载页面在此

我很高兴我可以在 Visual Studio 中完全开发我的 Silverlight 游戏。我对 VS IDE 非常熟悉,我不想承担使用 Expression Blend 或任何其他 Silverlight“艺术家导向”工具的学习曲线。

使用代码

为了开发游戏,我采用了基本策略,即有一个主循环,我将在其中根据一些变量重绘精灵。这是通过创建一个 `DispatcherTimer` 来实现的,它以预设的时间间隔调用我代码中的绘图例程。绘图例程负责遍历画布上的所有对象并更新它们的位置。例如,当一个太空小行星出现在屏幕上时,它已经被赋予了一个随机角度、速度和 X、Y 坐标。当计时器滴答时,太空小行星的 X、Y 值将使用一些基本的数学公式重新计算。这是一个示例

public Page() {
    InitializeComponent(); 

    //create the timer used as the main loop

    _mainLoop.Stop();
    _mainLoop.Interval = TimeSpan.Zero;

    //wire up the events
    _mainLoop.Tick += new EventHandler(mainLoop_Tick);

    StartGame();
}

查看上面的代码片段,您可以看到有一个名为 `_mainLoop` 的私有变量。`_mainLoop` 就像任何标准的 .NET 计时器对象一样。它有一个 `Start()` 和 `Stop()` 方法,它还有一个名为 `Tick` 的事件。为了重绘屏幕上的对象,我将一个事件处理程序连接到 `Tick` 事件。

为了使这个游戏正常运行,我必须开发代码来重绘太空小行星、星星和偶尔出现的 UFO。此外,我们必须重绘飞船及其子弹。为了简化问题,我将每个对象类别存储在一个单独的对象列表中。这样,我可以创建像 `DrawAsteroids` 这样的单独方法,它可以枚举太空小行星对象列表并更新它们的位置。

下面显示了 `DrawAsteroids` 方法。正如您所看到的,我正在遍历一个对象列表并调用 `Asteroid` 类的 `MoveForward` 方法。此外,在循环中,我正在检查太空小行星是否移出屏幕。如果移出,我将调整 X 或 Y 坐标,使其重新出现在屏幕的完全相反的一侧。这就是原始小行星游戏的工作方式。

void DrawAsteriods() {
   for(int i = _asteroids.Count - 1; i >= 0; i--) {
     Asteroid a = _asteroids[i];
     a.MoveForward();
     if(a.X >= (this.Width - a.Width))
       a.X = 1;
     else if(a.X <= 0)
       a.X = this.Width - a.Width;
     if(a.Y >= (this.Height - a.Height))
       a.Y = 1;
     else if(a.Y <= 0)
       a.Y = this.Height - a.Height;
   }
}

太空小行星的 `MoveForward` 方法非常简单。它带回了我高中数学课的美好回忆!基本上,第一步是将度数转换为弧度。然后,我使用 `Sin` 方法更新 X 坐标,该方法以弧度值作为输入参数。然后,我将结果乘以一个速度常数。Y 坐标以相同的方式计算,只是我们使用 `Cos` 方法。

public void MoveForward()
{
   double radians = Math.PI * _angle / 180.0;
   X += Math.Sin(radians) * SPEED;
   Y -= Math.Cos(radians) * SPEED;
}

处理按键事件

我在开发这个游戏时遇到的问题之一是按键事件处理。我早期发现我不能仅仅依赖标准的 `KeyDown` 事件,因为它没有正常触发。像这样的游戏需要对用户按下按键非常敏感。开箱即用的事件处理根本不合格。幸运的是,在进行了一些 Google 搜索之后,我发现其他人也有同样的问题。在某个时候,我发现了 `KeyState` 类。`KeyState` 类是一个静态类,它基本上负责处理游戏中的所有按键按下和按键抬起事件。它存储了按下的按键的状态,并提供了更灵敏的游戏体验。要检查按键是否被按下,您可以调用 `GetKeyState` 方法。为了连接 `KeyState` 类,您只需要调用 `HookEvents` 方法。以下是一些负责使飞船在屏幕上移动的代码。

if(KeyState.GetKeyState(Key.Up) == true) {
   ship.Thrust();
} 
else {
   ship.Drift();
}

在上面的代码片段中,我检查用户是否按下了键盘上的向上箭头。如果按下了,我调用 `Thrust` 方法。`Thrust` 方法类似于 `Asteroid` 的 `MoveForward` 方法,只是它在飞船后面显示一个小火焰,给人一种火箭发动机点火的错觉。

动态 XAML

在原始的小行星游戏中,有三种不同大小的太空小行星。我真的不想为小型、中型和大型太空小行星分别创建一个类,所以我想出了一种在运行时动态创建 XAML 的方法。不幸的是,Silverlight 似乎并不是为这种场景设计的很好,但尽管如此,我还是想出了一个让它工作的方法。

基本上,这里的概念是通过填充 `Path` 元素的 `Data` 属性来动态构建太空小行星。`Data` 属性使用路径标记语法定义如何绘制太空小行星。它基本上是一种迷你语言,可用于描述几何路径。解释路径标记语法并不容易。在我看来,它和正则表达式一样神秘,但在 MSDN 上可以找到一些很好的教程。

为了方便三种不同大小的太空小行星,我创建了一个将大小作为参数的构造函数。

public Asteroid(AsteroidSize size, Canvas parent ) 

现在大小已经定义,我可以在创建路径数据时将其作为指导。现在,只需使用一些随机数生成来绘制线条并创建太空小行星。

public string GetPathData()
{
   int radius = (int)_size * BASE_RADIUS; 
   string pathData = String.Empty;
   for (int i = 0; i < 18; i++)
   {
      float degrees = i * 20;
      Point pt = CreatePointFromAngle(degrees, 
                   radius * (rand.Next(70,99) * .01));
      if (degrees == 0) {
         pathData += string.Format("M{0},{1} L", 
                       (int)pt.X + radius, (int)pt.Y + radius);
      }
      else{
         pathData += string.Format("{0},{1} ", 
                       (int)pt.X + radius, (int)pt.Y + radius);
      }
   }
   pathData += "z";
   return String.Format("<Path xmlns='http://schemas.microsoft.com/" + 
                        "winfx/2006/xaml/presentation' xmlns:x='http://schemas." + 
                        "microsoft.com/winfx/2006/xaml\' Data='{0}'/>", 
                        pathData); 
}

碰撞检测

这个游戏最大的挑战是碰撞检测。幸运的是,bluerosegames.com 上的教程非常详细,为我提供了一个很好的起点。然而,我发现碰撞检测在很大程度上取决于客户端机器重绘屏幕的速度。例如,如果重绘屏幕过于频繁,可能会出现客户端无法足够快地处理数据,并且无法正确检测到碰撞的情况。因此,我最终在不同速度的机器上进行了大量测试,直到找到一个折衷方案。无论如何,碰撞检测有效,但仍远非完美。

`CheckCollision` 方法的理论是它执行两步测试。首先,它检查对象 A 周围的外部矩形是否与对象 B 相交。如果您将每个元素可视化为一个方形盒子或矩形,这将有所帮助。事实上,在我的某些调试过程中,我实际上修改了我的小行星代码,使它们周围都有一个明亮的黄色边框。这有助于我可视化实际发生的情况。

如果对象的外部矩形相交,则进行第二次更准确的检查。现在,检查这些对象的单独详细路径以查看单个像素是否重叠。如果它们重叠,则发生碰撞。

可能有更优雅或更万无一失的碰撞检测方法。如果有,我很乐意听取您的想法。这是我第一次尝试 Silverlight 游戏,所以请手下留情!

// <summary>
/// Determines if two elements are colliding,
/// using a 2-pass test (rect intersect, then HitTest)
/// </summary>
/// <param name="control1">The container control for the first element</param>
/// <param name="controlElem1">The first element</param>
/// <param name="control2">The container control for the second element</param>
/// <param name="controlElem2">The second element</param>
/// <returns>True if objects are colliding otherwise false</returns>
public static bool CheckCollision(FrameworkElement control1, 
              FrameworkElement controlElem1, FrameworkElement control2, 
              FrameworkElement controlElem2) {
   // first see if sprite rectangles collide
   Rect rect1 = UserControlBounds(control1);
   Rect rect2 = UserControlBounds(control2);
   rect1.Intersect(rect2);

   if(rect1 == Rect.Empty) {
      // no collision - GET OUT!
      return false;
   } else {
     bool bCollision = false;
     Point ptCheck = new Point();
     // now we do a more accurate pixel hit test
     for(int x = Convert.ToInt32(rect1.X); x < 
         Convert.ToInt32(rect1.X + rect1.Width); x++) {
        for(int y = Convert.ToInt32(rect1.Y); y < 
            Convert.ToInt32(rect1.Y + rect1.Height); y++) {
           ptCheck.X = x;
           ptCheck.Y = y;
           List<UIElement> hits = (List<UIElement>)
             System.Windows.Media.VisualTreeHelper.
             FindElementsInHostCoordinates(ptCheck, control1);
           if(hits.Contains(controlElem1)) {
              // we have a hit on the first control elem,
              // now see if the second elem has a similar hit
              List<UIElement> hits2 = (List<UIElement>)
                System.Windows.Media.VisualTreeHelper.
                FindElementsInHostCoordinates(ptCheck, control2);
              if(hits2.Contains(controlElem2)) {
                 bCollision = true;
                 break;
              }
     }
   }
   if(bCollision)
     break;
   }
   return bCollision;
  }
}

关注点

我注意到 Silverlight 和 XAML 的一件事是,它真的不适合继承和多态性之类的东西。因此,我不得不重复许多类中的代码块,因为我无法弄清楚如何使事物可重用。其中一些问题可能是由于这是我的第一个 Silverlight 应用程序,而且我还没有弄清楚所有细节。

在开发游戏时,我主要依赖两个网站获取信息,我想确保它们得到适当的鸣谢

Andy Beaulieu 制作了一个类似的游戏,我最终借鉴了我的游戏的一些部分。我偷了他的宇宙飞船的 XAML,因为我在平面设计方面完全是个文盲。

Blue Rose Games 是一个非常适合 Silverlight 游戏开发的网站。我强烈推荐他们的网站。

历史

  • 2009年1月8日 - 初稿。
© . All rights reserved.