Make a game with EaselJS and the HTML5 Canvas

Posted on: April 30th, 2013 by alemieux
Guessing Game

Play the game now

The HTML5 Canvas element is a container that can be used to dynamically draw graphics with a lot of help from JavaScript. The canvas tag itself is very simple:

                &lt;canvas id=<span style="color: #f32038;">"gamespace"</span> width=<span style="color: #f32038;">"800"</span> height=<span style="color: #f32038;">"600"</span>&gt;&lt;/canvas&gt;

An ID is given to the canvas so that we can reference it with JavaScript and a width and height is defined. Since the canvas is a graphical space, you’ll notice that when you right click on it, your browser will allow you to save it as an image.

You can draw some basic shapes and text into the canvas with JavaScript. You can also import graphics into the canvas. This makes the canvas a dynamic environment for building games, dynamic charts, and other web applications.

EaselJS

The native code for working with canvas is a little cumbersome. Having come from a Flash/ActionScript background, EaselJS is a much better way to work with canvas. Part of CreateJS, a robust JavaScript library specifically geared towards developers coming from an ActionScript background, EaselJS has a tremendously easy to read API and syntax that’s immediately recognizable.

Along with EaselJS, you can add animation to objects in the canvas with TweenJS. If you’ve ever worked with TweenLite from Greensock, you’ll feel right at home with TweenJS.

No HTML game is complete without sound effects and cheezy soundtracks. You can add audio to the canvas with SoundJS. Very similar to how AS3 handles sound, SoundJS lets you load sounds, and has complete and progress methods.

Finally, you can create attractive preloaders for loading all of the game content with PreloadJS. PreloadJS can monitor load events and can track the progress of loaded objects.

The online documentation and downloadable samples are a great starter. You can get the code via CDN, github, or zip.

Game Setup

In this example, I’m creating a simple guessing game using Star Wars as a theme. There will be a few visual questions presented to the user. If they answer correctly or incorrectly, visual and audio feedback will be provided. If they get all of the questions correct, they are presented with a congratulatory screen with visual and audio feedback. If they answer incorrectly on any questions, they are presented with a different screen with negative visual and audio feedback.

The HTML

There isn’t a lot going on in the HTML file itself. A few styles and a lot of JavaScript library loading. Here, I am keeping everything local, but you can use the CDN links for the libraries. It took a little while to figure out exactly which files needed to be loaded and in which order. I looked at the example files for help. These files were minified, but I changed the names before linking them to the HTML file. In total, there are 6 libraries I’m pulling in and my own script file, which contains all of the logic for the game. The JS files are loaded in as follows:

  • js/easeljs.js
  • js/tweenjs.js
  • js/game.js
  • js/Sound.js
  • js/HTMLAudioPlugin.js
  • js/WebAudioPlugin.js
  • js/preloadjs.js

Imagery and Sounds

I didn’t spend a great deal of time on the imagery, but was able to quickly find what I needed on Google Images. I made the buttons and the interface features in Photoshop and exported as PNGs. Then I used imageOptim to compress the files further to make them smaller. Some of the splash screens are pretty big. Fortunately, there’s a lot of sound bites all over the web, so I also made quick work of finding quotes from the Star Wars movie for the game. In the SoundJS example files, I noticed that 2 file formats were being used: MP3 and OGG. The MP3’s were easy to find, but I had to convert to the OGG format. So I used Media.io in order to do the conversions and download them. The audio files are very small.

The JavaScript

The initial setup of the game is in the init() function at the top of the code. Here, a bunch of variables are being set for game parameters, text, bitmaps to import, an array of sounds to import, and the preloader code.

The variables are a comma separated list of constants. I start by declaring the canvas:

                stage <span style="color: #a2ac6f;">= new</span> createjs.<span style="color: #f8cf6d; font-weight: bold;">Stage</span>(canvas)

Next we need the constructors for all of the images to be loaded. EaselJS has a bitmap class for that. Here’s an example:

                logo <span style="color: #a2ac6f;">= new</span> createjs.<span style="color: #f8cf6d; font-weight: bold;">Bitmap</span>("img/logo.png")

For creating text, EaselJS has a Text class. The constructor is very similar to the Bitmap class as seen above:

                inst <span style="color: #a2ac6f;">= new</span> createjs.<span style="color: #f8cf6d; font-weight: bold;">Text</span>()

For those times when we need to center objects on the canvas, we declare a variable for the centerX and centerY positions on the canvas:

                centerX <span style="color: #a2ac6f;">=</span> canvas.width/<span style="color: #4bbeed;">2</span>,<br />
                centerY <span style="color: #a2ac6f;">=</span> canvas.height/<span style="color: #4bbeed;">2</span>

For game progress, there are a few variables that need to be set:

                questionPosition <span style="color: #a2ac6f;">=</span> <span style="color: #4bbeed;">0</span>,<br />
                numCorrect <span style="color: #a2ac6f;">=</span> <span style="color: #4bbeed;">0</span>,<br />
                numWrong <span style="color: #a2ac6f;">=</span> <span style="color: #4bbeed;">0</span>,

questionPosition will help us keep track of which question we’re on. numCorrect and numWrong will keep track of the question answered correctly and incorrectly.

We create an array of sounds called manifest that we’ll be using in the game with paths to the different versions (MP3 and OGG). Each sound is given an id that we can call on later when we want to play that sound.

                assetPath <span style="color: #a2ac6f;">=</span> <span style="color: #f32038;">"snd/"</span>,
                manifest = [{src:assetsPath<span style="color: #a2ac6f;">+</span><span style="color: #f32038;">"Theme - Star Wars.mp3|"</span>+assetsPath<span style="color: #a2ac6f;">+</span><span style="color: #f32038;">"Theme - Star Wars.ogg"</span>, id:<span style="color: #f32038;">"theme"</span>},<br />
                 {src:assetsPath<span style="color: #a2ac6f;">+</span><span style="color: #f32038;">"blaster.mp3|"</span><span style="color: #a2ac6f;">+</span>assetsPath<span style="color: #a2ac6f;">+</span><span style="color: #f32038;">"blaster.ogg"</span>, id:<span style="color: #f32038;">"buttonSnd"</span>},<br />
                 {src:assetsPath<span style="color: #a2ac6f;">+</span><span style="color: #f32038;">"powrweak.mp3|"</span><span style="color: #a2ac6f;">+</span>assetsPath<span style="color: #a2ac6f;">+</span><span style="color: #f32038;">"powrweak.ogg"</span>, id:<span style="color: #f32038;">"powerWeak"</span>},<br />
                 {src:assetsPath<span style="color: #a2ac6f;">+</span><span style="color: #f32038;">"cocky.mp3|"</span><span style="color: #a2ac6f;">+</span>assetsPath<span style="color: #a2ac6f;">+</span><span style="color: #f32038;">"cocky.ogg"</span>, id:<span style="color: #f32038;">"cocky"</span>},<br />
                 {src:assetsPath<span style="color: #a2ac6f;">+</span><span style="color: #f32038;">"haveyou.mp3|"</span><span style="color: #a2ac6f;">+</span>assetsPath<span style="color: #a2ac6f;">+</span><span style="color: #f32038;">"haveyou.ogg"</span>, id:<span style="color: #f32038;">"haveYou"</span>},<br />
                 {src:assetsPath<span style="color: #a2ac6f;">+</span><span style="color: #f32038;">"strong.mp3|"</span><span style="color: #a2ac6f;">+</span>assetsPath<span style="color: #a2ac6f;">+</span><span style="color: #f32038;">"strong.ogg"</span>, id:<span style="color: #f32038;">"strong"</span>}],

Each sound is inserted in brackets and the two types of sounds are separated by a pipe.

For preloading the images, we need to set up a queue and make an array. PreloadJS can do more than preload images, but I was only interested in preloading the image content, since the sound files are very small.

The queue looks like this:

                queue.loadManifest([<span style="color: #f32038;">"img/starfield.png"</span>,<span style="color: #f32038;">"img/logo.png"</span>,<span style="color: #f32038;">"img/START-GAME.png"</span>...]);

Then, we tell the queue to load. You can fire functions with event listeners to watch for complete, progress, Here, I’m only reporting the results to the console:

                queue.load();<br />
                queue.addEventListener(<span style="color: #f32038;">"complete"</span>, showManifest);<br />
                queue.addEventListener(<span style="color: #f32038;">"progress"</span>, showProgress);<br />
                <span style="color: #4bbeed;">function</span> <span style="color: #f8cf6d; font-weight: bold;">showProgress</span>(evt) {<br />
                    <span style="color: #4bbeed;">var</span> perc = evt.loaded / evt.total;<br />
                    <span style="color: #e39247;">console</span>.log(Math.ceil(perc<span style="color: #a2ac6f;">*</span><span style="color: #4bbeed;">100</span>).toString());<br />
                }
                function showManifest() {<br />
                    <span style="color: #e39247;">console</span>.log(<span style="color: #f32038;">"Files are loaded");<br />
                }
                </span>

For all the sounds to play, a listener is added and the sound manifest is then registered. You can create fancy progress bars for the sounds to load and such with a progress handler. I didn’t need that here, so the function fileload() is simply reporting to the console when the files are all loaded:

                createjs.Sound.addEventListener(<span style="color: #f32038;">"fileload"</span>, createjs.proxy(soundLoaded, this));<br />
                createjs.Sound.registerManifest(manifest);<br />
                <br />
                <span style="color: #4bbeed;">function</span> <span style="color: #f8cf6d; font-weight: bold;">soundLoaded</span>(event) {<br />
                    <span style="color: #e39247;">console</span>.log(<span style="color: #f32038;">"sounds loaded"</span>);<br />
                }

At the beginning of the game, the Star Wars theme music plays as the logo fades in and then the instructions appear. To play any sound with SoundJS we use the play() function and feed it the id of the sound as a string:

                createjs.Sound.play(<span style="color: #f32038;">"theme"</span>);

Just as in the original Star Wars movie, the logo is seen on a starfile up close and then fades off into the distance. This effect is achieved using TweenJS and EaselJS. Much like the setInterval class in AS3, the Ticker class in EaselJS allows you to set up simple animations of canvas objects. Here, the starfile and logo are added to the stage:

                stage.addChild(bg,logo);

The Ticker class is started with a frame rate of 30fps and then the logo is animated with the scaleX and scaleY properties – which should be very familiar to AS3 developers.

                createjs.<span style="color: #f8cf6d; font-weight: bold;">Ticker</span>.setFPS(<span style="color: #4bbeed;">30</span>);<br />
                createjs.<span style="color: #f8cf6d; font-weight: bold;">Ticker</span>.addEventListener(<span style="color: #f32038;">"tick"</span>, startAnim);<br />
                <span style="color: #4bbeed;">function</span> <span style="color: #f8cf6d; font-weight: bold;">startAnim</span>(e) {    <br />
                    logo.x <span style="color: #a2ac6f;">=</span> centerX;<br />
                    logo.y <span style="color: #a2ac6f;">=</span> centerY;<br />
                    logo.scaleX <span style="color: #a2ac6f;">-=</span> <span style="color: #4bbeed;">0.005</span>;<br />
                    logo.scaleY <span style="color: #a2ac6f;">-=</span> <span style="color: #4bbeed;">0.005</span>;<br />
                    <br />
                    stage.update();<br />
                }

In addition to scaling down in size, the logo fades out. I decided to try this with TweenJS. First, we set the alpha of the logo to 1, just as in ActionScript the alpha values are on a range from 0 – 1. Then, we perform the tween. This is a long, slow tween over 3 and a half seconds long. TweenJS allows you to chain animations together. In this case, I’m calling an onComplete() function which brings in the instructions and the start button:

                createjs.Tween.get(logo).to({alpha: <span style="color: #4bbeed;">0</span>},<span style="color: #4bbeed;">3600</span>).call(onComplete);

This is very similar to the way we construct tweens with TweenLite in AS3:

                TweenLite.to(mc, 3.5, {alpha:0});

In TweenLite you specify the object to be tweened, the duration and the property or properties to be tweened. In TweenJS you specify the object, the property or properties to be tweened and the duration.

Finally, we come to then end of the game initialization with the onComplete handler. This will bring in the instruction text and the start button. As in ActionScript, event handlers need to be removed for garbage collection. I didn’t come across anything in the online documentation for this, but I know it’s a best practice.

                removeEventListener(<span style="color: #f32038;">"tick"</span>, startAnim);

The start button and the instruction text are both tweened in with TWeenJS. In EaselJS, there are several text properties that can be set. Here, I’m setting some basic settings like font, color, and lineHeight. For the font and color settings, you can specify those as you would with CSS. Once the text properties are set, the text is added to the stage and the alpha property is tweened.

                stage.addChild(startButton);<br />
                createjs.Tween.get(startButton).to({y:<span style="color: #4bbeed;">370</span>}, <span style="color: #4bbeed;">1000</span>);<br />
                startButton.x = <span style="color: #4bbeed;">245</span>;<br />
                startButton.y = <span style="color: #4bbeed;">620</span>;<br />
                <br />
                inst.text = <span style="color: #f32038;">"Welcome to the Star Wars Guessing Game. This game will test if\n you are a true Star Wars fan. In the next few screens, you will be shown some\n images from the Star Wars Universe. Identify the correct image to advance.\n If not, the Empire takes over the Galaxy.\n\n May the force be with you."</span>;<br />
                inst.font = <span style="color: #f32038;">"normal 24px Myriad Pro, 'MyriadPro', sans-serif"</span>;<br />
                inst.color = <span style="color: #f32038;">"#298FC2"</span>;<br />
                inst.linewidth = <span style="color: #4bbeed;">200</span>;<br />
                inst.textAlign = <span style="color: #f32038;">"center"</span>;<br />
                inst.textBaseline = <span style="color: #f32038;">"top"</span>;<br />
                <br />
                inst.lineHeight = <span style="color: #4bbeed;">26</span>;<br />   
                <br />
                inst.x = centerX;<br />
                inst.y = <span style="color: #4bbeed;">180</span>;<br />
                <br />
                stage.addChild(inst);<br />
                inst.alpha = <span style="color: #4bbeed;">0</span>;<br />
                createjs.Tween.get(inst).to({alpha:<span style="color: #4bbeed;">1</span>},<span style="color: #4bbeed;">1000</span>);

Questions

The question functions are very similar. A question area is dynamically drawn on the stage using the drawRoundRect() method. There are 5 parameters, X, Y, Width, Height, and Radius. The question text is added to the stage and 3 images related to the question are shown. The user clicks on the image they think is correct. We increment questionPosition. Either the answerCorrect() or answerWrong() functions will be called when they do that. TweenJS is used again to fade in the images:

                questionPosition<span style="color: #a2ac6f;">++</span>;<br />
                stage.removeChild(inst,startButton);<br />
                <br />
                qArea.graphics.beginStroke(<span style="color: #f32038;">"#298FC2"</span>);<br />
                qArea.graphics.setStrokeStyle(<span style="color: #4bbeed;">10</span>,<span style="color: #4bbeed;">0</span>,<span style="color: #4bbeed;">1</span>);<br />
                qArea.graphics.beginFill(<span style="color: #f32038;">"#ffffff"</span>);<br />
                qArea.graphics.drawRoundRect(<span style="color: #4bbeed;">50</span>, <span style="color: #4bbeed;">87</span>, <span style="color: #4bbeed;">700</span>, <span style="color: #4bbeed;">425</span>, <span style="color: #4bbeed;">10</span>);<br />
                <br /> 
                stage.addChild(qArea);<br />
                <br />
                stage.addChild(ewok);<br />
                ewok.alpha = <span style="color: #4bbeed;">0</span>;<br />
                createjs.Tween.get(ewok).to({alpha: <span style="color: #4bbeed;">1</span>},<span style="color: #4bbeed;">500</span>);<br />
                ewok.x = <span style="color: #4bbeed;">70</span>;<br />
                ewok.y = <span style="color: #4bbeed;">150</span>;<br />
                stage.addChild(wookie);<br />
                wookie.alpha = <span style="color: #4bbeed;">0</span>;<br />
                createjs.Tween.get(wookie).to({alpha: <span style="color: #4bbeed;">1</span>},<span style="color: #4bbeed;">500</span>);<br />
                wookie.x = <span style="color: #4bbeed;">290</span>;<br />
                wookie.y = <span style="color: #4bbeed;">150</span>;<br />
                stage.addChild(gungan);<br />
                gungan.alpha = <span style="color: #4bbeed;">0</span>;<br />
                createjs.Tween.get(gungan).to({alpha: <span style="color: #4bbeed;">1</span>},<span style="color: #4bbeed;">500</span>);<br />
                gungan.x = <span style="color: #4bbeed;">510</span>;<br />
                gungan.y = <span style="color: #4bbeed;">150</span>;<br />
                <br />
                q.text = q1;<br />
                q.font = <span style="color: #f32038;">"normal 24px Myriad Pro, 'MyriadPro', sans-serif"</span>;<br />
                q.color = <span style="color: #f32038;">"#298FC2"</span>;<br />
                q.textAlign = <span style="color: #f32038;">"center"</span>;<br />
                <br />
                stage.addChild(q);<br />
                q.x = <span style="color: #4bbeed;">400</span>;<br />
                q.y = <span style="color: #4bbeed;">450</span>;<br />
                <br />
                ewok.<spanc class="class">onClick = <span style="color: #4bbeed;">function</span>() {<br />
                    answerWrong();<br />
                }<br />
                gungan.<span style="color: #f8cf6d; font-weight: bold;">onClick</span> = <span style="color: #4bbeed;">function</span>() {<br />
                    answerWrong();<br />
                }<br />
                wookie.<span style="color: #f8cf6d; font-weight: bold;">onClick</span> = <span style="color: #4bbeed;">function</span>() {<br />
                    answerCorrect();<br />
                }<br />
                </spanc>

The order in which you add items to the stage is pretty important here. For example, if I added the question area last, it would hide all of the text and graphics. By adding it first, the text and graphics are loaded on top of it.

Again, garbage collection is important. I created a clearScreen() function to remove the some of the basic stuff and additionally removed objects in each subsequent question using removeChild().

Of course, the user starts the game by clicking the start button. Here, there are no rollover effects on the button. Those certainly could’ve been added with EaselJS, but rollovers don’t translate for tablet and mobile use anyway. Clicking the start button calls the question1() function and game play begins with the sound of a blaster!

                startButton.<span style="color: #f8cf6d; font-weight: bold;">onClick</span> = <span style="color: #4bbeed;">function</span>() {<br />
                    question1();<br />
                    createjs.Sound.play(<span style="color: #f32038;">"buttonSnd"</span>);<br />
                }

Checking the Answers

When a question is answered, it will be handled by answerCorrect() or answerWrong() methods, which will: play a sound, show a visual and allow the user to continue to the next question. If all of the questions have been answered, the continueFinal function is called.

                <span style="color: #4bbeed;">function</span> <span style="color: #f8cf6d; font-weight: bold;">answerCorrect</span>() {<br />
                    // console.log(<span style="color: #f32038;">"The force is strong with this one!"</span>);<br />
                    createjs.Sound.play(<span style="color: #f32038;">"strong"</span>);<br />
                    questionCorrect = true;<br />
                    numCorrect++;<br />
                    stage.addChild(rightAnswer);<br />
                    rightAnswer.x = <span style="color: #4bbeed;">50</span>;<br />
                    rightAnswer.y = <span style="color: #4bbeed;">87</span>;<br />
                    <br />
                    correctMsg.text = <span style="color: #f32038;">"The force is strong with this one!"</span>;<br />
                    correctMsg.font = <span style="color: #f32038;">"normal 24px Myriad Pro, 'MyriadPro', sans-serif"</span>;<br />
                    correctMsg.color = <span style="color: #f32038;">"#298FC2"</span>;<br />
                    correctMsg.textAlign = <span style="color: #f32038;">"center"</span>;<br />
                    stage.addChild(correctMsg);<br />
                    correctMsg.x = <span style="color: #4bbeed;">400</span>;<br />
                    correctMsg.y = <span style="color: #4bbeed;">450</span>;<br />
                    <br />
                    if(questionPosition&lt;3) {<br />
                        stage.addChild(nextQbutton);<br />
                        nextQbutton.regX = <span style="color: #4bbeed;">384/2</span>;<br />
                        nextQbutton.regY = <span style="color: #4bbeed;">56/2</span>;<br />
                        nextQbutton.x = centerX;<br />
                        nextQbutton.y = <span style="color: #4bbeed;">520</span>;<br />
                    }<br />
                    <br />
                    if(questionPosition==3) {<br />
                        continueFinal();<br />
                    }<br />
                }

The answerWrong() method is very similar only it plays a different sound, has a different text message, and shows a different image.

Ending the Game

If all of the questions have been answered, continueFinal() is fired, which basically shows a continue button. Once again, TweenJS is used to animate the button into place. When the user clicks on that button, the endGame() method is called.

                <span style="color: #4bbeed;">function</span> <span style="color: #f8cf6d; font-weight: bold;">continueFinal</span>() {<br />
                    stage.addChild(continueButton);<br />
                    continueButton.regX = <span style="color: #4bbeed;">384</span>/<span style="color: #4bbeed;">2</span>;<br />
                    continueButton.regY = <span style="color: #4bbeed;">56</span>/<span style="color: #4bbeed;">2</span>;<br />
                    continueButton.x = centerX;<br />
                    continueButton.y = <span style="color: #4bbeed;">0</span>;<br />
                    continueButton.alpha = <span style="color: #4bbeed;">0</span>;<br />
                    createjs.Tween.get(continueButton).to({y:<span style="color: #4bbeed;">520</span>, alpha: <span style="color: #4bbeed;">1</span>}, <span style="color: #4bbeed;">1000</span>);<br />
                    <br />
                    continueButton.<span style="color: #f8cf6d; font-weight: bold;">onClick</span> = <span style="color: #4bbeed;">function</span>() {<br />
                        endGame();<br />
                    }
                }

At the end of the game if you’ve answered all of the questions correctly, you’ll see an image of Luke, Leia, and Han and hear some audio. If you’ve missed any questions, then you’ll see Darth Vader and Obi-Wan Kenobi dueling with their lightsabers and hear Darth say “Your powers are weak old man”.

                function <span style="color: #f8cf6d; font-weight: bold;">endGame</span>() {<br />
                    clearScreen();<br />
                    stage.removeChild(continueButton);<br />
                    <span style="color: #4bbeed;">var</span> results <span style="color: #a2ac6f;">= new</span> createjs.<span style="color: #f8cf6d; font-weight: bold;">Text</span>();<br />
                    results.text =  <span style="color: #f32038;">""</span>;<br />
                    results.font = <span style="color: #f32038;">"normal 24px Myriad Pro, 'MyriadPro', sans-serif"</span>;<br />
                    results.color = <span style="color: #f32038;">"#298FC2"</span>;<br />
                    results.textAlign = <span style="color: #f32038;">"center"</span>;<br />
                    <br /> 
                     <span style="color: #a2ac6f;">if</span>(numCorrect==<span style="color: #4bbeed;">3</span>) {<br />
                        createjs.Sound.play(<span style="color: #f32038;">"cocky"</span>);<br />
                        stage.addChild(endGood);<br />
                        endGood.x = <span style="color: #4bbeed;">50</span>;<br />
                        endGood.y = <span style="color: #4bbeed;">87</span>;<br />
                        <br />
                        results.text = <span style="color: #f32038;">"Great job kid, don't get too cocky!"</span>;<br />
                        stage.addChild(results);<br />
                        results.x = <span style="color: #4bbeed;">400</span>;<br />
                        results.y = <span style="color: #4bbeed;">450</span>;<br />
                    } <span style="color: #a2ac6f;">else</span> {<br />
                        createjs.Sound.play(<span style="color: #f32038;">"powerWeak"</span>);<br />
                        stage.addChild(endBad);<br />
                        endBad.x = <span style="color: #4bbeed;">50</span>;<br />
                        endBad.y = <span style="color: #4bbeed;">87</span>;<br />
                        <br />
                        results.text = <span style="color: #f32038;">"Your powers are weak old man!"</span>;<br />
                        stage.addChild(results);<br />
                        results.x = <span style="color: #4bbeed;">400</span>;<br />
                        results.y = <span style="color: #4bbeed;">450</span>;<br />
                    }<br />
                }<br />

And that’s the end of the code review. There’s only 3 questions in here, but other questions can easily be added here. I suppose the questions could be randomized too. The point of this whole thing was to get a handle on CreateJS and see what it could do and to compare it with ActionScript.

Summary

I’ll admit, that the first time I looked at CreateJS, it was daunting and I was skeptical. But once I looked at the examples and really dug into the code, I could immediately see the potential and I hope you can too.

Download the source files below:

Download

Tweet this! | Bookmark at Delicious

Tags: , , , , , ,

Comments are closed.