Snake Tutorial Part 4: Collision
In the previous chapter, we created our Snake
instance and put it under the control of the player. This also required us to make some adjustments to MainScene
so that it could properly accommodate our snake's movement and user input requirements.
In this chapter, we'll be adding the final touches to our Snake game by adding collision detection and handling so our snake can interact with itself, walls, and apples.
By the end of this chapter, you'll have a complete Snake game!
Prerequisites
To complete the steps outlined in this chapter, you will need to have completed the steps in the previous chapter.
Step 1: Collision
Now that our player can control the snake, we need to give them some obstacles to avoid and goals to try for. We'll work on putting in apples later on, so for now we'll work on the obstacles: walls, and the snake itself.
In order for our game to detect when the snake has hit a wall or part of itself, we'll need to do some collision detection. Collision detection and handling is an incredibly important and often challenging subject in game design and development. Lucky for us, another benefit to snake being grid-based is that it makes collision checking and handling super easy!
We'll be able to handle all of it in a new method in our Snake
class.
Edit core/class/Snake.cs
and add the following method to Snake
:
private bool CheckCollision(Vector2 checkPos)
{
return
checkPos.X < 0 || checkPos.X > MainScene.GRID_SIZE - 1 || // check left/right walls
checkPos.Y < 0 || checkPos.Y > MainScene.GRID_SIZE - 1 || // check top/bottom walls
Positions.Contains(checkPos); // check self
}
If we call this method on the position our snake is trying to add to its position list and it returns true
, we know our snake has hit a wall or itself.
Now that we have our CheckCollision()
method, let's put it in MainScene.Move()
:
private void Move()
{
var newX = Head.X;
var newY = Head.Y;
switch (Direction)
{
case Direction.Up:
newY = Head.Y - 1;
break;
case Direction.Down:
newY = Head.Y + 1;
break;
case Direction.Left:
newX = Head.X - 1;
break;
case Direction.Right:
newX = Head.X + 1;
break;
}
var newPos = new Vector2(newX, newY);
// New section: collision check
if (CheckCollision(newPos))
{
// We collided! ...but now what?
}
Positions.Insert(0, newPos);
if (Positions.Count > Length)
{
Positions.Remove(Tail);
}
}
Great, now our snake will check if the next position will cause a collision every time it moves, and we can respond when a collision does happen.
But how should we respond to a collision? We have many options, but one thing we know for sure is that it probably shouldn't be the Snake
class' job to ultimately respond to a collision. A collision will change the game's state in a way that should be the responsibility of MainScene
.
So, how do we tell the MainScene
that the Snake
collided with something? There are two options that we may immediately choose from:
- Give
Snake
a reference to the MainScene and have theSnake
instance invoke a method or change a property ofMainScene
- Define an event in
Snake
that is invoked when a collision happens, and haveMainScene
subscribe to that event.
We're going to go with the second option.
The Snake.Collide Event
We'll be creating a new event in the Snake
class called Collide
. Edit /core/class/Snake.cs
and add the following event to the Snake
class:
public event EventHandler Collide;
Once that's in place, we can edit our Snake.Move()
method and invoke this event in the if(CheckCollision())
block:
private void Move()
{
var newX = Head.X;
var newY = Head.Y;
switch (Direction)
{
case Direction.Up:
newY = Head.Y - 1;
break;
case Direction.Down:
newY = Head.Y + 1;
break;
case Direction.Left:
newX = Head.X - 1;
break;
case Direction.Right:
newX = Head.X + 1;
break;
}
var newPos = new Vector2(newX, newY);
if (CheckCollision(newPos))
{
Collide?.Invoke(this, new EventArgs()); //<-- New item
}
Positions.Insert(0, newPos);
if (Positions.Count > Length)
{
Positions.Remove(Tail);
}
}
Now, whenever the Snake
detects a collision, its Collide
event will be invoked, alerting any subscribers that a collision has occurred.
MainScene
can now subscribe to this event and respond to it with its own event handler, which we'll call OnSnakeCollide()
. Edit /core/scene/MainScene.cs
and add the following method to MainScene
:
private void OnSnakeCollide(object sender, EventArgs e)
{
// We'll fill this out soon.
}
Now that we have an event handler for the Collide
event, let's hook them together in MainScene.Initialize()
:
protected override void Initialize()
{
Console.WriteLine("Main Scene Initialized!");
_keyboard = new KeyboardMonitor();
_tickTimer = _tickDelay;
_snake = new Snake(4, new Vector2(2, 2));
_snake.Collide += OnSnakeCollide;
}
Now MainScene.OnSnakeCollide()
will be invoked whenever _snake
detects a collision!
So now, let's think about what we want to happen when the snake collides itself with or a wall. We could close the game by calling Game.Exit()
in OnSnakeCollide()
, but it would be irritating to have to re-launch the game every time the game ends.
Instead, let's restart the game, resetting the snake to its initial size and position. To make this easier, let's create a NewGame()
method and move our Snake
initialization code in there. We'll also call this in our MainScene.Initialize
method so that we aren't definiting our snake initialization in two different places:
Edit core/scene/MainScene.cs
and make the following changes to MainScene
:
- Create
NewGame()
method, as follows:
private void NewGame()
{
_snake = new Snake(4, new Vector2(2, 2));
_snake.Collide += OnSnakeCollide;
}
- Adjust
Initialize()
as follows:
protected override void Initialize()
{
Console.WriteLine("Main Scene Initialized!");
_keyboard = new KeyboardMonitor();
_tickTimer = _tickDelay;
NewGame(); // <-- New item, replaces _snake = new Snake(...)
}
- Adjust
OnSnakeCollide()
as follows:
private void OnSnakeCollide(object sender, EventArgs e)
{
NewGame(); // <-- New item
}
Now, when a collision is detected, the game will automatically restart.
Step 2: Adding the Apple
We've added the obstacles that the player must avoid, but now we need a goal for the player to try for while they avoid the obstacles. Let's add the apple next.
Unlike our Snake
, our apple will be very simple and will not require a dedicated class to represent it. It will essentially be a single point on the game grid that we'll check if the snake contains each tick. This means we can store the apple in MainScene
as a Vector2
called _appleLocation
. We will also be placing the apple randomly, so we'll need a Random
instance to generate random numbers.
Edit core/scene/MainScene.cs
and add the following fields to MainScene
:
private Vector2 _appleLocation;
private Random _random = new Random();
Now, we need to place the apple on the grid. We'll need to replace the apple in a random location each time it is picked up, so we'll put the apple placement code into a method we'll call PlaceApple()
.
Edit core/scene/MainScene.cs
and add the following:
private void PlaceApple()
{
Vector2 location;
do
{
location = new Vector2(_random.Next(0, GRID_SIZE), _random.Next(0, GRID_SIZE));
}
while (_snake.Positions.Contains(location));
_appleLocation = location;
}
This method will generate a random location and check it to make sure the snake doesn't already occupy that location before placing the apple there. If the snake does occupy the location it generates, it'll continue to generate new locations until it finds one that is unoccupied.
Now that we have this method defined, let's call it in MainScene
to place the apple upon the start of a new game so that the player has one waiting for them.
Edit core/scene/MainScene.cs
and update the NewGame()
method as follows:
private void NewGame()
{
_snake = new Snake(4, new Vector2(2, 2));
_snake.Collide += OnSnakeCollide;
PlaceApple(); //<-- New item
}
Now an apple will be placed as soon as the game starts.
Drawing the Apple
Now that we've placed the apple on the grid, we need to draw it so the player knows where it is. We can do this by making a simple addition to our MainScene.Draw()
method.
Edit core/scene/MainScene.cs
and update the Draw()
method as follows:
protected override void Draw(GameTime gameTime)
{
SpriteBatch.Begin();
foreach (var segment in _snake.Positions)
{
SpriteBatch.Draw(
_snakeSprite.Texture,
new Rectangle((int)segment.X * GRID_CELL_SIZE, (int)segment.Y * GRID_CELL_SIZE,GRID_CELL_SIZE, GRID_CELL_SIZE),
_snakeSprite.Frame,
Color.White
);
}
// New section: apple draw code
SpriteBatch.Draw(
_appleSprite.Texture,
new Rectangle((int)_appleLocation.X * GRID_CELL_SIZE, (int)_appleLocation.Y *GRID_CELL_SIZE, GRID_CELL_SIZE, GRID_CELL_SIZE),
_appleSprite.Frame,
Color.White
);
SpriteBatch.End();
}
Now the apple will be drawn on screen at its correct position. The player can move the snake towards the apple and try to eat it, but nothing will happen quite yet. Next, we'll add collision detection for the apple`.
Eating the Apple
Although much of the collision code is handled through the Snake
class, since the MainScene
is responsible for placing the apple and keeping track of its location, we're going to have MainScene
handle checking and handling collision for the apple.
Just as before, since snake is grid-based, handling collision is very easy. We can check for collision with the apple each tick directly in MainScene.Update()
's tick-handling section.
Edit core/scene/MainScene.cs
and update the Update()
method as follows:
protected override void Update()
{
_keyboard.BeginUpdate(Keyboard.GetState());
// ... user input code ...
_keyboard.EndUpdate();
if (_tickTimer <= 0)
{
_snake.Move();
// New section: apple collision check
if (_snake.Positions.Contains(_appleLocation))
{
PlaceApple();
}
_tickTimer = _tickDelay;
}
else
{
_tickTimer--;
}
}
With this addition, when the snake touches the apple, a new one will immediately be placed in a random unoccupied space on the grid. However, even though the snake now "eats" the apple, it still doesn't grow.
Let's fix that. Edit core/class/Snake.cs
and add a new method called Grow()
as follows:
public void Grow()
{
Length++;
}
Then, let's call this new method in the tick-handling section of MainScene.Update()
:
protected override void Update()
{
_keyboard.BeginUpdate(Keyboard.GetState());
// ... user input code ...
_keyboard.EndUpdate();
if (_tickTimer <= 0)
{
_snake.Move();
if (_snake.Positions.Contains(_appleLocation))
{
_snake.Grow(); // <-- New item
PlaceApple();
}
_tickTimer = _tickDelay;
}
else
{
_tickTimer--;
}
}
And with that, now our snake will grow when it eats apples. What we have now is a complete, working game of Snake with all required features in place.
You've done it!
Conclusion
Congratulations! By completing this chapter, you have created your own recreation of the classic game, Snake! In doing so, you explored Ladybug's basic functionality, and are well on your way to creating bigger and better games.
Next Steps
Extending Snake: Self-Study Suggestions
Now that you have a basic complete Snake game, you could try adding to it! Some suggestions include:
- Adding a title screen and/or pause menu
- Tracking a score and rendering the score to the screen
- Adding audio
- Saving persistent high scores
More Tutorials
Now that you've mastered this introductory tutorial, take a look at some of the other tutorials we have! (More tutorials coming soon!)
Complete code for this chapter can be found here.