Prelude: What's this all about?
There has been a lot of online propaganda about making games using HTML5 and then deploying them to EVERY DEVICE ON THE PLANET... And why not, it make sense in theory... just create a game in the browser, wrap it in some sort of container and sell it for lots of monies in every store. How hard could it be? I've decided to give it a shot and find out for myself. I'm going to be making a clone of the awesome game SkiFree - I will be trying to get it to run on as many devices as I can get access to, but I'm going to skip Step 3 (profit)... oh and I'm doing all this as a SydJS talk because I think others might be interested.
Part 1: Sprite library please
My game engine is fairly simple, for the background we have a grid of equal sized squares. At the center of the screen we have a skier. Each time we update the world all background tiles are moved up a few pixels, giving us the perception that the skier is moving downward. Sounds simple right? Now we just need a way of drawing the tiles, and moving them every 16.7ms (60 frames per second). I'm going to say something very important right now, please pay attention: Do not write your own game engine, there are many out there. Just use them and spend the time saved with family and/or loved ones. For my sprite library I chose CreateJS which encapsulates a bunch of libraries (EaselJS, TweenJS, SoundJS, PreloadJS) design for "for building rich, interactive experiences with HTML5" - yep, that's what they say - I chose it because after an intensive 5 minutes of search stack overflow it was mentioned by a lot of people as being easy to integrate.
And now back to making a game. To make sure the user thinks we have a large of world to explore we shuffle some tiles around as they vanish off screen. That is, when a row goes off the top of the screen we take that row and insert it at the end of the grid. and when a column falls of the screen it gets inserted at the opposite side. With a few rows of padding in each dimension this grid can appear to be an infinite world.
Part 2: Enough theory, let's write some code
To get started with EaselJS we first need that magical <canvas> tag. You want to be certain to give it a height and width so that you have some dimensions to play with. I haven't looked at dealing with resizing of the canvas, and for a smart phone game it really shouldn't matter. Now let's create a stage...
canvasElement = document.getElementById(canvasElementId);
var stage = new Stage(canvasElement);
// allows touches to work
Touch.enable(stage);
The stage is the root level container where all your sprites will get added to in order for them to be drawn to the canvas. EaselJS will draw elements in the order that they appear in a container. That is, if you add a tree to the stage and then add a rock to the stage, the rock will appear in front of the tree. So to help with our rendering and the fact that we're moving background sprites around all the time I am using two containers inside the stage.
var bgContainer = new Container();
var skierContainer = new Container();
stage.addChild(bgContainer);
stage.addChild(skierContainer);
The Container object is simply a shell in which to house other sprites so that they are rendered in the correct order. It will let you do things like scale and skew, but for the purposes of this demo I'm just using for rendering order.
var world = newWorld(stage, width, height);
I have wrapped most of the functionality into this concept of a 'world' - theory being that you could have multiple worlds on screen if you really wanted to. To get my world to update I need to use the Ticker object. The Ticker will call .tick on whatever listener you give it, in my case I pass in the world that we just created. The first parameter passed to this tick function is the number of milliseconds since the last call. This can be used to bring the world into order since it's possible a few frames may have been skipped.
Ticker.addListener(world);
Ticker.useRAF = true;
Ticker.setFPS(60);
I'm going to encourage everyone to use the settings above because they are generally sensible. The useRAF option means that it will use requestAnimationFrame so that the world only updates when it's visible. The setFPS method sets the desired frames per second, you may end up with less if the system isn't so great.
Part 3: Ok, I'm bored of code, I want to see things on screen!
Since this is designed for a SydJS talk I'm going to be using Japanese characters for my sprites. The characters are as follows - taken from a friendly translation service:
- A person
- A tree
- A rock
- A crash (with some exclamation marks)
Adding a sprite to appear on screen is fairly straightforward. Load the bitmap, add it as a child to the stage/container and the give it some co-ordinates.
var skier = new Bitmap("images/person.png");
skier.regX = HALF_TILE_EDGE;
skier.regY = TILE_EDGE;
skier.x = (stageWidth * 0.5);
skier.y = (stageHeight * 0.5);
skierContainer.addChild(skier);
You will notice that I'm assigning the regX and regY properties. These are used to say which point in the sprite we want it to be anchored around. In this case I'm saying that the bottom-center of the skier sprite is where the center is. This is important when we skew the skier sprite to make it look like they're turning. Now that that is done the skier will appear on the screen. I'm not going to go through adding the other elements since the code is pretty heavily commented.
At every tick we need to move some tiles around like I described at the start, check to see if the skier has collided with something and update the score. So let's do some simple collision detection. I said simple because collision detection is a can of worms and frankly I don't think this game warrants it. If you really want to know more there are some really good articles that a quick search will turn up.
var obj = bgContainer.getObjectUnderPoint(skier.x, skier.y);
if(obj) {
// we collided
}
As you can see it's very very simple. We take the co-ordinates of the skier and ask the background container if there is an object that exists at those co-ordinates then we've crashed. I cheat a little with the crash since the correct way to do it would be to have an animation sequence that transitions the character from one state to another. Way too much work to morph Japanese characters so instead I fade one character out and fade another in using TweenJS. When the skier is done falling on his face I just fade back.
Tween.get(skier).to({alpha: 0}, 200).set({visible: false}); Tween.get(crash).set({visible: true}).to({alpha: 1}, 200); setTimeout(function(){
Tween.get(crash).to({alpha: 0}, 200).set({visible: false});
Tween.get(skier).set({visible: true}).to({alpha: 1}, 200);
}, 2300);
Tween has a very readable api. We get a target and then stack a set of actions to be performed on that target. We use the to method to add a tween to action, and we use the set method to add a set this value now action. We can also have it call functions when it is done animating. On the whole I really enjoyed this library even if the docs were a little hard to understand at first.
Ok, so we've gotten collision detection happening. Now let's add a score on the screen so we can feel like we're accomplishing something doing this. The Text object works the same as a sprite - give it some values and add it to the stage/container.
var scoreDisplay = new Text();
scoreDisplay.textAlign = "right";
scoreDisplay.x = stageWidth - 10;
scoreDisplay.y = 24;
scoreDisplay.font = "bold 24px monospace";
skierContainer.addChild(scoreDisplay);
And to update that score all I need to do is set the text property on it.
Part 5: Straight downhill is no fun, I want to turn
Let's hook up some mouse events so that we can direct the skier around the world. You'll notice I said mouse events and not touch events. That's because EaselJS just maps the touch events over to the mouse events - not sure why they do this because it means we don't get access to multitouch using this library. There are 3 directions our skier can go.
Slightly to the left (-1)
Slightly to the right (+1)
Direction of the skier is based on where the touch is in relation to the center of the screen. That is, if you touch a bit to the left, the skier moves a bit to the left. Same deal for touching on the right of the skier to make him move right. The MouseEvent gives us a stageX and a stageY location which we can use directly since we are not scaling anything in our world. There are localToGlobal and globalToLocal if you ever need to do stuff like that.
By setting a variable to the int values in the list above all we have to do is move the world left or right based on that value for it to look like the skier is moving. To add to the effect I skew the sprite along the Y axis so that it looks like the skier is facing that way.
I think that's enough for this blog post. A playable game can be found over here http://sugendran.github.com/ski-gratis/ and the source is on github over here https://github.com/sugendran/ski-gratis/
Stay tuned for a follow up post where I try to get this onto some test devices.