Porting Halloween Liche
While I am covering a lot of code for the game, not all the code is included. For complete code check out the GitHub repository at https://github.com/BillySpelchan/HalloweenLiche. Halloween Lich was a game that I created way back in 2004 using Flash. With the end-of-life of Flash imminent, I am in the process of porting my older games to HTML5. Originally I was going to take advantage of Adobe Animate in order to perform this task but the last time I used that tool I found that it did very little to port the code. My games tend to be code heavy so any games that I ported would require a lot of work and the produced JavaScript code tended to be larger than just creating sprite sheets and manually writing the code using Create.js. As the Create.js libraries are also large, I decided to experiment with porting a game to HTML5 without using any libraries (not even the BGLayers library I had started writing for HTML5 games back in 2012). I also wanted to play around with using ECMAScript 6 for the code so that I would have better object-oriented support. While there are still some people out there with browsers that do not support the ECMAScript 6 standard, they are in an increasingly small minority so I am going to make my life easier and go with a more modern version of JavaScript.
The advantage of libraries such as Create.js is that they provide a lot of functionality but with the cost of a lot of code. For larger games this may not be much of a concern, but for small games like Halloween Liche, this is overkill. To get away from the need for create.js, the graphics in the game were converted from vector graphics into a single sprite atlas as shown below:
The atlas produced has a rather large JSON file that describes the contents of the file. The information contained here is largely redundant and it could be drastically reduced in size so that it contained only the information that was needed by my game. As JSON is actually valid JavaScript, this condensed data file could be inserted directly into the code.
HalloweenLichAtlas = {
"Backdrop" : {"x":956,"y":132,"w":641,"h":482},
"Boom" : {"x":0,"y":618,"w":752,"h":544},
"ContinueButton" : {"x":0,"y":0,"w":527,"h":42, "over_x":531,"over_y":0, "down_x":1062,"down_y":0},
"EnergyBall" : {"x":964,"y":618,"w":203,"h":210, "frames":[
{"x":964,"y":618},{"x":1171,"y":618},{"x":1378,"y":618},{"x":1585,"y":618},
{"x":1792,"y":618},{"x":0,"y":1166},{"x":207,"y":1166},{"x":414,"y":1166},
{"x":621,"y":1166},{"x":828,"y":1166},{"x":1035,"y":1166},{"x":1242,"y":1166},
{"x":1449,"y":1166},{"x":1656,"y":1166},{"x":0,"y":1380},{"x":207,"y":1380},
{"x":414,"y":1380},{"x":621,"y":1380},{"x":828,"y":1380},{"x":964,"y":618} ] },
"Lich" : {"x":1035,"y":1380,"w":286,"h":408},
"PlayGame" : {"x":531,"y":46,"w":350,"h":82, "over_x":885,"over_y":46, "down_x":1239,"down_y":46},
"TextBallon" : {"x":1325,"y":1380,"w":76,"h":41},
"letters" : "1234567890ABCEHILNOPSTUVWY",
"bloodLetters" : [
{"x":1947,"y":46,"w":24,"h":50},{"x":1975,"y":46,"w":40,"h":48},{"x":0,"y":132,"w":34,"h":48},
{"x":38,"y":132,"w":40,"h":48},{"x":82,"y":132,"w":30,"h":48},{"x":116,"y":132,"w":32,"h":48},
{"x":152,"y":132,"w":36,"h":48}, {"x":192,"y":132,"w":32,"h":46},{"x":228,"y":132,"w":32,"h":50},
{"x":581,"y":132,"w":50,"h":46},{"x":264,"y":132,"w":50,"h":50}, {"x":318,"y":132,"w":35,"h":46},
{"x":357,"y":132,"w":40,"h":50}, {"x":401,"y":132,"w":30,"h":49}, {"x":435,"y":132,"w":42,"h":50},
{"x":481,"y":132,"w":12,"h":46}, {"x":497,"y":132,"w":30,"h":48}, {"x":531,"y":132,"w":46,"h":46},
{"x":581,"y":132,"w":50,"h":46}, {"x":635,"y":132,"w":32,"h":46}, {"x":671,"y":132,"w":24,"h":50},
{"x":913,"y":132,"w":39,"h":47}, {"x":699,"y":132,"w":48,"h":46}, {"x":751,"y":132,"w":46,"h":48},
{"x":801,"y":132,"w":64,"h":46}, {"x":869,"y":132,"w":40,"h":46} ]
}
This code describes the bounding boxes of all the items in the game. Buttons have a bit more information. Here the main bounding box represents the button in the normal up state. As the frames for the button are all the same size, the dimensions are not needed for the over and down states so just the coordinates for those two states are needed. Manually writing a button class is not overly difficult though you will need to manually handle the mouse events. You need to track the state of the button. A simple isInside() function is used by the mouse event handlers to see if the indicated event is within the button bounding box. It would be possible to do pixel-level accurate detection but it is not really worth the effort.
If the mouse is over the button when clicked it is considered down. I keep the down state until the mouse button is released with the button event sent only if the mouse is over s down button when it is released. While it would be possible to implement a proper event listing system, that is overkill for our needs so we just call the buttonClicked() method on the owner. We consider the button to be in the over state if it is not in the down state and the mouse button is over the button.
Drawing the button is simply a matter of using the current state of the button to figure out which image atlas coordinates to use. These coordinates are in the JSON object provided to the constructor with the default x and y properties used for up, over_x and over_y used for the over state, and down_x and down_y used for the down state. As stated above, the width (w) and height (h) are shared by all states.
class Button {
static BUTTON_UP = 0;
static BUTTON_OVER = 1;
static BUTTON_DOWN = 2;
constructor(owner, bid, atlasInfo, x, y) { this.owner = owner; this.bid = bid; this.atlasInfo = atlasInfo; this.x = x; this.y = y; this.x2 = x + atlasInfo.w; this.y2 = y + atlasInfo.h; this.state = Button.BUTTON_UP; } isInside(x,y) { if ((x >= this.x) && (x <= this.x2) && (y >= this.y) && (y <= this.y2)) return true; else return false; } mouseDown(x, y) { if (this.isInside(x,y)) this.state = Button.BUTTON_DOWN; } mouseMove(x, y) { if (this.isInside(x, y)){ if (this.state == Button.BUTTON_UP) this.state = Button.BUTTON_OVER; } else { if (this.state == Button.BUTTON_OVER) this.state = Button.BUTTON_UP; } } mouseUp(x, y) { if ((this.state == Button.BUTTON_DOWN) && (this.isInside(x,y))) this.owner.buttonClicked(this.bid); this.state = Button.BUTTON_UP; } render(ctx) { var rx = this.atlasInfo.x; var ry = this.atlasInfo.y; var w = this.atlasInfo.w; var h = this.atlasInfo.h; if (this.state == Button.BUTTON_OVER) { rx = this.atlasInfo.over_x; ry = this.atlasInfo.over_y; } else if (this.state == Button.BUTTON_DOWN) { rx = this.atlasInfo.down_x; ry = this.atlasInfo.down_y; } ctx.drawImage(HalloweenLichImg, rx, ry, w,h, this.x, this.y, w, h); }
}
Halloween Lich used a custom font that I created, with only the letters used in the game created. The Flash version would manually assemble these letters but we can easily automate this process. My JSON code simply adds a letters property that contains the list of letters and numbers available. This is followed by the bloodletters property which is an array of bounding boxes. The drawBloodText() function simply takes the string to draw, the coordinates, and a scaling factor then draws it to the canvas. Scaling is needed as the text needs to be drawn in different sizes based on where it is used.
Rendering a font that has been set up as a set of bounding boxes is easy. We find out which box to get the letter image from by grabbing the character to draw from the string and search for that letter in the letters property. If the letter is not found we assume a space character which simply skips 32*scale pixels. The width and height of the bounding box is multiplied by the scale to figure out the size to draw the letter with the scaled width also used to figure out where the next letter will be drawn.
function drawBloodText(ctx, s, x, y, scale) { var cur_x = x for (var cntr = 0; cntr < s.length; ++cntr) { letter = HalloweenLichAtlas.letters.indexOf(s.charAt(cntr)); if (letter < 0) cur_x = cur_x + 32 * scale; else { r = HalloweenLichAtlas.bloodLetters[letter]; ctx.drawImage(HalloweenLichImg, r.x, r.y, r.w, r.h, Math.floor(cur_x), y, Math.floor(r.w * scale), Math.floor(r.h * scale)); cur_x += Math.floor(r.w * scale) } } }
The energy ball is a special case as it is animated. This simply has the bounding box of the first frame followed by an array of points indicating the corner of the frames bounding box. As each frame is the same size, we can share the width and height properties.
As most of the energy Ball’s I’ll just focus on the core methods. The animation is controlled by the tick method. This is called every frame with the timestamp used to indicate the time in milliseconds when the frame started. In the constructor we created start_time and last_frame_time variables with the time the energy ball was instantiated. The frame to show is calculated assuming a playback rate of 30 frames per second with frames skipped if the frame-rate is lower than that. The time since the last frame is calculated to use as a delta for calculating the movement. Movement is calculated from the origin location towards a target location as well as starting size to ending size.
The actual drawing of the energy ball is done in the render method. This simply checks to see if the ball is still alive. If so it uses the coordinates as the center of the ball and calculates the bounding box of the ball by subtracting half the width from the x coordinate and half the height from the y coordinate. The source coordinates are simply taken from the atlas array using the current frame as the index.
Detecting if the energy ball has been clicked on is done by the checkIfDestroyed method. This creates a bounding box to check against the same way the render method did and then checks to see if the provided coordinates are inside this box. If they are then the energy ball is killed by setting alive to false.
tick(timestamp)
{
if (timestamp > this.ttl) {
this.alive = false;
return;
}
if (this.hitTarget) return;
this.curFrame = Math.floor((timestamp - this.start_time) / 33) % 20;
if (this.curFrame < 0) this.curFrame = 0;
var delta = (timestamp - this.last_frame_time) / 100.0//1000.0;
//console.log("timestamp " + timestamp + " d " + delta);
this.last_frame_time = timestamp;
var units = this.width / 20 + 1;
this.width += (units * delta);
this.height += (units * delta);
this.x += (units * this.velocity_x * delta);
this.y += (units * this.velocity_y * delta);
// check for reach target
if (this.width >= 200) {
this.hitTarget = true;
console.log("Energy ball reached target");
}
}
render(ctx) {
if ( ! this.alive ) return;
var bx = Math.floor(this.x - this.width / 2)
var by = Math.floor(this.y - this.height / 2)
var p = HalloweenLichAtlas.EnergyBall.frames[this.curFrame];
var spw = HalloweenLichAtlas.EnergyBall.w;
var sph = HalloweenLichAtlas.EnergyBall.h;
ctx.drawImage(HalloweenLichImg, p.x, p.y, spw,sph,
bx, by, Math.floor(this.width),Math.floor(this.height));
}
checkIfDestroyed(x, y) {
var x1 = Math.floor(this.x - this.width / 2)
var y1 = Math.floor(this.y - this.height / 2)
var x2 = x1 + this.width;
var y2 = y1 + this.height;
if ((x >= x1) && (x <= x2) && (y >= y1) && (y <= y2)) {
this.alive = false;
return true;
}
return false;
}
}
For larger Games, having a ScreenHandler class for switching between screens makes sense. For this game it is not really necessary but I didn’t know that until I had already implemented the bulk of the screens and it does simplify things. Screens all inherit from this skeleton class which has methods for mouse handling, ticks, and rendering as well as a reset method used when a screen is switched to. The reset method probably should be renamed onEnterScreen but as it is where the game is reset, I am leaving it as reset.
class ScreenHandler {
constructor(game) {
this.game = game;
}
restart() {
}
mouseDown(x, y) {
return false;
}
mouseMove(x, y) {
return false;
}
mouseUp(x, y) {
return false;
}
tick(timestamp) { }
render(ctx) { }
}
Screens are used in the game by instantiating all the screens and placing their references into an array. Each screen is assigned a constant that is used to reference it. HTML events trigger functions in the game class which simply pass the call to the current screen for handling. The switch screen method works by taking the current screen property and changing it to the indicated targeted screen by using the screen constant as an index into the screens array. It also calls the reset method.
class Game { // screen modes static MODE_LOADER = 0; static MODE_TITLE = 1; static MODE_GAME = 2; static MODE_LOSE = 3; static MODE_WIN = 4;
constructor() {
this.game_mode = Game.MODE_TITLE;
this.loading = new LoadingScreen(this);
this.game = new GameScreen(this);
this.title = new TitleScreen(this);
this.over = new LoseScreen(this);
this.win = new WinScreen(this);
this.curScreen = this.loading;
this.screens = [this.loading, this.title, this.game, this.over, this.win]
this.soundEnabled = true;
}
mouseDown(x, y) {return this.curScreen.mouseDown(x,y);}
mouseMove(x, y) {return this.curScreen.mouseMove(x,y);}
mouseUp(x, y) {return this.curScreen.mouseUp(x,y);}
tick(timestamp) {this.curScreen.tick(timestamp);}
render(ctx) {this.curScreen.render(ctx);}
switchScreen(mode) {
this.curScreen = this.screens[mode];
this.curScreen.restart();
}
setSound(enabled) {
this.soundEnabled = enabled;
}
playExplosionSound() {
if (this.soundEnabled)
HalloweenLicheExplosionSound.play();
}
playClickSound() {
if (this.soundEnabled)
HalloweenLicheClickSound.play();
}
playWinSound() {
if (this.soundEnabled)
HalloweenLicheWinSound.play();
}
}
Most of the screens are pretty straight forward with just some code for timing how long the screen is displayed and for drawing the display. I will not bother showing the code for these but will just quickly summarize the screens.
The loading screen checks to see if the assets have been loaded by checking the completed flag on the asset. Once completed, the title screen is called. While the assets are loading, we animate the word “Loading…” By drawing one letter at a time and looping the animation once all letters have been drawn.
The win screen simply draws a special message over the backdrop. Similarly, the lose screen draws an explosion over the backdrop and a message over the explosion.
The title screen is just like the other screens so far with the exception of also having to deal with buttons is required. We draw the backdrop, title text, and then the buttons in the render method. We also need to implement the mouse handling methods. These just call the equivalent event in the button class. Finally we need to implement the button handling method. As we assigned unique numbers to the buttons and when a button event is triggered this number is passed as a parameter, we simply look at this value and perform the appropriate action. These actions are simply switching screens for starting the game and toggling the sound state for the sound button.
<< Button handling code here>>
The game screen obviously is where we handle the game. This involves handling the liche, energy balls, player clicking, level progression and death detection. On construction we create an array of balls for tracking any energy ball’s that are active and place the liche at the center location. The reset method of this ScreenHandler simply calls the startLevel method which clears out the array of balls, sets a target location for the liche, sets the speed and number of balls for the level, and starts a delay timer for displaying the level text.
The tick method is where the game logic is handled. We start by cleaning up balls getting rid of balls at the end of the array (new balls are added to the beginning of the array for reasons that will be explained later) that are no longer alive. This is done to keek the array small without having to do costly slice operations and while miss some balls for a few frames is very effective as players tend to target closer balls which would be at the end of the list. We then loop through the balls and update them. If an energy ball has reached it’s target, we flag the game as over. The moveLich method is called to perform liche logic. If the game is over we play an explosion sound and switch to the game over screen. We check for a level win by seeing if there are no balls remaining and that the ball array is empty calling the startLevel method if the level has been cleared.
Moving the liche is simply moving the liche towards the target position and then checking if they have reached or passed the target picking a new target if they have. We then call handleLaunch.
While handleLaunch could have been merged with moveLich, it does have separate functionality so I prefer it as a separate method. We check if the time is past the next launch time and that there are still balls remaining. If so, we randomly pick a point on the screen and launch a ball by instantiating it with the liche position and target. This gets added to the beginning of the ball array so that it will be drawn before balls launched earlier so it will appear behind them. finally, we decrement the number of balls remaining.
The render method draws the background then the liche. We loop through any balls and draw them. As new drawings paint over top of what is on the screen, the ball array always adds new balls to the beginning of the list so proper drawing order is enforced. If the showLevelEnd time has not been passed we draw the current level text.
tick(timestamp) {
var removingBalls = true;
while ((this.balls.length >= 1) && (removingBalls)) {
if (this.balls[this.balls.length-1].alive) {
removingBalls = false;
} else {
this.balls.pop();
//console.log("removed ball from list - size now " + this.balls.length);
}
}
var lost = false;
for (var cntr = 0; cntr < this.balls.length; ++cntr) {
this.balls[cntr].tick(timestamp);
if (this.balls[cntr].hitTarget)
lost = true;
}
this.moveLich(timestamp);
if (lost) {
this.game.playExplosionSound();
this.game.switchScreen(Game.MODE_LOSE);
}
if ((this.balls.length <= 0) && (this.ballsRemaining <= 0))
this.startLevel(this.currentLevel + 1);
}
render(ctx) {
var r = HalloweenLichAtlas.Backdrop;
ctx.drawImage(HalloweenLichImg, r.x, r.y, r.w,r.h, 0, 0, 640,480);
ctx.drawImage(HalloweenLichImg, this.atlasLich.x, this.atlasLich.y, this.atlasLich.w,this.atlasLich.h,
Math.floor(this.liche.x), this.liche.y, 58,82);
for (var cntr = 0; cntr < this.balls.length; ++cntr) {
this.balls[cntr].render(ctx);
}
if (performance.now() < this.showLevelEnd)
drawBloodText(ctx, "LEVEL " + this.currentLevel, 200,150, 1);
}
moveLich(timestamp) {
//console.log("should move liche");
var delta = (timestamp - this.lastTick) / 1000.0;
this.lastTick = timestamp;
var reachedTarget = false;
if (this.licheTarget < this.liche.x) {
this.liche.x -= (this.licheSpeed * delta);
reachedTarget = this.licheTarget >= this.liche.x;
} else {
this.liche.x += (this.licheSpeed * delta);
reachedTarget = this.licheTarget <= this.liche.x;
}
if (reachedTarget)
{
this.licheTarget = Math.random() * 620;
}
this.handleLaunch(timestamp);
}
startLevel(lvl) {
while (this.balls.length > 0) this.balls.pop();
this.currentLevel = lvl;
this.licheTarget = 320;
this.licheSpeed = lvl * 25;
this.chanceLaunch = lvl * 5;
this.ballsRemaining = lvl * 5 + 5;
this.launchDelay = 500 - 40 * lvl;
if (lvl >= 3/*14*/) {
this.game.playWinSound();
this.game.switchScreen(Game.MODE_WIN);
}
this.showLevelEnd = performance.now() + 3000;
this.nextLaunch = this.showLevelEnd + this.launchDelay;
}
handleLaunch(timestamp) {
// first see if can launch
if (timestamp < this.nextLaunch) return;
if (this.ballsRemaining <= 0) return;
var bx = Math.random() * (640 - 40) + 20;
var by = Math.random() * (480 - 40) + 20;
var ball = new EnergyBall(this.liche.x+5,this.liche.y+10, bx,by);
this.balls.unshift(ball);
--this.ballsRemaining;
this.nextLaunch = Math.random() * this.launchDelay * 3 + this.launchDelay + timestamp;
console.log("Next launch at " + this.nextLaunch);
}
The HTML page holds the canvas and converts mouse events into x,y coordinates that we can use. This is all pretty straight forward so I won’t bother with that code.
Overall, this was a pretty simple game but there is a lot of room for improvement in the rendering code. We are always redrawing the entire screen each frame so simply detecting when the screen needs to be redrawn and doing the drawing only when necessary would improve performance.