Let’s Program A JavaScript Game 15: The Computer Is Out To Get You

Game AI!!!

In computer science we use the term “AI” or “artificial intelligence” to refer to sophisticated problem solving programs that use fairly advanced algorithms to process and learn from data.

But in the gaming world we tend to use the term “AI” to mean “however the computer makes decisions whether it’s intelligent or not”. An enemy that does nothing but walk around in circles and shoot bullets obviously isn’t processing or learning from data but most people would call it an “enemy AI” anyways.

So a lot of “Game AI” isn’t actually “Computer Science AI”.

But that’s a good thing! “Computer Science AI” is designed to solve math problems. “Game AI” is designed to make games more fun. Since they have different objectives it’s no surprise that they usually have completely different code.

Sure, sometimes making a game more fun does involve inventing a “Computer Science AI” that can do things like analyze and coordinate hundreds of units moving through a changing battlefield.

But you’d be surprised how often you can make a game feel fun and smart with nothing more than a stupid random number generator and half a dozen “if” statements.

Which is a a roundabout way of me admitting that our game AI is going to be pretty stupid and random. So if you were expecting something clever, I’m sorry. You’ll just have to go back and reread the swarm intelligence series.

Requirements

So what are the goals for our game AI? Here’s what I came up with:

  1. New platforms should sometimes be higher or lower than the platform before them
  2. Platforms should never spawn lower than the bottom of the screen or higher than the top
  3. Platforms should get smaller and further apart as the game goes on
  4. Platforms need to always be reachable by a single jump. No game-breaking impossible layouts allowed.
  5. Viruses should appear at random locations instead of following a pattern

Leap Of Faith

Let’s tackle the platform problem first. As a reminder our current platform generating code is in the updateGame function and looks like this.

//Remove platforms that have scrolled off screen and create new platforms to take their place
if(platforms[0].x + platforms[0].width < 0){
   //Drop first platform by shift entire list one space left
   for( i = 0; i < platforms.length-1; i++){
      platforms[i] = platforms[i+1];
   }

   //Add new platfrom to end of list by placing it a certain distance after second to last platform
   var newPlatform = new Object();
   newPlatform.x = platforms[platforms.length-2].x + maxPlatformLength + maxPlatformGap;
   newPlatform.y = 350;
   newPlatform.width = maxPlatformLength;
   newPlatform.height = 20;
   platforms[platforms.length-1] = newPlatform;
}

First up is adding some randomness to the height of the platforms, which is actually a trickier problem than you might think. Sure, we could just use a random integer to set the “y” value of every new platform but then we run a huge risk of creating platforms the player can’t actually jump to or creating platforms that are too easy to jump to.

The issue is that when a platform is higher than the player they have less time to reach it than usual before falling to their doom, while if the platform is beneath the player they have more time than usual to make the jump.

More specifically, if you remember back to part 9 we calculated that it took thirty frames for the player to go from on the ground, to jumping to being back on the ground.

But if a platform is at the top of the player’s jump he will only have half that much time, fifteen frames, to reach it before he has started moving downwards and is now stuck below the new platform.

On the other hand if the platform is below the player’s starting position he will have the full thirty frames plus another dozen or so frames of falling before he’s gone too far.

So we can’t just use the same set horizontal distance between platforms of different heights. Platforms above the player need to be closer together. Platforms below the player need to be further apart.

To pull this off do we’re going to write a math function that can calculate how many frames it takes for the player to jump from one height to another. For this to work we need to know:

  • height difference
  • jump velocity
  • y acceleration per frame

Now if you’ll remember back to physics the equation for movement looks a lot like this:

end height = start height + velocity * time + ½ acceleration * time^2

0 = start height – end height + velocity*time + ½ acceleration * time^2

0 = change in height + velocity*time + ½ acceleration * time^2

And from there we can use the quadratic equation to solve for time. Just throw this function anywhere in your script:

// Calculate how many frames a jumping player will be in the air
// Based on the vertical distance they have to cover, their jumping vertical velocity
// and their vertical acceleration
// This is basically the physics distance formula plugged into the quadratic equation
function getFramesUntilLanding(dist, vel, acc){
   var a = (-1 * vel + Math.sqrt(vel*vel - 2 * acc * dist))/(acc);
   var b = (-1 * vel - Math.sqrt(vel*vel - 2 * acc * dist))/(acc);
   return Math.max(a, b);
}

Remember that the quadratic equation usually has two answers, which makes sense for a jump. A player will be five pixels off the ground both right after he jumps and right before he lands. We are only interested in how long it takes to land at a certain height so we take the max of the two values.

Another thing to watch out for here is that certain inputs to this function will return zero answers. Specifically, if the player is only moving fast enough to jump 150 pixels upwards and you ask how long it will take him to jump 300 pixels the answer is going to be “NaN”, the special JavaScript value for “not a number”. That’s what you get when you try to do impossible math.

To avoid this we need to know how high our player can jump so we can avoid asking for anything bigger than that. Looking at the code in updateGame we can see that starting jump velocity is 15 pixels per frame and that acceleration is 1 pixel per frame. That means that the player will keep moving upwards for 15 frames. Plugging that into our handy movement equation shows:

max jump height = 15 pixels/frame * 15 frames – ½ pixels/frame^2 * (15 frames)^2

max jump height = 112 pixels.

To give ourselves a little wiggle room we’ll round down and take 100 pixels as the max jump height. This means that a new platform should never be more than 100 pixels higher than where the player started.

As for platforms below the player, there’s really no limit as long as they’re still on the screen. Since gravity works downwards there’s no reason the player couldn’t safely jump or fall to a platform hundreds of pixels beneath them.

Prepare For Code Dump

With all that math and planning out of the way I was able to update our platform code. Just replace the platform generating code from the last section with this shiny new version:

//Remove platforms that have scrolled off screen and create new platforms to take their place
if(platforms[0].x + platforms[0].width < 0){
   //Drop first platform by shift entire list one space left
   for( i = 0; i < platforms.length-1; i++){
      platforms[i] = platforms[i+1];
   }

   // Add new platfrom to end of list
   // Platform is randomly placed somewhere within the jump range of the player
   // As the game goes on platforms get smaller and farther apart
   // Calculated max height of jump is about 100
   // Platform can't be higher than the max jump or 100 pixels from top of screen
   var minPossibleHeight = Math.max(platforms[platforms.length-2].y - 100, 100);

   //Platform can't be lower than the bottom of the screen
   var maxPossibleHeight = 380;

   //Random height between the max and min
   var newPlatformHeight = Math.random()*(maxPossibleHeight - minPossibleHeight) + minPossibleHeight;
 
   //Calculate how far the platform can be by multiplying speed of 10 by the time the player will be in the air
   var maxPossibleDistance = 10 * getFramesUntilLanding(newPlatformHeight - platforms[platforms.length-2].y, 15, -1);
   
   var newPlatform = new Object();
   //As the player earns points the platforms should get closer to being maxPossibleDistance apart
   newPlatform.x = platforms[platforms.length-2].x +
                   platforms[platforms.length-2].width +
                   maxPossibleDistance*(0.5 + 0.5 * currentPoints/TOTAL_POINTS_TO_WIN);
   newPlatform.y = newPlatformHeight;
   //As the player earns points the platforms should get smaller
   newPlatform.width = maxPlatformLength *(1 - 0.5 * currentPoints/TOTAL_POINTS_TO_WIN);
   newPlatform.height = 20;
   platforms[platforms.length-1] = newPlatform;
}

I think the code comments explain everything pretty well, so I’m just going to hit the highlights here.

Just like before we start by shifting the entire list left to make way for the new platform.

We then calculate where the next platform should go by choosing a random number between the min and max possible heights. Remember that in screen coordinates the top of the screen is y=0 and it gets bigger as you go down, which is why we find the min by taking the players location and subtracting his 100 pixel jump height and why the max is slightly smaller than the height of the screen.

Once we know how high the next platform is going to be we figure out how far away it can be by using our handy jump frame function to find out how many frames it will take to jump that high and then multiplying by ten, which is the players speed of 5 pixels per frame PLUS and extra 5 pixels per frame from the fact that the entire screen is moving backwards. This tells us exactly how far forward the player can move during his jump to the new platform.

Now that we have a height and know exactly how far the player can jump we’re ready to build a new platform. Height just gets slotted in but width and distance now get adjusted based on how close the player is to winning the game. For distance we start with gaps that are 50% the maximum jump distance and slowly increase them by another 50% as the player earns points. Platform size is the opposite, starting at 100% of max size and decreasing by up to 50% as the player moves.

Now I will admit that a lot of this is bad code. Even with the comments there are a lot of hard to understand hard-coded numbers floating around. But at this point we’re still prototyping and a certain amount of messy experimentation is OK. We’ll clean this up a little later.

Simple But Vicious Predators

After all the math involved in fixing up platform generation you’ll be happy to hear we can make the virus spawn pattern work pretty decently by changing just one line in the virus generation code:

newVirus.y = 300;

to this:

newVirus.y = player.y -50 + 100*Math.random();

This guarantees that new viruses will spawn in the general vicinity of the player with just a little bit of jitter to hide what we’re doing. And since the player should be randomly jumping all over the place this should make the viruses also appear all over the place.

Congrats! The Core Gameplay Is Done Now

We have a player that can move and jump.

We have platforms that spawn randomly and become smaller and further apart as the game progresses.

We have viruses that semi-intelligently place themselves in the heroes path.

We have a score system that lets the player win.

So we’re basically done with the actual “game” portion of this project.

So what’s left? A surprisingly large amount of stuff!

First, this prototype code badly needs to be cleaned up. And then we’ll take a few more sessions to add some special effects, music and basic animations.

Then, and only then, will we truly be done.