We had a look at some of the basics of making a game using the Processing.js game library, so let's kick it up a notch by just getting down and making our own Super Mario game. When we're done with this part of the tutorial, we'll have a game with koopa troopers, platforms that we can jump up through but not fall down through, a hole with terrible muncher plants, coins, including a mysterious dragon coin, a giant banzai bill bullet that comes out of nowhere, and even some advanced things like not actually putting enemies in the level from the start, but only just before you get to where they're supposed to be.
And it's going to be pretty sweet. Let's get started!
Okay, almost. Before the first stop, we'll need a game with at least one level which has at least one level layer so that we can even put mario somewhere. Just like we did last time. So let's get cracking:
This sets up an empty, but for all intents and purposes functional, game world that we can stick Mario in:
So now we can make Mario. First, let's think of what we want Mario to be able to do:
So, let's find some pictures for all these different things:
You'll notice that some of these states use multiple pictures. This lets us create the illusion of "animation": if we loop through the images quickly enough, it'll see like Mario's animated, and that's good! So let's make a Mario!
And then let's add a mario to our world:
This is nice and all, but like the previous tutorial, nothing's happening. Mario just hangs there. So, let's make things a little more interesting. First, let's introduce some gravity.
We introduce three new global values:
The "down force" value determines how much players and NPCs etc will be "pushed down" towards the ground when they're not standing on anything. The "acceleration" value determines how much they will accelerate while they're falling, so that the longer they fall, the faster they fall, and the "dampening" value determines what happens when we give players or NPCs an impulse (= some speed). With dampening set to "1", the amount of speed is the same from one frame to the next. "0.5" will halve the amount of speed per frame, and "0" will kill off speed complete from one frame to the next.
Of course, we also need to make sure those forces apply to Mario, so we modify our constructor:
Excellent! now Mario's on the ground... but.. still not doing very much. So, time to hook up the controls. We will be using "WASD" controls again, so that 'W' means "up", 'A' means "left", 'S' means... ... well we're not actually going to use 'S', but 'D', means "right".
At this point, Mario is still fairly boring. Sure, we can control him, but none of those fancy states are being used, so let's refind the input handling (you may remember binding keys to motion controls in the previous tutorial) so that when we walk left or right, we actually show as running, and when we jump, we actually look like we're jumping.
hang on to your pants, this one's a biggy:
So what does this code do? Let's look at the three parts that this code can be looked at, separately. First, the first "if" statement.
The first "if" statement checks for the 'A' and 'D' keys, which correspond to walking left and right, respectively. The trick here is that we want to be able to press both keys at the same time, without one of them winning, so we check for the keys in two steps: 1) are either A or D pressed? If so, 2a) is A pressed, and 2b) is D pressed. If A is pressed, we will be walking left, so we give Mario an impulse of -2, horizontally, and 0, vertically (because running is a horizontal-only affair). However, if you scroll up to the pictures we had for running, they're for running to the right. In order to make sure we run to the left with the pictures oriented correctly, we need to make sure that they are flipped.
If D is pressed we do roughly the same thing, but in the opposite direction: a horizontal impulse of 2, rather than -2, and we make sure the pictures are NOT flipped, so look like they run to the right. Finally, at the end of the first "if" block, we make sure that Mario's state is indeed the running state, by setting the current state to "running". Makes sense, really!
Then, the second "if" block. In this block we check for the 'W' key, which is our jumping key. However, we can't "just jump", there's a few conditions that need to be met. First, we can only jump if 'W' is pressed. Okay, so that one is obvious. Second, we can only jump if we're not already jumping right now. This prevents us from jumping while we're already jumping. Because that would just be weird! Finally, we also don't want to jump unless we're actually standing on something. This last one, especially, is important. We don't want to be able to jump in mid air while we're falling from a platform, for instance. Or rather... for the moment, we don't, but once we're done with this game, maybe we actually do want to change that... hmmm... anyway, on to the rest of the code: All those conditions are combined with "&&" which is programming-code for "AND". So, we have if(something AND something AND something) { [then do this] }. We don't actually use the word "AND" for it, but use "&&" because that's how programming languages have been doing things for decades, so it's one of those things you quickly learn to use and expect everywhere.
So with all those conditions met, what do we do? Well, we give Mario a jolt of speed upwards, -35 in fact, and we make sure that his state is the "jumping" state, so that it looks like he's jumping. Perfect! So what's with that last block?
The last block checks "are we actually pressing any key?". If we're not pressing left, or up, or right, then it's a good idea to make sure that Mario looks like he's just standing there, so we set his state to "idle" if no relevant keys are pressed. So with all that work done, what does Mario look like?
Hmm... that's actually a bit too fast. By default, the game library will run our games at 60 frames per second. That's really high, so let's set that to 30 instead:
Now what does it look like?
That's better. On to making this prettier!
While the input handling we have works, you might notice that it's doing funny things when you jump and run left or right at the same time. So let's fix that. Instead of making jumping a normal state, let's make it special. The game library lets you add specific durations to states, so that when you change to them, they last for a specific number of frames, and then give you a signal that they're done. That way, we can prevent Mario from changing states or triggering behaviour while he's jumping. This means we have to do two things: 1) say that jumping is a special state, with a specific duration, and 2) make sure the input handling takes into account that we might not be able to change states while Mario is jumping. Let's-a-di-go!
As you can see, the only thing we have to change for the jumping
state is saying "this state has a duration". In this case, we've set
it to 15 frames (and since the game plays at 30 frames per second,
that means a jump lasts half a second) and that's all we have to do!
Then, in our input handling, we can make use of two special things.
First, the active
word means "the currently active state".
If we've set the current state to "idle", then active
points
to the idle state. If we've set it to "jumping", then active
points to the jumping state. That's pretty useful, because it lets us
check whether the "current state" is one of those special states that
we shouldn't interrupt: active.mayChange()
gives us a "yes"/"no"
answer to the question "is this a special state, and if so, has it finished
so that we may change to another state already?".
Making use of active.mayChange()
, we can now say: "IF
we're allowed to change states, then check if they running keys are
pressed. If so, change state to running. Otherwise, change it to just
standing around, being idle". So now we should be able to do strange
super-power jumping anymore if we jump and move to the left or right
at the same time:
So, now that we have a reasonably well-controlled Mario, I think it's time we jazzed this game up a little with some good looks. What do you think?
Let's start with that background. It's boring, right? I mean, sure, it's a pretty color but it just... a plain color. How about instead we add some background in the Mario style?
Let's add the following background:
Adding a background means treating it like a special kind of sprite, and adding this to the list of not-moving background sprites:
The important line is that addBackgroundSprite(...)
line. This says
"Add something, which is inside the ( and ), to the list of background sprites." with
the word "static" simply meaning "things that don't change". In this case we add
what is known as a "Tiling Sprite" - a picture that is repeated to fill a certain area
of the screen. Since we want the background to fill the entire level, we use a picture
of a typical Mario sky line, and make it repeat itself starting at (0,0), which is the
upper left corner of the level, all the way down to (width,height), which is the lower
right corner of the level.
We could keep it at that, but since we're updating the background anyway, let's make sure that Mario can't run out of the level:
These two extra boundaries sit *just* outside the level on the left and the right, like what we did in the previous tutorial to keep our "Thingy" inside the screen. Except this time our Thingy is a Mario! =D
And because we can, let's make one more change: a background is nice, but it would be even better if we could actually see that it repeats, by making the level wider than a single screen. So... let's do that!
Two things to note are that in our initialize
method,
we now make a MarioLevel that is twice as wide as the screen. In order
to make sure that game library doesn't get confused, we then also
tell it that the viewbox
for this game starts at (0,0),
which is the upper left of the screen, and is only screenWidth
wide, and screenHeight
high, so it is exactly the same
size as our game "window". If we left it at this, we would see Mario
run to the right and then disappear, but the viewbox is special: it
can follow a player around. So we make one final change:
We've done two things here. First, we've made the level layer always "know" what
the mario
word refers to. Now, if we refer to mario
in any
of the methods of the level layer, it will know what to use. That's important, because
the viewbox needs to know who it should be following around when the level layer is
being drawn. That's the second thing we changed. Normally a level layer has a premade
method called "draw" that it kind of "inherits" from the game library. But, it only
draws the level layer, it doesn't do anything else. If we want to make the viewbox
follow Mario around, we'll need to set up instructions that say "do what you normally
do, but ALSO follow mario!". So that's what we do: super.draw()
is the
line that says "do what you normally do", namely call the "draw" method that the level
layer already knows about, because it is part of its super class (remember super
classes from the previous tutorial?). Then the next instruction is to the viewbox:
viewbox.track(parent, mario)
tells the viewbox to follow mario around,
using the visual properties of the level that the level layer is in, which the
level layed calls its parent
. Whenever the level layer needs to
point to its owning level, we can use the word parent
and the game
library will know what to do. Especially now, that's pretty handy.
Okay! That's enough! We added background. We added walls to the left and right. We made the level bigger than the screen. We (hopefully!) made the screen "follow" Mario... what does it look like already!??!
Now that's what I call pretty cool! We can run around and the background will scroll along, until we reach either end of the level and we simply walk into the side walls.
So... how about we make it even cooler?
We've been putting it off long enough: having Mario walk on a thin little line works, but things will look a lot better if we make him walk on some actual ground. Or at least, if we make it LOOK as if he is. Let's start creating some ground, shall we?
Ground is actually quite similar to adding background. Since the only thing that determines whether Mario's "on something" or not is the (normally) invisible boundaries (which we can see right now because we're cheating and we're drawing them, even though normally you don't), we can actually just leave the boundary where it is, and set up some more background that makes it look like there's ground underneath the boundary line.
In order to make ground, we're going to use a few images. Two, in fact. One to look like the top soil, with grass, and one that fills in the region underneath:
The trick is to lay down a row of top soil, and then fill in the rest with filler. We've actually already seen how we are supposed to do this: we use a TilingSprite. First, let's define a method for "laying" ground:
This method expects four values when we use it: the first two are the top-left x/y coordinates for the ground we'll place, and the last two are the bottom-right x/y coordinates, so we're supposed to draw ground in the rectangle between those two coordinates. The top-most row has to be grassy ground, so we use the grassy picture, and make a TilingSprite that runs from (x1,y1), which is our indiated top left corner, to (x2,y1+16).
What do those values mean? Well, x2 is the horizontal coordinate for the right-most edge of the rectangle we want to fill. So that one's pretty easy. "y1+16" means "the top coordinate value, plus 16 pixels". We add 16 pixels because we know the picture for the grass is 16 pixels high. So we want to draw a row of grass that starts at (0,0) and is exactly 16 pixels high, and the normal width wide.
After that, we fill the rest with filler pictures. This doesn't start at (x1,y1), but at (x1,y1+16), because we need to start at the same horizontal position, but 16 pixels lower (or we'd be drawing filler on top of the grass!). And then it has to be wide enough to reach x2, horizontally, and high enough to reach y2, vertically.
Finally, because ground acts as something Mario can stand on, we add a boundary that runs along the top of the grass, so that Mario has something to stand on. And with that, we add some ground to our level layer:
Now we'll have ground running across the screen, from 32 pixels "outside" the screen on the left, to 32 pixels "outside" the screen on the right, filling an area with height "height of the screen, minus 48 pixels" to "height of the screen". What does that look like? Well, this:
What a difference some ground can make!
There's another thing we can do that also makes a neat difference, and that's adding a second layer of background that looks like it's farther away, so that when we walk, it moves slower than the normal background.
We can add "parallax", or the concept of faking distance by tricking the eyes into believing there's distance by making things that should seem "close" move fast and things that should seem "far" move slolwy, by adding new layers to our level. We're going to make it look like we have more background in the distance, so we'll add a new layer behind the main layer that has Mario on it. In fact, the code for it is not very long, can you tell what it does?
The important part is that new BackgroundLayer
class, which does something
special. Unlike a normal layer, such as the MarioLayer
, the BackgroundLayer
has a more elaborate call to tell its super class what's up: it gives it seven values instead
of one! The first value is the same as for normal layers: the Level that this layer should
be in. So far so good. Then, it tells it what that level's width and height are. This is important,
because those are needed to do something intelligent with the following four values: the first
two are "shifting" values, which let us say by how much this layer is shifted left or right,
as well as up or down. We don't want that right now, so we keep both of those zero. Then,
the two values after that tell the layer by how much it is scaled. This is very important!
To make it look like this layer is farther away, we want it to move slower than the main
level layer with Mario in it. Since we can't tell layers how fast they need to move, we
instead tell them that they are bigger or smaller than the main level. This makes the game
library squash or stretch the level, so that the start and end match up -- if we
add a layer that is larger than the main layer, it will be squashed, and it'll look
like it scrolls faster than the main layer. And of course the other way around; if we add
a layer that is smaller, it will be stretch, and will look like it's scrolling slower. So
that's what we do, we say that this new background layer is only 0.75 the size of the
main layer. So it should scroll slower. Let's find out:
go ahead, make Mario run to the right. Notice how our new background moves slower than the one we already had? Does it make it look like this new background is farther away? Cool, eh?
Now we have a great start for a real game: we have a Mario, we have cool backgrounds, and we have some ground to run on. Time to start adding finishing touches, what do you say?
Usually, a "platformer" such as the game we're making is called, has "platforms" that you can jump on, and from. Of course, our game doesn't have these... yet! But we can add them pretty easily by using the same idea as before: the part of a platform that we can stand on will be represented by a boundary, and everything else will just be a backgroud image, of course placed so that it looks like there is a platform. We'll be adding two types: a slanted platform, and straight platforms.
For the slant, we'll use a single image, and the code is pretty simple:
For the horizontal platforms we're going to put in a bit more work, using the same trick that we used for the normal ground: if we treat it as six images (three for the top soil, and three for the filler below it) then we can make any sized platform by always drawing the top left and right corners, and bottom left and right corners, and then "filling in the space between them " with the bits that go in the middle:
You may be wondering what that lc.align(LEFT, TOP);
does.
By default, sprites are placed with their center point wherever you tell them
to position to. If we call setPosition(0,0)
and the sprite is
a 16 by 16 pixel image, then 8 pixels will be on the left of (0,0), 8 will be
on the right, 8 above, and 8 below. Sometimes, that's not useful at all, and
you want to change that so that if you put an image at (0,0), the entire image
is to the left, right, top or bottom of that coordinate. In those cases, we
can use align(...,...)
to change the default placement. In this
case, we want the image's left side, and top, to be aligned with the coordinate
we give its setPosition
, so we say align(LEFT,TOP)
(we have to tell it how to do horizontal align first, and then vertical align).
So, not very complicated code, just "a lot of it" to make something really
useful happen: After we define our function addGroundPlatform
we
can call it as many times as we like to add platforms to our level. In the
above code, we added six platforms, and then a little above that we added
three slanted platforms, so... what does it look like?
That's more like it! Now we're starting to make a real game. Of course, there's still stuff missing. We want treasures, and enemies, and a way to win. At the least. So let's start adding those things in shall we?
In the game library, treasure is a form of "pickup". Pickups are things that can be picked up by either players or NPCs (non-player characters) by walking into them, and when they do, "something" can happen. Of course, we're in control, so what happens is up to us. Very handy.
If you've ever played a mario game, you might known that mario likes to collect coins, of all sizes and colors. In our game, we'll give him two different kinds of coins to pick up: normal coins, and the mysterious dragon coin:
In order to make the coin pickups, we start by making a master "Mario pickup"
thing, that we can use as a superclass
for every pickup we're going
to make in our game:
While this code doesn't do a great deal, it'll let us group things as "Mario Pickups", and that will become handy if we ever introduce a class of pickups that aren't for Mario.
So, let's write our coin pickups. They're pretty straightforward, so here goes:
That's it, that's all we have to define. Now if we want to use them in our
game, we simply make new Coin(...,...)
or new DragonCoin(...,...)
and then add them to our list of pickups, for the player only: