Generated by DocFX

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 the Snake instance invoke a method or change a property of MainScene
  • Define an event in Snake that is invoked when a collision happens, and have MainScene 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.