Drawing & Animation
Using the Win32 GDI #2
This three part tutorial first appeared some years ago on the old VBExplorer.com . Since then several errors and bugs have been discovered by various users on the VBExplorer.com Forums. This version reflects the changes made to overcome the bugs and erros. The text will note where a bug correction and / or update has been done, and of course also why.
A big 'Thank you' goes out to all the people reading and reporting the bugs in the previous version of this tutorial. Please provide comments, questions, bug reports etc. at the VBExplorer.com forums in the Graphics & Game Programming section.
Backbuffering, AutoRedraw and Refresh
In the previous section we used the AutoRedraw property and the Refresh function to force a copy of the form to be stored in memory and then updated. The same thing can of course be accomplished on a picture box.
There is also an alternative to this scheme, namely BackBuffering. Backbuffering is a simple technique, where you keep a 慶opy?of the gaming field (display area) in a non-visible area. All the sprites and other drawings are drawn into the backbuffer, which is then drawn all at once onto the display area.
If we could view the process it would look something like this:
Step 1: Draw all the masks and sprites onto the non-visible back buffer
Step 2: Draw everything in the back buffer to the visible area
So why does this produce flickerless animation? The main reason is that we only use one Blit operation to draw everything we need from the backbuffer to the front display area. This disables all the intermediate updates, which might occur between each blit operation, and thus produces a clean drawing. Get it? Basically you are doing all the work with the sprites and masks off screen, where the game player doesn't see it and blitting to the play area once. The other way all the operations take place in the visible play area.
Which method is then the best method? Well, it depends on your type of application and the design you have made. The sample project BACKBUF found in BACKBUF.ZIP demonstrates both methods and times the amount of time elapsed. We tested both of the methods with this sample project and with some additional projects. All the empirical data from the tests conclusively demonstrate that neither method is faster. The best would of course then be the AutoRedraw ?Refresh method, since it does not use the extra picture box to store the backbuffer in. But it also depends on the size of the drawing area you have. If it is big, then the Refresh ?Autoredraw method -might appear slightly slower than the backbuffering scheme. So the choice is really up to you, test your game with both schemes and use the one you like best.
Sprite Animations and StretchBlt
Now we know how to move a sprite around the window, but usually that is not enough to form a complete game. Sometimes there is also a need to change the actual sprite image, in order to accommodate certain conditions of a game.
The actual implement of such a scenario is actually quite simple, it is just a matter of changing the actual sprite picture. So if we had a need of a small ball Note: Will change this to a small animated character instead rotating around the screen, we would simply make each rotation in a drawing program and draw each in a specified order. Very much like a little cartoon animation block.
In the sample project ANIMATION contained in ANIMATION.ZIP we will do just that, create a ball with shifting colors. The first thing to note is that so far we have used a picture box to hold each separate bitmap. If we were to do that for each sprite in a game with several animated sprites, we would be using up quite a number of picture boxes. So instead, we will draw all the animation frames of the sprite in just one bitmap, and only draw the part of this bitmap that we need. The bitmap would look like this:
Notice the apparent black sprite, which actually represents a black circle.
We already know that the sprite is 64 pixel wide and 64 pixels high (this was of course preset when the sprites were made). We also know that we need to feed the BitBlt function with information on only the upper left corner of the sprite, and the dimensions of the sprite. So it is just a simple matter of moving this upper left point from each frame to the other. Since the image only has one row of sprites, the Y position will be constant (always 0). So the only really challenging thing is to move the X position a given distance in pixels with each new frame we blit. To accomplish we must keep an eye out for the current frame, which is to be displayed. Using a simple variable as a frame counter does this. This frame counter is updated every time the frame is changed, so it will always have a value equal to the current frame. If we also use this variable as a multiplier to the constant width of the sprite, we actually get exactly what we want.
As you can see from the illustration, then the upper-left X position of a frame is equal to the FrameNumber ?1 multiplied by the width of the sprite. So by using this scheme we move the X position by an amount which is equal to the width of the sprite, on each update of the frame.
FrameNumber = (FrameNumber Mod MaxFrames) + 1
This actually ensures that the FrameNumber variable will never be more than MaxFrames and never less than 1. The method also produces a problem, since when we use it to get the X positions we will get values in the range from 64 (Frame 1) to 640 (Frame 10). These values represent the rightmost X position of the sprites, which is of no use to the BitBlt function. The solution is of course to simply subtract 1 from the FrameNumber when we calculate the X position, and thereby getting X values in the range of 0 to 576, or all the leftmost positions of the sprite frames.
The same thing could be employed to keep the sprite from moving out of bounds of the window:
X = (X Mod Me.ScaleWidth) + 1
Y = (Y Mod Me.ScaleHeight) + 1
So now that we have all this wonderful background information, it should be simple to implement in a timer event and draw the thing:
Private Sub TimerAnimation_Timer()
Static X As Long
Static Y As Long
'Clear the form, since we do not have a background
Me.Cls
'Draw the mask
BitBlt Me.hDC, X, Y, SpriteWidth, SpriteHeight, picMask.hDC, _
(FrameNumber - 1) * SpriteWidth, 0, vbSrcAnd
'Draw the sprite
BitBlt Me.hDC, X, Y, SpriteWidth, SpriteHeight, picSprite.hDC, _
(FrameNumber - 1) * SpriteWidth, 0, vbSrcPaint
'Update frame number
FrameNumber = (FrameNumber Mod MaxFrames) + 1
'Update drawing positions
X = (X Mod Me.ScaleWidth) + 1
Y = (Y Mod Me.ScaleHeight) + 1
'Force an update of the form
Me.Refresh
End Sub
We employ the usual scheme of first drawing the mask and then the actual sprite. As you can see from the picture box with the masks, we have created a mask for all the sprites, and draw these mask frames in the same manner as the sprites. This would not be necessary in real life with this particular sprite example. Since each frame of this sprite's animation sequence is the same shape and therefore has the same transparent and visible areas we could have used the same mask for each circle.
Run the project and press the Start button. Observe how the sprite changes color as it moves down over the form.
More on Timing
This is all fine and good, but you may run into a situation where you do not want the same frame change (rate) as the timer interval. For example let's imagine that you have created a scene with some slow blinking lights which you want to blink once every second. If you were to blink the lights using the game loop, which we'll say is firing once every 20 milliseconds, the lights would blink so fast you wouldn't be able to see it. The problem here is to delay the blinking of the lamp, so it will only blink, or Blit, once each second and not every time the game loop is fired. That is why we will use the GetTickCount API, to check if a second has elapsed, and if it has, fire the changing of the lamp. The GetTickCount() function returns the number of milliseconds since Windows was started. By calling this function and using it to check the elapsed time since a frame was last updated, we can determine whether or not it is time to change the frame. For this task we need two variables, one to keep track of the current elapsed time, and one to keep track of the time since the last frame was updated. In the the ANIMATION project these are declared as LastTick (for the last time since a frame was updated) and CurrentTick (for the current time). A constant, representing the defined time we want between each frame update, is also needed. In the project the constant FrameTime is used for this. It is set to a value of 1 second.
The process of determining whether frame should be updated or not, is very simple. We first get the current time and store it in the CurrentTick variable. Then the time since the last frame update is subtracted. The result is then compared against the defined interval between the frames and if it is greater, the frame is updated. In the code if would look like this (The bold areas):
Private Sub TimerAnimation_Timer()
Static X As Long
Static Y As Long
'Clear the form, since we do not have a background
Me.Cls
'Draw the mask
BitBlt Me.hDC, X, Y, SpriteWidth, SpriteHeight, picMask.hDC, _
(FrameNumber - 1) * SpriteWidth, 0, vbSrcAnd
'Draw the sprite
BitBlt Me.hDC, X, Y, SpriteWidth, SpriteHeight, picSprite.hDC, _
(FrameNumber - 1) * SpriteWidth, 0, vbSrcPaint
'Check to see if we need to update th frame
If CurrentTick - LastTick > FrameTime Then
FrameNumber = (FrameNumber Mod MaxFrames) + 1
LastTick = GetTickCount()
End If
'Update drawing positions
X = (X Mod Me.ScaleWidth) + 1
Y = (Y Mod Me.ScaleHeight) + 1
'Force an update of the form
Me.Refresh
End Sub
If you run the sample project now, you can observe that the sprite is moved a small distance before the frame of the sprite is updated.
This is just one way to control the frame rate of a given sprite animation. You could also have used the traveled distance of the sprite to change the frame, or more commonly a user action could trigger a frame change.
We may want to use a blinking lamp as part of the demo project since we use the example and the graphical representation may be very helpful. We can also encourage them to change the value of FrameRate or add a slider control so that they can see how they can control the blinking-Burt.
StretchBlt
There is also another way of animating a sprite, which does not require extra bitmaps, but simply changes the drawn sprite directly. The function for this is the StretchBlt function, which, as the name implies, can stretch or shrink a sprite.
The StretchBlt function is very similar to the BltBit function. Both require a source and destination DC, and they both do raster operations. The difference is that StretchBlt will stretch or shrink the size of the source rectangle to fit the size of the destination rectangle. The declaration is as follows:
Declare Function StretchBlt Lib "gdi32" (ByVal hdc 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 nSrcWidth As Long, _
ByVal nSrcHeight As Long, ByVal dwRop As Long _
) As Long
With StretchBlt you can perform some tricks, that might otherwise require a new bitmap.
The Sample project STRETCHBLT found in STRETCHBLT.ZIP moves a sprite around the screen, stretching and shrinking it as it goes.
We have two picture boxes in the sample project, serving as storage for the sprites. The idea of the program is quite simple, stretch the sprite to a size of 96 pixels and then shrink it to 32 pixels while the sprite moves across the form.
Private Sub TimerStretch_Timer()
Static X As Long
Static Y As Long
'Clear the form, since we have no background
Me.Cls
If Shrinking Then
Stretch = Stretch - 2
Else
Stretch = Stretch + 2
End If
If Stretch < 32 Then Shrinking = False
If Stretch > MaxStretch Then Shrinking = True
'Stretch the sprite onto the form
StretchBlt Me.hdc, X, Y, Stretch, Stretch, picMask.hdc, 0, 0, _
SpriteWidth, SpriteHeight, vbSrcAnd
StretchBlt Me.hdc, X, Y, Stretch, Stretch, picSprite.hdc, 0, 0, _
SpriteWidth, SpriteHeight, vbSrcPaint
X = (X Mod Me.ScaleWidth) + 2
Y = (Y Mod Me.ScaleHeight) + 2
. Force update of the form
Me.Refresh
End Sub
The first thing that is done in code is to check the stretching variable. This variable can be in one of two states, Shrinking or Stretching. To identify the different states we'll create a Boolean variable named Shrinking and set it to either True of False, depending on the desired state. The Stretch variable is allowed to be either 32 pixels less or more than the original sprite size, if they get out of this range, the state is changed, and the opposite stretch action will be used.
The sprite is drawn in the usual way, first the mask and then the sprite. But instead of using the BitBlt function we call the StretchBlt function, and set the destination width and height to the value of the stretch variable.
As you can observe, the StretchBlt can be a useful function for doing simple tricks on a sprite. You should use it cautiously though, since it may be a little slower than the ordinary BitBlt function, since some cycles are used when it stretches or shrinks an image.
Using and creating memory DCs
The follwing section has been updated to reflect bug fixes.
In the previous version the bitmap handle was deleted in the GenerateDC function. This apparently cause some leaks as the created DC was not deleted correctly. Furthermore there were some wrong constant declaration and a error check which was faulty.
So far we have been using picture boxes as storage areas for the sprites. This does not come without a price since the picture box adds additional time and resource overhead to the animating scheme. This problem can be overcome by simply creating our own Device Contexts to hold the bitmaps. These Device Contexts reside in memory and allow us to perform the required operations without resorting to PictureBoxes. Before doing anything, some more specific information is required on what a Device Context is, and what can be done with it.
A Device Context is a Windows structure, with several important and useful attributes. Each of these attributes has a default value, which is set when the device context is created. The most important attribute of device context to us, is the Bitmap attribute. This attribute is initially set to nothing (meaning there is no bitmap associated with the device context). This attribute can be set by using the SelectObject API function.
To create a memory device context we use the API function CreateCompatibleDC, which returns a memory DC (a long value). In order to select the bitmap into the device context we need a handle to the specific bitmap. This handle can be obtained by using the LoadImage function. This function can load a bitmap from file, and return a handle to the loaded bitmap. The last thing to do is to select the handle of the loaded bitmap into our newly created memory DC, and we now have a useable device context for blitting.
The MEMORYDC sample, in the MEMORYDC sub-directory of the Chap1 directory, demonstrates the steps we have just outlined and will be exploring next, in creating a compatible device context and selecting a bitmap.
Let's put all of the required code into a reusable function which will take care of the creation of a memory device context for us and just requires a filename to the actual bitmap. The function will either return a device context (long value) or 0 if something went wrong.
The code for our function looks like this:
Public Function GenerateDC(FileName As String, ByRef MemDC As Long, ByRef hBitmap As Long) As Long
'Create a Device Context, compatible with the screen
MemDC = CreateCompatibleDC(0)
If MemDC = 0 Then
GenerateDC = 0
Exit Function
End If
'Load the image
hBitmap = LoadImage(0, FileName, IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE Or LR_LOADFROMFILE Or _ LR_CREATEDIBSECTION)
If hBitmap = 0 Then 'Failure in loading bitmap
DeleteDC MemDC
Exit Function
End If
'Throw the Bitmap into the Device Context
SelectObject MemDC, hBitmap
'Return OK
GenerateDC = 1
End Function
A device context created with the CreateCompatibleDC must be deleted by calling the DeleteDC API function and the Bitmap handle returned from LoadImage much be deleted with the DeleteObject API. So let's create a reusable function for this called DeleteGeneratedDC. This function takes two arguments, a DC to be deleted and the Bitmap Handle for the corresponding DC.
Private Function DeleteGeneratedDC(hBitmap As Long, MemDC As Long) As Long
DeleteGeneratedDC = DeleteDC(MemDC)
DeleteObject hBitmap
End Function
Run the sample project. Press the Load bitmap button. The bitmaps are now loaded and ready for use. We use the usual BltBit function to Blit them from the memory context and into the device context of the form. Press the Draw the sprite button, and observe how the sprite is blitted transparently onto the form.
One word of advice when using this scheme to create a memory device context: Be observant of the scope of the variables which you store the device contexts in. If the variable goes out of scope, and you have not deleted it with the DeleteDC function, you抣l start losing resources. So always delete the created device contexts to be on the safe side.
End of Part II of the three part Drawing and Animation Tutorial by Burt Abreu & S鴕en Skov. Download all Samples]