D E V L O G #3 - JS Modules & Basic Collision


So I was looking into making a basic form of collision engine, and have it that we can mark different objects as 'solid' for the purposes of collision.

First thing I did since last time is neaten up the current code and make it modular. From my full-time job I'm always in the habit of modulizing (word?) my code, so I've done the same here. I always use a very specific format for my JS modules that you may or may not have seen before:

var $Core = (function() {
  'use strict';
  // private stuff
  return {
    // public stuff
  }
}());

Essentially by putting it in a module we can reference anything in the returned public object from the "$Core" variable. Anything in the section above the return is private to that module, so stuff like variables and constants for that module in there. I usually use '$' for variable names that are global, and '_' for variables that are private.

Going forward I won't keep re-posting the whole module code for each module, I'll just be posting snippets, but just for this one as an example to give you an idea as to what all these modules will look like here's what the $Core looked like after I tidied up.

var $Core = (function() {
  'use strict';
  
  // room objects
  var _objects = [
    {x: 0, y: 0, width: 16, height: 16},
    {x: 16, y: 96, width: 16, height: 16},
    {x: 32, y: 48, width: 16, height: 16},
    {x: 0, y: 64, width: 16, height: 16},
  ];
  // player object
  var _player = {
    x: 96,
    y: 96,
    width: 16,
    height: 16,
  }
  // canvas
  var _canvas = document.getElementById('canvas');
  var _context = canvas.getContext('2d');
  // keys
  var _keys = {};
  return {
    // canvas render loop
    renderLoop: function() {
      _context.clearRect(0, 0, _canvas.clientWidth, _canvas.clientHeight);
      _context.fillStyle = '#0D2B45';
      _objects.forEach(function(o) {
        _context.fillRect(o.x, o.y, o.width, o.height);
      });
      if (_keys.ArrowLeft === true) _player.x -= 1;
      if (_keys.ArrowRight === true) _player.x += 1;
      if (_keys.ArrowUp === true) _player.y -= 1;
      if (_keys.ArrowDown === true) _player.y += 1;
      _context.fillStyle = '#D8BFD8';
      _context.fillRect(_player.x, _player.y, _player.width, _player.height);
    },
    // key press events
    keyEvent(ev, up) {
      console.log(ev.key + (up == true ? ' pressed' : ' released'));
      _keys[ev.key] = up;
    },
    // start game
    startGame: function() {
      document.addEventListener('keydown', function(ev) { $Core.keyEvent(ev, true); });
      document.addEventListener('keyup', function(ev) { $Core.keyEvent(ev, false); });
      setInterval($Core.renderLoop, 10);
    }
  }
}());
$Core.startGame();

You can see everything is now a lot more organised. Going forward as we work on specific functions, and they get more complicated I'll either split them out or make an entire new module for maintainability.

For example now we want to look at collision, we want to move the player movement into it's own function rather then having all that logic in the render loop. 

movePlayer: function() {
  if (_keys.ArrowLeft === true) _player.x -= 1;
  if (_keys.ArrowRight === true) _player.x += 1;
  if (_keys.ArrowUp === true) _player.y -= 1;
  if (_keys.ArrowDown === true) _player.y += 1;
}
startGame: function() {
  ...
  setInterval($Core.renderLoop, 10);
  setInterval($Core.movePlayer, 5);
}

As you can see this also means we can call this movement at a separate interval than the main render loop. This is so we can have the movement speed of the player independent, and we'll be able to play with it later down the line (slowing down the player with debuffs etc)

Currently we're just saying if the left key is pressed move the player left, but what we actually want to do is only move the player left as long as there are no solid objects in the way. To do this we'll need to have some code to check the next position of the player before we move it. To do that what we'll do is get the next position based on key input, check that position for any overlap against our objects, and then decide what to do based on what was found.

To make this easier I'm going to add a function to our objects that gives us the 'boundary' of the object. Later down the line we'll be able to use this to have boundaries independent of the actual sprite. Now I don't want to manually write that so we're going to make a new class for these objects, which I'm going to call "Entities".

// entity class
Entity: function(x, y, width, height) {
  return {
    x: x,
    y: y,
    width: width,
    height: height,
    boundary: function() {
      return {
        top: this.y,
        left: this.x,
        bottom: this.y + this.height,
        right: this.x + this.width
      }
    }
  }
}
// example
var entity = new $Factory.Entity(0, 0, 16, 16);

What we can then do is create a new Entity for each of the objects in our room, and it means they'll all inherit the properties we set here, in this case a boundary function which we can use to get the current boundary points. I've put this in a separate "$Factory" module, which I will use for any sort of class objects.

What we then want is to be able to get the boundary of an entity, but pass in some x and y co-ordinates to edit that boundary. That way we can not only get the boundary of say the player, but the boundary of the player if they moved 5 pixels to the left.

getBoundary: function(entity, deltaX, deltaY) {
  var boundary = entity.boundary();
  return {
    top: boundary.top + deltaY,
    left: boundary.left + deltaX,
    bottom: boundary.bottom + deltaY,
    right: boundary.right + deltaX
  }
},

Basically we're just getting the entity's current boundary, then adding any changes we want to it.

Then what we want to be able to do is for a given entity, check a list of other entities and see if there is an overlap in boundaries. Enter one of the most ridiculous things I've done so far:

checkBoundry: function(boundary, entities) {
  var colliders = [];
  // for each entity
  for (var i = 0; i < entities.length; i++) {
    var a = boundary;
    var b = entities[i].boundry();
    // check boundary agaisn't the entity
    if (a.right > b.left && a.right < b.right && a.top == b.top && a.bottom == b.bottom) colliders.push(boundries[i]); // left
    if (a.right > b.left && a.right < b.right && b.top > a.top && b.top < a.bottom) colliders.push(boundries[i]); // inner left
    if (a.bottom > b.top && a.bottom < b.bottom && a.right > b.left && a.right < b.right) colliders.push(boundries[i]); // top left 
    if (a.bottom > b.top && a.bottom < b.bottom && a.right == b.right && a.left == b.left) colliders.push(boundries[i]); // top
    if (a.bottom > b.top && a.bottom < b.bottom && b.left > a.left && b.left < a.right) colliders.push(boundries[i]); // inner top
    if (a.bottom > b.top && a.bottom < b.bottom && a.left > b.left && a.left < b.right) colliders.push(boundries[i]); // top right
    if (a.left > b.left && a.left < b.right && a.top == b.top && a.bottom == b.bottom) colliders.push(boundries[i]); // right
    if (a.left > b.left && a.left < b.right && b.top > a.top && b.top < a.bottom) colliders.push(boundries[i]); // inner right
    if (a.top > b.top && a.top < b.bottom && a.left > b.left && a.left < b.right) colliders.push(boundries[i]); // bottom right
    if (a.top > b.top && a.top < b.bottom && a.right == b.right && a.left == b.left) colliders.push(boundries[i]); // bottom
    if (a.top > b.top && a.top < b.bottom && b.left > a.left && b.left < a.right) colliders.push(boundries[i]); // inner bottom
    if (a.top > b.top && a.top < b.bottom && a.right > b.left && a.right < b.right) colliders.push(boundries[i]); // bottom left
  }
  // return any collisions
  return colliders;
}

Yes okay I know it's a lot, but what I realised is I can't do just a basic check as it won't cover all the possible directions. What we have here are the 8 possible directions (corners and sides) and then a further 4 checks in case somehow someone gets inside another entities.

This function can then be used to check a given boundary against a list of entities!

So going back to our movement function, what we can then do is work out the next x and y positions of the player, and check it against all the entities in our room.

movePlayer: function() {
  // get x + y changes
  var deltaX = _keys.ArrowLeft ? -1 : _keys.ArrowRight ? 1 : 0;
  var deltaY = _keys.ArrowUp ? -1 : _keys.ArrowDown ? 1 : 0;
  // get next x + y positions
  var nextX = $Collision.getBoundary(_player, deltaX, 0);
  var nextY = $Collision.getBoundary(_player, 0, deltaY);
  // check next x and y for collisions
  var nextXCollisions = $Collision.checkBoundary(nextX, _objects);
  var nextYCollisions = $Collision.checkBoundary(nextY, _objects);
  // if there are no x collisions, carry on
  if (nextXCollisions.length == 0) {
    _player.x += deltaX;
  }
  // if there are no y collisions, carry on
  if (nextYCollisions.length == 0) {
    _player.y += deltaY;
  }
}

With this running, we are always looking ahead at the next x or y position of the player, and making sure they are allowed to move there! Now as we move around our player, we get stopped when we reach any of our other entities:


It's basic, but it works great! What we can then do is add a property to our Entity class to mark it as 'solid' or not, and filter the objects used to check boundaries against based on this. Then we can have all sorts of objects in the room but only solid ones stopping the player.

That's all for this log! Next time I'm going to start messing around with getting tiles in the game rather than drawing everything as boxes.

T H I S T L E

Leave a comment

Log in with itch.io to leave a comment.