Sidescrollers 101
A Feature by Adam Perry

Part 0: The Loop

So you want to make a sidescroller. This is a multi-part series on advanced plotscripting that will take you from the most basic basics to the final steps needed to create your very own sidescroller.

Because this is the first installment, we're going to lay down a framework that we can build on in future issues. Take a good look:


include, plotscr.hsd
include, scancode.hsi

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

script, initialize, begin
# TODO: Initialize game here
suspend player
end

script, do game, begin
variable(playing)
playing := true

# Let's do The Loop!
while (playing) do (
if (key is pressed(key:esc)) then (playing := false)
# TODO: Other game-playing stuff goes here.
wait(1)
)
end

Okay, our script doesn't do much right now. Here's the quick and dirty: we have a new game script, which we'll use as the startup script. It calls an initialize script to do any setup stuff we need to do before starting. Finally, the do game script has a continuous loop. Pressing Esc will quit the game.

The most important part of the script is The Loop. Everything will go in there. (Does this sound like an exaggeration? Hardly!) Note the TODO comments. That means there's something left for us To Do there. We'll fill in those parts as we go along.

Ready to move on?

Part 1: Movement, Physics, and You

Today, we're going back to high school (or forward, if that's how you roll) to talk about movement. Specifically, we're going to talk about physics (kinematics, really) and just a little bit of calculus hidden in there. Yes, there's math in today's lesson, but it's all at the very heart of platformer magic. You'll want to understand how this stuff works.

Position

At a very fundamental level, movement is made up of positions. Right now, the hero is standing on the platform at the entrance of the level. In two seconds, he'll be further to the right. Position is represented in the OHRRPGCE in terms of pixels. (A lot of the time, you'll deal with it in terms of tiles, but for this lesson, let's forget about tiles.) Everything has pixel coordinates. The top left corner of your map is (0, 0); that is, X=0, Y=0. One pixel to the right of that would be (1, 0); that is, X=1, Y=0. As you go right, X increases, and as you go down, Y increases. An NPC's pixel coordinates are also measured from the top-left corner of his sprite.

A pixel is the smallest graphical unit. Your hero can't move half a pixel onscreen. However, even though we won't be able to represent it graphically, we still want to store the hero's position down to fractions of a pixel internally. This will make movement a lot smoother. To this end, we're going to multiply everything to do with movement by 10. Hamsterspeak doesn't handle fractions, so we're just going to have to do this ourselves. Make a mental note and move on.

Velocity and acceleration

From one frame to the next, the hero's position changes. He was five pixels to the left one tick ago, next tick he'll be five pixels to the right. The hero's X-velocity, or horizontal speed, is +5 pixels per frame.

Everything that moves has two velocity values: X-velocity and Y-velocity. We'll store these values in variables, since they'll change as the player moves the hero around. We'll also store the hero's position as variables, so we can remember fractions of pixels as mentioned above. While we're at it, let's define two more variables: friction and gravity.

global variable, begin
1, friction
2, gravity
3, hero-x
4, hero-y
5, hero-vx
6, hero-vy
end

What do gravity and friction mean? Think of gravity as how fast you fall and friction as how easy it is to slow down on a surface. The moon has low gravity; ice has low friction. In our script, friction and gravity are both acceleration values. Velocity is how fast position changes; acceleration is how fast velocity changes. You might want to play around with these values.

Let's initialize all of our new variables now. (You can give them different values if you'd like.) We can also remove that TODO flag from the initialize script.

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
end

Tada! We'll also update The Loop to use some of those values.

script, do game, begin
variable(playing)
playing := true

# Let's do The Loop!
while (playing) do (
if (key is pressed(key:esc)) then (playing := false)
# 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 is moving slower than friction, stop him
if (hero-vx << friction && hero-vx >> friction * -1) then (hero-vx := 0)

# If hero is moving right, accelerate left
if (hero-vx >= friction) then (hero-vx -= friction)

# If hero is moving left, accelerate right
if (hero-vx <= friction * -1) 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

Oh! Looks like a new script has popped up. Don't worry about it for now. Notice the part marked "Apply friction"? If the hero is on the ground, he'll come to a stop unless he keeps moving. Changing the value of friction will determine how quickly the hero slows down.

Accepting player input

Okay, the hero's position is being updated onscreen, but it's not changing. Let's allow the player to move the hero. First, let's make some new variables to decide how fast he can move.

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

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

Three new variables. Hero-speed is the hero's acceleration: how much faster will he move this frame than last frame? Hero-max-vx and hero-max-vy are what they sound like: the maximum x and y velocity. Play around with these values for some fun...

Now, let's change around The Loop to accept the player's input. It goes a little something like this:

 # 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

For now, our hero can't jump. He'll move left and right, though. There's one other change: we don't apply friction if the player is moving the hero in his current direction, otherwise he'd run more slowly on the ground. Friction is applied when trying to change direction, though. On a frictionless surface, it's just as hard to change direction as it is in midair. (This is also true in real life, but with different consequences.)

End of lesson: Our script

That's it for now! 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
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

The hero can move! Isn't that exciting? You should be excited. Right now, compiling this script will give you a warning, but you can ignore that. Go ahead, import this script into your RPG file and run it.

Next time: Jumps, Falls, and Walls. For an exciting sample of what's coming, try changing can fall to return true.