Part 1: Movement, Physics, and You
Part 2: Jumps, Falls, and Walls
Part 3: Animation
Back in action! We are back in action. Are you ready to learn how to add bad guys to your game? I am ready to tell you how to do it.
See Part 3: Animation for last month's plotscript. Since the full script is becoming so large, I'll no longer be including it at the beginning of each article. Remember, if you're lost (and with a script that large, there's no shame in it), you can always catch up via the links at the top.
Enemies: let's make some. In the dual interests of simplicity and brevity, this installment is only going to introduce a single enemy type. You've played Super Mario Bros., right? We're going to make a goomba. If you're familiar with plotscripting, you should be able to make other kinds of enemies with the template we'll introduce here, but we're only going to touch on that briefly here. Another issue might see the inclusion of more kinds of enemies, but not today.
An important note before you delve too deeply into the creation of your game: scripting will be much, much easier if you maintain your NPC IDs from map to map. If your Goomba is NPC 1 on one map, make him NPC 1 on every map. Seriously, this is important. The limit is 100 different NPCs per map at the time of writing, recently lifted from 36. This is more than ample for our needs. Let's reserve the first 20 NPC IDs (that's 0 through 19) for bad guys. That's more than we'll need, but that's good. Our goomba will be NPC 0.
Creating an enemy is a multi-part endeavor: first, we want to make him move -- but only when he's "in scope." If you replay SMB, you'll notice that enemies have a set starting position and they only start moving from that position once they're onscreen. The movement part of the equation isn't so hard -- we took care of the hero's movement in Part 1 and we can use the same logic for the enemies -- and figuring out if an enemy is onscreen is only a little tricky for reasons I'll mention in a bit. The other part of creating enemies is making them interact with the hero. In the case of our goomba enemy, we already know what the interactions look like: if the enemy touches the hero, then the hero dies, unless the enemy bumps into the hero from below the hero.
We'll start with the movement. The goomba has a very simple movement logic: he walks forward until he hits a wall. He can walk off cliffs and fall. He has a constant speed (falling aside), which makes things a little easier on us.
The first thing we need to do is add something to the big loop that processes enemy behavior for each frame. We'll create a function that we'll call do enemies to take care of this.
define constant, begin |
Finally, we have "other game-playing stuff" to put there. Note the new constant: "enemy:goomba". We'll use this instead of the number 0 to refer to our enemy's NPC ID. Note: If you're not intimately familiar with the difference between NPC instances, NPC IDs, and NPC references, this is a good time to stop reading this and check out the Hamster Republic wiki, which has loads of useful information on these topics. Trust me, it's going to get really confusing if you don't know the terminology. I'll try to make it entirely clear which one I'm referring to. NPC ID 0 is our goomba enemy, as mentioned above. Right now, we just have the one, so the first enemy ID is the same as the last enemy ID: 0. We have constants for these, too.
Right now, do enemies is empty. We'll iterate through every instance of NPC ID 0 and have each one perform a few checks. In Super Mario Bros. (and, indeed, in any sidescroller I can think of), all enemies start out inactive; that is, they don't move until they're onscreen or nearly onscreen. Otherwise, by the time you reached them, they'd already have wandered into a pit somewhere. We'll replicate this behavior with our goomba enemy. (In addition, older games have their enemies go "out of scope": if they wander too far offscreen, they despawn. This was probably due to hardware limitations. We won't duplicate that here: while it would be easy enough to do, there's no reason to do it.) So our enemy's logic looks a little like this:
In scripting language, that looks like this:
script, do enemies, begin |
That's a quick and dirty translation from our written description above to Hamsterspeak code. I won't go so far as to call it straightforward, but please make an attempt to understand what's going on. Notice that there are three new functions. We could have lumped all of the logic in the do enemies function, but separating it out makes it easier to read and understand. Note that we're using extra 1 to store whether an NPC is active (0 if it's inactive). If you're not familiar with NPC extras, you should look up the documentation -- they're variables attached to NPC instances that you can use for any purpose you want. Very handy. We're going to use extra 2 soon, too.
Several steps into our script, the enemy still isn't moving. Well, that's up next. Let's expand our script to make the enemy walk.
global variable, begin |
The enemy is animated now (you can tweak the lines related to enemy anim if you don't like it -- right now, he changes frames every other cycle). He also walks until he hits something and falls if there's nothing underfoot. Great! Note that unlike our hero, the goomba will actually face the direction he's moving. So you can draw the goomba like a normal sprite.
As promised, now we're using the enemy's second extra variable. It's storing the enemy's y-velocity. It increases when he's falling and resets to 0 when he's not.
Goombas can move through each other, but not through walls. (This behavior changes in later games, but it's true of the original Super Mario Bros.) When they hit a wall, there's a one-cycle delay as they change directions.
I referenced some new functions: npc can left, npc can right, and npc can fall. These are exactly like our existing can left, can right, and can fall functions, and we'll throw in a npc can rise function for good measure (the goomba won't use it, but another enemy might later).
script, npc can fall, npc, begin |
There's just one last thing to do now: deal with collision. We want to make it kill the goomba if the hero hits him from the top and kill the hero otherwise. (You might want the hero to be able to take more than one hit. We'll deal with that in a later installment.) The check for this will go in process enemy.
As an added bonus, how about a little death animation? When the goomba enemy is killed, he'll stay onscreen facing down for a second. Make sure to give him an appropriate squishy look on his down sprite.
define constant, begin |
A few notes: the corpse is represented by the NPC facing down. It stays onscreen for corpse time cycles, then the NPC is deleted. Optionally, you could extend the corpse time or make the corpses stay onscreen indefinitely. Corpses are still subject to gravity, but they can't hurt the hero. (This is the same behavior as with SMB's goombas.)
And now we have ourselves a goomba. This is our trickiest installment yet, and with any luck the trickiest to come. A lot of the work was infrastructure, so even if we made a more complicated enemy, there would be less new stuff to write.
Full plotscript coming up. Make sure to try out the sample file. Try making your own changes! For example:
include, plotscr.hsd include, scancode.hsi global variable, begin 1, friction 2, gravity 3, hero-x 4, hero-y 5, hero-vx 6, hero-vy 7, hero-speed 8, hero-max-vx 9, hero-max-vy 10, hero-jump-speed 11, hero-animation 12, hero-direction 13, enemy anim end define constant, begin 5, wiggle room 0, enemy:goomba 4, goomba walk speed 20, corpse time 0, first enemy id 0, last enemy id end plotscript, new game, begin initialize do game game over end script, initialize, begin suspend player gravity := 25 friction := 15 hero-x := hero pixel x(me) * 10 hero-y := hero pixel y(me) * 10 hero-vx := 0 hero-vy := 0 hero-speed := 25 hero-jump-speed := -145 hero-max-vx := 100 hero-max-vy := 100 hero-direction := right end script, do game, begin variable(playing) variable(hero can jump) playing := true # Let's do The Loop! while (playing) do ( # Accept player input if (key is pressed(key:esc)) then (playing := false) if (key is pressed(key:right)) then ( hero-direction := right hero-vx += hero-speed ) if (key is pressed(key:left)) then ( hero-direction := left hero-vx -= hero-speed ) if (key is pressed(key:alt) && hero can jump) then (hero-vy := hero-jump-speed) # Reduce speed if our hero's going too fast if (hero-vy >> hero-max-vy) then (hero-vy := hero-max-vy) if (hero-vx >> hero-max-vx) then (hero-vx := hero-max-vx) if (hero-vx << hero-max-vx * -1) then (hero-vx := hero-max-vx * -1) # TODO: Other game-playing stuff goes here. do enemies enemy anim += 1 if (enemy anim >> 3) then (enemy anim := 0) hero-x += hero-vx hero-y += hero-vy hero can jump := false if (can fall) then (hero-vy += gravity) else ( if (key is pressed(key:alt) == false) then (hero can jump := true) # Apply friction if (hero-vx << friction && hero-vx >> friction * -1 && key is pressed(key:left) == false && key is pressed(key:right) == false) then (hero-vx := 0) if (hero-vx >= friction && key is pressed(key:right) == false) then (hero-vx -= friction) if (hero-vx <= friction * -1 && key is pressed(key:left) == false) then (hero-vx += friction) ) if (hero-vy <= 0) then (can rise) if (hero-vx <= 0) then (can left) if (hero-vx >= 0) then (can right) put hero(me, hero-x/10, hero-y/10) animate hero wait(1) ) end script, can fall, begin variable (hy) # hero's y-position in maptiles hy := hero-y / 200 + 1 if( (read pass block((hero-x / 10 + wiggle room) / 20, hy), and, north wall) || (read pass block((hero-x / 10 + 20 -- wiggle room) / 20, hy), and, north wall) ) then ( if (hero-vy>=0) then ( hero-y := hero-y -- (hero-y, mod, 200) hero-vy := 0 ) return(false) ) else (return(true)) end script, can rise, begin variable (hy) hy := (hero-y) / 200 if( (read pass block((hero-x / 10 + wiggle room) / 20, hy), and, south wall) || (read pass block((hero-x / 10 + 20 -- wiggle room) / 20, hy), and, south wall) || (hero-y == 0) ) then ( hero-y := hero-y -- hero-y,mod,200 + 200 if (hero-vy<<0) then (hero-vy:=0) return(false) ) else (return(true)) end script, can left, begin variable (hx) hx := (hero-x--10) / 200 if( (readpassblock(hx,(hero-y) / 200), and, east wall) || (readpassblock(hx,(hero-y + 199) / 200), and, east wall) || (hero-x==0) ) then ( variable(new x) new x := 0 if (hero-x,mod,200 >> 100) then (newx := 200) if (hero-vx << 100, and, (hero-x,mod,200 >> 50)) then (newx := 200) hero-x := hero-x -- (hero-x,mod,200) + new x if (hero-vx << 0) then (hero-vx := 0) return(false) ) else (return(true)) ) script, can right, begin variable (hx) hx:= hero-x / 200 + 1 if ( (read pass block(hx, hero-y / 200), and, west wall) || (read pass block(hx, (hero-y + 199) / 200), and, westwall) ) then ( hero-x := hero-x -- (hero-x,mod,200) |
Next time: The HUD! "HUD" means "heads-up display," a fancy term for the status bars, life meters, and other displays that convey information to the player. This installation will take advantage of slices, which is currently a WIP feature. If there's not a new version of the OHRRPGCE by then, you might need a nightly build to take advantage of the latest changes.
Voting time! You vote this time for the topic you want me to cover the time after next. Send me a PM on Slime Salad or e-mail me to vote. Here are your choices for part 6:
Thanks for reading and remember to vote on your favorite topic! The link to download the SS101 example game is below.