Sidescrollers 101 - Part 2
 feature by Adam Perry

Part 1: Movement, Physics, and You

Part 2: Jumps, Falls, and Walls

Welcome to our second installment of Sidescrollers 101. Today, we'll finish the sidescrolling essentials: we'll make the hero jump and fall as well as let him collide with walls without going through them. In other words, you can make your own game after this lesson! Here's the script as of last time:


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
end

plotscript, new game, begin
initialize
do game
game over
end

script, initialize, begin
suspend player
gravity := 35
friction := 15
hero-x := hero pixel x(me) * 10
hero-y := hero pixel y(me) * 10
hero-vx := 0
hero-vy := 0
hero-speed := 10
hero-max-vx := 100
hero-max-vy := 100
end

script, do game, begin
variable(playing)
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-vx += hero-speed)
if (key is pressed(key:left)) then (hero-vx -= hero-speed)

# Reduce speed if our hero's going too fast
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.

hero-x += hero-vx
hero-y += hero-vy

if (can fall) then (hero-vy += gravity)
else (
# Apply friction
if (hero-vx << friction && hero-vx >> friction * -1
&& key is pressed(left) == false && key is pressed(right) == false)
then (hero-vx := 0)
if (hero-vx >= friction && key is pressed(right) == false)
then (hero-vx -= friction)
if (hero-vx <= friction * -1 && key is pressed(left) == false)
then (hero-vx += friction)
)

put hero(me, hero-x/10, hero-y/10)
wait(1)
)
end

script, can fall, begin
# TODO: Figure out if the hero can fall
# For now, we'll just say he can't
return(false)
end

If you don't remember what's going on here, you might want to take another look at Part 1 in last month's issue (linked above for your convenience).

Walls!

We've already got gravity in our script. The problem is that we're always telling the game that the hero can't fall. If you changed can fall to true last month, you got some falling action, but through the walls. Well, our first order of business today is to make the hero fall normally, stopping on standable surfaces. Here's the full can fall script:


define constant, begin
5, wiggle room
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 pixel y -- (hero pixel y, mod, 20)) * 10
hero-vy := 0
)
return(true)
)
else (return(false))
end

Okay, once again, a complicated script. The only thing you really need to know is that there's a new number called wiggle room, which is the amount of pixels a character is smaller than the full 20 pixels across. The bigger this number is, the more easily the hero will fall through gaps.

The technical stuff here is that we're checking the wallmap underneath the hero's feet. Because the hero might be standing on two tiles -- the one under his left and the one under his right -- we're checking both. If the hero was falling, the script aligns him with the floor beneath him. Got it? Good.

can left, can right, and can rise

So the hero can stand on things. Cool. But he can still move through walls. Let's fix that with a couple of new scripts that I'll call can left, can right, and can rise for lack of a better naming scheme. They'll do about what you expect them to do, which is to say about what can fall does but in a different direction.

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 pixel y -- hero pixel y,mod,20 + 20) * 10
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 := (heropixelx,mod,20) * 10

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

Complicated scripts again, but the good news is that they're a lot like the previous one. For technical reasons that give me a headache and that I won't get into, the can left script is more complicated when the hero is moving left very quickly, which is why there are a few extra lines there.

Integration time!

Good news: these are some of the most aggravating scripts to write. You get the benefit of my experience here. This is not the first version of these scripts -- it took me a long time to get them to work right. With them out of the way, you'll be able to have a very smooth sidescroller. Let's put them into the script, shall we? While we're at it, let's throw in jumping. This is a long script; the new parts are in bold.


global variable, begin
 10, hero-jump-speed
end

script, initialize, begin
  suspend player
  gravity := 35
  friction := 15
  hero-x := hero pixel x(me) * 10
  hero-y := hero pixel y(me) * 10
  hero-vx := 0
  hero-vy := 0
  hero-speed := 10
  hero-jump-speed := -110
  hero-max-vx := 100
  hero-max-vy := 100
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-vx += hero-speed)
    if (key is pressed(key:left)) then (hero-vx -= hero-speed)
    if (key is pressed(key:alt) && hero can jump) then (hero-vx := 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.

    hero-x += hero-vx
    hero-y += hero-vy

    hero can jump := false
    if (can fall) then (hero-vy += gravity)
    else (
      hero can jump := true
      # Apply friction
      if (hero-vx << friction && hero-vx >> friction * -1
          && key is pressed(left) == false && key is pressed(right) == false)
          then (hero-vx := 0)
      if (hero-vx >= friction && key is pressed(right) == false)
          then (hero-vx -= friction)
      if (hero-vx <= friction * -1 && key is pressed(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)
    wait(1)
  )
end

Important note: I've added a new variable called hero-jump-speed. Mess around with this to get the value you want. It needs to be negative, though, because the hero is moving up when he jumps. Other than that, the new bits are pretty straightforward, right?

End of lesson: Our script

A short but intense lesson! This is is what our script looks like at the end of the day:


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
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 := -110
hero-max-vx := 100
hero-max-vy := 100
end

define constant, begin
5, wiggle room
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-vx += hero-speed)
if (key is pressed(key:left)) then (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.

hero-x += hero-vx
hero-y += hero-vy

hero can jump := false
if (can fall) then (hero-vy += gravity)
else (
hero can jump := true
# Apply friction
if (hero-vx << friction && hero-vx >> friction * -1
&& key is pressed(left) == false && key is pressed(right) == false)
then (hero-vx := 0)
if (hero-vx >= friction && key is pressed(right) == false)
then (hero-vx -= friction)
if (hero-vx <= friction * -1 && key is pressed(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)
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

You can make a full sidescroller now. Cool, huh? But there's always more that you can add to your game.

Next time: Animation. Because a one-frame hero just isn't that exciting.