The community is working on translating this tutorial into Ukrainian, but it seems that no one has started the translation process for this article yet. If you can help us, then please click "More info".
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!
Collision Detection
Now that we have implemented the game area, the food and the snake, as well as continuous movement of the snake, we only need one final thing to make this look and act like an actual game: Collision detection. The concept evolves around constantly checking whether our Snake just hit something and we currently need it for two purposes: To see if the Snake just ate some food or if it hit an obstacle (the wall or its own tail).
The DoCollisionCheck() method
The collision detection will be performed in a method called DoCollisionCheck(), so we need to implement that. Here's how it currently should look:
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();
}
}
As promised, we do two checks: First we see if the current position of the snake's head matches the position of the current piece of food. If it does, we call the EatSnakeFood() method (more on that later). We then check if the position of the snake's head exceeds the boundaries of the GameArea, to see if the snake is on its way out of one of the borders. If it is, we call the EndGame() method. Finally, we check if the snake's head matches one of the body part positions - if it does, the snake just collided with its own tail, which will end the game as well, with a call to EndGame().
The EatSnakeFood() method
The EatSnakeFood() method is responsible for doing a couple of things, because as soon as the snake eats the current piece of food, we need to add a new one, in a new location, as well as adjust the score, the length of the snake and the current game speed. For the score, we need to declare a new local variable called currentScore:
public partial class SnakeWPFSample : Window
{
....
private int snakeLength;
private int currentScore = 0;
....
With that in place, add the EatSnakeFood() method:
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();
}
As mentioned, several things happens here:
- We increment the snakeLength and the currentScore variables by one to reflect the fact that the snake just caught a piece of food.
- We adjust the Interval of gameTickTimer, using the following rule: The currentScore is multiplied by 2 and then subtracted from the current interval (speed). This will make the speed grow exponentially along with the length of the snake, making the game increasingly difficult. We have previously defined a lower boundary for the speed, with the SnakeSpeedThreshold constant, meaning that the game speed never drops below a 100 ms interval.
- We then remove the piece of food just consumed by the snake and then we call the DrawSnakeFood() method which will add a new piece of food in a new location.
- Finally, we call the UpdateGameStatus() method, which looks like this:
private void UpdateGameStatus()
{
this.Title = "SnakeWPF - Score: " + currentScore + " - Game speed: " + gameTickTimer.Interval.TotalMilliseconds;
}
This method will simply update the Title property of the Window to reflect the current score and game speed. This is an easy way of showing the current status, which can easily be extended later on if desired.
The EndGame() method
We also need a little bit of code to execute when the game should end. We'll do this from the EndGame() method, which is currently called from the DoCollisionCheck() method. As you can see, it's currently very simple:
private void EndGame()
{
gameTickTimer.IsEnabled = false;
MessageBox.Show("Oooops, you died!\n\nTo start a new game, just press the Space bar...", "SnakeWPF");
}
Besides showing a message to the user about the unfortunate passing of our beloved snake, we simply stop the gameTickTimer. Since this timer is what causes all things to happen in the game, as soon as it's stopped, all movement and drawing also stops.
Final adjustments
We're now almost ready with the first draft of a fully-functional Snake game - in fact, we just need to make two minor adjustments. First, we need to make sure that the DoCollisionCheck() is called - this should happen as the last action performed in the MoveSnake() method, which we implemented previously:
private void MoveSnake()
{
.....
//... and then have it drawn!
DrawSnake();
// Finally: Check if it just hit something!
DoCollisionCheck();
}
Now collision detection is performed as soon as the snake has moved! Now remember how I told you that we implemented a simple variant of the StartNewGame() method? We need to expand it a bit, to make sure that we reset the score each time the game is (re)started, as we as a couple of other things. So, replace the StartNewGame() method with this slightly extended version:
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;
}
When a new game starts, the following things now happens:
- Since this might not be the first game, we need to make sure that any potential leftovers from a previous game is removed: This includes all existing parts of the snake, as well as leftover food.
- We also need to reset some of the variables to their initial settings, like the score, the length, the direction and the speed of the timer. We also add the initial snake head (which will be automatically expanded by the MoveSnake() method).
- We then call the DrawSnake() and DrawSnakeFood() methods to visually reflect that a new game was started.
- The we call the UpdateGameStatus() method.
- And finally, we're ready to start the gameTickTimer - it will immediately start ticking, basically setting the game in motion.
Summary
If you made it all the way through this article series: congratulations - you just built your first WPF game! Enjoy all your hard labor by running your project, pressing the Space key and start playing - even in this very simple implementation, Snake is a fun and addictive game!