Basic Platformer With Javascript and HTML
h
LucHutton (177)

BASIC PLATFORMER TUTORIAL

End Result: https://basic-platformer--luchutton.repl.co/
The Code: https://repl.it/@LucHutton/Basic-Platformer

Prerequisites And Advice:

For this tutorial you should already have a basic understanding of programming (preferrably in Javascript) as I will not be explaining basic Javascript concepts in this tutorial. If you wish to learn some/more Javascript I recommend you visit: https://developer.mozilla.org/en-US/docs/Web/JavaScript

Setup:

It is best if you follow along using a HTML + CSS + JS REPL, as when created the REPL will already have all the boiler plate stuff, but also because you will be able to see the result straight away on the same screen (when you run the code).

The first (and most important) line of code you will write will be in the index.html file. Don't worry this is the only bit of HTML you will have to write :).
Place the line of code after the opening body tag but before the script tag, like so:

<body>
  
  <canvas id="canvas" width="512" height="512"></canvas>
                                          
  <script src="script.js"></script>
</body>

Make sure the id of the canvas is "canvas", but you can change the width and height to whatever number you want as long as the width and height are multiples of 32!


Navigate yourself to the script.js file where we will be spending the remainder of this tutorial. The first thing that we need to do is to create a place in which we can store the attributes of the player, this will be done using an Object. For starters our player needs to have an X coordinate, a Y coordinate, a Width and a Height. For simplicity's sake the player will be a 32 x 32 pixel square, and the initial X will be set to half the canvas width, and Y will be set to half the canvas height.

  const player = {
    x: 256,
    y: 256,
    width: 32,
    height: 32
  }

Make sure the final value doesn't have a comma after it!
At the top of the Javascript file we need to create a reference to the canvas element we created in the first step like so:

const c = document.getElementById("canvas").getContext("2d");

It basically gets the canvas element -> specifys that we will be working in two dimensions -> sets the value of c to that.

Now we need a way to draw the player at its location with its size. To do this we will create a draw function, inside it we will need to specify the fill colour of the player and how to draw the player.
To specify the colour to fill the next object drawn, you update the canvas' fill colour.

c.fillStyle = "red";

You can set the colour to whatever you want.
On the next line you need to actually draw the player rectangle using a canvas function called fillRect which draws and fills the rectangle at the same time. The fillRect function takes four parameters in the following order: an x value, a y value, a width and a height.

c.fillRect(player.x, player.y, player.width, player.height);

As shown above, you access the attributes of an object using the template: objectName.attribute.
To call this draw function: at the bottom of the Javascript file put:

draw();

If you followed the steps above correctly your Javascript code should look like this:

const c = document.getElementById("canvas").getContext("2d");

const player = {
    x: 256,
    y: 256,
    width: 32,
    height: 32
}

function draw(){
  c.fillStyle = "red";
  c.fillRect(player.x, player.y, player.width, player.height);
}

draw();

When you run this code you should see a red square appear somewhere in the top-left of your screen.
Now remove the line calling the draw function as it was only for testing.


Going Loopy

Every meaningful game needs to have a main function that is looped, to update and draw new things so fast that it is almost imperceptible to the human eye. For our game we need a main function that is called when all the updating for one frame is done. So create a function called main, and inside you can put a call to the draw function:

function main(){
  draw();
}

In this state the main function will only be called once, to fix this we need to put another line below the call to draw that re-runs the main function.

function main(){
  draw();
  requestAnimationFrame(main);
}

It basically allows the HTML to render what is drawn and then calls the main function again.


Startup

If you run the code you will notice that nothing happens, this is because nothing calls the main function in the first place. What we need is something that runs when the page is fully loaded, luckily Javascript has a function just for this:

window.onload = function(){
  main();
}

When the page loads, whatever is in the function will be executed, so if you run the code now you should now see the red square appear again!


Level Making And Drawing

We will be making this game's level using a tile map based system because it is easy to use and design. The main concept is that there will be a multi-line string with each line being a certain length long filled with either a 0 or a 1 (Air or a Wall). You can define the level variable like:

const level = `0000000000000000
0000000000000000
0010000000000000
0000000000001111
0000111000000000
0000000000011111
0000000000000000
0000000000111111
0000000000011000
1110000000000000
0000000010000110
0001111111100000
0000000000000000
0000000000000000
0000000001111110
0000000000000000`;

This gives us the ability to create multiple levels really easily. Feel free to tweak the positions of the 1s and 0s until you see fit.

Side Note: Levels should really be defined in external text files, but file handling would distract us too much from the making of the game so I decided just to define it in the Javascript.

Before being able to use this level data we need to make a function that parses it into a 2D Array (Arrays within arrays (but only to one layer)). We need to:

  • Split the string up by line
  • Split each line up by character
  • Return that result
    To split by line and store the result in an array we can do:
const lines = lvl.split("\n");

Where lvl is the level data that we pass to the function.
To split each line in the array by characters we need to use the map function (Introduced in ES5).

const characters = lines.map(l => l.split(""));

If you are unsure on how to use the map function refer to this.
The final level-parsing function should look like:

function parse(lvl){
  const lines = lvl.split("\n");
  const characters = lines.map(l => l.split(""));
  return characters;
}

To make the level data accessible to all of the program we need to define a variable in the global scope, at the top of the Javascript file, below the c variable, write:

let currentLevel;

This just tells Javascript that we want this variable to be declared but that we don't need to use it yet.
In the window.onload function update the value of currentLevel to the return value of the parse function:

window.onload = function(){
  currentLevel = parse(level);
  main();
}

Now all we need to do is draw the level!
In the draw function we need to loop through each of the level data lines and on each line we need to loop through each character, and if the character is a 1 then we need to draw a square at the position on canvas relative to the character's position. Also before the looping we need to define a new colour for walls!

function draw(){
  c.fillStyle = "red";
  c.fillRect(player.x, player.y, player.width, player.height);
  c.fillStyle = "black";
  for (let row = 0; row < currentLevel.length; row++) {
    for (let col = 0; col < currentLevel[0].length; col++) {
      if (currentLevel[row][col] === "1") {
        c.fillRect(col * 32, row * 32, 32, 32);
      }
    }
  }
}

Here we are using that fillRect function again, but instead of the player we are drawing each wall tile.
If you now run the code you should see squares that match what is written in the level variable.


Handling User Input

Our game right now is pretty boring as there is nothing to do, only stare at the beautifully arranged squares that should now populate you screen. To fix this we need to add a way to listen for keyboard input and move the player based on which key is pressed. At the top of the Javascript file write the line:

let keysDown = {};

This is an empty object which will hold the keys currently pressed. If you are wondering why we need to store current keys down it is because this way allows multiple keys to be pressed at once.
To listen for key presses, and then store that key press in thekeysDown variable we will use an eventListener:

addEventListener("keydown", function(event){
  keysDown[event.keyCode] = true;
});

This function executes whenever the user presses a key on their keyboard, the key pressed information is stored in the event parameter, it then sets the key to true in keysDown. Now we need a similar function that executes when a key is released:

addEventListener("keyup", function(event){
  delete keysDown[event.keyCode];
});

It removes the entry of the released key from keysDown.


To detect and act upon keys that are pressed we need to create an input function that checks whether certain keys are pressed.

function input(){
  if(65 in keysDown){
    player.x -= 3;
  }
  
  if(68 in keysDown){
    player.x += 3;
  }
}

In Javascript keys are represented numerically, in this example 65 is the number for the "A" key and 68 is the number for the "D" key, if you wish to find out the keycodes for other keys you can use this website.
In main you need to call this function so that input is checked every time main is executed. If you run the code you should see that when you press "A" or "D" the player moves horizontally, but you may also notice that the red square's "previous states" are still visible, to fix this write the following line at the top of the draw function:

c.clearRect(0, 0, canvas.width, canvas.height);

Horizontal Collisions

When moving the red square you will notice that it passes straight through walls, this is because we haven't defined any collisions yet! But before we do collisions we need to create a function called getTile that gets the tile at a certain X and Y value:

function getTile(x,y){
  return(currentLevel[Math.floor(y / 32)][Math.floor(x / 32)]);
}

To start collisions we need to define another player attribute called speed, this will determine how fast the player can move horizontally:

const player = {
  x: 256,
  y: 256,
  width: 32,
  height: 32,
  speed: 3
}

Now move back to the input function and add two if statements around the updating of the player.x value, and update the previous code to add player.speed instead of 3:

function input(){
  if(65 in keysDown){
    if (getTile((player.x - player.speed) + 1, player.y + 16) !== "1") {
      player.x -= 3;
    }
  }
  
  if(68 in keysDown){
    if (getTile(((player.x + player.width) + player.speed) - 1, player.y + 16) !== "1") {
      player.x += 3;
    }
  }
}

This checks whether the tile, when the player will move by player.speed, at the player's location is a wall or air, if it is air then the player is allowed to move in the direction, else do nothing.
If you now run the code, the red square should stop whenever it hits a wall.


Gravity

This section will involve a little bit of physics, if you are unaware of how gravity and jumping works you can look at this website, but note that this knowledge isn't required to follow along the next section (it just clarifies things).

Firstly we need to update the player object with three attributes required for calculating Gravitational Potential Energy, they are:

  • Mass (mass)
  • Kinetic Energy On The Y-Axis (yke)
  • Gravitational Potential Energy (gpe)
const player = {
  x: 256,
  y: 256,
  width: 32,
  height: 32,
  speed: 3,
  mass: 64,
  yke: 0,
  gpe: 0
}

Now we need to create a function to update the player's Y position, the player's Y Kinetic Energy and the player's GPE, but first we will create a function that takes the player as a parameter and calculates the GPE of it. GPE = mass 9.8 height. As we will be working in pixels instead of meters we will need to divide the Gravitational Field Strength (9.8) by a million so it scales correctly, and as (0,0) on canvas is the top left we need to take the player's Y value from the height of the canvas (512 in my case), and finally so the GPE doesn't increase so quickly per pixel we will divide the player's 'height' by 32.

function calcGPE(obj) {
  return obj.mass * (9.8 / 1000000) * ((canvas.height - obj.height) - (obj.y / 32));
}

Now that that is out of the way we can create a function called gravity that takes the player as a parameter and:
1. Takes yke away from y
2. Takes GPE away from yke
3. Recalculates GPE

function gravity(obj){
  obj.y -= obj.yke;
  obj.yke -= obj.gpe;
  obj.gpe = calcGPE(obj);
}

Now add a call to gravity in the main function and pass player to it. Now if you run the code you should see that the red square now falls off the screen in a realistic manner.


Vertical Collisions

When our red square falls, it goes straight through the walls/floors, to fix this we need to add some more code to the gravity function. Firstly we will check for downwards collisions (landing on the floor), to do this we need to utilise the check squares function to see if the tile at the player's feet is a wall, if yes then we need to set the player's Y Kinetic Energy to 0 and move the player up until it looks seamless with the floor. Also as a preventative measure against future bugs we will need to check that the player is actually falling (Y Kinetic Energy is less than 0).

// Place Below Previous Code Written
if (getTile(obj.x + 32, (obj.y + 32)) !== "0" || getTile(obj.x, (obj.y + 32)) !== "0") {
      if (obj.yke <= 0){
        obj.yke = 0;
        obj.y -= (obj.y % 32);
      }
    }

Now we need to do the same for upwards collisions, and also we should only check one vertical collision if the other is false.

if (getTile(obj.x, obj.y) !== "0" || getTile(obj.x + 32, obj.y) !== "0") {
    if (obj.yke >= 0){
    obj.yke = -0.5;
    obj.y += 1;
    }
}

This does almost the same thing except it sets the Y Kinetic Energy to a small negative number so that it doesn't get stuck on the wall above.
Your gravity function should now look like this:

function gravity(obj) {
  obj.y -= obj.yke;
  obj.yke -= obj.gpe;
  obj.gpe = calcGPE(obj);

  if (getTile(obj.x, obj.y) !== "0" || getTile(obj.x + 32, obj.y) !== "0") {
    if (obj.yke >= 0){
    obj.yke = -0.5;
    obj.y += 1;
    }
  } else {
    if (getTile(obj.x + 32, (obj.y + 32)) !== "0" || getTile(obj.x, (obj.y + 32)) !== "0") {
      if (obj.yke <= 0){
        obj.yke = 0;
        obj.y -= (obj.y % 32);
      }
    }
  }
}

On running the code you can see that the red square stops when it hits a wall!


Jumping

Congratulations if you made it this far (this tutorial is very long...), this will be the final section before the basic platformer is complete! The final thing that we need to add is jumping. If we just move the player's Y value by a certain amount the jumping will look very unnatural, so instead we will increase the player's Y Kinetic Energy which gives it a nice realistic looking jump. The keycode for the "W" key (the jump key) is 87, so in the input function add a check for if 87 is in keysDown and if so increase the player's Y Kinetic Energy by however much you desire (5-8 is generally good), but to disallow the player to jump when directly below a wall we need to add a check to see if they are (and disallow jumping).

if (87 in keysDown && player.yke === 0) {
    if (getTile(player.x,player.y - 1) !== "1" && getTile(player.x + 32,player.y - 1) !== "1"){
    player.yke += 8;
    }
}

This also checks whether the player's Y Kinetic Energy is 0 (is on floor) to prevent mid-air jumping.
And now... if you run the code you should be able to jump!


Finished

Thank you for taking the time to read through this tutorial!
If you enjoyed this tutorial please give me an upvote!

by lucdadukey

You are viewing a single comment. View All