In our previous post we looked at the individual animations that make up the movements of our main character. One thing you may have noticed is that the sprite sheet I'm using has actually been scaled up to 200%. I'm not sure if that was done in the original game or by the person who extracted the graphics. I've left it as is as this is just to demonstrate the techniques and with todays high resolutions screens we would probably want to use higher quality images anyway.
There are however two key observations we can make from the sprites and the animations:
- Each animation should play in full and is then followed up by the next animation to create fluid motion. Some animations are split into multiple ones. Walking for instance has 3 animations, taking the first step, stopping at the end of the first step and continuing with the 2nd step. Play the first and 3rd in a repeated sequence and our character keeps on walking. If our character needs to stop moving after the first animation we need to play the second one to round the animation off, but we can stop on a dime if we're stopping after the 2nd step.
- If the character moves, the character moves a fixed amount. The movement
is always a multiple of 32 (or likely 16 in the original game).So after taking one step (1st + 2nd animation) we'll have moved 32
pixels. If we take two steps (1st + 3rd animation) we'll have moved 64
pixels. I've not checked the animation for running yet but my guess is
the movement will be either double or triple that.
Similarly climbing or falling we're moving up (or down) 96 pixels (3 * 32).
The one animation that stands out is rolling but I think that is caused by either missing a part of the animation or that I've gotten something wrong (I am trying to reverse engineer the animations used here). I've left it in the source code for now but I will likely remove it once we go to the next part to ensure we only have animations that work with our background.
Going from one animation to the next
So looking at our animations it is important to realize that only certain animations follow others and the animation that should follow is controlled by two other parameters:- What is allowed, i.e. we can't walk left if there is a wall
- What input is given by the user
As far as making our engine aware of the keyboard input I've taken out my temporary keyboard interface from the previous part and exposed the glfwGetKey method through a call back. Remember I didn't want to put any GLFW specific code in the game engine itself. I'm not 100% sure I'm going to keep this working the way it does right now but it will suffice for the time being. This function simply checks if a specific key is currently being pressed. Unlike our previous example we're not reacting on the keypress itself but on the current state of the keyboard. GLFW should simply be keeping a keymap of the keys currently pressed so this check should be pretty quick.
Instead of programming all the individual keypresses and reactions to them we're going to use a lookup table with a state engine. A state engine is nothing more then a fancy word for defining in what state our character currently is in i.e. if he's standing still, walking, jumping, facing left or right, etc.
Our state engine has three variables that track it:
- currentAnim, the current animation playing, which is equal to what our character is doing. This relates directly to our animation enumeration we already introduced in the last post.
- currentSprite, the current sprite within the animation that is currently being displayed.
- currentPos, the current position within our game world our character occupies.
This lookup table is defined using the following struct:
// structure to control our characters with typedef struct action_map { GLint animationEnding; // when this animation ends GLint startAnimation; // start this animation int keys[5]; // if these keys are pressed (ignoring zeroes) } action_map;
As we can see our action lookup table is defined by the animation that has just finished playing, the animation that should follow and up to 5 keys that the user should be pressing to make this happen.
We'll be checking our table top down and the first entry that matches our current state wins.
Here is a part of the table:
// and define our action map, the first action that matches defines our next animation action_map conradActions[] = { // looking left CR_ANIM_LOOK_LEFT, CR_ANIM_TURN_LEFT_TO_RIGHT, { GLFW_KEY_RIGHT , 0, 0, 0, 0}, // turn left to right if our right key is pressed CR_ANIM_LOOK_LEFT, CR_ANIM_WALK_LEFT_1, { GLFW_KEY_LEFT, 0, 0, 0, 0}, // start walking right CR_ANIM_LOOK_LEFT, CR_ANIM_LOOK_LEFT, { 0, 0, 0, 0, 0}, // keep looking left if no key is pressed CR_ANIM_TURN_LEFT_TO_RIGHT, CR_ANIM_LOOK_RIGHT, { 0, 0, 0, 0, 0}, // turn to looking right once finished // walking left CR_ANIM_WALK_LEFT_1, CR_ANIM_WALK_LEFT_2, { GLFW_KEY_LEFT, 0, 0, 0, 0}, // keep walking left CR_ANIM_WALK_LEFT_1, CR_ANIM_STOP_WALK_LEFT, { 0, 0, 0, 0, 0}, // stop walking left if no key is pressed CR_ANIM_WALK_LEFT_2, CR_ANIM_WALK_LEFT_1, { GLFW_KEY_LEFT, 0, 0, 0, 0}, // keep walking left CR_ANIM_WALK_LEFT_2, CR_ANIM_LOOK_LEFT, { 0, 0, 0, 0, 0}, // finished walking left if no key is pressed CR_ANIM_STOP_WALK_LEFT, CR_ANIM_LOOK_LEFT, { 0, 0, 0, 0, 0}, // finished walking left if no key is pressed
Now this is only a small section of our table but lets look at these entries more closely to try and explain how this works.
The first entry applies when our current animation is CR_ANIM_LOOK_LEFT which is a single frame animation of our character looking left. The animation that follows is CR_ANIM_TURN_LEFT_TO_RIGHT which is an animation that makes the character turn around. The key listed is GLFW_KEY_RIGHT which means this animation is triggered if the user is pressing the right arrow key. Note that we don't care about any other keys but also that we don't check any other keys. I.e. if they user is pressing the right and up key this is still the first entry that matches and this is what will happen. The order in which the animations are specified in this table are thus very important.
The second entry triggers our walking animation. If the character is currently standing still looking left (CR_ANIM_LOOK_LEFT) and the user has the left arrow key pressed, we start our walking animation.
The third entry is the action if the character is looking left and the user has no (matching) keys pressed. We'll just keep playing the same standing still animation over and over again until something else happens.
The next set of entries define the walk itself. These go through the combinations of cycling through the 1st (CR_ANIM_WALK_LEFT_1) and 3rd (CR_ANIM_WALK_LEFT_2) animations we talked about in our introduction above. If the user lets go of the left key depending on which animation is currently playing we either directly got back to standing still (CR_ANIM_LOOK_LEFT) or we first play our stop walking animation (CR_ANUM_STOP_WALK_LEFT).
A lookup table like this lets us easily add in more animations and how we should react to the users input.
Note that I have no way of knowing if the original game uses a similar approach or something very different. Also I've kept things simple, there are a few easy optimizations to speed things up, especially animations defined later in the table mean we have to do a lot of tests before we end up at the right one but that is for another day.
Our new update routine
Our final step is actually reacting to our keyboard commands and making it all work. For this we've enhanced our engineUpdate routine. The start of our routine is still pretty similar. I've changed the animation speed to roughly 12 frames a second but other then that we simply cycle through the sprites. Once our animation recycles we set animReset and do our keyboard check.
This part has changed:
// if our animation was reset we have a chance to change the animation if (animReset) { int nextAnim = -1; // if our current animation was played forward, we add our movement if (conradAnim[currentAnim].firstSprite<=conradAnim[currentAnim].lastSprite) { currentPos.x += conradAnim[currentAnim].moveX * SPRITE_SCALE; currentPos.y += conradAnim[currentAnim].moveY * SPRITE_SCALE; };
Once our animation has finished we need to check if we need to add the movement to our current position. We only do this if our animation was played in the normal direction.
// check our action map for (int a = 0; a < MAX_ACTIONS && nextAnim == -1; a++) { if (conradActions[a].animationEnding == currentAnim) { // assume this will be our next animation until proven differently nextAnim = conradActions[a].startAnimation; // check if we're missing any keys for (int k = 0; k < 5 && nextAnim != -1; k++) { int key = conradActions[a].keys[k]; if (key == 0) { // no need to check further k = 4; } else if (!engineKeyPressedCallback(key)) { nextAnim = -1; }; }; }; }; // if -1 We're missing something currentAnim = nextAnim == -1 ? 0 : nextAnim; currentSprite = conradAnim[currentAnim].firstSprite;
This is our main logic scanning our table until we found the correct animation and keypresses. We just loop through our entire table until we find the right one. If we can't find anything our table is incomplete, oops, so we just default back to our first animation.
// if our new animation is going to be played in reverse, we subtract our movement if (conradAnim[currentAnim].firstSprite>conradAnim[currentAnim].lastSprite) { currentPos.x -= conradAnim[currentAnim].moveX * SPRITE_SCALE; currentPos.y -= conradAnim[currentAnim].moveY * SPRITE_SCALE; };
Finally, if we're playing an animation in reverse we need to subtract our movement first. I've only used this for backwards rolls.
If you compile and run the sample application at this point in time you should be able to make Conrad walk left and right, roll, and making him climb. He still lacks a way to get back down :)
Download the source code here
So where from here?
Well the next step is putting a proper background in place and make Conrad interact with it so he can only climb where it makes sense and drop back down.It may take awhile before I get this done. First off I need to spend some time on getting the graphics sorted out but I've also got a few other things on my plate so I may not be working on this any time soon.
No comments:
Post a Comment