Sidescrollers 101 - Part 5
A Feature by Adam Perry

Part 1: Movement, Physics, and You
Part 2: Jumps, Falls, and Walls
Part 3: Animation
Part 4: Enemies and NPCs

Part 5: The HUD

Time for 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 the ypsiliform version of the OHRRPGCE hasn't released by the time you read this, you might need a nightly build to take advantage of the latest changes.

See Part 4 for last month's plotscript. Since the full script has become so large, I'll no longer be including it at the beginning of each article. Remember to get up to speed via the links at the top of the article.

Last time, we created a goomba enemy. You can jump on it to kill it; if you bump into it some other way, you die instantly. We'll be tweaking that behavior slightly in this issue, since we'll be giving the hero a life meter -- and with it, the ability to take a hit.

Before we dive in, let's decide what elements the HUD will include. An effective way to do this is to make a sketch (yes, on paper) of what the screen will look like: just the layout, not the hero or the level or anything. Unfortunately, I can't see your sketch, so I'm going to go with something fairly generic.

In particular, I'll be putting a portrait of the hero at the top left, and to the right of that, a life meter. Below the life meter, I'll display the number of remaining lives. On the top right, I'll put a timer and a score indicator. (At this point, you might want to skip to the bottom and download the example game so that you can get a look at what I'm describing here. In fact, I recommend it.)

Creating a HUD used to be a more laborious affair: you'd have to arrange NPCs just right, then update them every cycle to make sure they were positioned right. Slices (essentially a fancy term for "sprites"), and, less recently, strings (programmer parlance for a variable that holds words instead of just a number) make our job much easier. Let's start off by defining a few things.

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
14, hero-hp
15, hero-max-hp,
16, score
17, lives left
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 hero-max-hp := 3
hero-hp := hero-max-hp
score := 0
lives left := 3
end

Okay, so maybe your hero has a different amount of life, or maybe you're not using points or a timer. It's hard to account for what you're going to do in your game, and while I can't accomodate every kind of game in this one article, hopefully the things you learn here are explained clearly enouch so that you can accommodate them to your own game.

Next up, we're going to... put all the information on the screen! In a refreshing departure from last issue's build-up, we're jumping right to the payoff this time. Before you see the script, though, it's time for a word on slices.

Slices (also referred to as "plotsprites") allow us to load up and display any of the sprites that you've drawn. Hero sprites? Enemy sprites? Attack sprites? Textbox border sprites? All of these and more are fair game. Just about the only thing you can't load into a plotsprite is a maptile. You can manipulate these graphics, move them around, and such. Very useful! Let's take a look at plotsprites in action.

global variable, begin
...
14, hero-hp
15, hero-max-hp
16, score
17, lives left
18, hero-portrait
19, lifebar
end script, initialize, begin ... hero-hp := hero-max-hp score := 0 hero-portrait := load portrait sprite(0)
lifebar := load small enemy sprite(0)
place sprite(hero-portrait, 5, 5)
place sprite(lifebar, 55, 5)
end

Simple as that, the portrait and lifebar appear. Notice that I used a small enemy sprite for the lifebar. I didn't have to use that specifically, but I wanted something wider than a walkabout. Similarly, I could have used anything else instead of a portrait sprite for the portrait, but the portrait made the most sense in this case.

We still need to add the strings, but first, let's make the lifebar work. We'll make it so that the player doesn't instantly die from each hit and update the lifebar when life is lost.

global variable, begin
...
19, lifebar
20, hero-invincibility 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 10, hero invincibility duration end script, do game, begin ... # TODO: Other game-playing stuff goes here. do enemies enemy anim += 1 if (enemy anim >> 3) then (enemy anim := 0) if (hero-invincibility >> 0) then (hero-invincibility -= 1) ... end script, hurt hero, begin if (hero-invincibility <= 0) then ( # For now, taking damage = death hero-hp -= 1
hero-invincibility := hero invincibility duration
update lifebar
if (hero-hp == 0) then (
fade screen out(63, 0, 0) game over )
)
end script, update lifebar, begin
replace small enemy sprite(lifebar, hero-max-hp -- hero-hp)
end

Now our hero can get hurt and the result is reflected on the lifebar. Note that this implementation of update lifebar assumes that you've arranged your lifebar graphics in the same order as the example game -- that is, full to empty. It'll work for a lifebar of any length as long as you've got the right number of graphics (one for each possible HP value, from max to zero). If you expect the length of your lifebar to vary throughout the game, you'll probably need to edit that script.

Now, let's add the strings. We'll use five strings: one to display the number of lives left, one that says "SCORE," one that displays the score, one that says "TIME," and one that displays the time. If you've used strings before, this should all be very straightforward.

To display the time, we're going to use a timer. If you've never used timers, check out the set timer command. It'll save us a lot of work.

define constant, begin
5, wiggle room
0, enemy:goomba
4, goomba walk speed
20, corpse time
0, first enemy id
0, last enemy id
0, string:lives
1, string:score label
2, string:score value
3, string:time label
4, string:time value
end script, initialize, begin ... lifebar := load small enemy sprite(0) place sprite(hero-portrait, 5, 5) place sprite(lifebar, 55, 5) $0 = "x "
append number(string:lives, lives left)
show string at(string:lives, 55, 20)
$1 = "SCORE"
show string at(string:score label, 160, 5)
append number(string:score value, score)
show string at(string:score value, 160, 15)
$3 = "TIME"
show string at(string:time label, 250, 5)
set timer(1, 200, 10, @time up, string:time value)
show string at(string:time value, 250, 15)
end script, process enemy, npc, begin ... # Collision! Check if hero fell onto goomba. if (npc pixel y(npc) -- 20 >> (hero-y -- hero-vy) / 10) then ( # Goomba dies set npc direction(npc, down) set npc extra(npc, extra 1, corpse time) score += 100
update score
) else ( hurt hero ) ) ) ) end script, update score, begin
clear string(string:score value)
append number(string:score value, score)
end

script, time up, begin
fade screen out(0, 0, 0)
game over
end

The timer takes care of updating the time string, so all we have to worry about is updating the lives and the score. But we don't actually have lives yet – that'll have to wait for the next issue. I gave the player 100 points per goomba squish. Generous, aren't I? Right now, that's the only way to get points.

The HUD is finished! Wasn't that quick? We probably won't touch it again anytime soon, but you should give some thought to customizing it to suit your purposes. How about adding the following touches to your own game?

Full plotscript approaches! What do you do?


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
 14, hero-hp
 15, hero-max-hp
 16, score
 17, lives left
 18, hero-portrait
 19, lifebar
 20, hero-invincibility
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
  0, string:lives
  1, string:score label
  2, string:score value
  3, string:time label
  4, string:time value
  10, hero invincibility duration
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
  hero-max-hp := 3
  hero-hp := hero-max-hp
  score := 0
  lives left := 3
  hero-portrait := load portrait sprite(0)
  lifebar := load small enemy sprite(0)
  place sprite(hero-portrait, 5, 5)
  place sprite(lifebar, 55, 5)
  $0 = "x "
  append number(string:lives, lives left)
  show string at(string:lives, 55, 20)
  $1 = "SCORE"
  show string at(string:score label, 160, 5)
  append number(string:score value, score)
  show string at(string:score value, 160, 15)
  $3 = "TIME"
  show string at(string:time label, 250, 5)
  set timer(1, 200, 10, @time up, string:time value)
  show string at(string:time value, 250, 15)
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)
    if (hero-invincibility >> 0) then (hero-invincibility -= 1)

    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-vx <= 0) then (can left)
    if (hero-vx >= 0) then (can right)
    if (hero-vy <= 0) then (can rise)
    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)

if (hero-vx >> 0) then (hero-vx := 0)
return(false)
)
else (return(true))
end

script, animate hero, begin
if (hero-direction == right) then (
set hero frame(me, 1)
) else (
set hero frame(me, 0)
)

if (hero-vy << 0) then (
# Jumping
set hero direction(me, 2)
) else (
if (hero-vy >> 0) then (
# Falling
set hero direction(me, 3)
) else (
if (hero-vx <> 0) then (
# Cycle between walking and standing
if (hero-vx << 0) then (hero-animation -= hero-vx)
if (hero-vx >> 0) then (hero-animation += hero-vx)
if (hero-animation >= hero-max-vx * 2)
then (hero-animation -= hero-max-vx * 2)
if (hero-animation >> hero-max-vx) then
(
set hero direction(me, 0)
) else (
set hero direction(me, 1)
)
) else (
# Standing
set hero direction(me, 0)
)
)
)
end

script, do enemies, begin
# Iterate through all enemy NPCs and perform their per-cycle actions
variable(i, j, npc, count)
for (i, first enemy id, last enemy id) do (
count := npc copy count(enemy:goomba) -- 1 # count goes from 0..n-1
for (j, 0, count) do (
npc := npc reference(i, j)
if (enemy is active(npc) == false) then (
if (npc is onscreen(npc)) then (activate enemy(npc))
)

# Check again, since it might have activated since last time
if (enemy is active(npc)) then (
process enemy(npc)
)
)
)
end

script, enemy is active, npc, begin
return (npc extra(npc, extra 1))
end

script, activate enemy, npc, begin
set npc extra(npc, extra 1, true)
end

script, npc is onscreen, npc, begin
if (npc pixel x(npc) >> camera pixel x -- 20 &&
camera pixel x >> npc pixel x(npc) -- 320 &&
npc pixel y(npc) >> camera pixel y -- 20 &&
camera pixel y >> npc pixel y(npc) -- 200) then (
return(true)
) else (
return(false)
)
end

script, process enemy, npc, begin
switch(get npc id(npc)) do (
case(enemy:goomba) do (
# Make our goomba walk
set npc frame(npc, enemy anim / 2)
if (npc direction(npc) == left) then (
put npc(npc, npc pixel x(npc) -- goomba walk speed, npc pixel y(npc))
if (npc can left(npc) == false) then (set npc direction(npc, right))
) else (
if (npc direction(npc) == right) then (
put npc(npc, npc pixel x(npc) + goomba walk speed, npc pixel y(npc))
if (npc can right(npc) == false) then (set npc direction(npc, left))
) else (
# Run down the time until we delete the enemy
set npc extra(npc, extra 1, npc extra(npc, extra 1) -- 1)
if (npc extra(npc, extra 1) == 0) then (
destroy npc(npc)
exit returning(0) # Quit out of this script
)
)
)
put npc(npc, npc pixel x(npc), npc pixel y(npc) + npc extra(npc, extra 2) / 10)
if (npc can fall(npc)) then (
set npc extra(npc, extra 2, npc extra(npc, extra 2) + gravity)
if (npc extra(npc, extra 2) >> hero-max-vy) then (set npc extra(npc, extra 2, hero-max-vy))
) else (
set npc extra(npc, extra 2, 0)
)

# Check for hero collisions
if ((hero-x / 10) -- npc pixel x(npc) << 20 &&
npc pixel x(npc) -- (hero-x / 10) << 20 &&
(hero-y / 10) -- npc pixel y(npc) << 20 &&
npc pixel y(npc) -- (hero-y / 10) << 20 &&
npc direction(npc) <> down) then (
# Collision! Check if hero fell onto goomba.
if (npc pixel y(npc) -- 20 >> (hero-y -- hero-vy) / 10) then (
# Goomba dies
set npc direction(npc, down)
set npc extra(npc, extra 1, corpse time)
# Hero bounces
hero-vy := hero-jump-speed / 2
if (key is pressed(key:alt)) then (hero-vy := hero-jump-speed)
score += 100
update score
) else (
hurt hero
)
)
)
)
end

script, npc can fall, npc, begin
variable (ny) # npc's y-position in maptiles
ny := npc pixel y(npc) / 20 + 1
if(
(read pass block((npc pixel x(npc) + wiggle room) / 20, ny), and, north wall)
||
(read pass block((npc pixel x(npc) + 20 -- wiggle room) / 20, ny), and, north wall)
)
then (
put npc(npc, npc pixel x(npc), npc pixel y(npc) -- npc pixel y(npc),mod,20)
return(false)
)
else (return(true))
end

script, npc can rise, npc, begin
variable (ny)
ny := npc pixel y(npc) / 20
if(
(read pass block((npc pixel x(npc) / 10 + wiggle room) / 20, ny), and, south wall)
||
(read pass block((npc pixel x(npc) / 10 + 20 -- wiggle room) / 20, ny), and, south wall)
||
(npc pixel y(npc) == 0)
)
then (
put npc(npc, npc pixel x(npc), npc pixel y(npc) -- npc pixel y(npc),mod,20 + 20)
return(false)
)
else (return(true))
end

script, npc can left, npc, begin
variable (nx)
nx := (npc pixel x(npc)--10) / 20
if(
(readpassblock(nx,(npc pixel y(npc)) / 20), and, east wall)
||
(readpassblock(nx,(npc pixel y(npc) + 19) / 20), and, east wall)
||
(npc pixel x(npc)==0)
) then (
variable(new x)
new x := 0
if (npc pixel x(npc),mod,20 >> 10) then (newx := 20)
put npc(npc, npc pixel x(npc) -- (npc pixel x(npc),mod,20) + new x, npc pixel y(npc))

return(false)
)
else (return(true))
)

script, npc can right, npc, begin
variable (nx)
nx:= npc pixel x(npc) / 20 + 1
if (
(read pass block(nx, npc pixel y(npc) / 20), and, west wall)
||
(read pass block(nx, (npc pixel y(npc) + 19) / 20), and, westwall)
)
then (
put npc(npc, npc pixel x(npc) -- (npc pixel x(npc),mod,20), npc pixel y(npc))

return(false)
)
else (return(true))
end

script, hurt hero, begin
if (hero-invincibility <= 0) then (
hero-hp -= 1
hero-invincibility := hero invincibility duration
update lifebar
if (hero-hp == 0) then (
fade screen out(63, 0, 0)
game over
)
)
end

script, update lifebar, begin
replace small enemy sprite(lifebar, hero-max-hp -- hero-hp)
end

script, update score, begin
clear string(string:score value)
append number(string:score value, score)
end

script, time up, begin
fade screen out(0, 0, 0)
game over
end

It gets bigger every time! Don't forget to check out the example game. The link is below.

Next time: Doors, levels, checkpoints! All of this and more! Whether you're planning a strict level-to-level flow like Super Mario Bros., freeform Metroidvania exploration, a "world map" approach like SMB3, or something else entirely, you won't want to miss the next issue. The best part? After the next issue, you've got everything you need to make a full-length game.

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.

Download the Example Game