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
551 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 6 - Detecting Player Input |
---|
This part of the tutorial describes how to respond to keys pressed by the player. You'll learn about the Application.OnKey method and the GetAsyncKeyState Windows API function. |
In this blog
Return to the Flappy Bird in Excel VBA Tutorial index.
Download Flappy Owl Pt6a - Using OnKey.
Download Flappy Owl Pt6b - GetAsyncKeyState.
A full list of Excel training courses and training courses in VBA that we offer.
In this part of the tutorial we'll look at how to detect when the player presses keys on the keyboard and perform the appropriate action when they do. You can download the finished workbook here.
In our game we'd like the bird to move upwards when the player presses the up arrow key and for it to dive downwards when they press the down arrow key.
To start with we're going to stop the bird from bouncing when it hits the floor. Go to the modBirdCode module and remove these lines from the UpdateBird procedure:
'reverse the movement direction (makes the bird bounce)
BirdVerticalMovement = -8
Now declare two constants at the top of the module, just below Option Explicit:
Private Const FlapHeight As Integer = -8
Private Const DiveDepth As Integer = 8
We can modify these numbers later to tweak the way the game works but these values will do for now. Now add two simple subroutines to the bottom of the module:
Private Sub Flap()
BirdVerticalMovement = FlapHeight
End Sub
Private Sub Dive()
BirdVerticalMovement = DiveDepth
End Sub
One simple way to create a keyboard shortcut to run a subroutine is to use the Application.OnKey method. Go back to the modGameCode module and add a new subroutine to the bottom which looks like this:
Private Sub SetGameKeys()
Application.OnKey "{UP}", "Flap"
Application.OnKey "{DOWN}", "Dive"
End Sub
The OnKey method accepts two arguments: the first is the key to which you're assigning a procedure; the second is the name of the procedure that you're assigning. Note that, even though we declared the Flap and Dive procedures as private, we can still specify their names here.
While we're here I'd also like to assign a key which the user can press to end the game. Add the following line to the SetGameKeys procedure:
Application.OnKey "{TAB}", "TerminateGame"
Normally you'd use the Escape key to end a game, but in this case we're going to use the Tab key instead. The reason is that pressing this key will end the game and also reset all of the keys back to their original functions. For the Escape key, this means that it will go back to being the key which interrupts execution of our code. The danger here is that if a user presses the Escape key twice in quick succession the first press will stop the game but the second press might interrupt any tidying up code that we run after that. One way to avoid this would be to make sure that resetting the Escape key is the last thing that happens but I prefer to avoid the risk. In fact, we can go a step further and make sure that the Escape key won't do anything at all while our code is running by adding this line to the same procedure:
Application.OnKey "{ESC}", ""
The empty string passed to the second parameter tells the Escape key to do nothing when it is pressed.
We'll also need a procedure which resets all of the keys that we've changed at the end. To do this you can use the OnKey method and simply omit the second argument. Add another subroutine which will do this. At the end you should have two subroutines which look like this:
Private Sub SetGameKeys()
Application.OnKey "{ESC}", ""
Application.OnKey "{UP}", "Flap"
Application.OnKey "{DOWN}", "Dive"
Application.OnKey "{TAB}", "TerminateGame"
End Sub
Private Sub ResetKeys()
Application.OnKey "{UP}"
Application.OnKey "{DOWN}"
Application.OnKey "{TAB}"
Application.OnKey "{ESC}"
End Sub
Note that the Escape key is the first one to be disabled and the last one to be re-enabled.
If you had a lot of keys to assign for your game it would probably be better to store them and the names of their associated subroutines in worksheet cells (or even a text file). You could then loop over the range of cells to assign each key to a procedure rather than writing an explicit instruction for each one. This is the technique I've used in the full version of the game.
Now we need to activate our keys when the game starts and deactivate them when the game ends. Add a line which calls the SetGameKeys procedure to the top of the InitialiseGame subroutine:
Public Sub InitialiseGame()
'Called once when game first starts
'Used to set starting parameters
'Begins the game timer
SetGameKeys
Now change the TerminateGame procedure so that it looks like this:
Public Sub TerminateGame()
'Called once when game ends
'Used to tidy up
TerminateTimer
shMenu.Activate
ResetKeys
End Sub
Before we test the game again we need to change the UpdateBird procedure too. Now that we have the ability to move up as well as down we need to ensure that we don't try to move up past the top of the worksheet. Go back to the modBirdCode module and replace the UpdateBird procedure with this version:
Public Sub UpdateBird()
Dim TargetRow As Integer
'remember the cell that the bird was in
'at the start of this procedure call
Set BirdPreviousCell = BirdCell
'calculate how many cells to fall and
'ensure this isn't faster than the dive speed
BirdVerticalMovement = WorksheetFunction.Min( _
BirdVerticalMovement + Gravity, _
DiveDepth)
'calculate the row number of the target cell
TargetRow = BirdCell.Row + BirdVerticalMovement
'check if the destination row is past the floor
If TargetRow >= FloorRange.Row Then
'if so, set the target row to 1 row above the floor
TargetRow = FloorRange.Row - 1
BirdVerticalMovement = 0
'check if destination row is above the top of the sheet
ElseIf TargetRow <= 1 Then
'if so set the target row to the top row
TargetRow = 1
BirdVerticalMovement = 0
End If
'store the new destination cell
Set BirdCell = shTest.Cells(TargetRow, BirdCell.Column)
End Sub
You should now be able to test the game and press the up and down arrow keys to influence the bird's height. When you get bored you can either click your Stop Game button with the mouse or just press the Tab key on the keyboard. If the code didn't work, check everything on this page or just download the working version of the workbook.
One of the problems with using the basic OnKey method is that if a player holds down the up arrow key it will interrupt whatever is currently happening. If the player holds down the key it will also continuously call the procedure attached to that key press. Try doing this by running the game and holding down the up arrow - you should see the game stutter as it tries to run the Flap procedure continuously.
Just as with our timing functions we can use the Windows API to provide us with functions that allow us to control things in much more detail. We'll start by declaring a function with the snappy title of GetAsyncKeyState. Copy this to the appropriate place in your public declarations module:
Public Declare Function GetAsyncKeyState Lib "user32" ( _
ByVal vKey As Long) As Integer
Here's the version for 64-bit editions of Office 2010 or later:
Public Declare PtrSafe Function GetAsyncKeyState Lib "user32" ( _
ByVal vKey As Long) As Integer
This function accepts a code number corresponding to a key on the keyboard and returns a value representing whether the key is up or down. We're going to use this to replace the simple OnKey method that we used earlier.
It's worth mentioning that there is another Windows API function for getting the state of a single key called GetKeyState. The differences between GetKeyState and GetAsyncKeyState are subtle but important. Essentially, we're using GetAsyncKeyState because we're interested in the current status of a key, not its status when some other event occurred.
Although we're going to detect player input using our fancy new Windows API function we're not going to completely abandon the OnKey method. We want to ensure that the up and down arrow keys don't perform their normal function while the game is running so we can still use our SetGameKeys procedure to deactivate them. Change the procedure so that it looks like this:
Private Sub SetGameKeys()
Application.OnKey "{ESC}", ""
Application.OnKey "{UP}", ""
Application.OnKey "{DOWN}", ""
Application.OnKey "{TAB}", ""
End Sub
This prevents any of the four listed keys from performing any action while the game is running. Unfortunately, that also means that pressing Tab won't stop the game any longer. We can fix that by adding a line of code to the UpdateAndDrawGame procedure. 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
UpdateBird
DrawBird
End Sub
We pass the value of a constant, vbKeyTab, into the GetAsyncKeyState function. If the function doesn't return 0 we know that the key is being pressed so we call the TerminateGame procedure. This means that we can successfully stop the game again by pressing the Tab key.
You can see the list of VBA key constants by pressing CTRL + SPACEBAR to show the IntelliSense list.
The IntelliSense list contains all of the key constants that you'll need.
We can now add code to detect if the player presses the up or down arrow keys. Go back to the modBirdCode module and add two lines to the top of the UpdateBird procedure, below the TargetRow variable, so that it looks like this:
Public Sub UpdateBird()
Dim TargetRow As Integer
If GetAsyncKeyState(vbKeyUp) <> 0 Then Flap
If GetAsyncKeyState(vbKeyDown) <> 0 Then Dive
You can now run the game again to test it. You should find that pressing the up and down arrow keys perform the appropriate action. You may also have spotted another small problem...
If you hold down the up arrow key you'll see that the bird flies to the top of the screen and stays there. That's definitely not a behaviour we want in our game. We only want to execute the Flap subroutine if the key is pressed down in the current tick of the game clock but wasn't pressed down in the previous one.
The easiest way to do this is to read the state of the up and down arrow keys into variables each time the bird is updated. Add the following two declarations to the top of the modBirdCode module:
Private PreviousUpKeyState As Integer
Private PreviousDownKeyState As Integer
We'll need to initialise these variables when the bird is initialised, so add the following two lines to the end of the InitialiseBird procedure:
PreviousUpKeyState = GetAsyncKeyState(vbKeyUp)
PreviousDownKeyState = GetAsyncKeyState(vbKeyDown)
These lines check if the keys are being pressed at the point the InitialiseBird procedure is called. We could just as easily have set these variables to 0 which our code would interpret as the keys not being pressed, regardless of the actual state of the keys.
Next we need to modify the If statements we added to the UpdateBird procedure earlier. As the code to do this is a bit longer we should create a separate subroutine for it. Add a new subroutine to the bottom of the same module which looks like this:
Private Sub CheckKeys()
If GetAsyncKeyState(vbKeyUp) <> 0 And _
PreviousUpKeyState = 0 Then Flap
If GetAsyncKeyState(vbKeyDown) <> 0 And _
PreviousDownKeyState = 0 Then Dive
PreviousUpKeyState = GetAsyncKeyState(vbKeyUp)
PreviousDownKeyState = GetAsyncKeyState(vbKeyDown)
End Sub
The If statements check if each key is being pressed down during this tick of the game clock but was not pressed down in the previous tick. Once we've performed the appropriate action we then store the current state of each key in the appropriate variable ready for the next tick of the clock.
Now we just need to modify the UpdateBird procedure to call the one we've just created. Remove the If statements we added to the UpdateBird procedure earlier and replace them with a single call to the CheckKeys procedure. The first few lines of the subroutine should look like this:
Public Sub UpdateBird()
Dim TargetRow As Integer
CheckKeys
You can now run the game again and check that everything works as intended. If it doesn't you can download the working example from the link at the top of the page.
In the UpdateAndDrawGame procedure we don't need to test if the Tab key was being pressed in the previous update. The reason is that pressing the Tab key immediately ends the game, meaning that the UpdateAndDrawGame subroutine won't be called again anyway. If it makes you feel better you can still add the code to check if the Tab key was previously being pressed but I'm not going to.
In our simple game the player only needs to use two keys to play so it's fairly convenient to store the previous state of each key in separate variables and that's what we're going to stick with in this tutorial.
In a more complex game however, it would rapidly become tedious to store each key one-by-one. Instead, we could use a Windows API function to store the state of the entire keyboard in an array. The function is called GetKeyboardState and, if you're interested, you can see a quick example of how it works here.
I think that our game has looked ugly enough for long enough so the next tutorial is going to focus on upgrading the art of our game. I use the term "art" in the vaguest possible sense of the word - you'll understand why when you witness my child-like drawing skills in the next part of this tutorial.
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.