Part Overview
Step 4: Acquire the Device
5.3 Sprites and Animation
5.3.3 Page Flipping Animation
// Turn on the alpha test.
HR(gd3dDevice - >SetRenderState(D3DRS_ALPHATESTENABLE, true));
// Set ship's orientation.
D3DXMATRIX R;
D3DXMatrixRotationZ(&R, mShipRotation);
HR(mSprite - >SetTransform(&R));
// Draw the ship.
HR(mSprite - >Draw(mShipTex, 0, &mShipCenter, 0, D3DCOLOR_XRGB(255, 255, 255)));
HR(mSprite - >Flush());
// Turn off the alpha test.
HR(gd3dDevice - >SetRenderState(D3DRS_ALPHATESTENABLE, false));
}
Recall that in §5.3.2.7, we set some alpha test render states, and also gave an explanation of what the alpha test does for us. Although we set some properties of the alpha test previously, we did not actually enable it. So the very first thing we do before we draw the ship sprite is enable the alpha test.
Although we do not explicitly translate the ship (we translate the background in the opposite direction instead), we do explicitly rotate the ship. This is accomplished by applying a rotation transformation based on the current angle the sprite makes with the y-axis.
Finally, we draw the ship sprite and then disable the alpha test because we are done with it.
5.3.2.14 drawBullets
The process of drawing the bullet sprites is similar to drawing the ship sprite:
void SpriteDemo::drawBullets() {
// Turn on alpha blending.
HR(gd3dDevice - >SetRenderState(D3DRS_ALPHABLENDENABLE, true));
// For each bullet...
std::list<BulletInfo>::iterator i = mBulletList.begin();
while( i != mBulletList.end() )
HR(gd3dDevice - >SetRenderState(D3DRS_ALPHABLENDENABLE, false));
}
The first thing to note is that we do not use alpha testing with the bullets; instead, we use alpha blending. Both techniques require an alpha channel, but unlike the alpha test, which simply accepts or rejects pixels, alpha blending blends the texture image pixels with the back buffer pixels on which the textured sprite is drawn.
This is useful for effects (such as gunfire, laser beams, and smoke) that are not completely opaque.
In alpha blending, the alpha channel pixels can be thought of as weights that specify how much color to use from the texture image (source color) and from the back buffer pixels (destination color) when generating the new back buffer pixel color. Alpha pixels close to white weight the source pixels more heavily, and alpha pixels close to black weight the destination pixels more heavily. For example, pure white uses 100% source color, and pure black uses 100% destination color.
Thus, if you do not want to overwrite parts of the back buffer, then specify black in the alpha channel for those pixels (sort of like alpha testing).
Now, once alpha blending is enabled, to draw the bullets we simply loop through each one in the list, set its rotation and translation matrix to orient and position it, and then draw it. Lastly, before exiting the method, we disable alpha blending.
5.3.3 Page Flipping Animation
In the previous section, we animated the graphics by changing their positions slightly every frame. By changing an object's position by small increments over time, a continuous motion is observed. Another way we can carry out animation is by using a page flipping technique. The idea is to have an artist generate the animation as a sequence of images (or frames of animation). Then, by displaying the frames (which differ only slightly) in rapid succession, the still frames are brought to life to produce an animation. For games, this animation technique is particularly useful for effects like explosions or fire, as shown in Figure 5.9.
Figure 5.9: Each frame of animation is stored in the texture (including alpha info). After a small amount of time has passed, we increment to the next frame in the animation.
Note We call a texture that contains several sub-textures, like Figure 5.9, a texture atlas.
As far as implementing this technique goes, the basic idea is to store all the frames representing the animation in one texture file. We start by using the first frame, then after a (small) defined time period elapses, we increment to the next frame. For example, if we wanted the animation to play at 20 frames per second, we'd increment to the next texture every time 0.05 seconds elapsed, until the animation is completed.
Recall that the second parameter, pSrcRect , of ID3DXSprite::Draw , allows us to specify a rectangular subset of a texture to use for the sprite. Previously, we always wanted to use the whole texture, and so specified null. However, in our present situation, we have a whole bunch of frame images on one texture, so we will want to use this parameter and specify the rectangle of the particular frame we want to use at a given time. In Figure 5.9, suppose that each frame has dimensions of 64×64 pixels. Since there are five rows and six columns, the entire texture has dimensions of 384×320 pixels. The rectangle corresponding to the frame at position row i and column j is given by:
Rij = {(j)·64, (i)·64, (j+1)·64,(i+1)·64}
where i and j are zero-based indices; where we are using screen coordinates (i.e., +y-axis goes down); and the rectangle components are given in the format Rij = { left, top, right, bottom}. The following code illustrates the animation process (it is based on animating Figure 5.9):
Initialization:
// Keep internal data member to keep track of current // animation frame.
mCurrFrame = 0;
For each game loop cycle, update as follows:
// Keep track of how much time has accumulated.
For each game loop cycle, render as follows:
// Compute rectangle on texture atlas of the current frame
HR(gd3dDevice - >SetRenderState(D3DRS_ALPHABLENDENABLE, true));
// Don't move explosion - - set identity matrix.
D3DXMATRIX M;
D3DXMatrixIdentity(&M);
HR(mSprite - >SetTransform(&M));
HR(mSprite - >Draw(mFrames, &R, &mSpriteCenter, 0, D3DCOLOR_XRGB(255, 255, 255)));
HR(mSprite - >End());
// Turn off alpha blending.
HR(gd3dDevice - >SetRenderState(D3DRS_ALPHABLENDENABLE, false));
Warning Your sprite texture dimensions should be powers of 2; that is, 2n for some n. Otherwise, the sprites will be scaled to powers of 2 by the
D3DXCreateTextureFromFile function, which can mess up some assumptions. Therefore, in the implementation of animating Figure 5.9, even though we only need a 384×320 texture, we augment the texture to 512×512 and waste space. In practice, you would not want to waste this space, but for a demo it is not anything to worry about. Even so, we can prevent D3DX from scaling our texture if we use the
D3DXCreateTextureFromFileEx function and specify the D3DX_DEFAULT_NONPOW2 flag . Nonetheless, this extended function is too complicated to get into at this point, and therefore, we stick to our current plan of augmenting the texture to 512×512 and wasting space.
Sometimes, it is useful to loop the animation; that is, once it reaches the last texture, it loops back to the beginning. This can be used for continuously animating things that roughly repeat themselves after some short time period — like waves or fire. Note that the animation must be designed for looping so that the last frame coincides with the first frame; otherwise, a discontinuity in the animation will occur.
One thing to watch out for with this technique is memory requirements. Just 30 256×256 24-bit uncompressed images require close to 6 megabytes of memory (a significant amount of memory for a small effect). Moreover, a 30-frame animation only can be made to last around a second, as you need to change the frames at around 24 to 30 frames per second for the animation illusion to occur. Therefore, this technique is best used with small images, such as 64×64 bitmaps, and for short animations like explosions that last less than a second or with short animations that can be lengthened by looping. It is worth noting, however, that Direct3D supports a compressed texture format, which can save a lot of memory. This compressed texture format will be covered in Chapter 11.
For brevity, we do not embed the code here, but we have heavily annotated a demo called PageFlipDemo, which implements the page flipping animation technique.
A screenshot is provided in Figure 5.10.
Figure 5.10: A screenshot of the Page Flip demo. The demo may appear grainy since we blow the texture up (i.e., magnify it) for this demo. Ideally, for best quality, the texture pixel dimensions should match the screen pixel dimensions onto which the texture gets mapped so that distortion is
minimized.
5.4 Summary
The performance counter is a high-resolution timer that provides accurate timing measurements needed for measuring small time differentials, such as the time elapsed between frames. The performance timer works in time units called counts. The QueryPerformanceFrequency outputs the counts per second of the performance timer, which can then be used to convert from units of counts to seconds. The current time value of the performance timer (measured in counts) is obtained with the QueryPerformanceCounter function.
To compute the frames per second (FPS), we count the number of frames processed over some time interval ∆t. Let n be the number of frames counted over time ∆t, then the average frames per second over that time interval is fpsavg = n/∆t. The frame rate can give misleading conclusions about performance; the time it takes to process a frame is more informative. The amount of time, in seconds, spent processing a frame is the reciprocal of the frame rate, i.e., 1/fpsavg. We can convert that time value from seconds to milliseconds by multiplying it by 1000.
Direct Input allows us to bypass the message system and work directly with the input drivers, thereby making it more efficient. To use Direct Input, you need to #include <dinput.h> and link dinput8.lib. IDirectInput8 is used to enumerate input devices on the system and for obtaining pointers to IDirectInputDevice8 objects. The IDirectInputDevice8 interface is our software representation of a physical input device (e.g., mouse, keyboard, joystick) and allows us to communicate with that input device. We use the term polling to refer to the process of taking a snapshot of the current state of the input device (e.g., is a key/button pressed, how far has the mouse moved since the last time we polled).
A sprite refers to a small 2D image. D3DX represents a sprite with the ID3DXSprite interface; a pointer to an object of this type may be obtained with the D3DXCreateSprite function.
We can animate our game objects by changing their position, orientation, and size in small increments over time; this provides the illusion of smooth continuous motion. Another technique is to rapidly display the frames of a pre-made animation in rapid succession, as an animator would do with a flipbook.
5.5 Exercises
1. You can modify the z-component of the sprites to change their depth, thus making them smaller or bigger. Recall the following code, from onResetDevice, that sets up the camera:
D3DXMATRIX V;
D3DXVECTOR3 pos(0.0f, 0.0f, - 1000.0f);
D3DXVECTOR3 up(0.0f, 1.0f, 0.0f);
D3DXVECTOR3 target(0.0f, 0.0f, 0.0f);
D3DXMatrixLookAtLH(&V, &pos, &target, &up);
HR(gd3dDevice - >SetTransform(D3DTS_VIEW, &V));
Here pos positions the camera -1000 units on the z-axis. Thus, to make a sprite bigger (closer to the camera), you would move its depth value toward -1000. Experiment by changing the depth of the ship's position. Also experiment by changing the z-coordinate of pos to zoom the camera in and out.
2. The Sprite demo used our Direct Input class for keyboard input only. Write a program that changes the z-coordinate of the camera (see exercise 1) based on mouse movement along the y-axis.
3. Create or find some free images of alien spaceships. Modify these images by inserting an alpha channel and marking the pixels that are to be displayed. Then modify the Sprite demo by using these modified images to include enemy ship sprites.
4. Modify the Sprite demo again by defining a motion path for the enemy ships. You might have them move along a line back and forth, follow the player-controlled ship, or something else of your own choosing.
5. With the techniques described in this chapter, you can implement many old classic 2D arcade games, such as Pong, Asteroids, Space Invaders, and many others. Try to design and implement a Pong and Asteroids clone.