Using Graphics: Making a Lander Game
Firstly, we need the API declares. We need ones for GetTickCount, BitBlt, SetPixelV and GetAsyncKeyState functions and the VK_DOWN constant. Paste these into the declarations section of the form, and make sure that they are defined as 'Private' not 'Public'. We also need 3 variables to keep control of the craft, vSpeed, LandY and Fuel, all type double. So far, we have this:
Option Explicit 'API Declares Private Declare Function GetTickCount _ Lib "kernel32" () As Long
Private Declare Function BitBlt Lib "gdi32" _ (ByVal hDestDC As Long, ByVal x As Long, _ ByVal y As Long, ByVal nWidth As Long, _ ByVal nHeight As Long, ByVal hSrcDC As Long, _ ByVal xSrc As Long, ByVal ySrc As Long, _ ByVal dwRop As Long) As Long Private Declare Function SetPixelV Lib "gdi32" _ (ByVal hdc As Long, ByVal x As Long, _ ByVal y As Long, ByVal crColor As Long) As Long
Private Declare Function GetAsyncKeyState _ Lib "user32" (ByVal vKey As Long) As Integer Private Const VK_DOWN = &H28
' Vertical speed of the craft Private vSpeed As Double ' The y coordinate of the craft Private LandY As Double ' The amount of fuel left Private Fuel As Double
We also need to set up the default values for these variables. Therefore in the Form_Load event, set Fuel to 700, a value suitable low to make it hard, and vSpeed and LandY to 0. Since we don't want to display all the decimal places of these double numbers, we must use the format command to display the numbers. The height is also a little trick, as it must be derived from the LandY variable and displayed as the height above the ground. Here is code to display all three values to 1 decimal place:
txtvspeed.Text = Format(vSpeed, "0.0") txtfuel.Text = Format(Fuel, "0.0") txtheight.Text = Format(picEarth.ScaleHeight _ - piclander.ScaleHeight - 30 - LandY, "0.0")
Nice and easy so far. We also need some code in the command button to enable the timer, thus starting the game. It is also worth disabling the command button just for tidiness.
Now for the more tricky bit of tmrGravity. Although the timer is set to go off every millisecond, it does not really fire 1000 times every second, so we must find another way to measure the time between one call and the next. We will do this by using a static variable. This kind of variable retains its value between callings of a procedure. Therefore, if its value is set to 1 at the end of one calling, it will still be 1 next time it is called. Put these declarations into the Timer event:
Static curtime As Long Dim timenow As Long Dim timediff As Long
The GetTickCount API function returns the number of milliseconds elapsed since Windows started, so this is ideal. During each timer event, the static variable will be set with the number of milliseconds, then the next time the event is raised, we can work out how long it was since the last time the event was called and thus calculate speeds.
This static variable will be set to 0 the first time the event is called since it has not been set previously. Therefore, when it is equal to 0, we must draw the stars and the ground, using the code earlier:
If curtime = 0 Then ' Draw the earth picEarth.Line (0, picEarth.ScaleHeight - 30) _ -(picEarth.ScaleWidth, picEarth.ScaleHeight), _ vbWhite, BF Randomize Timer Dim starx As Long, stary As Long For starx = 0 To picEarth.ScaleWidth For stary = 0 To picEarth.ScaleHeight - 30 If Rnd * 1000 < 5 Then SetPixelV picEarth.hdc, starx, stary, vbYellow End If Next Next timenow = GetTickCount curtime = timenow End If
I mentioned earlier that we would be using the vbSrcInvert BitBlt operation to put the space craft onto the back ground. The operation that is being done by vbSrcInvert operation is this:
Destination = Source XOR Destination
This is just one method of pasting the craft onto the stars, but the beauty of using XOR is that it is reversible. If you repeat the operation a second time, the results of the first call will be completely undone. This means that we can call the at the end of the timer event to paint the craft onto the stars, then again at the beginning of the next one to delete the craft, ready for the new position.
Therefore, if it is not the first time the event has been called, we need to do this painting. Add this code in before the 'End If' of the previous snippet:
'etc...curtime = timenow
Else timenow = GetTickCount ' If it isn't the first time, put back the previous background BitBlt picEarth.hdc, 150, LandY, piclander.ScaleWidth, _ piclander.ScaleHeight, piclander.hdc, 0, 0, vbSrcInvert
' End If...
We now have a completely blank sky of stars, and can work out the elapsed time since the event was last called:
timediff = timenow - curtime
The next thing to take into account is the speed of the craft, and the effects of gravity on it. This is the code that is needed:
' Calculate new vertical speed based on g vSpeed = vSpeed - ((timediff / 1000) * 10)
The 10 in that equation is the acceleration due to gravity, usually measured in metres per second per second. Since timediff is in milliseconds, we must convert that to seconds before we can use it. That is most of the maths out of the way now, just the thrusters to go... Two things must be true for the thrusters to work. Firstly, they player must be pressing the down key, and secondly, there must be some fuel. This little snippet does just that, and beep if there is no fuel left:
If GetAsyncKeyState(VK_DOWN) <> 0 Then If Fuel > 0 Then ' Apply thrust: 15 is the acceleration produced vSpeed = vSpeed + ((timediff / 1000) * 15) Fuel = Fuel - ((timediff / 1000) * 150) ' Check that fuel does not go below 0 If Fuel < 0 Then Fuel = 0 Else Beep End If End If
As you can see, the formulae used here are similar to the gravity, except that they act upwards not downwards (hence vSpeed = vSpeed + rather than -).
Now we are nearly there. We need to update the craft's position, update the text boxes and also the 'time last called':
LandY = LandY - vSpeed
' Update text boxes txtvspeed.Text = Format(vSpeed, "0.0") txtfuel.Text = Format(Fuel, "0.0") txtheight.Text = Format(picEarth.ScaleHeight _ - piclander.ScaleHeight - 30 - LandY, "0.0")
' Update the 'last called time' curtime = timenow
The last bit of logic to check is whether the craft has touched down. This is done by checking the LandY variable to see if we have entered the ground. If the craft has 'touched' down, we need to check the speed to make sure that it has not gone too fast and crashed! At the same time, the craft must be drawn, either in a successful or smashed up state. If the ship has not touched down, then it should just be redraw as usual:
' If it has touched down... If LandY >= picEarth.ScaleHeight - 30 - _ piclander.ScaleHeight Then ' Make sure that it is on the surface LandY = picEarth.ScaleHeight - 30 - piclander.ScaleHeight txtheight.Text = Format(picEarth.ScaleHeight _ - piclander.ScaleHeight - 30 - LandY, "0.0") ' Stop the timer and disable the pause button... ' the game is over! tmrgravity.Enabled = False ' Figure out if it was a safe landing or not, ' and paint the appropriate craft If vSpeed > -2 Then ' If it was safe, then the craft remains intact BitBlt picEarth.hdc, 150, LandY, piclander.ScaleWidth, _ piclander.ScaleHeight, piclander.hdc, 0, 0, vbSrcInvert MsgBox "Congratulations! You have landed successfully!" Else ' If it was moving too fast, it blows up! BitBlt picEarth.hdc, 150, LandY, piclander.ScaleWidth, _ piclander.ScaleHeight, picsmash.hdc, 0, 0, vbSrcInvert MsgBox "Smash! Oooops!" End If
' paint the craft into its new position. BitBlt picEarth.hdc, 150, LandY, piclander.ScaleWidth, _ piclander.ScaleHeight, piclander.hdc, 0, 0, vbSrcInvert
And there you have it..your very own game.
Page 7 of 8