WPF 中的简单贪吃蛇游戏






4.55/5 (14投票s)
如何在 WPF 中编程一个简单的 Canvas 贪吃蛇游戏
引言
我的目标是尝试用非常简单的词语和例子来解释一些基本的编程概念。
背景
贪吃蛇游戏(以及大多数此类游戏)背后的主要思想是“欺骗”用户,让他们认为一系列帧实际上是一个移动的物体。我在实现中构建了这样一个概念。“运动”效果基于一系列 Timer 事件,每个事件都在特定的时间间隔“滴答”作响。蛇的头部在一个新的位置绘制,略微偏离运动方向。蛇尾的末端从我们的画布上擦除,从而产生运动的错觉。

使用代码
这个简单游戏的开发是使用 Microsoft Visual Studio 2010,并采用 Windows Presentation Foundation(又名 WPF)编码方案。使用 XAML 文件(用于使用 .NET 框架为 WPF 应用程序创建 GUI 的默认声明性标记语言)来创建画布,我们的蛇将使用它作为游乐场。
<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Snake!" Height="422" Width="642" ResizeMode="NoResize">
    <Grid Background="Black">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto" />
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Canvas Name="paintCanvas" Background="White"
                Grid.Column="1" HorizontalAlignment="Stretch" MaxWidth="642" MaxHeight="422"></Canvas>
    </Grid>
<Window>   
这里没有什么真正令人惊讶的。语法是我们创建一个简单的 WPF 窗体所需的。 (在这里使用 Microsoft Visual Studio 的设计面板将完成大部分编码工作)。在主 Grid 的顶部,我们只需添加一个画布。画布的大小必须精确,因为我们使用相同的值来限制蛇的移动。
MaxWidth="642" MaxHeight="422"
这里还有另一个重要的注意事项:为什么我选择使用“canvas”元素?这当然不是最好的实现,因为当蛇的大小增加时,一段时间后图形创建会出现明显的延迟。不过,它足以满足本文的需要,即展示“UIElementCollection” Children 的强大功能。
此集合描述了放置在画布上的图形元素。因此,我使用 DispatcherTimer 的每一次“Tick”在画布上绘制蛇身体的一部分(在本例中是圆)。为此,我使用 paintSnake 方法,并向其传递一个 Point 类型的参数,该参数描述了蛇头当前的��置。
private void paintSnake(Point currentposition)  {
    /* This method is used to paint a frame of the snake´s body
     * each time it is called. */
    Ellipse newEllipse = new Ellipse();
    newEllipse.Fill = snakeColor;
    newEllipse.Width = headSize;
    newEllipse.Height = headSize;
    Canvas.SetTop(newEllipse, currentposition.Y);
    Canvas.SetLeft(newEllipse, currentposition.X);
    int count = paintCanvas.Children.Count;
    paintCanvas.Children.Add(newEllipse);
    snakePoints.Add(currentposition);
    // Restrict the tail of the snake
    if (count > length)
    {
        paintCanvas.Children.RemoveAt(count - length + 9);
        snakePoints.RemoveAt(count - length);
    }
}
请注意
paintCanvas.Children.Add(newEllipse);
命令。此命令是在画布上实际绘制圆的一部分的方式。此外,正如代码注释中所述,
// Restrict the tail of the snake
if (count > length)
{
    paintCanvas.Children.RemoveAt(count - length + 10);
    snakePoints.RemoveAt(count - length);
}
我计算 UIElement 集合的元素数量,如果它们的大小超过了已绘制在画布上的元素(减去 10 个“红色”食物块),我就会擦除蛇身体的末端,即尾巴。结合 DispatchTimer 的效果,我们在画布上创造了运动的错觉。
游戏初始化如下
public Window1()
{
    InitializeComponent();
    DispatcherTimer timer = new DispatcherTimer();
    timer.Tick += new EventHandler(timer_Tick);
    /* Here user can change the speed of the snake.
     * Possible speeds are FAST, MODERATE, SLOW and DAMNSLOW */
    timer.Interval = MODERATE;
    timer.Start();
    this.KeyDown += new KeyEventHandler(OnButtonKeyDown);
    paintSnake(startingPoint);
    currentPosition = startingPoint;
    // Instantiate Food Objects
    for (int n = 0; n < 10; n++)
    {
        paintBonus(n);
    }
}
我从初始化 Grid 和 Canvas 对象开始。我创建一个 DispatcherTimer 对象,并在为其分配了一个 EventHandler 后设置其滴答间隔。最后,我启动 timer 对象。
一个 KeyEventHandler 将负责处理玩家的按键输入,因为他试图移动蛇。 paintBonus() 方法使用循环中的随机生成器在画布上绘制前十个随机食物对象。
private void paintBonus(int index)
{
    Point bonusPoint = new Point(rnd.Next(5, 620), rnd.Next(5, 380));
    Ellipse newEllipse = new Ellipse();
    newEllipse.Fill = Brushes.Red;
    newEllipse.Width = headSize;
    newEllipse.Height = headSize;
    Canvas.SetTop(newEllipse, bonusPoint.Y);
    Canvas.SetLeft(newEllipse, bonusPoint.X);
    paintCanvas.Children.Insert(index, newEllipse);
    bonusPoints.Insert(index, bonusPoint);
}
它的工作原理与上面的 paintSnake() 方法精神相同。不过,此时我们需要一个新的 Point 对象列表,我们稍后会检查它们是否被吃掉了。这个列表显然被称为 bonusPoints。
所以元素被绘制出来了,蛇头也绘制出来了,我们准备好玩了!但要真正开始游戏,我们需要处理游戏玩法。我们有两个事件,一个是按下控制键,另一个是计时器滴答作响。
private void OnButtonKeyDown(object sender, KeyEventArgs e)
{
    switch (e.Key)
    {
        case Key.Down:
            if (previousDirection != (int)MOVINGDIRECTION.UPWARDS)
                direction = (int)MOVINGDIRECTION.DOWNWARDS;
            break;
        case Key.Up:
            if (previousDirection != (int)MOVINGDIRECTION.DOWNWARDS)
                direction = (int)MOVINGDIRECTION.UPWARDS;
            break;
        case Key.Left:
            if (previousDirection != (int)MOVINGDIRECTION.TORIGHT)
                direction = (int)MOVINGDIRECTION.TOLEFT;
            break;
        case Key.Right:
            if (previousDirection != (int)MOVINGDIRECTION.TOLEFT)
                direction = (int)MOVINGDIRECTION.TORIGHT;
            break;
    }
    previousDirection = direction;
}
这里的内容 pretty much self-explanatory。我们处理箭头按钮的按下,首先检查当前的移动方向是否与新的方向完全相反。我们不希望我们的蛇撞到自己的身体,对吧?
我们必须遵守的规则是:
- 不要撞墙。
- 不要撞到自己的身体。
- 吃食物对象。
最后,我们需要设置我们的计时器每次滴答时执行的操作。在这里,我们检查蛇是否按照游戏规则正常移动(我是否提到 canvas 和 Grid 必须是不可调整大小的?)。
private void timer_Tick(object sender, EventArgs e)
{
    // Expand the body of the snake to the direction of movement
    switch (direction)
    {
        case (int)MOVINGDIRECTION.DOWNWARDS:
            currentPosition.Y += 1;
            paintSnake(currentPosition);
            break;
        case (int)MOVINGDIRECTION.UPWARDS:
            currentPosition.Y -= 1;
            paintSnake(currentPosition);
            break;
        case (int)MOVINGDIRECTION.TOLEFT:
            currentPosition.X -= 1;
            paintSnake(currentPosition);
            break;
        case (int)MOVINGDIRECTION.TORIGHT:
            currentPosition.X += 1;
            paintSnake(currentPosition);
            break;
    }
    // Restrict to boundaries of the Canvas
    if ((currentPosition.X < 5) || (currentPosition.X > 620) ||
        (currentPosition.Y < 5) || (currentPosition.Y > 380))
        GameOver();
    // Hitting a bonus Point causes the lengthen-Snake Effect
    int n = 0;
    foreach (Point point in bonusPoints)
    {
        if ((Math.Abs(point.X - currentPosition.X) < headSize) &&
            (Math.Abs(point.Y - currentPosition.Y) < headSize))
        {
            length += 10;
            score += 10;
            // In the case of food consumption, erase the food object
            // from the list of bonuses as well as from the canvas
            bonusPoints.RemoveAt(n);
            paintCanvas.Children.RemoveAt(n);
            paintBonus(n);
            break;
        }
        n++;
    }
    // Restrict hits to body of Snake
    for (int q = 0; q < (snakePoints.Count - headSize*2); q++)
    {
        Point point = new Point(snakePoints[q].X, snakePoints[q].Y);
        if ((Math.Abs(point.X - currentPosition.X) < (headSize)) &&
             (Math.Abs(point.Y - currentPosition.Y) < (headSize)) )
        {
            GameOver();
            break;
        }
    }
}
首先要做的是:我们需要绘制蛇。是的,如上所述,timer 对象的主要用途是绘制不断移动的图形。因此,我们检查运动方向,并朝该方向绘制蛇身体的一部分。每滴答一次。
我们使用一个简单的 if 子句检查蛇头是否在预定义的边界内。如果不在,我们调用 GameOver() 方法来显示分数并结束游戏。
我们测试是否消耗了一个食物对象。这是通过测量蛇头与画布上每个食物对象之间的距离差来测试的。如果差值小于蛇头的大小,则该食物对象被视为“已消耗”。在这种情况下,我们将其从奖金列表和画布中删除。然后我们创建一个新的。
最后,我们需要检查蛇头是否撞到了自己的身体。因此,我们测量蛇头与身体剩余点之间的 X 和 Y 距离差。(“脖子”上的点,即“头部”圆圈之后的那些点被排除在外,以避免“自杀”效果)。
关注点
总结一下本文的内容,您可以在这里找到有关创建视觉效果基础知识的信息。这绝不是创建游戏的通用方法,WPF 的画布元素也不是万能工具。不过,它提供了一个很好的教学示例。欢迎您发表评论。
历史
还没有历史记录。希望我能尽快发布一个适用于 Windows Phone 平台的更新版本。


