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()方法里,因此我们需要去实现该方法,这里是它目前的样子:
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()责任做一些事情,因为贪吃蛇吃掉当前的一块食物,我们需要添加一个新的,在新的位置,还有调整分数,蛇的长度和当前游戏速度。为了这个分数,我们需要定义一个新的本地变量叫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();
}
如上所述,一些事情发生在这:
private void UpdateGameStatus()
{
this.Title = "SnakeWPF - Score: " + currentScore + " - Game speed: " + gameTickTimer.Interval.TotalMilliseconds;
}
这个方法将会简单地更新Window 的Title属性去反应当前分数和游戏速度。这是一个展示当前状态简单的方法,如果需要,可以在以后轻松扩展。
我们也需要一小段代码在游戏应该结束的时候执行。我们将会从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;
}
当一个新的游戏开始,会发生以下这些事情:
如果你所有方法都照着这系列文章做:恭喜- 你刚刚制作了你的第一个WPF游戏!!启动你的项目去享受你所有努力的付出,按下Space键然后开始游戏,即使是一个简单的实现,贪吃蛇是一个有趣和上瘾的游戏!