Sidescrollers 101 - Part 6
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

Part 6: Doors & More

Up until now, you've had to cram your game onto a single map. Today, that changes. Are you ready to tear down all your boundaries?

See Part 5 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. It might be worth your while to browse through the archive at the top of the article.

Last time, we created a HUD: life meters and more. We're diving into increasingly game-specific territory: not all games have a HUD, not all games have goomba-types. Well, let's pull out for a bit. Every game does need some way of transitioning between maps. Generically, we'll call these "doors," but they come in three types:

A less ambitious SS101 might cover just one or two of these types, but not this article. I'm going to do all three!

Note that this is not necessarily a good idea for your game -- it's rare to see a game use all three of these. Super Mario Bros. uses location triggers (flagpoles) and action triggers (pipes); Metroid uses screen-wraps exclusively; Sonic the Hedgehog uses location triggers exclusively. Offhand, Super Mario Bros. 2 is the only game I can think of that uses all three, and then you could argue that the screen-wraps were only used because of hardware limitations. Less is often more in this case.

Note that screen-wraps are essentially a specific instance of location triggers, but it won't make sense to treat them that way in the code. Behaviorally, screen-wraps are often displayed as Zelda-style scrolling, with the new screen pushing the old screen out of the way. There's no easy way to do that in the OHRRPGCE, though, so we won't try. (Famously, Sword of Jade does use that scrolling effect, but only within the same map. It still has the fade out/fade in transition from map to map.)

Here's a quick rundown on how each type of door will work in our script:

Screen-wraps are the easiest type to detect -- and by "detect," I mean "figure out when it's being used." It's easy to figure out that the player is at the edge of the screen. On the other hand, there's no built-in method of figuring out what to do with the player at that point, so we'll need a special convention for deciding which door to use.

Location triggers and action triggers will be handled in the same way: as invisible NPCs. The difference is that we'll check for the location triggers every cycle and we'll only check the action triggers when the "door button" is held. For our purposes, the door button will be the up key. These NPCs will call a script that uses a door -- the OHRRPGCE's built-in doors, that is.

Note that you can also use action-triggered NPCs for non-door purposes! This is your ticket to creating a world that the player can interact with. Use these NPCs for all the normal NPC uses: they can be switches to be flipped, people and creatures to talk to, or signs to give the player direction.

We'll talk more about those door types later. For now, let's jump into the script and implement screen-wraps. I mentioned we'd need a convention for deciding which door to use. We'll use door 0 for the top of the screen, door 1 for the right side of the screen, door 2 for the bottom of the screen, and door 3 for the left side of the screen. Conveniently, these numbers are the values Hamsterspeak uses for the constants up, right, down, and left.

    # 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

if (hero-x + 200 >> map width * 200) then (screenwrap(right))
if (hero-x << 0) then (screenwrap(left))
if (hero-y + 200 >> map height * 200) then (use door(down))
if (hero-y << 0) then (screenwrap(up))
... script, screenwrap, direction, begin
variable(old map)
old map := current map
use door(direction)
if (direction == left) then (
if (old map == current map) then (
# No map change -- stop hero from moving left
hero-x := 0
if (hero-vx << 0) then (hero-vx := 0)
) else (
hero-x := map width * 200 -- 200
# Don't update y position -- remember it instead
)
)
if (direction == right) then (
if (old map == current map) then (
# No map change -- stop hero from moving right
hero-x := map width * 200 -- 200
if (hero-vx >> 0) then (hero-vx := 0)
) else (
hero-x := 0
# Don't update y position -- remember it instead
)
)
if (direction == up) then (
if (old map == current map) then (
# No map change -- stop hero from moving up
hero-y := 0
if (hero-vy << 0) then (hero-vy := 0)
) else (
hero-y := map height * 200 -- 200
# Don't update x position -- remember it instead
# Give the player a vertical boost
hero-vy := hero-jump-speed
)
)
if (direction == down) then (
if (old map == current map) then (
# No map change -- kill the player instead
hero-hp := 0
) else (
hero-y := 0
# Don't update x position -- remember it instead
# Make the player fall
if (hero-vy << 0) then (hero-vy := 0)
)
)
end

Bingo, we have screenwraps. The downside of this approach is that you'll need to make sure to reserve the first four doors on each map (doors 0-3). If the doors don't go anywhere, then the edges of the screen act as walls, except for the bottom, which becomes an instant-death pit. (Feel free to change this behavior.) Another caveat is that you can't link to the map you came from. Also, you should be kind to your players: either make every fall fatal or make them all link to another map. Don't make them guess.

Now for the location triggers and action triggers. We'll do them in one go, since they're identical. Recall that we've reserved NPC IDs 0 through 19 for enemies. We'll use IDs 20 through 29 for location triggers and 30 through 49 for action triggers.

global variable, begin
1, friction
2, gravity
...
19, lifebar
20, hero-invincibility
21, used door 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 20, first location trigger
29, last location trigger
30, first action trigger
49, last action trigger
end ... if (hero-x + 200 >> map width * 200) then (screenwrap(right)) if (hero-x << 0) then (screenwrap(left)) if (hero-y + 200 >> map height * 200) then (use door(down)) if (hero-y << 0) then (screenwrap(up)) check npcs

if (used door) then (
# Player changed maps. Update location variables.
hero-x := hero pixel x * 10
hero-y := hero pixel y * 10
hero-vx := 0
hero-vy := 0
used door := false
)
... script, check npcs, begin
variable(i, j, ref)
# Location triggers
for(i, first location trigger, last location trigger) do (
for (j, 0, npc copy count(i) -- 1) do (
ref := npc reference(i, j)
if (npc near hero(ref)) then (
use npc(ref)
wait for text box
)
)
)
# Action triggers -- only check these when up is held
if (key is pressed(key:up) || key is pressed(joy:y up)) then (
for(i, first action trigger, last action trigger) do (
for (j, 0, npc copy count(i) -- 1) do (
ref := npc reference(i, j)
if (npc near hero(ref)) then (
use npc(ref)
wait for text box
)
)
)
)
end

plotscript, npc door, which, begin
use door(which)
used door := true
end

script, npc near hero, ref, begin
variable(nx, ny, hx, hy)
nx := npc pixel x(ref)
ny := npc pixel y(ref)
hx := hero-x / 10
hy := hero-y / 10
if (nx -- hx << 20 && hx -- nx << 20 &&
ny -- hy << 20 && hy -- ny << 20)
then (return (true))
else (return (false))
end

"Why do I need 20 action triggers," you ask? Good news! As previously mentioned, you can use them as regular NPCs, not just doors. You might want more of them. Go crazy.

But as far as setting up your door NPCs is concerned, you'll want to make them blank NPCs (no walkabout graphic) and give them the npc door plotscript, with the door number as the script argument.

That's it for doors! By special request, though, here's a bonus feature. James has asked that I mention gamepad support. While I don't personally have a gamepad, it's easy to allow gamepad users to use their gamepads.

    if (key is pressed(key:right) || key is pressed(joy:x right)) then (
hero-direction := right
hero-vx += hero-speed
)
if (key is pressed(key:left) || key is pressed(joy:x left)) then (
hero-direction := left
hero-vx -= hero-speed
)
if ((key is pressed(key:alt) || key is pressed(joy:button 1)) && hero can jump) then (hero-vy := hero-jump-speed)

...

# Action triggers -- only check these when up is held
if (key is pressed(key:up) || key is pressed(joy:y up)) then (

Simple as that, you have gamepad support. (Note that joy:button 2 might be a better choice for a jump button.)

And that concludes this month's tutorial. Warning: large script below.


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
  21, used door
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
  20, first location trigger
  29, last location trigger
  30, first action trigger
  49, last action trigger
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) || key is pressed(joy:x right)) then (
      hero-direction := right
      hero-vx += hero-speed
    )
    if (key is pressed(key:left) || key is pressed(joy:x left)) then (
      hero-direction := left
      hero-vx -= hero-speed
    )
    if ((key is pressed(key:alt) || key is pressed(joy:button 1)) && 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
    
    if (hero-x + 200 >> map width * 200) then (screenwrap(right))
    if (hero-x << 0) then (screenwrap(left))
    if (hero-y + 200 >> map height * 200) then (use door(down))
    if (hero-y << 0) then (screenwrap(up))
    
    check npcs
    
    if (used door) then (
      # Player changed maps. Update location variables.
      hero-x := hero pixel x * 10
      hero-y := hero pixel y * 10
      hero-vx := 0
      hero-vy := 0
      used door := false
    )
    
    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

script, screenwrap, direction, begin
variable(old map)
old map := current map
use door(direction)
if (direction == left) then (
if (old map == current map) then (
# No map change -- stop hero from moving left
hero-x := 0
if (hero-vx << 0) then (hero-vx := 0)
) else (
hero-x := map width * 200 -- 200
# Don't update y position -- remember it instead
)
)
if (direction == right) then (
if (old map == current map) then (
# No map change -- stop hero from moving right
hero-x := map width * 200 -- 200
if (hero-vx >> 0) then (hero-vx := 0)
) else (
hero-x := 0
# Don't update y position -- remember it instead
)
)
if (direction == up) then (
if (old map == current map) then (
# No map change -- stop hero from moving up
hero-y := 0
if (hero-vy << 0) then (hero-vy := 0)
) else (
hero-y := map height * 200 -- 200
# Don't update x position -- remember it instead
# Give the player a vertical boost
hero-vy := hero-jump-speed
)
)
if (direction == down) then (
if (old map == current map) then (
# No map change -- kill the player instead
hero-hp := 0
) else (
hero-y := 0
# Don't update x position -- remember it instead
# Make the player fall
if (hero-vy << 0) then (hero-vy := 0)
)
)
end

script, check npcs, begin
variable(i, j, ref)
# Location triggers
for(i, first location trigger, last location trigger) do (
for (j, 0, npc copy count(i) -- 1) do (
ref := npc reference(i, j)
if (npc near hero(ref)) then (
use npc(ref)
wait for text box
)
)
)
# Action triggers -- only check these when up is held
if (key is pressed(key:up) || key is pressed(joy:y up)) then (
for(i, first action trigger, last action trigger) do (
for (j, 0, npc copy count(i) -- 1) do (
ref := npc reference(i, j)
if (npc near hero(ref)) then (
use npc(ref)
wait for text box
)
)
)
)
end

plotscript, npc door, which, begin
use door(which)
used door := true
end

script, npc near hero, ref, begin
variable(nx, ny, hx, hy)
nx := npc pixel x(ref)
ny := npc pixel y(ref)
hx := hero-x / 10
hy := hero-y / 10
if (nx -- hx << 20 && hx -- nx << 20 &&
ny -- hy << 20 && hy -- ny << 20)
then (return (true))
else (return (false))
end

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

Next time: Various improvements to the script. No votes were received this month, sadly! Make sure to vote for the next topic.

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 8:

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