Read our blogs, tips and tutorials
Try our exercises or test your skills
Watch our tutorial videos or shorts
Take a self-paced course
Read our recent newsletters
License our courseware
Book expert consultancy
Buy our publications
Get help in using our site
538 attributed reviews in the last 3 years
Refreshingly small course sizes
Outstandingly good courseware
Whizzy online classrooms
Wise Owl trainers only (no freelancers)
Almost no cancellations
We have genuine integrity
We invoice after training
Review 30+ years of Wise Owl
View our top 100 clients
Search our website
We also send out useful tips in a monthly email newsletter ...
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. |
In this blog
Return to the Flappy Bird in Excel VBA Tutorial index.
Download Flappy Owl Pt11 - Collisions.
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.
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.
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.
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
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.
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:
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:
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.
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.
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.
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:
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.
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:
When colliding with the side of the wall the bird will overlap by a single column.
When the bird collides with the top wall while moving upwards it stops just below the wall.
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.
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.
Some other pages relevant to the above blog include:
Kingsmoor House
Railway Street
GLOSSOP
SK13 2AA
Landmark Offices
99 Bishopsgate
LONDON
EC2M 3XD
Holiday Inn
25 Aytoun Street
MANCHESTER
M1 3AE
© Wise Owl Business Solutions Ltd 2024. All Rights Reserved.