Black Magic
In computer programming a “Magic Number” is a number that has been hard coded into your program. Something like “myresults = 42 * myinput”.
This is a bad thing.
When your program needs a specific hard coded number (like the speed of light or the boiling point of water) you should put that number into a well named variable or constant and then use that name throughout your program.
Now let’s take a look at some common magic number problems, why they are bad and how to solve them with named variables.
First magic number problem: They can be hard to understand. Just looking at a number isn’t enough to tell you what it’s for, which can make reading code very hard.
//These magic numbers make it impossible to tell what this program is doing
function estimateFinalPrice(originalPrice){
return originalPrice * 0.9 * 1.25 + 5.25;
}
//Switching the magic numbers to well named variables makes the function
//easy to understand
function estimateFinalPrice(originalPrice){
var DISCOUNT = 0.1;
var TAX = 0.25;
var SHIPPING = 5.25;
return originalPrice * (1 – DISCOUNT) * (1 + TAX) + SHIPPING;
}
Second magic number problem: they can be hard to tell apart. Imagine having to update a program with the same number hard coded in multiple places. Do all the numbers represent the same thing, or are they different things that just happen to have the same value? Should you update all the numbers, or only some of them?
//This function has three identical magic numbers... but are they all
//the same thing?
function getVacationPrice(guests){
var meals = getMealCost(3*guests);
var parkPass = getParkCost(3*guests);
var room = getRoomCost(3*guests);
return meals + parkPass + room;
}
//Using well named variables make it obvious we are dealing with two different
//numbers that just happen to be the same... for now.
function getVacationPrice(guests){
var VACATION_DAYS=3;
var NUMBER_OF_PARKS=3;
var meals = getMealCost(VACATION_DAYS * guests);
var parkPass = getParkCost(NUMBER_OF_PARKS * guests);
var room = getRoomCost(VACATION_DAYS * guests);
return meals + parkPass + room;
}
Final magic number problem: They are hard to maintain. If one of your magic numbers needs to be updated you have to track down every place it appears in code, make sure it really is the number you think it is and then change it. That’s a lot of work for just doing something like changing tax rates.
Sure, you might think you could just use a find and replace but that’s a really bad idea. Try to replace “7” with “10” and you’ve also accidentally turned “17” into “110”. Or maybe you have two different magic number 7s and you only want to change one of them. Can’t do that with a simple find and replace.
//Good luck maintaining this thing! Forget to update one number and your
//program is broken
function tooMuchMagic(){
var a = 7 * 7 * 24;
var b = 7 – 24 * 7;
return a * b / 7 +24;
}
//This is much better. You can just change one variable definition and
//everything automatically updates
// Note, this is not real physics (as far as I know)
function noMagic(){
var WAVELENGTH = 7;
var FREQUENCY = 24;
var a = WAVELENGTH * WAVELENGTH * FREQUENCY;
var b = WAVELENGTH – FREQUENCY * WAVELENGTH;
return a * b / WAVELENGTH – FREQUENCY;
}
Some Numbers Aren’t Magic
Before you go off and start adding constants to all your code we should admit that there are a couple circumstances where hard coding a number into your program is OK.
The basic rule of thumb is: If the number is small, unlikely to change and it’s obvious what it’s for then hard coding it is OK.
One big example is iterators. You start with a number and then add 1 to it until you reach some final goal like the end of an array. This is a pattern that everybody is familiar with so hard coding values is fine.
// Using a named variable for an iterator is overkill and ugly
var ITERATION_STEP = 1;
var ITERATION_START = 0;
var GOAL = 10;
var iterator = ITERATION_START;
while(iterator < GOAL){
iterator = iterator + ITERATION_STEP;
}
//We all know how iterators work. Hard coding numbers is just fine
var GOAL = 10;
var iterator = 0;
while (iterator < GOAL){
iterator = iterator + 1;
}
Other places that hard coded numbers are usually okay:
- Simple array offsets, like getting position 0 from an array or using -1 to look at the last item.
- Extremely well known facts like 1000 milliseconds in a second
- Simple math, like doubling a number or raising something to the second power.
- Inside math functions. Things like the quadratic equation are well known and don’t change, so hard coding them is fine. You can just type in “4”, no need to come up with a “QUADRATIC_EQUATION_CONSTANT_2” variable.
Time For A Witch Hunt
Now that we’re all up to speed on why magic numbers are bad it’s time to clean up Defrag Cycle. During early development and testing I hard coded a ton of numbers, but now that we’re trying to fine tune the game it’d be really nice to move everything into clean global variables that can be safely tweaked.
This will also have the advantage of making the code easier to read. I’ll probably even be able to get rid of some of my comments. No need to say things like “We’re adding 400 here because that’s how tall the screen is” when we have an obvious variable like “SCREEN_HEIGHT”.
I will include a link to a fully cleaned up version of the code at the end of this article to make up for the fact that I’m not going to show every single change I make.
Let The Hunt Begin
Looking at the top of our file we’re already using well named constants for platform generation, virus generation and the number of points needed to win the game. So there’s a handful of problems we don’t have to worry about.
Scrolling down almost a hundred lines we find our first magic number at the end of the gameLoop function: we hard coded in “50” for the number of milliseconds per frame. Even worse, we hard coded it in two separate places. And worst of all we later use a hard coded “20 frames per second” (the inverse of 50 milliseconds per frame) to calculate the display timer way down in both drawScreen and drawWinScreen.
Let’s fix that by creating a “MILLISECONDS_PER_FRAME” variable and inserting it into all three functions. Now we can speed up and slow down the game by just changing one global variable.
Next problem area is updateGame, and boy is it a problem area! We’ve hard coded how often the player gets points, how fast the player moves, how fast the player jumps, how fast the viruses and platforms move and included several hard copies of things like the size of the screen and the size of the player. There’s a lot here to fix.
First off is creating a FRAMES_PER_POINT constant and using that to calculate how often the player gets points. We’ll also want to use this constant down in the graze calculation area. Remember that we want the graze points to be awarded in between normal points, so instead of checking whether or not the currentFrameCount is evenly divisible by FRAMES_PER_POINT we’re going to check to see if there is a remainder equal to the halfway point between normal frames.
//Award bonus points if the player is grazing a virus
//Offset bonus point award frame from normal award frame
//This makes the point counter update twice as fast during a graze
if(grazeCollision){
if(currentFrameCount % FRAMES_PER_POINT == Math.floor(FRAMES_PER_POINT/2)){
currentPoints++;
}
}
Math.floor is important for those cases where FRAMES_PER_POINT is odd. If FRAMES_PER_POINT is something like “5” the halfway point will be “2.5”. But since currentFrameCount and FRAMES_PER_POINT are both whole numbers you will never get a “2.5” remainder. You have to round down to “2” or the bonus points would never be awarded.
Next up let’s replace all those hard references to movement speeds:
var PLAYER_SPEED = 5; //The player's horizontal movement speed
var PLAYER_JUMP_SPEED = 15; //The upwards speed of the player's jump
var GRAVITY = 1; //How much the player accelerates downward per frame
var TERMINAL_VELOCITY = 15; //The maximum speed the player can fall
var PLATFORM_SPEED = 5; //How fast platforms move to the left
var VIRUS_SPEED = 5; //How fast the enemy viruses' move to the left
Once you’re done inserting all these speed constants into your code we’ll move on to removing hard references to screen and player size:
var SCREEN_HEIGHT = 400;
var SCREEN_WIDTH = 600;
var PLAYER_HEIGHT = 48;
var PLAYER_WIDTH = 75;
We can use the screen variables for drawing the background at the beginning of every draw function, for helping to check when the player is out of boundaries and to check when the player has fallen off the screen. Now we can adjust the size of the game screen by changing two variables instead of having to individually update multiple draw functions and boundary checks.
Player size constants are even more useful and can be used for checking screen boundaries, aligning the player with platforms and calculating hit boxes. For example, our new getFeetHitbox function looks like this:
// Get a 5 pixel tall hitbox all along the bottom of the player sprite.
// Used for detecting when the player has landed on a platform
function getFeetHitbox(xPos, yPos){
var feetHitbox = new Object();
feetHitbox.x = xPos;
feetHitbox.y = yPos+PLAYER_HEIGHT-5;
feetHitbox.width = PLAYER_WIDTH;
feetHitbox.height = 5;
return feetHitbox
}
This function will reliably create a hitbox all along the bottom of the player even if we change the size and shape of the player. Much better than the old version which only worked for player sprites that were exactly 48×75 pixels.
Notice that we still have a hard coded “5” inside this function. We’re going to just leave that there. It is never used outside this function, so maintenance isn’t a concern. Plus the function comment makes it very obvious what the 5 is for. So this one isn’t really a bad magic number.
The other hitbox functions also have local semi-magic numbers that I’m not going to remove for the same reasons.
Moving right along it looks like I’m still hard coding the height of new platforms along with the default spawning location of viruses and platforms. So let’s toss those into constants.
Let’s see… what’s left? Player spawn location is hard coded but that only shows up in one place and is easy to understand so we’ll leave that alone. The game intro, game over and game win screens all have hard coded drawing co-ordinates but I think that’s OK because it’s obvious they’re being used to arrange a specific screen and the co-ordinates are function specific and never reused.
So I guess we’re just about… wait! There’s still one last big problem.
Derived Constants
In our new platform generating code we have a constant “100” that represents how high the player can reasonably jump. We use this to set the minimum possible y coordinate that a new platform can have (it should never be so high that the player can’t jump to it).
You might think this is an easy fix. Just throw in a “MAX_JUMP_HEIGHT=100” and call it a day.
But our calculated safe jump height of 100 only holds true when the player can jump at 15 pixels per frame and falls at one pixel per frame per frame. If jump speed or gravity changes so will the player’s jumping height.
So we can’t just assign a number to the MAX_JUMP_HEIGHT constant. Instead we’re going to need a function that can calculate a max jump height based on the player speed and gravity constants.
It’s not hard to write though. We start by dividing jump speed by gravity to find out how many frames it takes for the player to stop moving upwards. Then we plug that into a basic distance equation, multiply by 0.9 to give the player a little breathing room on his jumps and use that for our constant.
// Calculate how high the player can jump
// Uses basic distance formulate: distance = velocity * time + 1/2 acceleration * time^2
function getMaxJumpHeight(jumpSpeed, gravity){
var framesOfMovement = jumpSpeed/gravity; //How long until gravity overpowers the jump
var maxJumpHeight = jumpSpeed * framesOfMovement - gravity/2 * framesOfMovement *framesOfMovement;
return Math.floor(maxJumpHeight * 0.9); //Give the player a little breathing room
}
And with that we can finally program in our MAX_JUMP_HEIGHT constant
var MAX_JUMP_HEIGHT = getMaxJumpHeight(PLAYER_JUMP_SPEED, GRAVITY);
And with that we can slay our last magic number
//Platform can't be higher a jump from the current platform or 100 pixels from top of screen
var minPossibleHeight = Math.max(platforms[platforms.length-2].y - MAX_JUMP_HEIGHT, 100);
I’ll be keeping that other 100 since the comment makes it obvious what it’s for and it’s only ever used in this line.
See All The Changes
Here’s the latest copy of the magic free code.
With that out of the way we can start working on the finishing touches for the game: Music, sound effects and special effects.