Drawing & Animation II

原创 2002年03月08日 08:58:00

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:

showMedium.asp?58_LCID1033

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.

showMedium.asp?59_LCID1033

 

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:

showMedium.asp?60_LCID1033

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.

 

showMedium.asp?61_LCID1033

 

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.

showMedium.asp?62_LCID1033

 

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]

Drawing Line Animation

REFS: http://www.soundstep.com/blog/2008/05/08/line-bitmap-drawing-with-tweener-bezier/ http://w...
  • zlxadhkust
  • zlxadhkust
  • 2011年10月05日 23:06
  • 310

Drawing & Animation I

Drawing & AnimationUsing the Win32 GDI #1 This three part tutorial first appeared some years ago on ...
  • sonicdater
  • sonicdater
  • 2002年03月08日 08:58
  • 681

Drawing & Animation III

Drawing & AnimationUsing the Win32 GDI #3 This three part tutorial first appeared some years ago on ...
  • sonicdater
  • sonicdater
  • 2002年03月08日 08:58
  • 679

改进的《Combining Sketch and Tone for Pencil Drawing Production》铅笔画算法

本文实现的铅笔画算法,基于论文《Combining Sketch and Tone for Pencil Drawing Production》算法实现。不过有些细节并没有完全按照论文算法实现,而是根...
  • u013085897
  • u013085897
  • 2017年02月04日 21:04
  • 1070

Combining Sketch and Tone for Pencil Drawing Production的优化过程

Combining Sketch and Tone for Pencil Drawing Production, 铅笔画生成算法的优化过程。
  • qingshan0201
  • qingshan0201
  • 2016年04月25日 23:08
  • 2123

Allegro PCB 的Drawing Option在16.6的位置

分到几个地方:display——status和setup——design parameters
  • sdvch
  • sdvch
  • 2013年04月23日 14:02
  • 4947

View too large to fit into drawing cache, needs 6400000 bytes, only 3686400 available

View too large to fit into drawing cache, needs 6400000 bytes, only 3686400 available
  • a31081314
  • a31081314
  • 2017年01月03日 11:26
  • 742

基于《Combining Sketch and Tone for Pencil Drawing Production》的图像铅笔画算法的实现

一,借鉴: 本文借鉴了CSDN博主风吹夏天对此论文算法的理解:风吹夏天的图像铅笔画算法,以及香港中文大学Cewu Lu等人写的该论文的主页。原文作者和博主风吹夏天都给过代码,但是代码不全。我仔细看了原...
  • qibofang
  • qibofang
  • 2016年05月23日 19:09
  • 5109

arcgis的One or more layers failed to draw: 问题

症状:One or more layers failed to draw: PSA: Shapefile is corrupted. Drawing aborted."   解决方案:I wo...
  • hailiannanhai
  • hailiannanhai
  • 2011年03月23日 13:49
  • 7725

iOS的Drawing

14.1 Quartz概述 14.2 绘制基本几何图形 14.3 绘制图像和文本 14.4 坐标 14.5 变换 14.6 图像拾取器   14.1 Quartz概述 Quartz是...
  • yinxianwei88
  • yinxianwei88
  • 2014年03月07日 10:17
  • 1092
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Drawing & Animation II
举报原因:
原因补充:

(最多只允许输入30个字)