|
OBEYBREW.COM | Tutorials | A Crash Course In HuC Part 7 Document is being finalized. It may contain flaws. This message will be removed when finalization is... well, finalized.A Crash Course In HuCPart 7 - The State Of The StateThis particular tutorial is less about HuC and more about technique. By the end of this tutorial, you will have an almost functional game engine. Get ready!What You'll LearnToday, we introduce scroll wrapping and state machines. scroll wrapping is when the map scrolls beyond the range of the virtual window. 'Huh?' Well, the virtual window is normally 64x32 unless we make it something else, and yet we're going to be using a map that's much larger. By reloading portions of the virtual window, we're able to show more than normal. And state machines are wonderful pieces of code that make creating characters a complete breeze! Okay, not a real breeze... there's a lot of work involved, but as you will see, they are both flexible and powerful!Getting PreppedWe're starting another new source this time but, as before, keep the previous sources handy to copy some stuff. Also, we're using completely new files this time... ole Bonk's getting a powerup!Bonk's facelift Let The Battle Commence!Okay... this one's going to be huge. You will definitely want to practice your commenting here!The first thing we do, of course, is set up the #include and main function. Then, we're going to load our shiny new Bonk sprite. #incspr(bonk, "bonksprites.pcx", 0, 0, 2, 18); #incpal(bonkpal, bonksprites.pcx); Now, we will also load our spiffy new game world and tiles: #incbin(levelmap, tut7map.fmp"); #incchr_ex(leveltiles, "tut7pal0.pcx", 0, 0, 20, 7, 0,\ "tut7pal1.pcx", 0, 0, 20, 2, 1,\ "tut7pal1.pcx", 0, 16, 11, 1, 1); #incpal(levelpal0, tut7pal0.pcx"); #incpal(levelpal1, tut7pal1.pcx"); #incpal(levelpal2, tut7pal2.pcx"); Alright, so them's the basics. But now, we need some additional things before we dive into function code. We're first going to set up a bunch of #defines for later use. #define DIR_LEFT 1 #define DIR_RIGHT 2 #define SCR_W 33 #define SCR_H 32 #define CHAR_LEFT_THRESHOLD 96 #define CHAR_RIGHT_THRESHOLD 128 int bonkx, bonky, j1, j2, mapx, mapy, vmapx, vmapy, lastmapx, lastmapy, t3, jumpspeed, jumpdelta; int MAP_X_THRESHOLD, MAP_X_LOW_THRESHOLD; char tics, frame, state, direction; Finally, let's re-copy some old source into this new program. spr_make and the original pause function from tutorial 5 will do nicely here. The Bonk Is Back!We'll be using some familiar code to set up the game engine. Right inside your main function, we'll take care of some init stuff.init_satb(); tics = 0; frame = 0; bonkx = 104; bonky = 153; spr_make(0, bonkx, bonky, 0x5100, FLIP_MAS|SIZE_MAS, NO_FLIP|SZ_32x32, 0, 1); load_palette(16, bonkpal, 1); set_color(256, 0); load_vram(0x5000, bonk, 0x900); satb_update(); And yes, of course, we need a game world to play in. However, it's going to be a little different this time. MAP_X_THRESHOLD = 1055; MAP_X_LOW_THRESHOLD = 0; set_map_data(levelmap, 164, 28); set_tile_data(leveltiles); load_tile(0x1000); load_map(0, 0, 0, 0, 34, 28); load_palette(0, levelpal0, 1); load_palette(1, levelpal1, 1); Now, we have some variables to initialize. mapx = 0; lastmapx = 0; mapy = 0; lastmapy = 0; vmapx = 0; vmapy = 0; And finally... the game loop. It's going to be really simple this time, as the rest will be handled by custom functions. for(;;) { j1 = joy(0); j2 = joytrg(0); if(j2 & JOY_STRT) { pause(); } else { bonk_state_machine(); } satb_update(); vsync(); } State A FactSo then... state machines... what are they? Well, in the simplest terms, a state machine is a subprogram that controls an entity. In this case, our state machine will be a collection of functions that control Bonk. practical examples of state machines in the real world might include your car... the gear shift could be considered a state machine, as it can put your car in park, neutral, drive, reverse, etc. Each of those would be considered a state controlling the car, which would be considered the entity. Also you have what are called state controllers... these are things that can alter the state, or change it outright. The gear shift itself would be the largest state controller, as it can directly change the state of the car. However, the acceleration pedal and the brake pedal also alter the state of the car, and affect the conditions in which the gear shift can change the car's state. For instance, you can't shift from park to drive without pressing the brake. In drive, the accelerator controls the speedup of the car, thus altering the car's current state but not changing its state. Confused yet? Sorry if so.What The Func?So then, we're going to start writing Bonk's lil state machine. The first thing we need to know are what states Bonk can be in. For this tutorial, we're going to have a total of seven states: idle, walking, about to jump, jumping up, falling down, landing, and bonking! We'll start with the basic one: idle. Add this code to the bonk_state_machine function:if(state == 0) bonk_state_0(); if(tics == 0) { spr_pattern(0x5100); tics = 1; } First things first... let's make him bonk! if(j2 & JOY_B) { /* set bonk in bonking state */ tics = 0; frame = 0; state = 6; spr_pattern(0x5700); } if(state == 6) bonk_state_6(); bonk_state_6() { tics++; if(tics > 5) spr_pattern (0x5800); if(direction == DIR_LEFT) { spr_x(bonkx-8); } else { spr_x(bonkx+8); } if(tics > 10) { spr_x(bonkx); tics = 0; state = 0; } } Get To A Better StateNo, Bonk doesn't have car insurance. However, we need to improve our idle state. So far, Bonk can only bonk. Let's make him move. After we're done with this detail, we'll add in another new concept... scroll wrapping. Go back to bonk_state_0 and add in two new state controllers:if(j1 & JOY_LEFT) { spr_ctrl(FLIP_MAS, FLIP_X); tics = 0; frame = 0; state = 1; spr_pattern(0x5000); direction = DIR_LEFT; } if(j1 & JOY_RGHT) { spr_ctrl(FLIP_MAS, NO_FLIP); tics = 0; frame = 0; state = 1; spr_pattern(0x5000); direction = DIR_RIGHT; } Around The WorldWe are going to fill in our bonk_state_1 function next. As we do this, we're going to notice that we're in need of another new function. So, let's get to it!char stillwalking; stillwalking = 0; tics++; if (tics > 4) { tics = 0; frame++; if (frame > 5) frame = 0; if (frame == 0) spr_pattern(0x5100); if (frame == 1) spr_pattern(0x5200); if (frame == 2) spr_pattern(0x5300); if (frame == 3) spr_pattern(0x5200); if (frame == 4) spr_pattern(0x5100); if (frame == 5) spr_pattern(0x5000); } Next, we have to add state controllers for this state. Since we're already walking, we have to make sure Bonk keeps walking. So, we do this: if(j1 & JOY_LEFT) stillwalking = 1; if(j1 & JOY_RGHT) stillwalking = 1; if(j2 & JOY_B) { tics = 0; frame = 0; state = 6; spr_pattern(0x5700); } Now, back to his movement. Here's where stillwalking makes its mark. if(stillwalking == 0) { state = 0; tics = 0; frame = 0; } else { move_in_world(); } move_in_world() { char map_scroll_dir; map_scroll_dir = 0; if (direction == DIR_LEFT) { if (vmapx > MAP_X_LOW_THRESHOLD) { if (bonkx < CHAR_LEFT_THRESHOLD) { map_scroll_dir = DIR_LEFT; } else { bonkx--; } } else { bonkx--; } } if (direction == DIR_RIGHT) { if (vmapx < MAP_X_THRESHOLD) { if (bonkx > CHAR_RIGHT_THRESHOLD) { map_scroll_dir = DIR_RIGHT; } else { bonkx++; } } else { bonkx++; } } spr_x(bonkx); move_map(map_scroll_dir); } The first thing you see is map_scroll_dir. This is similar to the flag variable we used before, only this one can either be 0, 1, or 2 rather than just 0 or 1. direction was set back in state 0 when we started this whole thing off and will tell us which if block to look at. Now we get into some technical details. vmapx keeps track of the current scroll of the map. We check it against the boundaries of the map. In this case, the lowest value is 0 and the highest value is 1055. How did we reach this weird number? Well, we take the total number of pixels in the map, which we can get by multiplying the number of tiles wide by 8 and subtract 256 from it, which is the size of the screen in pixels. This gives us 1056. But since we're starting from 0 and not 1, we knock 1 off of that value for 1055. Math ftw! Okay, so that covers the movement of the scroll. So, what else do we have? Well, we have the char threshold lines next... these determine whether or not we should scroll the map at all, based on where Bonk is in relation to the screen borders. If you've ever played Exile II, you'll notice that they really messed this part up! The thresholds shouldn't be too far apart as they were in Exile II. We'll use 96 and 128, as these are pretty decent values for this. So, what does this all mean? Well it goes like this... if Bonk is at the char threshold, check to see if there's any more map to scroll... if there is, scroll the map... if there's not, just move Bonk... if neither of these conditions are met (like if he's inside the two char thresholds or to an extreme side of the map), also just move Bonk. These are the basics of world movement. Now ... we've got a new function at the end, right after we tell HuC to set Bonk's position. Here's where things get a bit more technical. move_map(whichway) char whichway; { if (whichway == 0) return; if (whichway == DIR_RIGHT) vmapx++; if (whichway == DIR_LEFT) vmapx--; mapx = vmapx >> 3; mapy = vmapy >> 3; if (whichway == DIR_RIGHT) { if ((lastmapx != mapx) || (lastmapy != mapy)) { t3 = (mapx+32) & 0xFF; load_map(t3,mapy,t3,mapy,1,SCR_H); } } if (whichway == DIR_LEFT) { if ((lastmapx != mapx) || (lastmapy != mapy)) load_map(mapx,mapy,mapx,mapy,1,SCR_H); } scroll(0,vmapx,vmapy,0,223,0xC0); lastmapx = mapx; lastmapy = mapy; } Okay so... if we were to compile and run this right now, you'd have Bonk walking through the game world. We're still not done yet though. We're going to finish off the states we started and then fill in the last few states we've not covered yet. No Pointer Sisters HereBonk's not gonna jump for love this time but we're definitely going to make him jump. Bonk's jump is the most complex sequence so far; it's made up of four individual states: start, up, down, and landing. State 2 is the first state in this sequence. So then... we can enable this by adding the following state controllers to our state 0 and state 1 functions:if(j2 & JOY_A) { tics = 0; frame = 0; state = 2; spr_pattern(0x5400); } State 2 is really going to be an easy one. In state 2, there's no movement allowed, so we don't have to have any state controllers based on player input. So, its code is gonna be puny. bonk_state_2() { tics++; if(tics > 4) { jumpspeed = 4; jumpdelta = 70; tics = 0; /* tics is going to be used in a different way in state 3 ... you'll see! */ spr_pattern(0x5500); state = 3; } } Now, let's add state 3, which is Bonk's jump up state. bonk_state_3() { bonky -= jumpspeed; tics += jumpspeed; if(tics > jumpdelta) { spr_pattern(0x5600); jumpspeed--; if(jumpspeed == 0) { tics = 0; state = 4; jumpspeed = 1; } } spr_y(bonky); } 'Wait a second... you should be able to move while jumping, right?' Very good, grasshopper, you noticed! Or did you really? No matter... well, we will also add in some code to affect the in-flight movement of Bonk: if(j1 & JOY_LEFT) { spr_ctrl(FLIP_MAS, FLIP_X); direction = DIR_LEFT; move_in_world(); } if(j1 & JOY_RGHT) { spr_ctrl(FLIP_MAS, NO_FLIP); direction = DIR_RIGHT; move_in_world(); } Coming Down With SomethingYou almost have a fully working jump! Now, coming down is going to be slightly different. First of all, our movement speed is only set to 1... we'll need to increase it. Also, we're now not concerned with the jump distance.bonk_state_4() { bonky += jumpspeed; if(jumpspeed < 4) jumpspeed++; if(bonky > 152) { bonky = 153; frame = 0; tics = 0; state = 5; spr_pattern(0x5400); } if(j1 & JOY_LEFT) { spr_ctrl(FLIP_MAS, FLIP_X); direction = DIR_LEFT; move_in_world(); } if(j1 & JOY_RGHT) { spr_ctrl(FLIP_MAS, NO_FLIP); direction = DIR_RIGHT; move_in_world(); } spr_y(bonky); } State 5 is the last state we'll add, and it too is pretty darn simplistic. bonk_state_5() { tics++; if(tics > 4) { tics = 0; state = 0; frame = 0; } } So... What's NextYou now have a largely functional game engine base. It's a basic side-scroller with scroll wrapping and a basic state machine. What else can we do? One thing that might help is to add multi-directional scrolling. All we have to do is build on top of what we already have. We can build on our knowledge of state machines to create an enemy (yes, enemies can use state machines too!). But surely there must be something new we can cover, right? Right there is! Since we're going to add vertical scrolling, it'll be a good time to introduce sprite-to-tile collision using HuC's built-in functions. Also, since we're going to be adding an enemy... or two ... or more... it'll be a good time to get more familiar with arrays.... and their limitations.Full Program Listing#include "huc.h" /* defines make life easier and code more readable... */ #define DIR_LEFT 1 #define DIR_RIGHT 2 #define SCR_W 33 #define SCR_H 32 #define CHAR_LEFT_THRESHOLD 96 #define CHAR_RIGHT_THRESHOLD 128 /* our new Bonk sprites! */ #incspr(bonk,"bonksprites.pcx",0,0,2,18); #incpal(bonkpal,"bonksprites.pcx"); /* Welcome to Bonk's world! */ #incbin(levelmap,"tut7map.fmp"); #incchr_ex(leveltiles,"tut7pal0.pcx",0,0,20,7,0,\ "tut7pal1.pcx",0,0,20,2,1,\ "tut7pal1.pcx",0,16,11,1,1); #incpal(levelpal0,"tut7pal0.pcx"); #incpal(levelpal1,"tut7pal1.pcx"); /* Globals ftw */ int bonkx, bonky, j1, j2, mapx, mapy, vmapx, vmapy, lastmapx, lastmapy, t3, jumpspeed, jumpdelta; int MAP_X_THRESHOLD,MAP_X_LOW_THRESHOLD; char tics, frame, state, direction; main() { /* set up Bonk! */ init_satb(); tics = 0; frame = 0; bonkx = 104; bonky = 153; spr_make(0,bonkx,bonky,0x5100,FLIP_MAS|SIZE_MAS,NO_FLIP|SZ_32x32,0,1); load_palette(16,bonkpal,1); set_color(256,0); load_vram(0x5000,bonk,0x900); satb_update(); /* set up Bonk's world! */ MAP_X_THRESHOLD = 1055; MAP_X_LOW_THRESHOLD = 0; set_map_data(levelmap,164,28); set_tile_data(leveltiles); load_tile(0x1000); load_map(0,0,0,0,34,28); load_palette(0,levelpal0,1); load_palette(1,levelpal1,1); /* stuff for handling the map scrollie thingo */ mapx = 0; lastmapx = 0; mapy = 0; lastmapy = 0; vmapx = 0; vmapy = 0; /* main game loop! */ for(;;) { j1 = joy(0); j2 = joytrg(0); if (j2 & JOY_STRT) { pause(); } else { bonk_state_machine(); } satb_update(); vsync(); } } spr_make(spriteno,spritex,spritey,spritepattern,ctrl1,ctrl2,sprpal,sprpri) int spriteno,spritex,spritey,spritepattern,ctrl1,ctrl2,sprpal,sprpri; { spr_set(spriteno); spr_x(spritex); spr_y(spritey); spr_pattern(spritepattern); spr_ctrl(ctrl1,ctrl2); spr_pal(sprpal); spr_pri(sprpri); } pause() { for(;;) { vsync(); if (joytrg(0) & JOY_STRT) return; } } bonk_state_machine() { /* Bonk's State Machine: 0 - Idle (all buttons available) 1 - Walking (all buttons available) 2 - About to jump (all buttons disabled) 3 - Jumping (directions available only) 4 - Jumping down (directions available only) 5 - Landing (all buttons disabled) 6 - Bonking (all buttons disabled) */ if (state == 0) bonk_state_0(); if (state == 1) bonk_state_1(); if (state == 2) bonk_state_2(); if (state == 3) bonk_state_3(); if (state == 4) bonk_state_4(); if (state == 5) bonk_state_5(); if (state == 6) bonk_state_6(); } bonk_state_0() { /* this is Bonk's idle stance... if the tics are equal to 0, set his frame to default and his tic counter to 1 */ if (tics == 0) spr_pattern(0x5100); tics = 1; /* most other states can be set in this state */ if (j1 & JOY_LEFT) { /* start Bonk moving to the left! */ spr_ctrl(FLIP_MAS,FLIP_X); tics = 0; frame = 0; state = 1; spr_pattern(0x5000); direction = DIR_LEFT; } if (j1 & JOY_RGHT) { /* start Bonk moving to the right! */ spr_ctrl(FLIP_MAS,NO_FLIP); tics = 0; frame = 0; state = 1; spr_pattern(0x5000); direction = DIR_RIGHT; } if(j2 & JOY_B) { /* set Bonk in bonking state! */ tics = 0; frame = 0; state = 6; spr_pattern(0x5700); } if(j2 & JOY_A) { /* make Bonk jump! */ tics = 0; frame = 0; state = 2; spr_pattern(0x5400); } } bonk_state_1() { /* Bonk's walking state! */ char stillwalking; stillwalking = 0; tics++; if (tics > 4) { tics = 0; frame++; if (frame > 5) frame = 0; if (frame == 0) spr_pattern(0x5100); if (frame == 1) spr_pattern(0x5200); if (frame == 2) spr_pattern(0x5300); if (frame == 3) spr_pattern(0x5200); if (frame == 4) spr_pattern(0x5100); if (frame == 5) spr_pattern(0x5000); } if (j1 & JOY_LEFT) { stillwalking = 1; } if(j1 & JOY_RGHT) { stillwalking = 1; } if(j2 & JOY_B) { /* set Bonk in bonking state! */ tics = 0; frame = 0; state = 6; spr_pattern(0x5700); } if(j2 & JOY_A) { /* make Bonk jump! */ tics = 0; frame = 0; state = 2; spr_pattern(0x5400); } if (stillwalking == 0) { state = 0; tics = 0; frame = 0; } else { move_in_world(); } } bonk_state_2() { /* Bonk's pre-jump prep state! all controls are disabled in this one */ tics++; if (tics > 4) { jumpspeed = 4; jumpdelta = 70; tics = 0; /* tics is going to be used in a different way in state 3... you'll see! */ spr_pattern(0x5500); state = 3; } } bonk_state_3() { /* Bonk's jumping up state! */ bonky-=jumpspeed; /* here, we're going to use tics to determine how high Bonk has jumped, and compare it to the jump delta */ tics+=jumpspeed; if (tics > jumpdelta) { /* if the total jump distance exceeds the jump delta, reduce the upward motion until it reaches zero */ /* the reduction in upward motion makes the jump appear a little more natural than just immediately changing directions */ spr_pattern(0x5600); jumpspeed--; if (jumpspeed<0) { /* when the jump speed decreases past 0, we will need to set Bonk in state 4 */ tics = 0; state = 4; jumpspeed = 1; } } if (j1 & JOY_LEFT) { spr_ctrl(FLIP_MAS,FLIP_X); direction = DIR_LEFT; move_in_world(); } if (j1 & JOY_RGHT) { spr_ctrl(FLIP_MAS,NO_FLIP); direction = DIR_RIGHT; move_in_world(); } spr_y(bonky); } bonk_state_4() { /* Bonk's falling down state! */ bonky+=jumpspeed; if (jumpspeed < 4) jumpspeed++; if (bonky > 152) { bonky = 153; frame = 0; tics = 0; state = 5; spr_pattern(0x5400); } if (j1 & JOY_LEFT) { spr_ctrl(FLIP_MAS,FLIP_X); direction = DIR_LEFT; move_in_world(); } if (j1 & JOY_RGHT) { spr_ctrl(FLIP_MAS,NO_FLIP); direction = DIR_RIGHT; move_in_world(); } spr_y(bonky); } bonk_state_5() { /* Bonk's landing state; same frame as state 2, but reverts to state 0 when finished rather than state 3 */ tics++; if (tics > 4) { tics = 0; state = 0; frame = 0; } } bonk_state_6() { /* Bonk's bonking state! all controls are disabled in this one; this is just an animation handler/state controller */ tics++; if (tics == 5) { spr_pattern(0x5800); /* we need to adjust Bonk's position, as this sprite is a few pixels to the left normally... this is why we need 'direction'! */ if (direction == DIR_LEFT) { spr_x(bonkx-8); } else { spr_x(bonkx+8); } } if (tics > 10) { /* as this animation ends, restore the sprite to its normal posision and set the state back to 0 */ spr_x(bonkx); tics = 0; state = 0; } } move_in_world() { char map_scroll_dir; map_scroll_dir = 0; /* how this should work: -if character is at a direction threshhold but not at the map threshhold, scroll the map -move character's sprite otherwise */ /* are we moving left? */ if (direction == DIR_LEFT) { /* check for map threshhold */ if (vmapx > MAP_X_LOW_THRESHOLD) { /* map threshhold has not been reached, so check for screen threshhold */ if (bonkx < CHAR_LEFT_THRESHOLD) { /* we're at the screen threshhold, so we'll be scrolling the map now! */ map_scroll_dir = DIR_LEFT; } else { /* just move */ bonkx--; } } else { /* we're at the map threshhold...no scrolling allowed! */ bonkx--; } } /* are we moving right? */ if (direction == DIR_RIGHT) { /* check for map threshhold */ if (vmapx < MAP_X_THRESHOLD) { /* map threshhold has not been reached, so check for screen threshhold */ if (bonkx > CHAR_RIGHT_THRESHOLD) { /* we're at the screen threshhold, so we'll be scrolling the map now! */ map_scroll_dir = DIR_RIGHT; } else { /* just move */ bonkx++; } } else { /* we're at the map threshhold...no scrolling allowed! */ bonkx++; } } spr_x(bonkx); move_map(map_scroll_dir); } move_map(whichway) char whichway; { if (whichway == 0) return; if (whichway == DIR_RIGHT) vmapx++; if (whichway == DIR_LEFT) vmapx--; mapx = vmapx >> 3; mapy = vmapy >> 3; if (whichway == DIR_RIGHT) { if ((lastmapx != mapx) || (lastmapy != mapy)) { t3 = (mapx+32) & 0xFF; load_map(t3,mapy,t3,mapy,1,SCR_H); } } if (whichway == DIR_LEFT) { if ((lastmapx != mapx) || (lastmapy != mapy)) load_map(mapx,mapy,mapx,mapy,1,SCR_H); } scroll(0,vmapx,vmapy,0,223,0xC0); lastmapx = mapx; lastmapy = mapy; } See alsoA Crash Course In HuC - Part 8 (coming soon) |