|
Drawing & Animation
Using the Win32 GDI #2
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 copy 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, 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, 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
CurrentTick = GetTickCount()
'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 the form to update
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, 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
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) As Long
Dim DC As Long
Dim hBitmap As Long
'Create a Device Context, compatible with the screen
DC = CreateCompatibleDC(0)
If DC < 1 Then
GenerateDC = 0
Exit Function
End If
'Load the image....
'BIG NOTE: This function is not supported under NT, there you can not
'specify the LR_LOADFROMFILE flag
hBitmap = LoadImage(0, FileName, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE Or LR_CREATEDIBSECTION)
If hBitmap = 0 Then 'Failure in loading bitmap
DeleteDC DC
GenerateDC = 0
Exit Function
End If
'Assign the Bitmap to the Device Context
SelectObject DC, hBitmap
'Return the device context
GenerateDC = DC
'Delete the bitmap handle object
DeleteObject hBitmap
End Function
Notice that the bitmap handle
returned from the LoadImage function is deleted after we are done
using it with a call to the DeleteObject API function.
This ensures that we release the resources used when loading a
bitmap into memory.
A device context created with
the CreateCompatibleDC must be deleted by calling the DeleteDC
API function. So let's create a reusable function for this called
DeleteGeneratedDC. This function takes one argument, a DC
to be deleted.
Private Function DeleteGeneratedDC(DC As Long) As Long
If DC > 0 Then
DeleteGeneratedDC = DeleteDC(DC)
Else
DeleteGeneratedDC = 0
End If
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, youll start losing
resources. So always delete the created device contexts to be on
the safe side.
[Drawing & Animation #1] [Drawing & Animation #2] [Drawing & Animation #3] [Download All Samples]
These tutorials were originally developed by
Soren Christensen and Burt Abreu as part of a book which we were working on.
The book idea didn't come to fruition and so we decided to post the completed
chapters here in the hopes that you would find them useful. We retain copyright
to this material and you may not reproduce it, post it, or otherwise disseminate
it in any fashion without our express written consent with the exception of making
copies for your personal use.
|