Kha Shmup Tutorial Part 6

In the last blog post we started spawning enemies randomly on the screen, this time we’re going to be adding some collision detection so the player can shoot. We’ll start how we always do - basic, and the proceed to get more advanced. Since we will be developing a few different cases of collision, let’s add an interface for anything that has a hitbox - which we will use as our bounding box determining where game entities collide.

Hitboxed.hx

package;

interface Hitboxed {
  public var hitbox: Hitbox;
  public function hit(): Void;
}

Pretty simple - we provide access to a variable hitbox, and we provide a method to call when the hitbox is hit. Let’s look at the implementation of the referenced hitbox:

Hitbox.hx

package;

class Hitbox {
  public var rectangle(default, null): Rectangle;
  public var parentX: Int;
  public var parentY: Int;
  public var offsetX: Int;
  public var offsetY: Int;
  public var width: Int;
  public var height: Int;

  public function new(parentX: Int, parentY: Int, offsetX: Int, offsetY: Int, width: Int, height: Int) {
    this.parentX = parentX;
    this.parentY = parentY;
    this.offsetX = offsetX;
    this.offsetY = offsetY;
    this.width = width;
    this.height = height;

    rectangle = new Rectangle(parentX + offsetX, parentY + offsetY, width, height);
  }

  public function overlaps(other: Hitbox): Bool {
    return rectangle.overlaps(other.rectangle);
  }

  public function updatePosition(parentX: Int, parentY: Int): Void {
    this.parentX = parentX;
    this.parentY = parentX;
    rectangle.x = parentX + offsetX;
    rectangle.y = parentY + offsetY;
    rectangle.width = width;
    rectangle.height = height;
  }
}

I’ll explain this class more in a bit, but let’s first glance at Rectangle.hx

Rectangle.hx

package;

class Rectangle {
  public var x: Int;
  public var y: Int;
  public var width: Int;
  public var height: Int;

  public function new(x: Int, y: Int, width: Int, height: Int) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
  }

  public function overlaps(other: Rectangle): Bool {
    return x <= other.x + other.width && 
           x + width >= other.x && 
           y <= other.y + other.height && 
           y + height >= other.y;
  }
}

Rectangle is pretty much what it sounds like - it has a 2d position, and a width and a height. It also provides a method for testing if another rectangle overlaps it. If one part of this rectangle is even touching another rectangle this method will return true.

This method of collision detection is known as “Axis Aligned Bounding Box” - which is a fancy term for saying “check if two boxes, aligned by axis, are overlapping.” The “axis-aligned” part is important - you can’t test for hitboxes that have been rotated differently - there are algorithms for that, but this one is geared for boxes that essentially have the same degree of rotation as each other.

Back to Hitbox.hx - you may wonder why I chose to use a seperate data structure instead of just using a rectangle directly. The reason why I chose this is mostly due to the different between the parent position and the offset. Often in games you don’t neccessarily have a character’s hitbox completely surround their sprite, and this is especially true in shmup games. You’ll sometimes have the hitbox represent some smaller internal structure - maybe just the cockpit of the ship, or sometimes you may just want to have a little leeway space among the sprite so they can get very close to others without triggering collision.

The updatePosition method exists so that the parent object can pass its own updated position to the hitbox, and then the hitbox itself and its internal rectangle gets adjusted accourdingly. You can ofcourse get fancy and have custom setter properties for the hitbox’s position that automatically updates the rectangle, but I feel this simple solution works for our case.

Hopefully this all makes sense but if it doesn’t or you have a better, simpler way of handling this please let me know. Let’s go ahead and implement this interface in Bullet.hx and Enemy.hx:

Bullet.hx

// ...

class Bullet implements Hitboxed {
  // ...

  public var hitbox: Hitbox;

  public function new(x: Int, y: Int, image: Image) {
    this.image = image;
    hitbox = new Hitbox(x, y, 0, 0, image.width, image.height);
    activate(x, y);
  }
    
  public function hit(): Void {
    isActive = false;
  }

  public function update(deltaTime: Float): Void {
    if (!isActive) {
      return;
    }

    y -= Math.round(speed * deltaTime);
    hitbox.updatePosition(x, y);
  }

  // ...
}

Enemy.hx

package;

import kha.Assets;
import kha.Image;
import kha.audio1.Audio;
import kha.graphics2.Graphics;

class Enemy implements Hitboxed {
  // ...
  public var hitbox: Hitbox;

  // ...
  public function new(x: Int, y: Int, image: Image) {
    this.image = image;
    hitbox = new Hitbox(x, y, 2, 0, image.width - 4, Std.int(image.height / 2));
    activate(x, y);
  }

  public function hit(): Void {
    Audio.play(Assets.sounds.enemyExplosion, false);
    isActive = false;
  }

  public function update(deltaTime: Float): Void {
    if (!isActive) {
      return;
    }

    y += Math.round(speed * deltaTime);
    hitbox.updatePosition(x, y);
  }
    
  // ...
}

Now I want to develop a convenience handler for performing these collision checks.

CollisionHandler.hx

package;

class CollisionHandler {
  // checks if 2 entities collide, and if so 'hits' them both
  public static function handleBiCollision(h1: Hitboxed, h2: Hitboxed): Void {
    if (testCollision(h1, h2)) {
      h1.hit();
      h2.hit();
    }
  }

  // compare every entity from leftGroup with every entity in rightGroup
  public static function handleGroupCollisions(leftGroup: Array<Hitboxed>, rightGroup: Array<Hitboxed>): Void {
    for (left in leftGroup) {
      for (right in rightGroup) {
        handleBiCollision(left, right);
      }
    }
  }

  public static function testCollision(h1: Hitboxed, h2: Hitboxed): Bool {
    return h1.hitbox.overlaps(h2.hitbox);
  }
}

This is a pretty basic class that (currently) requires no state. It provides a few static convenience methods for iterating over groups of Hitboxed classes, tests for collision between two entities, and hits both if they do collide. Let’s tie this into KhaShmup.hx, abbreviate for space:

KhaShmup.hx

// ...

class KhaShmup {
  // ...

  private function update() {
    timer.update();
    enemySpawner.update(timer.deltaTime);
    updateShip();
    handleCollisions();
  }

  private function handleCollisions() {
    var bullets: Array<Hitboxed> = cast ship.gun.getActiveBullets();
    var enemies: Array<Hitboxed> = cast enemySpawner.getActiveEnemies();

    CollisionHandler.handleGroupCollisions(bullets, enemies);
  }    

  // ...
}

The case call on both of these methods are a little weird. This call basically tells the typechecker that we are casting from the provided types (which will be Array and Array) to the declared type of Array. This is because type parameters in Array are _invariant_. The concept of variance in computer science is somewhat technical but interesting if you are into type theory, so you can read more about it [here](https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)) and [here is a link](http://haxe.org/manual/type-system-variance.html) to it being addressed in the Haxe documentation.

Note: I originally had gun be private in Ship.hx, but I decided to make it public just to make calling this method on it easier. If I really wanted to keep gun private, which isn’t neccessarily a bad idea, I would just provide a similar getActiveBullets method in Ship that called down to Gun’s method.

Speaking of these methods, let’s define them:

Gun.hx

// ...

class Gun {
  // ...
  public function getActiveBullets(): Array<Bullet> {
    var actives = new Array<Bullet>();

    for (i in 0...bullets.length) {
      if (bullets[i].isActive) {
        actives.push(bullets[i]);
      }
    }
    return actives;
  }
  // ...  
}

EnemySpawner.hx

// ...

class EnemySpawner {
  // ...
  public function getActiveEnemies(): Array<Enemy> {
    var actives = new Array<Enemy>();

    for (i in 0...enemies.length) {
      if (enemies[i].isActive) {
        actives.push(enemies[i]);
      }
    }
    return actives;
  }
  // ...  
}

Running the Application

Running now should have similar results as before - you can move, enemies spawn randomly, but now when you shoot an enemy it should disappear and play an explosion sound.

Check my part-6 branch if this does not work for you: https://github.com/jamiltron/KhaShmup/tree/part-6

In part 7 we will continue on this thread, adding in basic animations for the enemy ships’ destruction.