The framework I created demonstrates a lot of tricks, as well as basic functions common in games. Features include:
- Frame-based sprite animation
- Sprite scaling, rotation, and transparency
- Keyboard input management
- Ability to scale to browser size while maintaining original aspect ratio
Furthermore, I've added functions such as a "wait until loaded" feature that ensures the game doesn't start until all related assets have been fully loaded. It currently has no progress indicator (just a static "Loading..." message), so you may want to add one of those if you decide to use any of the framework for your own games.
Now, let's go over some of the highlights of the framework for HoT MetaL JaZz, beginning with the front-end: "hotmetal.html".
hotmetal.html
When hotmetal.html loads, it runs the script function "startGame()", which is embedded right into the HTML page. Within the "startGame()" function, we create a new "GameCore" object, the constructor for which takes several parameters. Let's go over those quickly:
The first argument is the primary canvas - what the player will see. The next two arguments specify a maximum width and height for the primary canvas - this is mainly to ensure that the canvas doesn't get so large that performance begins to degrade too far. The next argument assigns the backbuffer canvas. The next two arguments are the width and height of the backbuffer - this value does not change; instead, this is the "natural" size of the backbuffer. The final argument is the desired frames-per-second timing at which to run the game.
Also, the body element of the page is given a function to run when the browser is resize: "game.resize()". We will go over this function as we go through the "gamecore.js" file.
gamecore.js
The constructor for the GameCore object is fairly straightforward - we assign values, and run a couple of functions.
We begin by setting some variables to keep track of our canvases and their sizes. Then we call "this.resize()" to size the canvas for a "best fit" within the browser's viewing area. We also assign a GameInputManager to the GameCore. As for the logic of GameCore, we use a demonstration logic script called "HotMetalLogic" - this contains all the code specific to how the actual game functions.
After this is done, we call "this.init()". Here, we set up the contexts for our canvases and save them. We clear both contexts, and draw a simple loading screen. At the end of the method, we call "this.loadData()".
The GameCore.loadData() method simply offloads the data-loading tasks to the logic part of the game. It will then run "this.assertReady()", which will cause the game to wait until the logic is fully loaded and ready.
The GameCore.assertReady() function will call the logic script's "images.loadImages()" function. The logic.images object is an instance of GameImageManager, and its "loadImages()" function will (re)assign the URL to each image loaded by the logic script. The function sets the GameImageManager.ready property to "true" if all images have finished loading. GameCore.assertReady will start running the logic (this.logic.run()) if the GameImageManager.ready property is set to true; otherwise, it will wait 1000 milliseconds (1 second) and run the check again. This will continue until the GameImageManager lets us know it is ready.
GameCore.resize() is an interesting function. It will find the "best fit" for the game's primary canvas within the browser window, all while maintaining the original aspect ratio of the canvas. First, we get the width-wise aspect ratio, and the "reverse", height-wise aspect ratio. We query the browser's "inner" window width and height, and then run a few checks to make sure the resize fits properly based on the aspect ratios. When all is said and done, you can resize your browser any way you want and have the primary canvas fit inside - with no portion of the primary canvas stretching past the window boundaries.
gameinputmanager.js
The GameInputManager object is quite simple - it sets up some functions for the document to run whenever a key is pressed or released. It also will release all input whenever the document loses keyboard focus via the document.onblur event. To determine whether a particular key is pressed, just check the GameInputManager.keyControls[keyCode] variable - it will be "true" if the key with the specified keyCode is pressed; otherwise, it will return "false" or "undefined".
gameimagemanager.js
This file contains two object constructors. The first is GameImageManager; the second is TaggedImage.
GameImageManager sets up an array of TaggedImage objects, which stores each image inserted into the manager along with a string ID for each. To begin adding images to the GameImageManager, use the function GameImageManager.addImage(filename, tag). The filename is the URL of the image to load; tag is a string identifier used to store and access the image. For example, you can use the code:
myImageManager.addImage("images/my-image.png", "sprite");
The file will be associated with the string "sprite". Thus, to retrieve the image from the GameImageManager instance, you would use:
var img = myImageManager.getImage("sprite");
Finally, the "loadImages()" function will check the "complete" status of each image loaded into the GameImageManager. If it finds that all images have a "complete" status, it will set the GameImageManager's "ready" flag to true.
gamesprite.js
This file contains three object constructors: GameSprite; GameSpriteProperty; and GameSpriteAnimation.
GameSprite.update(elapsedTime) will begin its task by checking whether the sprite is animated - if so, it calls the update(elapsedTime) function of the animation sequence. It continues by ensuring the sprite speed hasn't exceeded the designated maximum, and then updating the sprite's position by factoring its velocity in.
GameSprite.draw(context) takes the backbuffer context as its sole argument. The function sets the appropriate globalAlpha, translation, rotation, and scale of the context to match those of the sprite, and draws the sprite to the context. If the sprite is animated, it will draw the current frame; otherwise, it will draw the image parameter used in the GameSprite's constructor.
GameSprite.setProperty(name, value) and GameSprite.getProperty(name) allow the sprite to have an arbitrary set of properties. For example, you can add a "Hit Points" property to the sprite, without having to write a new sprite class. The GameSpriteProperty class simply holds a name for the property and its value.
The GameSpriteAnimation(image, width, height, delay) constructor takes an image, divides it into "cells" of the given width and height, and delays frame transitions by "delay" milliseconds. The update function simply checks whether it needs to transition to the next frame, by comparing the current frame time with the delay of the transition. If it's time to transition, the frame index is incremented. If the frame index has gone past the last frame index, the animation "loops" back to the first frame.
hotmetallogic.js
The HotMetalLogic class is the "glue" that ties all the previous classes together. It uses a GameImageManager to load and assign images, it interacts with sprites using the GameInputManager (use the arrow keys to move around), and handles the AI for different sprites.
When playing the game, you need to avoid the fireballs shooting out of the flares on the four corners of the play area. As the flares shoot out fireballs, they gradually weaken until they disappear. When all flares and fireballs have been extinguished, the game ends. You lose points each time a fireball hits you.
Of course, this is just a modest demo of what is possible. There are better ways to set up a framework for a game - this is just to get your creative juices flowing.
Some suggestions for improvements:
Make the GameCore capable of holding an array of logic scripts, and switch between them. For example, run a logic script for a main menu, and another logic script for the game in action.
Sort sprite drawing order by Y: The farther from the bottom of the canvas a sprite is, the sooner it should be drawn, so that sprites appear behind or in front of their appropriate peers.