AnimationThe first thing we should do is to think about what we want to happen when the player presses an arrow key. You may be thinking, "Oh, that's simple: the character should walk from one tile to the next," but that is not always the case. In pixel by tile (PxT) scrolling, this is what happens. As any movement key is pressed, the character walks from one tile to the next. An alternate method is called pixel by pixel (PxP) scrolling, and allows you a bit more freedom, because as soon as the player releases the arrow key, the character stops moving, even if he's between two tiles. The good part about PxP scrolling is that it can take away from the feeling that your character is locked into a grid, especially if you also employ eight-directional movement. The bad part is that it makes collision detection a bit tricky. I'm not going to cover PxP scrolling here, since it's a good idea to get PxT up and running first so you can get a feel for creating a simple game engine, but think about it... it does require a bit more thought, but once you've come up with a good way to handle it, it's easily worth a little extra effort. All right, so we're going to move one tile at a time. Our animation code checks if animation is necessary by simply looking at the The only thing that even deserves any thought is to consider which frames are displayed when. We have three frames of animation for each direction: standing still, right foot forward, and left foot forward. This animation should play in the sequence: 0, 1, 0, 2, 0, 1, 0, 2, etc. So it doesn't look like he's taking ridiculously small steps, we'll say that a movement from one tile to the next is done in four steps: 0 to 1, 1 to 0, 0 to 2, 2 to 0. This will actually appear as two full steps on the screen. To set a reasonable character speed, let's shift the character by four pixels every frame. So a movement from one tile to the next will look like this:
Hopefully this will clear things up a bit. The leftmost column is simply a numbering system for game frames -- these are happening at about 30 per second. The middle column represents To further demystify things, let's get some code down that implements the table above. What needs to happen? First, the code should check if movement in a given direction is occurring. Second, the offset should be updated. Third, the code should check to see if the character has finished moving from one tile to the next. If so, reset the movement variable. We could write this in two cases, one for horizontal movement and one for vertical, but just in the interest of keeping things easy to understand, I'm going to split it into four instead, one for each direction. I'm also going to replace player.move with simply move, because eventually this will be a function that gets used for players and NPCs. Let's see how it goes. // first check for movement south if (move.yMove > 0) { move.yOffset += 4; if (move.yOffset == 0) move.yMove = 0; } // now check north if (move.yMove < 0) { move.yOffset -= 4; if (move.yOffset == 0) move.yMove = 0; } // check movement east if (move.xMove > 0) { move.xOffset += 4; if (move.xOffset == 0) move.xMove = 0; } // check movement west if (move.xMove < 0) { move.xOffset -= 4; if (move.xOffset == 0) move.xMove = 0; } This takes care of moving the player around from step to step, and so there's only one more step remaining: we still have to draw the player on the screen. The only thing that's not obvious about this is how to find the coordinates at which to draw the player. After all, he won't always be at the center of the screen. The answer is simply to calculate his coordinates in terms of world coordinates, and then subtract the camera coordinates from them to get the final rendering position. Then we just get the correct frame like we discussed earlier, and voila, we've finally got what we want! Here's the code: // set up a default source RECT RECT rcSrc = {0, 0, 32, 64}, rcDest; int nFrame; // first figure out which frame to use nFrame = move.nFace; if (((move.xMove) && ((abs(move.xOffset) == 4) || (abs(move.xOffset) == 8))) || ((move.yMove) && ((abs(move.yOffset) == 4) || (abs(move.yOffset) == 8)))) nFrame++; if (((move.xMove) && ((abs(move.xOffset) == 20) || (abs(move.xOffset) == 24))) || ((move.yMove) && ((abs(move.yOffset) == 20) || (abs(move.yOffset) == 24)))) nFrame += 2; // update rcSrc to correct frame rcSrc.left += (nFrame<<5); rcSrc.right += (nFrame<<); // set up rcDest rcDest.left = (move.xTile<<5) + move.xOffset - mapdata.xCamera; rcDest.top = (move.yTile<<5) + move.yOffset - mapdata.yCamera - 32; rcDest.right = rcDest.left + 32; rcDest.bottom = rcDest.top + 64; // blit sprite lpddsBack->Blt(&rcDest, lpddsCharacters, &rcSrc, DDBLT_WAIT | DDBLT_KEYSRC, NULL); The only part of this code that might strike you as odd is that I'm subtracting 32 from the y-value of the character. Remember, that's because we calculated the screen location of the tile on which the character is standing... but the character is two tiles tall, so we need to plot him one tile above his actual location to make sure he's standing in the right place. Now the character responds to arrow keypresses by walking in the appropriate direction. The animation follows exactly what we set up, and the map scrolls to center the character when he's not near the map's edge. About bloody time, hey? :) He's probably going to get tired of wandering around all by himself though, so let's throw some NPCs in there. We've actually done most of the work already... |