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:
- If the hero is moving up (i.e. his y-velocity is less than
0), use the jumping frame.
- If the hero is moving down (i.e. his y-velocity is greater
than 0), use the falling frame.
- If the hero is moving left or right (i.e. his x-velocity is
non-zero), cycle between the standing and walking frames.
- 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:
- Add the absolute value of the hero's x-velocity to hero-animation
- If hero-animation is at least twice the
maximum x-velocity, then subtract that amount
- If the resulting value is less than the maximum x-velocity,
use one frame; if not, use the other
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:
- Doors and levels. Want your game to
expand past the first map? This topic will teach you how to transition
from one map to another.
- Powerups. Give your hero superpowers!
This topic will show you how to create your own powerups to provide
various effects.
- The HUD. Want to display information
onscreen? The WIP version of the OHRRPGCE provides additional support
to allow you to display things like life meters.
- Projectiles. Fireballs and such. This
topic will teach you how to give ranged attacks to hero and enemy alike.
Thanks for reading and remember to vote on your favorite
topic! The link to download the SS101 example game is below.