Sprites and Animation Part 3

Written by Larry Salomon Jr.

Introduction
In the final part of this series, we will wrap things up by looking at how I495.EXE was built. This will hopefully get you on your way to using animation within your own applications, although it should be noted that no article or series of articles will ever be able to give an exhaustive treatment of this topic. And when there are applications like Autodesk and Renderman available (albeit expensive), hard-coded animations are rare.

Capabilities
Before we can begin designing, we have to know what it is that we are designing - what will the application do, how much of the behavior can be controlled by the user, etc. Fortunately, I495.EXE allows for little control by the user, so the majority of the application is deterministic, i.e. we can predict what will happen with high accuracy (this statement is a bit inaccurate depending on how it is interpreted, since we use random number generators). Enumerated below are the intended capabilities of the application: Now that we have a good idea of the capabilities of the application, we can begin the design with the goals that we have set in mind.
 * 1) It should have vehicles enter from the top right which proceed to the left and from the bottom left which proceed to the right.
 * 2) It should not allow the vehicles to have individual velocities, i.e. all vehicles have the same speed.
 * 3) It should allow the user to control the density of the traffic via the keyboard.

Design
Because we have an easy to use "create sprite" function, it is easier for us to create two bitmaps for each vehicle type, one for each direction, and to create two sprites from these bitmaps. Although this is not a good design policy, by knowing which bitmap id's correspond to which vehicle type and direction, we can build a table of sprite information which contains the following information:
 * the sprite handle
 * the direction that the sprite should travel
 * the vehicle type
 * ...plus other information such as sprite size

The corresponding data structure for this is typedef struct _SPRITEINFO { HSPRITE hsSprite; SIZEL szlSize; ULONG ulType; ULONG ulState; } SPRITEINFO, *PSPRITEINFO;
 * 1) define SIT_CAR                 0x0001L
 * 2) define SIT_TRUCK               0x0002L
 * 3) define SIT_GOESLEFT            0x0004L
 * 4) define SIT_GOESRIGHT           0x0008L
 * 1) define SIS_UNUSED              0L
 * 2) define SIS_VISIBLE             1L
 * 3) define SIS_ENTERTOP            2L
 * 4) define SIS_EXITTOP             3L
 * 5) define SIS_ENTERBOTTOM         4L
 * 6) define SIS_EXITBOTTOM          5L

The explanation of the SPRITEINFO structure follows:
 * hsSprite:handle of the sprite.
 * szlSprite:size of the sprite. This is used to determine the value of ulState.
 * ulType:type of the sprite and direction it travels (SIT_* constant).
 * ulState:state of the sprite (SIS_* constant).

Animation
The animation itself is nothing more than good management of an array of SPRITEINFO structures from within a WM_TIMER message. The pseudocode for the logic is


 * 1) If nothing is entering on top and a random number is 0, find an unused sprite that goes from right to left and set its state to "enter on top".
 * 2) If nothing is entering on bottom and a random number is 0, find an unused sprite that goes from left to right and set its state to "enter on bottom".
 * 3) For all sprites that are not marked as "unused", increment or decrement their horizontal position as appropriate, and update their status to reflect if they are entering, on screen, exiting, or no longer visible ("unused").

Once you understand this, you should have no trouble interpreting the corresponding code below: case WM_TIMER: switch (SHORT1FROMMP(mpParm1)) { case TID_TRAFFIC: {         HPS hpsWnd; PSPRITEINFO psiSprite; POINTL ptlPos; ULONG ulIndex; hpsWnd=WinGetPS(hwndWnd); psiSprite=findSprite(pidData->asiSprites,SIS_ENTERTOP,0); if ((psiSprite==NULL) && (rand%pidData->lMod==0)) { psiSprite=findSprite(pidData->asiSprites,                                 SIS_UNUSED,                                  SIT_GOESLEFT); if (psiSprite!=NULL) { psiSprite->ulState=SIS_ENTERTOP; ptlPos.x=pidData->szlPlay.cx; ptlPos.y=pidData->szlPlay.cy/2+40; SprSetSpritePosition(hpsWnd,psiSprite->hsSprite,&ptlPos); SprSetSpriteVisibility(hpsWnd,psiSprite->hsSprite,TRUE); } /* endif */ } /* endif */ psiSprite=findSprite(pidData->asiSprites,SIS_ENTERBOTTOM,0); if ((psiSprite==NULL) && (rand%pidData->lMod==0)) { psiSprite=findSprite(pidData->asiSprites,                                 SIS_UNUSED,                                  SIT_GOESRIGHT); if (psiSprite!=NULL) { psiSprite->ulState=SIS_ENTERBOTTOM; ptlPos.x=(-psiSprite->szlSize.cx); ptlPos.y=pidData->szlPlay.cy/2-60; SprSetSpritePosition(hpsWnd,psiSprite->hsSprite,&ptlPos); SprSetSpriteVisibility(hpsWnd,psiSprite->hsSprite,TRUE); } /* endif */ } /* endif */ for (ulIndex=0; ulIndexasiSprites[ulIndex]; if (psiSprite->ulState==SIS_UNUSED) { continue; } /* endif */ SprQuerySpritePosition(psiSprite->hsSprite,&ptlPos); if ((psiSprite->ulType & SIT_GOESLEFT)!=0) { ptlPos.x-=DELTA_X; if (ptlPos.x+psiSprite->szlSize.cx<0) { psiSprite->ulState=SIS_UNUSED; } else if (ptlPos.x<0) { psiSprite->ulState=SIS_EXITTOP; } else if (ptlPos.x+psiSprite->szlSize.cx>pidData->szlPlay.cx) { psiSprite->ulState=SIS_ENTERTOP; } else { psiSprite->ulState=SIS_VISIBLE; } /* endif */ } else { ptlPos.x+=DELTA_X; if (ptlPos.x>pidData->szlPlay.cx) { psiSprite->ulState=SIS_UNUSED; } else if (ptlPos.x+psiSprite->szlSize.cx>pidData->szlPlay.cx) { psiSprite->ulState=SIS_EXITBOTTOM; } else if (ptlPos.x<0) { psiSprite->ulState=SIS_ENTERBOTTOM; } else { psiSprite->ulState=SIS_VISIBLE; } /* endif */ } /* endif */ if (psiSprite->ulState!=SIS_UNUSED) { SprSetSpritePosition(hpsWnd,psiSprite->hsSprite,&ptlPos); } else { SprSetSpriteVisibility(hpsWnd,psiSprite->hsSprite,FALSE); } /* endif */ } /* endfor */ WinReleasePS(hpsWnd); }      break; default: return WinDefWindowProc(hwndWnd,ulMsg,mpParm1,mpParm2); } /* endswitch */ break;

You should notice the reference to pidData->lMod in the determination of whether a sprite is to begin entering the display area. This is a changing value which can be adjusted using the "+" and "-" keys; the code is trivial WM_COMMAND processing with the two keys mapped to accelerators.

The rest of the code is, honestly, initialization and termination processing (other than the "standard" PM code). Everything you need to compile I495.EXE is provided as I495.ZIP, which can be found in sources 0207.zip

Summary
This month we said our farewells to this series by examining the animation in a simple application - I495.EXE. And while it was said earlier that hard-coded animations are rare, I would like to conclude that, although rare, a good animation sequence can spruce up any "About box" (or any other part of an application) and is well worth the effort of coding it.

Any questions can be sent directly to me via email at the address listed at the end of this issue.