Introduction:
Flash Professional CC introduced a document type in which you can create an HTML5 Canvas document. It publishes HTML5 content using the CreateJS libraries (but you could easily use other libraries if you wanted to).
Although this feature has been out for a while, there is little to no information (at least I couldn’t find much), about how to make something more complex in it (like a game).
I love this development because (from Adobe documentation) “Flash publishes to HTML5 by leveraging the Canvas API. It translates objects created on stage in to their Canvas counterparts.” Meaning, I don’t have to worry about re-animating everything to accommodate a new development environment, and I can continue to use the assets already created… as well as the tools.
This is still so much easier than having to do everything through code (XML or other).
In this tutorial I hope to provide a concise resource on building an HTML5 document in Flash.
I will be updating this post as things surface.
* DOWNLOAD the source files here: Haxatron2000_HTML5_Tutorial.zip
* Play the finished game here: http://haxatron.alienmelon.com/browserhax/
* Visit the Haxatron 2000 website here: http://haxatron.alienmelon.com/
Links about Flash and the HTML5 Canvas document type:
* Build HTML5 Canvas interactive games with Flash Professional CC
* What is this “HTML Canvas” document in Flash Pro CC 13.1?
* Flash Professional Help / Creating and publishing an HTML5 Canvas document
Other Resources:
From what I’ve seen Adobe’s tools are still the best when it comes to creating vector art/content.
Yes, I am biased, because it fits into my workflow so well. :)
Nevertheless, some other resources regarding vector/SVG tools are:
* A great list of SVG animation/asset programs.
Some libraries to help get started with SVG:
* Snap.svg
“the Snap.svg JavaScript library makes working with your SVG assets as easy as jQuery makes working with the DOM.”
* Raphaël—JavaScript Library
“Raphaël is a small JavaScript library that should simplify your work with vector graphics on the web.”
* SVG.jg
“A lightweight library for manipulating and animating SVG.”
*************
Issues Encountered & Resolved (Things To Look Out For):
Sounds loaded externally (via createjs.LoadQueue…) will fail in Safari, unless the sounds are also physically included in the library. Nevertheless, they DO work in Firefox, and Chrome without doing so.
Sounds will also not work in Safari unless the quicktime plugin is installed (as per SoundJS documentation: http://createjs.com/Docs/SoundJS/classes/Sound.html).
Frame numbers in EaselJS start at 0 instead of 1. For example, this affects gotoAndStop and gotoAndPlay calls.
You cannot modify the generated .html page, it will be overwritten next time you publish in Flash (not safe, like in Edge Animate), and your changes will be lost. My solution was to do everything in javascript, and not touch the .html page.
*************
SETTING UP THE DOCUMENT
All setting up and styling (css, scripts, style, etc…) is done in the document. I’ve kept it out of the generated .html page on purpose (reasons mentioned above). What I hope to do here is provide as many examples as possible in order to help point you (or a googler) in the right direction.
Creating a better structure is up to you.
So, walking through the setup…
@fontface
rule. Described in further detail in the Fonts section bellow.
/*JAVASCRIPT*/ var font_fixedsys = document.createElement("style"); font_fixedsys.appendChild(document.createTextNode("\ @font-face {\ font-family: '" + 'fixedsys_excelsior_3' + "';\ src: url('" + 'fontquirrels/fsex300-webfont.eot?#iefix' + "') format('embedded-opentype'),\ url('" + 'fontquirrels/fsex300-webfont.woff' + "') format('woff'),\ url('" + 'fontquirrels/fsex300-webfont.ttf' + "') format('truetype'),\ url('" + 'fontquirrels/fsex300-webfont.svg#fixedsys_excelsior_3.01Rg' + "') format('svg');\ font-weight: normal;\ font-style: normal;\ }\ ")); document.head.appendChild(font_fixedsys);
Including other javascript libraries is just as easy. You would use document.getElementsByTagName("head")[0].appendChild(script);
to do so.
Further links on the topic are, How to add jQuery in JS file, Include a JavaScript file in a JavaScript file, How to include a JavaScript file in another JavaScript file?, and Include jQuery from another JavaScript file.
In this case, I am importing keymaster.js
(https://github.com/madrobby/keymaster) — a simple micro-library for defining and dispatching keyboard shortcuts in web applications. In case of a game it’s good because it will reduce cross browser issues with keyboard keys (to a certain extent). Example of usage is in the Keyboard section.
//Example of usage bellow in /*KEYBOARD*/ section var js_misc = document.createElement("script"); js_misc.src = "libs/keymaster.js"; js_misc.type = "text/javascript"; document.getElementsByTagName("head")[0].appendChild(js_misc);
I am also controlling page elements, properties, style, etc…
Things like setting the background color of the page (and other styles).
For more information refer to Traversing an HTML table with JavaScript and DOM Interfaces
Or HTML DOM Style Object
As a good explanation on what I’m doing.
In this case, I am saving references to page body, and the canvas — see var page_body
and var page_canvas
.
Then I’m applying styles to them.
I’m referencing page_canvas
in order to get the current height and width (useful for responsive designs, etc).
var page_body = document.getElementsByTagName("body")[0]; page_body.style.backgroundColor = "black"; //note, alternative would be: document.body.style.backgroundColor = "black"; page_body.style.overflow = "hidden";//hide scrollbars page_body.style.position = "fixed";//no scroll. ever. //the canvas (this is the game) var page_canvas = document.getElementsByTagName("canvas")[0]; stageWidth = page_canvas.width; stageHeight = page_canvas.height;
Then I’m also displaying an “overlay” image above the page (scanlines).
Appending a div (or something else) above everything, and loading content into it (like an image), would look like,
//Scanlines (I like scanlines) //alternatively, you should consider: http://createjs.com/Docs/EaselJS/classes/DOMElement.html var overlay = document.createElement("div_overlay"); //make sure it's before the canvas - so inserting before document.body.insertBefore(overlay, page_canvas); //create image and append overlay var img = document.createElement("img"); img.src = "images/IMG_Scalines_DARK.png"; overlay.appendChild(img); //place it above everything (zIndex) overlay.style.position = "absolute"; overlay.style.zIndex="999";
ALTERNATIVELY, aside from all that I just mentioned, EaselJS comes with a DOMElement Class
.
It’s recommended you use that. See: http://createjs.com/Docs/EaselJS/classes/DOMElement.html
As a final note, we will want the page to be full screen, and preferably centered.
I picked two approaches to this which I thought would match common problems people might have/wish to solve.
Option #1 is for scaling the canvas proportionately (no centering). Thanks goes to CreateJS community support, because no one likes reinventing the wheel. :)
function onResize() { //OPTION 1: //Scales proportionately // browser viewport size var w = window.innerWidth; var h = window.innerHeight; // stage dimensions var ow = 960; // your stage width var oh = 640; // your stage height // keep aspect ratio var scale = Math.min(w / ow, h / oh); var newHeight = ow * scale; var newWidth = oh * scale; // stage.scaleX = scale; stage.scaleY = scale; // adjust canvas size stage.canvas.width = newHeight; stage.canvas.height = newWidth; //set width and height variables again //var page_canvas = document.getElementsByTagName("canvas")[0]; //stageWidth = page_canvas.width; //stageHeight = page_canvas.height; // // update the stage stage.update() } // window.onresize = function() { onResize(); } //call it on first run onResize();
Then there’s Option 2. This resizes and centers everything.
For more information on doing this refer to How to dynamically resize and center a Flash CreateJS canvas element animation –Which is a great discussion with more tips.
function onResize() { //OPTION 2: scale and center var widthToHeight = stageWidth / stageHeight; var newWidth = window.innerWidth; var newHeight = window.innerHeight; var newWidthToHeight = newWidth / newHeight; // if (newWidthToHeight > widthToHeight) { newWidth = newHeight * widthToHeight; page_canvas.style.height = newHeight + "px"; page_canvas.style.width = newWidth + "px"; } else { newHeight = newWidth / widthToHeight; page_canvas.style.height = newHeight + "px"; page_canvas.style.width = newWidth + "px"; } scale = newWidthToHeight / widthToHeight; stage.width = newWidth; stage.height = newHeight; page_canvas.style.marginTop = ((window.innerHeight - newHeight) / 2) + "px"; page_canvas.style.marginLeft = ((window.innerWidth - newWidth) / 2) + "px"; // } // window.onresize = function() { onResize(); } //call on first run onResize();
*************
FONTS, TEXT FIELDS & EFFECTS
For starters, the link to the Text Class
:
http://createjs.com/Docs/EaselJS/classes/Text.html
There are a few ways of getting fonts to work in your project. It isn’t as simple as “publishing” your project. The font will not show for visitors/players unless that font is installed on their machine.
So you can use a couple of methods to get that to work.
You can use @fontface
.
In this tutorial that is what I’m doing. The font I’m using is Fixedsys Excelsior, as generated from fontsquirrel with some fall back fonts (monaco, lucidia console, etc…).
Note that @fontface
may fail if the font is not loaded when it’s drawn.
You can also use Bitmap Text (more advisable for games, but unsupported). A tutorial for implementing this is:
Bitmap Font Support for CreateJS
Or web fonts:
http://www.createjs.com/tutorials/Fonts/
Like I mentioned earlier, modifying the generated .html page will result in your changes being lost next time you publish. To get around this I challenged myself to not touching the .html page, and relying on javascript for everything.
There are a number of ways of doing this (for example Appending Style Nodes with Javascript, or like a simple google search would show).
I’m certain there are better ways, but I’m doing it this way.
Following is the @fontface
rule:
var font_fixedsys = document.createElement("style"); font_fixedsys.appendChild(document.createTextNode("\ @font-face {\ font-family: '" + 'fixedsys_excelsior_3' + "';\ src: url('" + 'fontquirrels/fsex300-webfont.eot?#iefix' + "') format('embedded-opentype'),\ url('" + 'fontquirrels/fsex300-webfont.woff' + "') format('woff'),\ url('" + 'fontquirrels/fsex300-webfont.ttf' + "') format('truetype'),\ url('" + 'fontquirrels/fsex300-webfont.svg#fixedsys_excelsior_3.01Rg' + "') format('svg');\ font-weight: normal;\ font-style: normal;\ }\ ")); document.head.appendChild(font_fixedsys);
This gets the font ready to use. You could use just one source, but most recommended formats are included in this case.
There are going to be many text fields, so I’m saving the properties as variables.
//text field properties txtCol = "#00FF00";//the text color txtFont = "26px 'fixedsys_excelsior_3', 'Lucida Console', Monaco, monospace"; //large text field txtFont_sml = "15px 'fixedsys_excelsior_3', 'Lucida Console', Monaco, monospace"; //small subtext
And now create the text field, with the font.
//Create text fields txt_title = new createjs.Text("Haxatron 2000", txtFont, txtCol); txt_title.x = 384; txt_title.y = 197; this.addChild(txt_title); //add to stage //shadow is much like glow, so I'll use shadow //color String = The color of the shadow. //offsetX Number = The x offset of the shadow in pixels. //offsetY Number = The y offset of the shadow in pixels. //blur Number = The size of the blurring effect. //See: http://www.createjs.com/Docs/EaselJS/classes/Shadow.html for more txt_title.shadow = new createjs.Shadow("#00FF00", 3, 0, 10);
Here the field is txt_title
.
The game has a glow filter applied to all text fields. CreateJS does not have a glow filter, so I’m using shadow (which is similar).
See: http://www.createjs.com/Docs/EaselJS/classes/Shadow.html for more information.
Text Effects (Write Out):
The game writes out text to the created text fields. It’s a cute, and classic effect.
The function is a simple one,
//Write out text var writeInt; writeText = function(myText, myTextField) { var i = 0; function write() { if (i<=myText.length) { myTextField.text = myText.substr(0, i)+"_"; randTypeSnd(); i = i+1; } else { myTextField.text = myText.substr(0, i); clearInterval(writeInt); } } clearInterval(writeInt); writeInt = setInterval(write, 40); }
To use it you call, writeText("My text as string.", txt_myTextField);
This function, and most of the game, uses setInterval
a lot, so as to not deviate from the original code too much. For other options see the Ticker Class: http://createjs.com/Docs/EaselJS/classes/Ticker.html
In the above example, as long as i
is less than the length
of your string continue writing. Increment i
with every letter written.
With each character added to the text field, a random typing sound plays. That’s what randTypeSnd();
is.
Clearing Text Field (Scene Change):
The game is keyboard only. Once you’ve made your selection from any of the menus, the text fields need to be cleared before proceeding to the next screen. There is one function called clearText
responsible for this… Which is as simple as,
//clearing all text fields //for going between scenes clearText = function(){ var arr_fields = new Array(txt_title, txt_start, txt_faq, txt_msg, txt_msg_win, txt_stats_win, txt_seconds_win, txt_tries_win, txt_options_win, txt_option1_win, txt_option2_win); for(i=0; i<arr_fields.length; ++i){ arr_fields[i].text = ""; }; }
And that’s it for text…
*************
SOUND
Two things that are important. They are the SoundJS and PreloadJS libraries. If you’re familiar with HTML5 development, then you’re familiar with the messiness that is cross-browser audio support. SoundJS solves most of that.
Two great articles are:
SoundJS: Mobile Safe Approach
and SoundJS: Preloading Audio
They cover this pretty well.
The documentation for SoundJS is here http://www.createjs.com/Docs/SoundJS/modules/SoundJS.html
Check out the examples here (sourcecode): https://github.com/CreateJS/SoundJS/tree/master/examples
And here is a great example of use in a game: https://github.com/CreateJS/SoundJS/blob/master/examples/Game.html
Note, sounds will not play in Safari unless they are also included in the library (if you’re working with Flash CC). This does not happen in Chrome or Firefox. Sounds can be loaded from an external source without that problem in both those browsers.
Sounds in Safari also require the quicktime plugin to be installed in order to play.
Following is a breakdown of the VERY basics of playing a sound:
//to play a sound that's in the library createjs.Sound.play("Die"); //to play a sound that's in a library, and use again //AUD_Die is the reference. AUD_Die = createjs.Sound.play("sounds/Die.mp3");//call once //play anytime after that AUD_Die.play(); //to loop a sound //http://createjs.com/Docs/SoundJS/classes/SoundInstance.html /*Once a SoundInstance is created, a reference can be stored that can be used to control the audio directly through the SoundInstance. If the reference is not stored, the SoundInstance will play out its audio (and any loops), and is then de-referenced from the Sound class*/ // var myInstance = createjs.Sound.play("sounds/Die.mp3", {loop:2});//loop:NUM = number of times to loop myInstance.addEventListener("loop", handleLoop); function handleLoop(event) { myInstance.volume = myInstance.volume * 0.5;//fades out... }
For more detail, see the Sound docs: http://www.createjs.com/Docs/SoundJS/classes/Sound.html
Sound can be used as a plugin with PreloadJS to help preload audio properly. When it’s preloaded with PreloadJS it is automatically registered. If sound is not preloaded from the outset it will do an internal load, as a result audio may not play immediately the first time you call it. It is recommended that you preload all audio.
This is what I’m doing. All audio is loaded externally from the “sounds
” folder, and preloaded.
First I save the assetPath
(directory), and then make a manifest
— register them for loading and playback.
id
= the name you want to give them.
src
= the filename.
The last line, var queue
, creates a new LoadQueue
instance.
LoadQueue
can preload either a single file, or queue of files. Here it’s many files.
//based on: http://createjs.com/tutorials/SoundJS%20and%20PreloadJS/ //assetsPath = A path that will be prepended on to the //source parameter of all items in the queue before they are loaded. var assetsPath = "sounds/"; var snd_manifest = [ {id:"Die", src:"Die.ogg"}, {id:"Intro", src:"Intro.ogg"}, {id:"Move1", src:"Move1.ogg"}, {id:"Move2", src:"Move2.ogg"}, {id:"Spawn", src:"Spawn.ogg"}, {id:"TransitionNewScene", src:"TransitionNewScene.ogg"}, {id:"Aud_Type1", src:"Typing1.ogg"}, {id:"Aud_Type2", src:"Typing2.ogg"}, {id:"Aud_Type3", src:"TypingNewline.ogg"}, {id:"TypingSelect", src:"TypingSelect.ogg"}, {id:"Win", src:"Win.ogg"} ]; //[useXHR=true], [basePath=""] var queue = new createjs.LoadQueue(true, assetsPath);
As you’ve noticed I’m specifying .ogg
as the default sound format.
SoundJS will only preload audio that is supported (by the users browser). If multiple formats are provided, only the one that the browser can play will be preloaded.
You can specify other alternate extensions via alternateExtensions
. See: http://www.createjs.com/Docs/SoundJS/classes/Sound.html#property_alternateExtensions
It’s an array of extensions to attempt to use when loading sound, if the default is unsupported by the active plugin.
I’m giving mp3
, and wav
as alternatives.
//alternateExtensions = array of extensions to attempt to use when loading sound createjs.Sound.alternateExtensions = ["mp3", "wav"]; //Install the SoundJS sound class -- to load the HTML audio. //Should be installed before loading any audio files. queue.installPlugin(createjs.Sound);
PreloadJS supports the following listeners:
complete
: fired when a queue completes loading all files
error
: fired when the queue encounters an error with any file.
progress
: Progress for the entire queue has changed.
fileload
: A single file has completed loading.
fileprogress
: Progress for a single file has changes. Note that only files loaded with XHR
(or possibly by plugins) will fire progress events other than 0 or 100%.
I have two. One for when the load is complete
(which plays — continues), and the other to keep track of the loading progress
(in this case it just sends to console.log
).
//two event listeners - one when load is complete, and the other to display the load progress queue.addEventListener("complete", handleComplete); queue.addEventListener("progress", handleProgress);
Load manifest
is to load multiple sounds/a set of them. See: http://createjs.com/Docs/PreloadJS/classes/LoadQueue.html#method_loadManifest
queue.loadManifest(snd_manifest); //to load an individual file: //queue.loadFile({id:"Die", src:"sounds/Die.ogg"}); function handleComplete(evt) { console.log("Loading sound complete."); playSound("Spawn");//"LOADED" sound indication txt_title.text = ""; //play the timeline after loading (continue) _stage.play(); } function handleProgress(evt) { //console.log("Event Loading: " + (queue.progress.toFixed(2) * 100) + "%"); }
Finally, I have two functions. One for playing a sound. The other for playing a sound loop.
If you wanted to play a sound, now all you would have to do is call playSound("SOUND_NAME");
function playSound(name) { return createjs.Sound.play(name); } function playSoundLoop(name, loops){ return createjs.Sound.play(name, {loop:loops}); };
The game has this cute little typing effect. Whenever a word gets typed, a random type sound plays.
The function for that looks like this:
//set up an array of typing sounds Arr_TypingSnds = new Array("Aud_Type1", "Aud_Type2"); //chose a random typing sound and play it //for text writing randTypeSnd = function() { var randNum = Math.ceil(Math.random()*(Arr_TypingSnds.length))-1; var randomSound = Arr_TypingSnds[randNum]; if(randomSound==undefined){ randomSound = Arr_TypingSnds[0]; }; playSound(randomSound); }
*************
KEYBOARD CONTROLS
I have two examples here.
If problems arise, for example, numbers may differ cross browsers, I would recommend using keymaster.js
(https://github.com/madrobby/keymaster) over the “classic” version.
keymaster.js
is the javascript library we appended earlier.
Following is how you would implement keymaster.js
//keymaster.js keyboard: //create ticker: http://createjs.com/Docs/EaselJS/classes/Ticker.html //to handle keyboard input createjs.Ticker.addEventListener("tick", keyboard_ticker); function keyboard_ticker(event) { if (key.isPressed("up") || key.isPressed("w")) { console.log("UP pressed"); } if (key.isPressed("down") || key.isPressed("s")) { console.log("DOWN pressed"); } if (key.isPressed("left") || key.isPressed("a")) { console.log("LEFT pressed"); } if (key.isPressed("right") || key.isPressed("d")) { console.log("RIGHT pressed"); } }
You can see, it’s a lot simpler.
I am using the “normal” keyboard functionality (document.onkeydown
and document.onkeyup
).
Other good examples of (similar) keyboard use in createjs games are:
https://github.com/CreateJS/EaselJS/blob/master/examples/Game.html
and https://github.com/javierj/EaselJS/blob/master/KeyboardDemo/DemoKeyboard.html
So, first set up the key codes:
KEY_ENTER = 13; KEY_SPACE = 32; KEY_ESC = 27; KEY_UP = 38; KEY_DOWN = 40; KEY_LEFT = 37; KEY_RIGHT = 39; KEY_W = 87; KEY_A = 65; KEY_S = 83; KEY_D = 68;
And then create the key down event:
evnt_KEYDOWN = function(e){ //cross browser issues exist if(!e){ var e = window.event; } // console.log("press " + e.keyCode); // if(_scene == "faq"){ //escape - back to menu if(e.keyCode == KEY_ESC || e.keyCode == KEY_SPACE || e.keyCode == KEY_ENTER){ playSound("TypingSelect"); selectedMenuItem = "menu"; transition(); }; }; // if(_scene == "menu"){ //make menu sellection if(e.keyCode == KEY_SPACE || e.keyCode == KEY_ENTER){ playSound("TypingSelect"); transition(); }; //up/down on menu items //only two items, so, simply put... if(e.keyCode == KEY_UP){ writeText("START",txt_start); selectItem("game",txt_start,txt_faq); }; if(e.keyCode == KEY_DOWN){ writeText("WIKI",txt_faq); selectItem("faq",txt_faq,txt_start); }; }; // if(_scene == "menu_win"){ //select - if "submit" submit to twitter - if "game" then hack again if(e.keyCode == KEY_SPACE || e.keyCode == KEY_ENTER){ if (selectedMenuItem == "submit") { tinyURL(); } if(selectedMenuItem == "game"){ playSound("TypingSelect"); transition(); } }; if(e.keyCode == KEY_UP){ writeText("Submit This Hack",txt_option1_win); selectItem("submit",txt_option1_win,txt_option2_win); }; if(e.keyCode == KEY_DOWN){ writeText("Hack Again",txt_option2_win); selectItem("game",txt_option2_win,txt_option1_win); }; }; // };
There are several “scenes”/areas in the game (excluding the actual game), all of which do something different.
I’m using one keyboard event for all of them (excluding the game). If _scene
variable is equal to that area, then keyboard functionality for that unlocks.
writeText("",txt_field)
is the write text function. When you select a menu item it should write out the text (indicating that you select it).
The other function selectItem("scene",txt_field1,txt_field2)
is setting up the variables for your next selection, and toggling the alpha between the two text fields (to make it obvious which one you’re selecting).
It looks like this:
//For menu item selection //toggles the alpha between items, //and sets the selectedMenuItem to current selection selectItem = function(selectedItm, currItem, prevItem) { playSound("Aud_Type3"); selectedMenuItem = selectedItm; prevItem.alpha = 1; currItem.alpha = 0.5; }
Once you want to select that (SPACE
, or ENTER
). Another function, transition();
, is called to go to that selected scene/area.
transition();
looks like:
//transition //plays a sound when you change screens //also handles miscelaneous clearing transition = function() { playSound("TransitionNewScene"); // //clear all fields clearText(); // _stage.gotoAndPlay(selectedMenuItem); }
“menu_win
” keyboard event is just a little different in that it also offers you the choice to submit your stats to twitter. tinyURL();
Which is:
//Twitter (submit everything to twitter) -- see menu_win tinyURL = function(){ var url = "http://twitter.com/home?status=I haxed teh system with Haxatron 2000: "+pageURL+" in "+seconds+" seconds, and "+tries+" tries. Haxing is tricky business."; window.open(url,"_blank"); }
*************
THE GAME ENGINE
Starting with the higher level game specific logic, this first section handles the map, map tiles, and drawing/redrawing it.
There are three types of tiles:
Exit
= the exit/goal to navigate the player to.
Player
= the little green guy – controllable character.
Memory
= the main tile of the game. It has 3 states – up, down, error (red).
First of all I’ll walk you through the variables…
/////////// // Higher-level, global game-specific logic and resources. ////////// var gameTileParent; //Sprite var gameLoops = {mapRedraw: -1}; //Object, see: http://www.w3schools.com/js/js_objects.asp for more info var gameTileSize = 100;//the size of the tile in both width and height var gameCurrentMap; //Object - see gameLoadMap(...) for what goes in this //intervals for changing and redrawing, see gameStartMapRedraw() var changeInt = 30; var redrawInt = 50; var shouldCenterMap = true;//true = centered. false = not... In the event this is a map that should not be centered
var gameTileParent
will be the container that all the tiles, and player are placed in.
From the docs http://createjs.com/Docs/EaselJS/classes/Container.html : “A Container is a nestable display list that allows you to work with compound display elements. For example you could group arm, leg, torso and head Bitmap instances together into a Person Container, and transform them as a group, while still being able to move the individual parts relative to each other. Children of containers have their transform and alpha properties concatenated with their parent Container.”
So it’s nice they though of that. :)
var gameLoops = {mapRedraw: -1};
is one of the many object literals (http://www.w3schools.com/js/js_objects.asp) of this game. gameLoops
is in charge of the two intervals that redraw, and shuffle the map. They will look like this:
gameLoops.mapRedraw = setInterval(gameRedrawMap, redrawInt);
gameLoops.mapShuffle = setInterval(gameShuffleMap, changeInt);
This will take place in a function called gameStartMapRedraw()
, which I’ll walk you through in a moment…
var gameTileSize
= the size of the tile in both width
and height
, for easy changing.
var gameCurrentMap;
Is another object that will keep all the maps properties (set in gameLoadMap(...)
). Tiles, width, and height.
For example: gameCurrentMap = {map: tiles, width: mapWidth, height: mapHeight};
The interval values – at what rate the tiles change, and the map redraws:
var changeInt = 30;
var redrawInt = 50;
var shouldCenterMap
= boolean. Should the map be centered or not. True
is centered, false
not.
Moving on to the functions, the first one is gameLoadMap(tiles, mapWidth, mapHeight)
//this is what gets called to start everything function gameLoadMap(tiles, mapWidth, mapHeight) { //Using a Container for this: http://createjs.com/Docs/EaselJS/classes/Container.html var gamemc = new createjs.Container();//new createjs.MovieClip(); gameCurrentMap = {map: tiles, width: mapWidth, height: mapHeight}; // _stage.addChild(gamemc); gamemc.name = "gamemc"; gameTileParent = gamemc; // gameStartMapRedraw(); }
This is what gets called (in init()
) to create the map. In creates the container gameTileParent
(var gamemc
) on _stage
. As well as setting the current maps properties (according to what you pass to it later, in init()
).
Then it starts the redrawing gameStartMapRedraw();
gameStartMapRedraw
is a simple function that starts our intervals (as part of the gameLoops
object).
function gameStartMapRedraw() { if (gameLoops.mapRedraw == -1) { gameLoops.mapRedraw = setInterval(gameRedrawMap, redrawInt); gameLoops.mapShuffle = setInterval(gameShuffleMap, changeInt); } }
Called in the gameRedrawMap()
interval (which I’ll get to), gameRenderTile
creates a new tile if none exist (if (tile.mc == null)
).
Everything else, after that, manages updates (if relocated since last redraw).
function gameRenderTile(tile) { // Create totally new tile if (tile.mc == null) { // var mc = new tile.tile(); gameTileParent.addChild(mc); mc.name = "tile_" + tile.x + "_" + tile.y; // mc.x = tile.x * gameTileSize; mc.y = tile.y * gameTileSize; mc.stop(); tile.mc = mc; } // Check if tile relocated since last redraw else { if (tile.mc.x != (tile.x * gameTileSize) || (tile.mc.y != tile.y * gameTileSize)) { tile.mc.x = tile.x * gameTileSize; tile.mc.y = tile.y * gameTileSize; tile.x = Math.floor(tile.mc.x / gameTileSize); tile.y = Math.floor(tile.mc.y / gameTileSize); } } tile.mc.stop(); if (!tile.isPlayer) tile.mc.gotoAndStop(tile.opening);//if it's not the player, then... }
Now, gameRedrawMap()
. This is called in the interval (gameLoops.mapRedraw
).
gameRedrawMap()
updates the entire map, saves it (see the loop).
The loop gathers all the gameCurrentMap.map
tiles and then calls gameRenderTile(tile)
on them.
Thus the map is redrawn.
The map is centered, and also playerCheckTile();
is called, incase the player is on a deadly tile.
This keeps the player from idling on a deadly tile.
function gameRedrawMap() { //Update the entire map - save it //this called in an interval //loop through the map, gather all the tiles //update them, which takes place in gameRenderTile again //also centers map and checks if player is on deadly tile (so he doesn't idle on one) // var tile = null; var player = null; // for (var a = 0; a != gameCurrentMap.map.length; ++a) { tile = gameCurrentMap.map[a]; gameRenderTile(tile); } centerMap(); //checked here and in keyboard so he doesn't idle on a deadly tile playerCheckTile(); }
centerMap()
is fairly self explanatory. It just takes the entire gameTileParent
container and sets the x, y to center of stage.
//center the map - called in the gameRedrawMap interval function centerMap(){ if(shouldCenterMap == true){ gameTileParent.x = Math.floor((stageWidth / 2) - (containerWidth / 2)) - 100; gameTileParent.y = Math.floor((stageHeight / 2) - (containerHeight / 2)) -50; //-50 added to pull it up a bit }; };
One very important function here is gameMakeTile(symbol, tx, ty)
.
This is called twice in this game (init()
and in playerMake()
).
Basically whenever you create a new tile, this will set the tiles properties.
tile:symbol
= the type
x:tx, y:tx
= placement
opening
= frame to play
//responsible for setting the tile's properties... //called in any init function - twice in init() & in playerMake() //tile:symbol = the type, x:tx, y:tx = placement, opening = frame to play function gameMakeTile(symbol, tx, ty) { return {tile:symbol, x:tx, y:ty, opening:(Math.round(Math.random()))}; }
Note that when you add a movieclip / addChild
to stage (with code), we all know that we need a linkage identifier. That’s when you go to properties>actionscript>export for actionscript
. In this case (HTML5 document), that is disabled.
The only workaround I was able to find was to manually add a copy of all desired addChild
to the stage (second frame – there’s a note).
After that you can reference them by the name they have in the library.
Example:
var myMC = new lib.LINKAGE(); this.addChild(myMC);//myMC is added to stage!
So, back to the engine, if I wanted to add a copy of Exit I would specify lib.Exit
as the symbol:
gameMakeTile(lib.Exit, ExitX, ExitY);
Or lib.Player
, lib.Memory
…
And finally we have our very last function gameShuffleMap()
which is also called in the interval (gameLoops.mapRedraw
).
This changes the graphical state of a tile (Memory tile), to either “deadly”, or “walkable”.
It should never change under the player. That’s what while (tile1.isPlayer || (tile1.x == player.x && tile1.y == player.y)
) is for. If it’s under the player, then try again.
The tile1.opening
at the end sets it to one of the 3 states.
//Change the graphical state of a tile (if it should be "deadly" or "walkable"), but not under player function gameShuffleMap() { var tile1 = gameCurrentMap.map[(Math.floor(Math.random() * gameCurrentMap.map.length + 1)-1)]; //No changing under player! Do again... while (tile1.isPlayer || (tile1.x == player.x && tile1.y == player.y) ) { //now the setInterval for this function is accurate tile1 = gameCurrentMap.map[(Math.floor(Math.random() * gameCurrentMap.map.length + 1)-1)]; } tile1.opening = Math.floor(Math.random() * 3); //to test win (no deadly tiles) //tile1.opening = Math.floor(Math.random() * 2); }
Next we have our Player’s control and logic.
Again, starting with the variables…
/////////// // Player control and render logic ////////// // dr = Direction as string: up, down, left, right // I left this in from the touchscreen controls version // This is here incase touchscreen controls are added back in // Each touchscreen button would set dir... // Keyboard now uses it as well. var dr = ""; var player; //Object var playerCurrentTile = null; //Object -- so keyboard knows if player can go there, or if it will kill player
var dir = "";
Is a direction string. Up, down, left, and right. This was left in from the touchscreen controls version of the game. It’s here incase touchscreen support is added back (I have it as an App in the App Store incase you’re interested :) ).
Each touchscreen button would set a dir
. Keyboard now uses it as well. So essentially this online version could support both.
var player;
Is the player! As an object, because properties are saved/set when player is created in playerMake(symbol, px, py)
. Explained bellow.
var playerCurrentTile;
Is the current tile the player is on. Used in keyboard movement (see if player can move to the next tile). Set in the keyup event.
The first thing will be the function for making the player, playerMake(symbol, px, py);
This sets the player object to what gameMakeTile(...)
returns (now you have the basic properties of player). And specify that this tile isPlayer
(true). Return the player because later we will assign a variable var currPlayer;
to whatever this returns.
function playerMake(symbol, px, py) { player = gameMakeTile(symbol, px, py); player.isPlayer = true; return player; }
We will also have two functions for setting and clearing the keyboard. Easier for killing/restarting the game.
function setKeyboard(){ // document.onkeydown = playerKeyboardMove; document.onkeyup = playerKeyboardUp; // }; function clearKeyboard(){ document.onkeydown = null; document.onkeyup = null; };
So now we’ll start with the key up event. There are two functions associated with it.
The first is playerKeyboardUpdate()
.
playerKeyboardUpdate()
is called in two areas. The key up event, and in init()
(to set the starting value of playerCurrentTile
).
This sets playerCurrentTile
to the tile the player is on by looping through the game map, and checking if the tile x/y is equal to player. Once it finds it then set it.
playerKeyboardUpdate()
isn’t the actual keyboard event, playerKeyboardUp(event)
(following block) because, if I wanted to add touchscreen controls, I could now reference playerKeyboardUpdate()
for the touch up event.
//KEY_UP function playerKeyboardUpdate() { // Tile player is on right now for (var i = 0; i != gameCurrentMap.map.length; ++i) { if (gameCurrentMap.map[i].x == player.x && gameCurrentMap.map[i].y == player.y && gameCurrentMap.map[i] != player) { playerCurrentTile = gameCurrentMap.map[i]; break; } } }
Now to set the actual key up event:
function playerKeyboardUp(event){//KeyboardEvent playerKeyboardUpdate(); }
As usual key down is a lot more elaborate…
The lines that look like: && playerCurrentTile.opening == 1
Keep the player from walking in a direction that the tile does not allow.
Notice the gameTileParent.getChildByName("tile_"+(player.x-1)+"_"+player.y) != null
before that? That will keep the player from walking off the actual game grid, away from the game…
So we have that to keep them on the board.
The last if
statement (if (dr != "")
) will set the player sprite to face the direction you just pressed.
Then we check the tile with playerCheckTile();
(explained bellow).
//KEY_DOWN function playerKeyboardMove(e) {//KeyboardEvent // if(!e){ var e = window.event; } //var dr = ""; // Direction: left | up | down | right switch (e.keyCode){ case KEY_LEFT: if (gameTileParent.getChildByName("tile_"+(player.x-1)+"_"+player.y) != null && playerCurrentTile.opening == 1) { player.x--; dr = "left"; playSound("Move1"); } break; case KEY_UP: if (gameTileParent.getChildByName("tile_"+player.x+"_"+(player.y-1)) != null && playerCurrentTile.opening == 0) { player.y--; dr = "up"; playSound("Move2"); } break; case KEY_RIGHT: if (gameTileParent.getChildByName("tile_"+(player.x+1)+"_"+player.y) != null && playerCurrentTile.opening == 1) { player.x++; dr = "right"; playSound("Move2"); } break; case KEY_DOWN: if (gameTileParent.getChildByName("tile_"+player.x+"_"+(player.y+1)) != null && playerCurrentTile.opening == 0) { player.y++; dr = "down"; playSound("Move1"); } break; } if (dr != "") { player.mc.gotoAndStop(dr); } playerCheckTile(); }
playerCheckTile()
checks the tile the player just walked on and determines if the player should explode (die), or bloat with joy (win).
The first condition (dying) is fairly self explanatory. Checks against the timeline position (what frame), and if it’s the “error” tile then die.
Starting the game again (calling demoLose();
) is called at the end of the player’s die animation. There are better ways of doing that, but this was the simplest.
The win condition checks to see if the player actually made it to exit. Notice the ExitX
, and ExitY
variables. We save the x/y of Exit incase we want to change it, or make it random with every new game. It adds a level of flexibility…
An interesting problem I ran into while porting this was that the player would sometimes freeze on win (the animation does not play, thus the win function does not get called, and you don’t proceed past this point). The issue is random and seems to occur more in Chrome than it does in Firefox (sorry, I know, blaming browsers). I “fixed” this bug by just forcing win with an interval
(if it goes beyond a certain time) then just call it. I didn’t figure out exactly why this happens and why the win animation does not trigger, but you could probably credit it to the fact that it would be better to just remove the player when you win, and place win animation as a separate added child, and handle the win/die events purely through code, breaking up the player movieclip. Better restructuring would be necessary… but in this case, good enough!
function playerCheckTile(){ var currLabel = player.mc.getCurrentLabel(); //Die condition if (gameTileParent.getChildByName("tile_" + player.x + "_" + player.y).timeline.position == 2) { player.mc.gotoAndStop("die"); } //Win condition if(gameTileParent.getChildByName("tile_" + player.x + "_" + player.y)==gameTileParent.getChildByName("tile_" + ExitX + "_" + ExitY)){//if it's Exit tile: [object Exit] //Start win failsafe if(currLabel!="win"){ clearKeyboard(); winInt = setInterval(winInterval,1000); }; //default player.mc.gotoAndStop("win"); } }
Here’s that “win failsafe” just mentioned:
//WIN FAILSAFE: //If the player does not trigger the "win" animation for whatever reason //(problem seems to sometimes occur, and I'm not sure if what it is, or how to recreate it, or if it still exists...) //Countdown wait, and then force demoWin(); if the animation doesn't trigger & call it function winInterval(){ winCnt+=1; if(winCnt>3){ clearInterval(winInt); demoWin(); }; };
Moving on to starting the game!
We have our variables…
/////////// // Start /////////// //The current map's width and height var currMapWidth; //Number var currMapHeight; //Number //To calculate the width and height of the map container (used in centering) //take gameTileSize and multiply by currMapWidth, and currMapHeight //CreateJS does not support .width yet, so this is how I handle it... var containerWidth; var containerHeight; // var currMapTiles; //Array //The current "Player" sprite - in this case, the little green guy var currPlayer; //Object //Placement of the Exit //I'm saving this here because location of exit may vary //--Depending on how much you decide to customize this var ExitX; var ExitY; //seconds and tries is your ranking //the player will be congratulated on this at the end //so both variables are set to global scope seconds = 0; tries = 0; var timeInt; //uint //Win failsafe (if, for whatever reason, win might not trigger) winInt = 0; var winCnt = 0;
var currMapWidth
, and var currMapHeight;
are the maps width and height. They’re set in init
. Values go by a tile basis, so larger numbers mean more tiles.
var containerWidth
and var containerHeight;
are new to this. I calculate the width and height of the map container (used for centering) by taking the gameTileSize
and multiplying that by currMapWidth
, or currMapHeight
. This is necessary because CreateJS does not support .width
or .height
yet (a workaround is available here https://github.com/ibnYusrat/Get-Shape-Width-Height-in-EaselJS ).
var currMapTiles;
Are all the maps tiles. Pushed to with gameMakeTile (currMapTiles.push(gameMakeTile(lib.Exit, ExitX, ExitY));
for example).
var currPlayer;
Is the player object. Updated when the player is created. Example, currPlayer = playerMake(lib.Player, 0, 2);
var ExitX;
and var ExitY;
are the placement of the exit. So it’s easier to check against and determine if the tile the player is on is the exit. Exit placement may vary (if that behavior was desired).
For our ranking system we have,
seconds = 0;
tries = 0;
Which will be like the player’s “score” and they are congratulated on it when they win… as well as it being sent to twitter. :)
And finally,
var timeInt; //uint
//Win failsafe (if, for whatever reason, win might not trigger)
winInt = 0;
var winCnt = 0;
Are the intervals. Time interval is what increments the seconds variable (how long you’ve played before winning).
The time
function is a simple one, just incrementing seconds.
//the time interval function time(){ seconds+=1; };
Following that is another miscellaneous function meant for “sloppily” destroying all intervals. This is a safety precaution just incase the interval didn’t get cleared for some reason.
function killAllIntervals() { //console.log("killAllIntervals() - murder called"); //murder for (var i = 0; i<100; i++) { clearInterval(i); } }
Finally, we have init()
, which starts the game!
It clears any variables (resets them), sets up any necessary values (like the container width/height), and creates all tiles (player, memory, exit).
Both arrays are first cleared currMapTiles = []
, before repopulated. For example, all tiles are pushed to the currMapTiles
array.
After currMapTiles
is done being populated with necessary tile information, gameLoadMap(currMapTiles, currMapWidth, currMapHeight);
is called to start it up.
setKeyboard();
enables the keyboard controls.
After the “Spawn” sound is played, the playerKeyboardUpdate()
is called just to capture the current player position for first run.
function init(){ winCnt = 0; dr = ""; currPlayer = null; gameCurrentMap = null; currMapWidth = 7; currMapHeight = 4; // containerWidth = gameTileSize*currMapWidth; containerHeight = gameTileSize*currMapHeight; // currMapTiles = []; demoLibrary = []; //*Make the Player currPlayer = playerMake(lib.Player, 0, 2); // for (var i = 0; i <= currMapWidth; ++i) { for (var j = 0; j <= currMapHeight; ++j) { var chosen = Math.floor(Math.random() * 1); --chosen; //*Make the Memory tiles currMapTiles.push(gameMakeTile(lib.Memory, i, j)); } } //Place Exit tile -- in this case at the end of the map //First the variables ExitX = currMapWidth+1; ExitY = Math.floor(currMapHeight/2); //*Make the Exit tile currMapTiles.push(gameMakeTile(lib.Exit, ExitX, ExitY)); currMapTiles.push(currPlayer); gameLoadMap(currMapTiles, currMapWidth, currMapHeight); // shouldCenterMap = true; setKeyboard(); playSound("Spawn"); // playerKeyboardUpdate(); };
After init()
we have uninit()
which is a bit simpler. It clears the game (removes all children). This is used in demoLose()
(called in the player’s death animation), and demoWin()
.
function uninit(){ shouldCenterMap = false; clearKeyboard(); // for (var j = 0; j < gameTileParent.numChildren; j++){ gameTileParent.removeChild(gameTileParent.getChildAt(j)); } _stage.removeChild(gameTileParent); };
Then we have demoWin
, and demoLose
demoWin
is kills the entire game. All intervals are cleared, uninit()
also called, and the default keyboard (other menu keyboard) restarted.
This sends the timeline to the “win” scene.
//Scope of win, and lose has been changed to be global //They are called within the demo player - at the end of its respective animation demoWin = function() { clearInterval(gameLoops.mapRedraw); clearInterval(gameLoops.mapShuffle); clearInterval(timeInt); clearInterval(winInt); killAllIntervals(); uninit(); addDefaultKeys();//start the default keyboard again //go to win! selectedMenuItem = "win"; transition(); }
demoLose()
simply increments the player’s tries, uninit();
, and starts the game again, init();
.
demoLose = function() { tries+=1; uninit(); init(); }
Then finally we start the game with the following.
removeDefaultKeys();//remove default keyboard init(); timeInt = setInterval(time,1000);
That’s it! The game is running.
*************
CONCLUSION
After having worked with this feature of Flash CC I’m impressed. It’s a lot more flexible/powerful than it’s given credit, and I’m excited to see where Adobe will be taking it in the future.
I hope it’s going to grow in future versions/releases, as I think it’s silly to separate Flash from HTML5 when the two could (potentially) work so well together (Flash is still superior when it comes to handling vector graphics).
Download all sourcefiles here, and take a look:
http://nathalielawhead.com/sourcefiles/Haxatron2000/HTML5/
You can play the end result here:
http://haxatron.alienmelon.com/browserhax
Visit the PREVIOUS tutorial: Building A Game In Edge Animate (Offender)
hi I saw this article and it made me think of the game MrMine. I played it at MrMine.com. It is very similar
This post is great. Made my day. Keep up the good work!
You Are Amazing, Keep it up your good work. The Information You gave is helping me so much. Thank You.