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






4.74/5 (15投票s)
一个使用碰撞检测的 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日 - 初稿。