Flappy Bird in Excel VBA Part 11 - Detecting Collisions
In this part of the tutorial you'll learn how to make the game detect collisions between the bird and the obstacles.

Posted by Andrew Gould on 24 April 2014

You need a minimum screen resolution of about 700 pixels width to see our blogs. This is because they contain diagrams and tables which would not be viewable easily on a mobile phone or small laptop. Please use a larger tablet, notebook or desktop computer, or change your screen resolution settings.

Flappy Bird in Excel VBA - Detecting Collisions

Useful Links

Return to the Flappy Bird in Excel VBA Tutorial index.

Download Flappy Owl Pt11 - Collisions.

Introduction

In real 2D games there are two main approaches to detecting collisions between objects. There's complex, pixel-perfect collision-detection where the games tests  each coloured pixel of an image to check whether it overlaps with any coloured pixel of another image. This is very accurate but expensive in processing terms. The other technique involves testing whether one rectangle (or other basic geometric shape) overlaps with another at any point. This is less accurate for images which aren't perfectly rectangular but it faster in processing terms and easier to implement.

The application class in Excel VBA actually provides a convenient way to test whether one range of cells overlaps another; the Intersect method. We'll use this to test whether the bird has collided with the wall. This will mean that the collision-detection won't be perfect but in this case it's worth sacrificing accuracy for speed.

Collision

In our game if any of the cells within the pink border overlap with a wall shape we'll consider this to be a collision.

 

Creating the Colliding Property

We'll create a property within the bird class which will tell the game whether the bird is colliding with another object. Head back to the clsBird class module and add the following private variable to the top of the module:

Private BirdCurrentRectangle As Range

Now go to the Update method of the bird class and add a line of code to the end of the subroutine:

Set BirdCurrentRectangle = _

Range(BirdCell, BirdCell.Offset(BirdHeight - 1, BirdWidth - 1))

End Sub

While we're here it's worth making a quick change for a very small performance improvement. Near the top of the Update method find this section of code:

'remember the cell that the bird was in

'at the start of this procedure call

Set BirdPreviousRectangle = _

Range(BirdCell, BirdCell.Offset(BirdHeight - 1, BirdWidth - 1))

Replace the line above with the version shown below:

'remember the cell that the bird was in

'at the start of this procedure call

Set BirdPreviousRectangle = BirdCurrentRectangle

Now add one line to end of the Public Property Set GameSheet procedure so that it looks like this:

Set BirdCurrentRectangle = _

Range(BirdCell, BirdCell.Offset(BirdHeight - 1, BirdWidth - 1))

End Property

Now we can get on and create the property that will tell us if the bird is colliding with another game object. Add the following procedure to the class module:

Public Property Get Colliding( _

TopWallRange As Range, _

BottomWallRange As Range) As Boolean

If Not Application.Intersect( _

BirdCurrentRectangle, _

TopWallRange) Is Nothing Then

Colliding = True

ElseIf Not Application.Intersect( _

BirdCurrentRectangle, _

BottomWallRange) Is Nothing Then

Colliding = True

Else

Colliding = False

End If

End Property

The Application.Intersect method accepts two range objects and returns True if the two ranges overlap.

Getting the Wall Ranges

The property that we've just created requires us to pass in the range of the object for which we're testing collisions. That means we'll need some way for the wall class to provide us with the range of cells that the walls occupy.

Go back to the clsWall class module and add the following two properties:

Public Property Get TopWallRange() As Range

Set TopWallRange = Range( _

WallTopCell, _

WallTopCell.Offset(WallTopHeight - 1, WallWidth - 1))

End Property

Public Property Get BottomWallRange() As Range

Set BottomWallRange = Range( _

WallBottomCell, _

WallBottomCell.Offset(WallBottomHeight -1 , WallWidth - 1))

End Property

Testing for Collisions During the Game

Now we can go back to the clsGameCode module and add some code to the UpdateAndDrawGame subroutine. For the time being we'll assume that if the bird collides with a wall we'll stop the game and return the player to the menu worksheet.

Change the UpdateAndDrawGame subroutine so that it looks like this:

Public Sub UpdateAndDrawGame()

'Called by the SetTimer function

'Runs once for each tick of the timer clock

'Updates all game logic

'Draws all game objects

If GetAsyncKeyState(vbKeyTab) <> 0 Then

TerminateGame

Exit Sub

End If

Bird.Update

Wall.Update

Bird.Draw

Wall.Draw

'test for collisions and exit the game

If Bird.Colliding(Wall.TopWallRange, Wall.BottomWallRange) Then

TerminateGame

Exit Sub

End If

End Sub

You should now be able to run the game and find that if you hit the wall you'll be unceremoniously dumped to the game menu.

Adjusting the Bird Start Position

One small problem with the way the game works at the moment is that because the wall moves 16 columns each time the game updates it's possible that the wall could completely overlap the bird before any collisions are detected. In fact, that's exactly what happens with the current sizes of all of the game objects. If you could pause the game at the point it ends this is what you would see:

Owl in wall

The bird is well and truly embedded in the wall before any collisions are detected.

 

If you want to see this effect yourself you could go to the clsGameSheet class and comment out the line which deletes the game sheet. You'd also need to go to the UpdateAndDrawGame subroutine and change the order in which the bird and wall are drawn. When you run the game and collide with a wall you can go back to the game sheet and see where the bird ended up.

To prevent the player seeing this slightly rubbish effect we're simply going to adjust the starting position of the bird so that it won't be overlapped by the wall before a collision is detected. It's worth looking a little more closely at how far the bird sits inside the wall first:

Bird embedded

Measuring the number of columns by selecting the cells reveals that the wall overlaps by 16 columns.

 

Head back to the clsBird class module and find the Public Property GameSheet procedure. Edit the line which sets the BirdCell variable so that the start of the procedure looks like this:

Public Property Set GameSheet(Value As clsGameSheet)

Set pGameSheet = Value

Set BirdCell = _

pGameSheet.GameRange.Cells(Int(pGameSheet.GameHeight / 2), 41)

All we've done is changed the starting column of the bird from 40 to 41. If you run the game again now you might be able to see that the collisions are detected before the wall completely overlaps the bird.

Bird overlap

Now the collision is detected as soon as the bird image overlaps the wall by just a single column.

 

If you changed your code to see the effect of the overlap earlier then it's time to change it back so that the game sheet is deleted at the end of each game and so that the bird is drawn before the wall.

Adjusting the Bird's Vertical Movement

It's worth mentioning that we have a similar problem if the bird moves up or down too quickly when it's in the gap between the top and bottom walls.

Bird overlapped

Moving up too quickly results in the bird being embedded in the wall again.

 

Solving this problem is a little more convoluted. If we establish a collision has taken place between the bird and top wall this is the basic logic of what needs to happen:

  • Check if the bird is moving up.
  • Check if the top of the bird was below the bottom of the top wall in the previous frame.
  • Check if the top of the bird is above the bottom of the top wall in the current frame.
  • If so, move the bird so that it sits just below the bottom of the top wall.

We'll need to do something similar to ensure that the bird doesn't become embedded in the bottom wall as well. In the clsBird class module, replace the Colliding property with this version:

Public Property Get Colliding( _

TopWallRange As Range, _

BottomWallRange As Range) As Boolean

Dim BirdTopRowPrev As Integer

Dim BirdTopRowCurr As Integer

Dim BirdBottomRowPrev As Integer

Dim BirdBottomRowCurr As Integer

Dim TopWallBottomRow As Integer

Dim BottomWallTopRow As Integer

If Not Application.Intersect(BirdCurrentRectangle, _

TopWallRange) Is Nothing Then

'check if bird is moving up

If BirdVerticalMovement < 0 Then

BirdTopRowPrev = BirdPreviousRectangle.Rows(1).Row

TopWallBottomRow = _

TopWallRange.Rows(TopWallRange.Rows.Count).Row

'check if top of bird was below bottom of top wall

If BirdTopRowPrev > TopWallBottomRow Then

BirdTopRowCurr = _

BirdCurrentRectangle.Rows(1).Row

'check if top of bird is above bottom of top wall

If BirdTopRowCurr < TopWallBottomRow Then

'move bird to below the bottom of the top wall

Set BirdCell = pGameSheet.GameSheet.Cells( _

TopWallBottomRow + 1, _

BirdCell.Column)

Set BirdCurrentRectangle = _

Range(BirdCell, BirdCell.Offset( _

BirdHeight - 1, BirdWidth - 1))

End If

End If

End If

Colliding = True

ElseIf Not Application.Intersect( _

BirdCurrentRectangle, _

BottomWallRange) Is Nothing Then

'check if bird is moving down

If BirdVerticalMovement > 0 Then

BirdBottomRowPrev = _

BirdPreviousRectangle.Rows(BirdHeight).Row

BottomWallTopRow = BottomWallRange.Rows(1).Row

'check if bottom of bird was above top of bottom wall

If BirdBottomRowPrev < BottomWallTopRow Then

BirdBottomRowCurr = _

BirdCurrentRectangle.Rows(BirdHeight).Row

'check if bottom of bird is now below

If BirdBottomRowCurr > BottomWallTopRow Then

'move bird to above the top of the bottom wall

Set BirdCell = pGameSheet.GameSheet.Cells( _

BottomWallTopRow - BirdHeight, _

BirdCell.Column)

Set BirdCurrentRectangle = _

Range(BirdCell, BirdCell.Offset( _

BirdHeight - 1, BirdWidth - 1))

End If

End If

End If

Colliding = True

Else

Colliding = False

End If

End Property

Before running the game we also need to make a small change to the UpdateAndDrawGame subroutine in the modGameCode module. Change it so that it looks like this:

Public Sub UpdateAndDrawGame()

'Called by the SetTimer function

'Runs once for each tick of the timer clock

'Updates all game logic

'Draws all game objects

If GetAsyncKeyState(vbKeyTab) <> 0 Then

TerminateGame

Exit Sub

End If

Bird.Update

Wall.Update

'test for collisions and exit the game

If Bird.Colliding(Wall.TopWallRange, Wall.BottomWallRange) Then

Bird.Draw

Wall.Draw

TerminateGame

Exit Sub

End If

Bird.Draw

Wall.Draw

End Sub

The Colliding property of the bird class can now change the position of the bird so it's important that we draw the bird in its new position if a collision is detected. We also need to make sure that the bird and wall are drawn even if no collisions are detected so we need to call the Draw method of both objects after the check for collisions as well.

Testing the Game

That was somewhat complicated, but the end result should be that the bird will never become embedded within the wall, as shown in the images below:

Side collision

When colliding with the side of the wall the bird will overlap by a single column.

 
Top collision

When the bird collides with the top wall while moving upwards it stops just below the wall.

 
Bottom collision

When the bird collides with the bottom wall while moving downwards it stops just above the wall.

 

Give the game a test - with the collision-detection implemented it should be a little more challenging than it was previously! If your code doesn't work you can download a working copy from the top of the page.

What's Next

Now that the game is becoming a little more complicated it's worth thinking about how we tell the game what to do each time it is updated. The next part of the tutorial shows you how to implement a state system to help us with this task.

This blog has 0 threads Add post