Sidescrollers 101 - Part 3
 A Feature by Adam Perry

Part 1: Movement, Physics, and You
Part 2: Jumps, Falls, and Walls

Part 3: Animation

Hello again! We are back from a tremendous hiatus. If you need to get caught up, there will always be an index to the previous chapters at the top of every article. Okay? Ready to go?

This month, as promised back in May, we'll be animating your hero. This is actually pretty easy, so this tutorial will be a little short. Also, starting with this issue, we'll be including a sample file, courtesy TwinHamster. Thanks! You can find the download link at the bottom of this document.

Before we begin, here's the updated plotscript. Note that it has been corrected since May's issue and several bugs were fixed that existed in the previously published script.


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 := -145
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 (
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)
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

Does that seem long? Maybe! I guess it isn't exactly the simplest thing to make a running, jumping action star.

So, animation. This is a topic that has evolved over the years: in the beginning, characters often had as few as three sprites in each direction (standing, walking, and jumping). With technological limits on animation virtually nonexistant nowadays, modern 2D sidescroller heroes sometimes have hundreds of frames of animation. For a great example of a very robust animation, take a look at Alucard in Castlevania: Symphony of the Night. The OHRRPGCE, however, was designed with a framerate more appropriate to RPGs than action games, so some of the animation effects you see in commercial games may not look as good here. For the purposes of this tutorial, we'll be giving the hero eight frames of animation (to coincide neatly with the eight frames in a standard walkabout), but if you want to create more complicated animations, I'll do my best to make it easy to plug them into the script.

The first thing we'll want to do is set up animation variables and an empty script.

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

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

...

# 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)

...

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)

...

script, animate hero, begin
end

We've created global variable 11, called hero-animation, and a new script, called animate hero. The new script is called as the very last thing we do, so we can figure out exactly which animation sprite to assign to the hero during each frame. We also took care of deciding which way the hero should face: when the player presses left, the hero faces left, and when the player presses right, the hero faces right. Straightforward enough, right?

For this tutorial, we'll have four frames of animation in two directions each: standing, walking, jumping, and falling. Our animate hero script needs rules to decide which frame is used. In order of precedence, here are our rules:

  1. If the hero is moving up (i.e. his y-velocity is less than 0), use the jumping frame.
  2. If the hero is moving down (i.e. his y-velocity is greater than 0), use the falling frame.
  3. If the hero is moving left or right (i.e. his x-velocity is non-zero), cycle between the standing and walking frames.
  4. Otherwise, use the standing frame.

In scripting language, that looks like this:


script, animate hero, begin
if (hero-vy << 0) then (
# Jumping
) else (
if (hero-vy >> 0) then (
# Falling
) else (
if (hero-vx <> 0) then (
# Cycle between walking and standing
) else (
# Standing
)
)
)
end

That's a simple translation from our rules table to Hamsterspeak code. It's all comments and no functional change right now, though, because we need to make a decision. How will we store the frames within the editor?

The editor tells you that you're drawing walkabouts to face up, left, down, and right, with "A frames" and "B frames" for each direction. Forget about all of that, since it doesn't matter. You can set it up any way you want, as long as your script is backing it. To make things as easy as possible, though, we'll use "A frames" for our hero facing left and "B frames" for our hero facing right. That way, we only need to add a few lines of code to set the hero's direction, regardless of which frame he's in:

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
) else (
if (hero-vy >> 0) then (
# Falling
) else (
if (hero-vx <> 0) then (
# Cycle between walking and standing
) else (
# Standing
)
)
)
end

Like I said, it doesn't matter if you set it up differently, but it'll make your script slightly more complicated. This next part matters even less: choosing which frame goes where. We'll put them in the following order: standing, walking, jumping, falling. The editor thinks of these as up, right, down, and left, and Hamsterspeak thinks of them as the numbers 0, 1, 2, and 3.

As a side note, these next few steps become a little complicated if you want more than eight frames of animation -- but not much more. Basically, you'll need to make sure you're using the correct walkabout set as well as the correct frame within the walkabout set. You should add a set hero picture before each set hero direction in that case, and you'll need to define more rules about your animation.

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
) else (
# Standing
set hero direction(me, 0)
)
)
)
end

The only thing left now is to decide how to animate the hero when he's moving. There are several ways to go about this, but in my opinion, it looks best if the frames cycle faster when the hero is moving faster. What we'll do is increase hero-animation based on hero-vx and use hero-animation to decide which frame we're using.

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

In case that newest block isn't so intuitive, here's the process:

The result, if it's not immediately obvious, is that when the hero is running at full speed, he'll be switching frames every cycle. If he's moving more slowly, then he'll animate more slowly. Neat? Neat!

If you wanted to add more frames, you could use hero-animation to keep track of which frame the hero is currently on. You could also use a different animation for when the hero is at full speed, like the old Sonic games or Mario 3.

And that's it for animating your hero! Make sure to download the sample file and try making your own. Here's the full plotscript:


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

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-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.

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

We're done with the hero for now. He can move around and jump. Later, we might want him to be able to do more -- give him fancy moves or attacks -- but right now, we should focus on giving him something to do.

Next time: Our first enemy!

But that's not all! Starting the issue after next (that's part 5, SS101 fans!), I'll be doing topics by vote. 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 5:

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