11.03.2015

LMaGT 2 - Basic collisions with jQuery

In the last post, I ended with a couple static enemies, and a player that can move left and right. It's a pretty good start, but needs a bit more work before we can really call it a working prototype. The player needs to be able to shoot, and the enemies need to know they're being shot. The first part is easy, the second part has a couple harder bits but it can be done! Like last time, you can (click here) to get this project folder and follow along.

Before jumping in, it's worth pointing out that shooting bullets (or whatever else you'd like to call them) needs to be handled like player movement. A key-binding needs to be added for keyDown and keyUp, but doing it this way will fire a shot every frame the button is held down. You can leave it that way if you want, but I want a bit more control over the firing button. First, this gets added into the main.js file to detect that you're shooting:

var keyShoot = false;
var bulletWaitTime = 10; // Number of frames to wait
var bulletTimer = 0;        // Number of frames waited
function handleKeyDown(e) {
  if (e.keyCode == 32) {
    keyShoot = true;
  }
};

function handleKeyUp(e) {
  if (e.keyCode == 32) {
    keyShoot = false;
    bulletTimer = bulletWaitTime;
  }
};

function calculateInput() {
  // This bit keeps the player within the bounds of the screen
  var playerPos = parseInt($player.css('left').replace('px', ''));
  if (playerPos <= 30) {
    keyLeft = false;
    $player.stop().css({left: '30px'});
  } else if (playerPos >= ($gameArea.width() - $player.width() - 30)) {
    keyRight = false;
    $player.stop().css({left: $gameArea.width() - $player.width() - 30 + 'px'});
  }

  /* Left and Right movement functions go here! */

  if (keyShoot && bulletTimer >= bulletWaitTime) {
    createBullet();
    bulletTimer = 0;
  } else if (bulletTimer < bulletWaitTime) {
    bulletTimer++;
  }
};


When the spacebar is pressed down, a bullet element is created, bulletTimer is set to zero, and will tick back up towards bulletWaitTime each frame. Releasing the spacebar resets the timer, allowing another shot to be fired. You can also adjust the rate of fire by changing bulletWaitTime. Nice! Now that they're being detected properly, we can work on making it appear on screen, and moving. You can use this first bit for some basic css styling:

/* In main.css */
  .bullet {
box-sizing: border-box;
display: inline-block;
position: absolute;
margin: 0;
padding: 0;
width: 5px;
height: 20px;
background-color: gold;
border: 2px dashed white;
}

/* In main.js */
var bulletSpeed = 100;
var $player = '';
var $gameArea = '';

function createBullet() {
var bulletPos = {
left: $player.position().left + ($player.width() / 2),
top: $player.position().top - 30
};
$gameArea.append($('<div/>').addClass('bullet').css(bulletPos));
};

function moveBullets() {
$('.bullet').animate({
'top': '-=' + bulletSpeed
},{
duration: 100,
easing: 'linear'
}).dequeue();
};

function mainLoop() {
 calculateInput();
 moveBullets();
};

$(document).ready(function() {
$player = $('.player');
$gameArea = $('#game-area');
});


Bullets spawn in the #game-area relative to the player's position, centered just above the .player element. They animate the same way the player does, but instead of listening for input, moveBullets() gets called as part of the mainLoop() each frame. Before you can try it out, there's a little bit of cleanup to do. We can move the .player div out of the #player-box into the #game-area, and get rid of the #player-box completely. We can also consolidate the css, making a couple minor changes to set up for the next couple steps. This blog is more about function than design though, so I suggest checking the project folder for specifics. Now that things are cleaned up, you can try it out! The player moves and the bullets fire. This is also a good spot to try out different animation speeds, easing styles, different behaviours to make it your own. Once you've got it moving like you want, let's talk about collisions.

Said briefly, collisions are when 2 or more defined areas come in contact and/or overlap. For the purposes of this project, that means we need to know the boundaries of each enemy box and each bullet. It doesn't matter if the enemies overlap, but if a bullet shares any of the same space with an enemy, we need to know which ones are colliding and destroy them both. So where do we start? There's a lot of ways to do it, this is just one way I've come up with. It needs some fine tuning before it's perfect, but it works great for now.

function detectCollision() {
  $('.bullet').each(function() {
    var $bullet = $(this);
  var bulletPos = {
  top: $bullet.position().top,
  left: $bullet.position().left - 2,
  right: $bullet.position().left + $bullet.width() + 2,
  bottom: $bullet.position.top + $bullet.height()
  }; 
  var hit = false;

  $('.enemy').each(function() {
if (!hit) {
var $enemy = $(this);
var enemy = {
top: $enemy.position().top,
left: $enemy.position().left,
  right: $enemy.position().left + $enemy.width(),
  bottom: $enemy.position().top + $enemy.height()
};

  if (bulletPos.top <= enemy.bottom) {
if (bulletPos.right > enemy.left && bulletPos.left < enemy.right ||
bulletPos.left < enemy.right && bulletPos.right > enemy.left ) {
  hit = true;
  $bullet.remove();
  $enemy.remove();
  }
  }
  }
  });

  if (bulletPos.top < 0 - $bullet.height()) {
  $bullet.remove();
  }
  })
};

function mainLoop() {
  calculateInput();
  moveBullets();
  detectCollision();
};

Each frame, the positions of each bullet and each enemy are fetched and compared. By comparing opposite sides (bullet top to enemy bottom, etc) you can determine if 2 sides are overlapping, and call it a successful collision. The little bit at the end is another check to destroy bullets whenever they go offscreen. When you try this out, you'll see a new problem. Enemies are getting pushed to the left when destroyed. They need to be displayed absolutely, which means they need to be placed automatically. It also means we can get remove the ones we placed in the index.html, leaving 2 empty divs in a <section>. The css can be consolidated a bit more too, as seen in the project folder linked above.

Here's a simple enemy placement function. Based on the size of the #enemy-box and your .enemies, this will fills the area with as many enemies as it can, while maintaining even spacing between them. It only needs to be called once (for now) during the $document.ready section. Like the previous function, this is a rough draft for testing, and will be fine tuned later.

function propagateEnemies() {
  var $enemybox = $('#enemy-box');
var enemySize = 100 + (15 * 2); // Tile size, plus spacing on both sides

var x = Math.floor($enemybox.width() / enemySize);
var xSpacing = ($enemybox.width() % enemySize) / (x + 1);
var y = Math.floor($enemybox.height() / enemySize);
var ySpacing = ($enemybox.height() % enemySize) / (y + 1);

for (var j = 0; j < y; j++) {
for (var i = 0; i < x; i++) {
var position = {
'top': (j * enemySize) + 15 + (ySpacing * 2) + 'px',
'left': (i * enemySize) + 15 + (xSpacing * 2) + 'px'
};
$enemybox.append($('<div/>').addClass('enemy').css(position));
  }
  }
};

$(document).ready(function() {
window.addEventListener('keydown', handleKeyDown, true);
window.addEventListener('keyup', handleKeyUp, true);
$player = $('.player');
$gameArea = $('#game-area');
propagateEnemies();
setInterval(mainLoop, 1000 / fps);
});

That's a good spot to end on. I like how it's coming together so far. The next entry will be the introduction of the navbar, and  'levels' that can be loaded from html content.