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:
- It should have vehicles enter from the top right which proceed to the
left and from the bottom left which proceed to the right.
- It should not allow the vehicles to have individual velocities, i.e. all
vehicles have the same speed.
- It should allow the user to control the density of the traffic via the
keyboard.
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.
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
#define SIT_CAR 0x0001L
#define SIT_TRUCK 0x0002L
#define SIT_GOESLEFT 0x0004L
#define SIT_GOESRIGHT 0x0008L
#define SIS_UNUSED 0L
#define SIS_VISIBLE 1L
#define SIS_ENTERTOP 2L
#define SIS_EXITTOP 3L
#define SIS_ENTERBOTTOM 4L
#define SIS_EXITBOTTOM 5L
typedef struct _SPRITEINFO {
HSPRITE hsSprite;
SIZEL szlSize;
ULONG ulType;
ULONG ulState;
} SPRITEINFO, *PSPRITEINFO;
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).
SIS_UNUSED not currently being used
SIS_VISIBLE on screen
SIS_ENTERTOP entering on the top half of the road
SIS_EXITTOP exiting on the top half of the road
SIS_ENTERBOTTOM entering on the bottom half of the road
SIS_EXITBOTTOM exiting on the bottom half of the road
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
- 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".
- 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".
- 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; ulIndex<MAX_VEHICLES; ulIndex++) {
psiSprite=&pidData->asiSprites[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.