This will be the last step in this 2D adventure but it leaves us with a fully functional 2D engine that allows our character to walk around a 2D map. While there are plenty of things to do from here it is a good starting point to go from.
A lot has changed in the source to get to this next level so this is a bit of a read. I'll try to handle each area one step at a time.
Housekeeping
Obviously some housekeeping had to be done to make some of the code written so far more generic and reusable. Some of these we'll look into in more depth but in broad strokes:- tilemap.h (and the associated shaders) has been updated to allow any sized map to be used without having to make changes to the code. We thus store the size of the map and size of our tiles image file in our structure and send this information to the shaders
- spritesheep.h (and the associated shaders) has similarly been updated
- all of the hardcoded data in engine.h and engine.c has been moved into a new header file called gamedata.h. Obviously in a real game this information should be stored in files that can be loaded as they are needed to create a game with multiple levels instead of stored inside source files but for our example code this is easier. I moved everything into a separate file to set this data apart.
A new map
We're no longer using the sample desert map from our tiled map editor but a map I've flung together from some screenshots from the first level of the game flashback. This leaves a lot to be desired and really needs to be cleaned up but for our purpose it does well.I've put the original maps you can edit in the tilemap editor within the folder structure in the github repository (Support/Flashback map) but they are not used directly. Instead the CSV exporter was used to export the maps and embed them in the gamedata.h header file.
Note that the map has two layers, one layer is the map we're actually rendering to screen in the game, the other allows us to mark where in our map our character can walk and where he can climb up or down. This is done by using a simple colored tileset so we can use the indexes it creates:
0 = empty space
1 = conrad can walk here
2 = conrad can climb up when facing left
3 = conrad can climb up when facing right
This is still very simplistic as I've limited the actions Conrad can do but if all his animations were implemented we'd probably need a few more to indicate where Conrad can only roll or where he can jump. More on this later.
Our map is now also scaled 1:1 so each tile is rendered as a 32.0 x 32.0 tile in our coordinate system. We also render Conrad at this scale which is very important because our animations line up with our tiles. Conrad will thus always be rendered at the right side of a tile.
The same applies to our currentPos global variable that holds Conrads position which is now a position within our tilemap (measured from the center of our tilemap). Our default position of -18, 9 (which is now set in our engineLoad function) puts Conrad at the left bottom of our map.
Enhancements to the action map
This is probably where the largest changes have taken place. The action map that was introduced in the last write-up has been changed drastically.First it no longer is a single array through which we scan. Instead every animation has its own followup action map. This is simply a speed up because instead of having to scan through the entire map, we only scan through those entries that actually can be a followup action to our current animation.
Our animation structure now has a new member called followUpActions which is a pointer to an action_map array.
// now define our animations // note that movements are multiples of 32 animation conradAnim[] = { 0, 0, false, 0.0, 0.0, conradLookLeftActions, // 0 - look left 0, 6, false, 0.0, 0.0, conradLeftToRightActions, // 1 - turn from looking left to looking right ...Note also that our moveX and moveY members now refer to our movement within our map and are multiplied by 32.0 to get our movement on screen. I've cheated a little with some of the animations which is something that should be cleaned up at some point.
Our first entry in our animation array refers to Conrad simply standing still and looking to the left. The conradLookLeftActions array being pointed to as the followUpActions member is an array that contains all followup actions possible when Conrad is looking to the left and looks as follows:
// Now we define all the followup actions for each of our actions action_map conradLookLeftActions[] = { CR_ANIM_TURN_LEFT_TO_RIGHT, keysRight, conradCanStand, // turn left to right if our right key is pressed CR_ANIM_WALK_LEFT_1, keysLeft, conradCanWalkLeft, // start walking left CR_ANIM_GET_DOWN_LEFT, keysDown, conradCanCrouch, // start getting down facing left CR_ANIM_CLIMB_DOWN_LEFT, keysDown, conradCanGoDownLeft, // start climbing down facing left CR_ANIM_JUMP_UP_LEFT, keysUp, conradCanStand, // jump up facing left CR_ANIM_LOOK_LEFT, NULL, conradCanStand, // keep looking left if no key is pressed CR_ANIM_FALLING_LEFT, NULL, NULL, // must be falling.. CR_END, NULL, NULL, // end (we shouldn't get here) };Note that our array ends with an entry "CR_END, NULL, NULL". In theory we will never evaluate this but it is a safety net, kind of like a NULL terminator.
The first member is our followup action, the second member are the keypresses associated with our followup action (we had this already in a slightly different form) and our third member points to structures that check our second layer in our map to see if the movement is allowed (i.e. Conrad can't walk to the left if there is a tree in the way).
When our second or third member is NULL it is ignored and the action is possible.
We'll look at these two members more closely further on.
In our previous tutorial the loop that checked our action map was embedded in our engineUpdate function. I've moved the logic into new functions to make it easier to maintain. In our engineUpdate routine we know what the current animation is and thus have a pointer to the action map for that specific animation. We simply call a new function called getNextAnim with a pointer to that map which will return the next animation we should play:
... nextAnim = getNextAnim(conradAnim[currentAnim].followUpActions); ...Couldn't be simpler. The magic happens in getNextAnim which simply looks at each entry in our action map until it finds the first one that can be used:
// obtain our next animation type GLint getNextAnim(action_map *followUpActions) { GLint nextAnim = -1; if (followUpActions != NULL) { // check our action map while ((nextAnim == -1) & (followUpActions->startAnimation != CR_END)) { // assume this will be our next animation until proven differently nextAnim = followUpActions->startAnimation; // check if we're missing any keys if (!areKeysPressed(followUpActions->keys)) { // keep looking... nextAnim = -1; followUpActions++; // check next } else if (!canMoveThere(followUpActions->moveMap)) { // keep looking... nextAnim = -1; followUpActions++; // check next }; }; }; return nextAnim; };
We check our keypresses first and whether we can move to that location.
The keypress logic remains nearly the same as the previous implementation but instead of hardcoding up to 5 keypresses we now use a zero terminated array. Our first entry in our action map points to the array called keysRight which is defined as:
int keysRight[] = { GLFW_KEY_RIGHT, 0 };Our canMoveThere function works very similarly but has a slightly more complex structure it points at, here for example is our conradCanWalkLeft array and our idCanWalk array it references:
unsigned char idCanWalk[] = { 1, 2, 3, 0}; ... move_map conradCanWalkLeft[] = { -1, 0, idCanWalk, 0, 0, NULL, };The first entry in conradCanWalkLeft checks if conrad can move one tile to the left. It does this by checking if the value of the tile in our second map appears in our idCanWalk array which is once again a zero terminated array.
Note that as a result we can't check for a zero value for our tile which is fine as we've defined zero to be empty space.
Moving out of the screen
The last thing I hadn't addressed in the previous version was Conrad walking off screen. As Conrad is now constraint within our map I simply update the view when Conrad is about to leave the screen. There are nicer ways to handle this but for our purpose this will do fine. The code was added right after our followup animation check.Where too from here?
We now have a working but very basic engine for a platform game. Obviously there is a need to complete the animations that are missing and tweak the existing ones. One good example is to break the roll up into 4 steps instead of rolling 4 tiles at once. The original game allowed you to roll even if there were only 2 tiles between Conrad and a wall but he would simply bounce off the wall. This could be easily triggered by using another tile type in the second map to indicate the wall and adding in that once you attempt to roll there, the bounce off the wall animation starts playing. Similarly we can't jump off a ledge or do long jumps yet which are really cool to add.Another issue to tackle is that Conrad is currently drawn over the background map which isn't always correct:
Here Conrad is standing in front of the thingy on the ground while he should be standing behind it. This can be solved by either having a stencil for our image and tweaking the Z-value when drawing the tiled map or it could be done by creating multiple layers in our map having a background layer drawn behind Conrad and a foreground layer drawn in front of Conrad.
Finally completely missing here are the bad guys and other obstructions. For now I'll leave that up to your own imagination :)
Download the source code here
What's next?
As I mentioned up top, this is the last write-up I've planned on the subject of 2D within the confines of this tutorial. I probably will write additional blog posts to clear up certain things I've done here and I may tweak the examples as time goes by.Next I wish to jump back into the world of 3D which I've left alone for far too long but we're not going to leave our platform example. We'll have to sidetrack a little to explain some basics but then we're coming right back to our platform game and we'll look into transforming it into a 3D environment.
No comments:
Post a Comment