This Is Only The Beginning…
Our code so far makes a good proof of concept. We’ve shown that we can indeed move the player around, land on platforms, crash into enemies and switch between different game states. In other words, we can safely say that it is possible to build our game using JavaScript.
But our code also has a lot of problems, glitches and loose ends. So today we’re going to be cleaning those up and starting the transition from “Semi-functional Prototype” to “Product You Wouldn’t Be Embarrassed To Show Your Friends”.
As part of this we’re going to be rewriting a TON of code. But don’t worry, I’ll post a link to a live version of code at the end of this article so if you have trouble following my description of what I’m doing you can just grab the complete code and take a look for yourself.
Wait! I’m Not Ready Yet
Here’s an interesting little fact for you: When you use JavaScript to assign an image to a variable the code doesn’t stop and wait, it keeps running and loads the image in the background. That means that your code might accidentally try to use an image that hasn’t finished loading yet, which is bad.
Now in a simple game with just a couple small images this probably isn’t a problem. But if you have a big background image or lots and lots of tiles and enemies and animations you might end up with a game that tries to run while 90% of its graphics are still being loaded.
To avoid this problem we’re going to have to tell our code specifically to not run any actual game logic until after the images are finished loading. An easy way to do this is with the onload property of the JavaScript image object. This lets you attach a function to an image and have that function run once the image finishes loading.
Using this we can chain together our images like this:
var cycleImage = new Image();
var virusImage = new Image();
function canvasTest(){
loadCycleImage();
}
function loadCycleImage(){
cycleImage = new Image();
cycleImage.onload=function(){loadVirusImage();};
cycleImage.src='cycle.png';
}
function loadVirusImage(){
virusImage = new Image();
virusImage.onload=function(){gameLoop();};
virusImage.src='virus.png';
}
Now canvasTest doesn’t immediately start the game but instead calls loadCycleImage. loadCycleImage loads cycle.png and then calls loadVirusImage, which load virus.png and then starts the game loop.
Now obviously the more images you have the longer this chain gets, which introduces two problems: First, a long chain is hard to maintain by hand. Second, a large chain leaves your game “frozen” for several minutes since nothing gets drawn to the screen until the chain has completed.
You can solve the code maintenance problem by writing a new recursive function that accepts an entire list of image objects and object URLs. It sets up the first image to loading and when it’s done it then calls itself with the rest of the lest. When the list is finally empty it calls gameLoop() instead. This lets you have one function and one loop instead of having to hand type a hundred different loading functions that chain into each other.
The “frozen” game problem is easy too. Just write some sort of drawResourceLoadingScreen screen function that displays a helpful message like “Now loading image X out of 45”. Call that method at the beginning of every step of the chain so the user knows why the game hasn’t started yet.
But since I’m only loading two images I’ll leave both of these tasks as exercises for the reader.
Initialization: One of Top Ten Most Computer Programmy Word Out There
Let’s continue with this theme of “Things we shouldn’t just leave at the top of our script” and talk about initializing game objects.
Before our games starts we obviously need to generate the player’s position, the starting platforms and the starting viruses. Currently we do that all up at the top of code.
But this means that our game only gets set up once, when the player loads the page. This is the reason why the player can get stuck inside a virus after a game over. Once the virus is in play it stays in play no matter how many times the player dies and respawns.
It would be much better if the game was reset every time the player switched from the start screen to the main game. Then instead of getting stuck inside a virus the player would get a fresh virus-free start when they died.
This is an easy problem to refactor. We just grab the setup code from the top of the script and stuff it into a function named initializeGame. Then we call initialize game right before we switch to the main game state.
The only possible trick is that it’s important to remember that global variables like players, platforms and viruses need to stay at the top of the script so our game functions can share them. We’re only moving the code that sets their values into a function, the declarations can stay put.
//Put the player in the starting location and generate the
//first platforms and enemies
function initializeGame(){
//Reset player location
player.x = 100;
player.y = 100;
player.yVel = 0;
player.onGround = false;
//Generate starting platforms into global array
//Uses global variables to control platfrom generation
for(i = 0; i < maxPlatformCount; i++){
var newPlatform = new Object();
newPlatform.x = i * (maxPlatformLength + maxPlatformGap);
newPlatform.y = 350;
newPlatform.width = maxPlatformLength;
newPlatform.height = 20;
platforms[i] = newPlatform;
}
//Generate starting viruses into global array (all off screen at first)
//Uses global variables to control virus generation
for(i = 0; i < maxVirusCount; i++){
var newVirus = new Object();
newVirus.x = virusStartingOffset + i * (virusWidth + maxVirusGap);
newVirus.y = 200;
newVirus.width = virusWidth;
newVirus.height = virusHeight;
viruses[i] = newVirus;
}
}
And then we just have to slip a reference to this code into our startScreenUpdate function:
//Check to see if the user is ready to start the game
function updateStartScreen(){
if(playerInput.up){
initializeGame();
gameState = STATE_RUNNING;
}
}
Side note: Initializing a game can take a long time, especially if you have to load new graphics or run an expensive algorithm. In these scenarios it’s considered polite to put up a “Loading” screen so the player knows the game isn’t stuck.
Since we’re only initializing a few dozen values this function will run too fast to notice, making a loading screen pointless. But if you were making a more complex game you might want to build a simple “Initializing next stage” screen drawing function and add it right before the initialize call.
Fixing A Few Minor Issues
That’s it for our big two problems, but as long as we’re disecting code let’s fix a few tiny bugs I noticed.
First off, the player shouldn’t be allowed to move off screen. So let’s add this code somewhere inside updateGame
//Keep the 75px wide player inside the bound of the 600px screen
if(player.x<0){
player.x=0;
}
if(player.x+75>600){
player.x=600-75;
}
Good programmers will notice that these functions use a crazy amount of magic numbers. It would probably be a good idea to replace them by constant variables like PLAYER_WIDTH and SCREEN_WIDTH.
Another quick issue: The player can currently do an air jump by falling off a platform and then hitting up. This is because we only set “onGround” to false when the player jumps. So let’s update our code to set that to false whenever the player starts moving downwards by changing this:
player.y += player.yVel;
player.yVel +=1;
if(player.yVel>=15){
player.yVel=15;
}
into this:
player.y += player.yVel;
player.yVel +=1;
//Assume player is no longer on groud. Platfrom collision later in loop can change this
player.onGround=false;
if(player.yVel>=15){
player.yVel=15;
}
The positioning of this code is very important! It comes AFTER the player tries to jump but BEFORE the platform collision code that sets onGround to true.
This means that if the player was on a platform at the end of last frame he can still jump because the onGround false code hasn’t kicked in yet.
It also means that if the player stays on a platform for the entire loop the onGround false code will be immediately reversed by the platform checking code that comes after it, so the player can still jump next frame even though onGround was temporarily false.
The only way for onGround to get set to false and stay false is if the player isn’t on a platform, which is exactly what we wanted. In fact, this works so well that you can remove the onGround=false from the up key handling code if you want. It’s redundant now.
As long as we’re changing things up, let’s have all viruses spawn at y=300 instead of y=200. This puts them at about the same level as the player and makes jumping over them easier during testing.
Finally, having to click a button just to start the game doesn’t make a lot of sense now that we have an actual Start Screen. So let’s rename canvasTest to startGame and link it to the loading of the page instead of a button press.
<body onload="startGame()" onkeydown="doKeyDown(event);" onkeyup="doKeyUp(event);">
<canvas id="gameCanvas" width=600 height=400 style="border:1px solid black;"></canvas>
</body>
The Code So Far
Here it is. Play it. Download it. Study it.
I Want To Be A Winner
With the game a little bit cleaner than before we’re all set to add in a scoring system and a win condition. Then we’ll be able to actually beat our game instead of just jumping over the same boring obstacles again and again until we get bored.