TOC

This article has been localized into Chinese by the community.

创建一个游戏:WPF贪吃蛇:
Chapter introduction:

In this article series, we're building a complete Snake game from scratch. It makes sense to start with the Introduction and then work your way through the articles one by one, to get the full understanding.

If you want to get the complete source code for the game at once, to get started modifying and learning from it right now, consider downloading all our samples!

碰撞检测

现在,我们已经实现了游戏区域,食物还有贪吃蛇,以及贪吃蛇持续的移动,我们只需要一件最终的东西去让看起来像一个真正的游戏:碰撞检测。这个概念围绕着是否我们的贪吃蛇刚好撞到某些东西而发展,然后我们目前有两个目的需要它:去看它是否刚好吃到一些食物,或者是否撞到一个障碍物(墙或者自己的尾部)

DoCollisionCheck()方法

碰撞检测将会被执行在一个叫DoCollisionCheck()方法里,因此我们需要去实现该方法,这里是它目前的样子:

private void DoCollisionCheck()
{
    SnakePart snakeHead = snakeParts[snakeParts.Count - 1];
   
    if((snakeHead.Position.X == Canvas.GetLeft(snakeFood)) && (snakeHead.Position.Y == Canvas.GetTop(snakeFood)))
    {        
EatSnakeFood();
return;
    }

    if((snakeHead.Position.Y < 0) || (snakeHead.Position.Y >= GameArea.ActualHeight) ||
(snakeHead.Position.X < 0) || (snakeHead.Position.X >= GameArea.ActualWidth))
    {
EndGame();
    }

    foreach(SnakePart snakeBodyPart in snakeParts.Take(snakeParts.Count - 1))
    {
if((snakeHead.Position.X == snakeBodyPart.Position.X) && (snakeHead.Position.Y == snakeBodyPart.Position.Y))
    EndGame();
    }
}

如承诺那样,我们做了两个检验:第一个我们看看是否当前的蛇的头部位置匹配当前食物的乌贼骨。如果是,我们调用EatSnakeFood()方法(之后有更多详细)。我们然后检验是否蛇头部位置越过了游戏区域的边界,去看看是否贪吃蛇是在出其中一个边界的路上。如果是我们调用EndGame()方法。最后,我们检验是否蛇的头部位置匹配到其中一个身体的位置-如果是,就算贪吃蛇只是碰撞到自己的尾部,也将会通过调用EndGame()方法结束

EatSnakeFood()方法

EatSnakeFood()责任做一些事情,因为贪吃蛇吃掉当前的一块食物,我们需要添加一个新的,在新的位置,还有调整分数,蛇的长度和当前游戏速度。为了这个分数,我们需要定义一个新的本地变量叫currentScore:

public partial class SnakeWPFSample : Window  
{  
    ....  
    private int snakeLength;  
    private int currentScore = 0;  
    ....

有了这些,加入EatSnakeFood()方法里

private void EatSnakeFood()
{
    snakeLength++;
    currentScore++;
    int timerInterval = Math.Max(SnakeSpeedThreshold, (int)gameTickTimer.Interval.TotalMilliseconds - (currentScore * 2));
    gameTickTimer.Interval = TimeSpan.FromMilliseconds(timerInterval);    
    GameArea.Children.Remove(snakeFood);
    DrawSnakeFood();
    UpdateGameStatus();
}

如上所述,一些事情发生在这:

  • 我们通过增加snakeLength currentScore变量加一,去反应蛇刚抓捕到一块食物的事实。
  • 我们调整gameTickTimerInterval时间间隔,用以下规则:当前的间隔(速度)减去当前分数乘以2。这将让速度伴随着蛇的长度成倍成长,让游戏变得困难。我们之前已经用一个SnakeSpeedThreshold常量声明一个速度的下边界,意味着游戏速度永远不会低于在100毫秒的时间间隔
  • 当移除掉一块被蛇消耗掉的食物,然后我们调用DrawSnakeFood()方法,它将会新增一个新的食物在新的地点。
  • 最后,我们调用UpdateGameStatus()方法,它看起来是这样:
private void UpdateGameStatus()
{
    this.Title = "SnakeWPF - Score: " + currentScore + " - Game speed: " + gameTickTimer.Interval.TotalMilliseconds;
}

这个方法将会简单地更新WindowTitle属性去反应当前分数和游戏速度。这是一个展示当前状态简单的方法,如果需要,可以在以后轻松扩展。

EndGame()方法

我们也需要一小段代码在游戏应该结束的时候执行。我们将会从EndGame()方法做这些,目前从DoCollisionCheck()方法调用它,如你所见,它目前非常的简单:

private void EndGame()
{
    gameTickTimer.IsEnabled = false;
    MessageBox.Show("Oooops, you died!\n\nTo start a new game, just press the Space bar...", "SnakeWPF");
}

除了展示给用户我们亲爱的贪吃蛇不幸去世的一个消息,我们还直接停止gameTickTimer。因为这个定时器能够导致游戏的所有事情发生,一旦它停止,所有移动和绘制也会停止

最终调整

我们现在为一个功能完整的贪吃蛇游戏准备第一稿草案-事实上,我们只需要做两次较小的调整。首先,我们需要去确定DoCollisionCheck()方法被调用-这应该发生在 MoveSnake()方法执行的最后一步,我们之前实现了:

private void MoveSnake()
{
    .....
   
    //... and then have it drawn!
    DrawSnake();
    // Finally: Check if it just hit something!
    DoCollisionCheck();    
}

现在,一旦蛇移动,碰撞检测就被执行了!现在还记住我告诉你我们如何去实现一个简单StartNewGame()方法的变种?我们需要去扩展一些,去确定每次游戏(重新)开始我们重置分数,还有一些其它的东西。因此,用这个稍微扩展的版本去替换StartNewGame()方法:

private void StartNewGame()
{
    // Remove potential dead snake parts and leftover food...
    foreach(SnakePart snakeBodyPart in snakeParts)
    {
if(snakeBodyPart.UiElement != null)
    GameArea.Children.Remove(snakeBodyPart.UiElement);
    }
    snakeParts.Clear();
    if(snakeFood != null)
GameArea.Children.Remove(snakeFood);

    // Reset stuff
    currentScore = 0;
    snakeLength = SnakeStartLength;
    snakeDirection = SnakeDirection.Right;
    snakeParts.Add(new SnakePart() { Position = new Point(SnakeSquareSize * 5, SnakeSquareSize * 5) });
    gameTickTimer.Interval = TimeSpan.FromMilliseconds(SnakeStartSpeed);

    // Draw the snake again and some new food...
    DrawSnake();
    DrawSnakeFood();

    // Update status
    UpdateGameStatus();

    // Go!    
    gameTickTimer.IsEnabled = true;
}

当一个新的游戏开始,会发生以下这些事情:

  • 因为这可能不是第一次游戏,我们需要去确定任何潜在的残羹剩饭从上一次游戏中被移除:这包括所有蛇存在的部分和剩余的食物。
  • 我们也需要去重置一些变量它们的初始值设定,像分数长度方向和计时器速度。我们也添加初始蛇的头部(它将会被自动通过MoveSnake()方法扩展)
  • 我们然后调用DrawSnake()DrawSnakeFood()方法来直观反映一个新游戏的启动
  • 我们调用UpdateGameStatus() 方法
  • 然后最后,我们准备去启动gameTickTimer-它将会立即开始滴答,基本上是在启动游戏。

小结

如果你所有方法都照着这系列文章做:恭喜- 你刚刚制作了你的第一个WPF游戏!!启动你的项目去享受你所有努力的付出,按下Space键然后开始游戏,即使是一个简单的实现,贪吃蛇是一个有趣和上瘾的游戏!

This article has been fully translated into the following languages: Is your preferred language not on the list? Click here to help us translate this article into your language!