Kha Shmup Tutorial Part 4

Previously on KhaSmup we added keyboard support and limited the ship to only be pilotable on the visible screen. In this tutorial we are going to add the ability to shoot as well as the sounds of the shot. First thing we have to do is add a bullet.

Bullet.hx

package;

import kha.Image;
import kha.graphics2.Graphics;

class Bullet {
  private var image: Image;

  public var x: Int;
  public var y: Int;
  public var speed: Int = 600;
  public var isActive: Bool = true;

  public function new(x: Int, y: Int, image: Image) {
    this.image = image;
    activate(x, y);
  }

  public function activate(x: Int, y: Int) {
    this.x = x;
    this.y = y;
    isActive = true;
  }

  public function render(g: Graphics): Void {
    if (!isActive) {
      return;
    }

    g.drawImage(image, x, y);
  }

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

    y -= Math.round(speed * deltaTime);
  }
}

Most of this should be pretty obvious - it shows a lot of similarity to the Ship. The major difference is we have an isActive field. You may be wondering what this is for - let’s take a look at Gun.hx, which is what we will be using to manage these bullets:

Gun.hx

package;

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

class Gun {
  private var bulletImage: Image;
  private var bulletSound: Sound;
  private var shotInterval: Float;
  private var cooldownLeft: Float;
  private var bullets: Array<Bullet>;

  public function new(shotInterval: Float, bulletImage: Image, bulletSound: Sound) {
    this.shotInterval = shotInterval;
    this.bulletImage = bulletImage;
    this.bulletSound = bulletSound;
    cooldownLeft = 0;
    bullets = new Array<Bullet>();
  }

  public function shoot(x: Int, y: Int): Void {
    if (cooldownLeft <= 0) {
      Audio.play(bulletSound, false);
      cooldownLeft = shotInterval;
      var adjX: Int = x - Std.int(bulletImage.width / 2);

      for (i in 0...bullets.length) {
        if (!bullets[i].isActive) {
          bullets[i].activate(adjX, y);
          return;
        }
      }

      bullets.push(new Bullet(adjX, y, bulletImage));
    }
  }

  public function update(deltaTime: Float) {
    cooldownLeft -= deltaTime;
    if (cooldownLeft < 0) {
      cooldownLeft = 0;
    }

    for (i in 0...bullets.length) {
      var bullet = bullets[i];
      bullet.update(deltaTime);

      if (bullet.isActive && bullet.y + bulletImage.height < 0) {
        bullets[i].isActive = false;
      }
    }
  }

  public function render(g: Graphics) {
    for (i in 0...bullets.length) {
      bullets[i].render(g);
    }
  }
}

This class is has a few new features. For one, it uses a countdown feature - we have a shotInterval variable that defines how long in seconds we wait between two shots. When the user shoots, we check if the cooldownLeft is at zero, and if it is - we set it to the interval , create a bullet, and play a sound. The Audio.play method takes a sound and a boolean to specify if you want it to loop or not. This sound is played by calling Audio.play, which is how we can interface with Kha’s audio1 api.

The bullet creation may look a little interesting - we have an array, and when we create a bullet we iterate through this array looking for an inactive bullet, and if we don’t find one - we create a new one. If we do happen to find an inactive bulelt we just use the inactive one and we activate it.

This is a very naive form of object pooling. The idea behind pooling is that sometimes object initialization is costly, so we decide to limit this cost by keeping around already initialized objects. This can also help out with languages that have garbage collection such as Haxe. Garbage collection is a great tool, but it can sometimes hamper the experience of games - causing a spike in lag as unused objects are cleaned up. By pooling we still hold onto references of objects, preventing them from being collected until we no longer hold a reference to the pool AND the object.

Objects that are good candidates for pooling are those that you tend to create and destroy a whole lot - like these bullets. Generally you will want to profile your game (using a tool such as hxScout) before you commit to pooling - as holding a ton of objects in memory has its own downsides.

There are much more general implementations of pools out there - and you will probably want to use something a little stronger if you start making a bigger game, but this implementation was chosen to be demonstrative and to introduce the concept if you’ve never seen it.

Note that we have the bullet coming right out of the gun - but obviously you can change this up and make the gun cooler by having two bullets shoot, or even something crazier.

Now that we have our gun, let’s add it to our ship:

Ship.hx

package;

import kha.Image;
import kha.graphics2.Graphics;

class Ship {
  private var image: Image;
  private var gun: Gun;
  private var gunOffsetY = 10;

  public var x: Int;
  public var y: Int;
  public var width(get, null): Int;
  public var height(get, null): Int;
  public var speed = 300.0;

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

  public function attachGun(gun: Gun) {
    this.gun = gun;
  }

  function get_width(): Int {
    return image.width;
  }

  function get_height(): Int {
    return image.height;
  }

  public function render(g: Graphics) {
    if (gun != null) {
      gun.render(g);
    }
    g.drawImage(image, x, y);
  }

  public function update(controls: Controls, deltaTime: Float) {
    if (controls.left && !controls.right) {
      x -= Math.round(speed * deltaTime);
    } else if (controls.right && !controls.left) {
      x += Math.round(speed * deltaTime);
    }

    if (controls.up && !controls.down) {
      y -= Math.round(speed * deltaTime);
    } else if (controls.down && !controls.up) {
      y += Math.round(speed * deltaTime);
    }

    if (gun != null) {
      if (controls.shoot) {
        gun.shoot(x + Std.int(image.width / 2), y - gunOffsetY);
      }
      gun.update(deltaTime);
    }
  } 
}

The gun gets inserted in pretty much as is, the only major difference is that we check if this gun is null before rendering it or shooting it. This allows us to instantiate ships without guns.

Obviously we have to update our controls.

Controls.hx

package;

import kha.Key;

class Controls {
  public var left: Bool;
  public var right: Bool;
  public var up: Bool;
  public var down: Bool;
  public var shoot: Bool;

  public function new() {}

  public function keyDown(key: Key, value: String) { 
    switch (key) {
    case LEFT:
      left = true;
    case RIGHT:
      right = true;
    case UP:
      up = true;
    case DOWN:
      down = true;
    case CHAR:
      if (value == "z") {
        shoot = true;
      }
    default:
      // no-op
    }
  }

  public function keyUp(key: Key, value: String) { 
    switch (key) {
    case LEFT:
      left = false;
    case RIGHT:
      right = false;
    case UP:
      up = false;
    case DOWN:
      down = false;
    case CHAR:
      if (value == "z") {
        shoot = false;
      }
    default:
      //no-op
    }
  }
}

The addition here is that we add a new switch case to both methods checking for CHAR, which is the key type representing a character. Now this gets into the kind-of weird part of Kha sending a char key’s value as a string - we have to then check the value in a sub-condition. Not exactly how I would have chosen to do this, but it works.

Let’s wrap this up by updating KhaShmup:

KhaShmup.hx

package;

import kha.Assets;
import kha.Color;
import kha.Framebuffer;
import kha.Image;
import kha.Key;
import kha.Scaler;
import kha.System;
import kha.input.Keyboard;

class KhaShmup {

  private static var bgColor = Color.fromValue(0x26004d);

  public static inline var screenWidth = 800;
  public static inline var screenHeight = 600;
  public static inline var gunSpeed = 0.25;

  private var backbuffer: Image;
  private var controls: Controls;
  private var initialized = false;
  private var ship: Ship;
  private var timer: Timer;

  public function new() {
    Assets.loadEverything(loadingFinished);
  }

  private function loadingFinished(): Void {
    initialized = true;

    // create a buffer to draw to
    backbuffer = Image.createRenderTarget(screenWidth, screenHeight);

    // create our player
    var shipImg = Assets.images.playerShip;
    ship = new Ship(Std.int(screenWidth / 2) - Std.int(shipImg.width / 2), 
      Std.int(screenHeight / 2) - Std.int(shipImg.height / 2), 
      shipImg);
    controls = new Controls();
    timer = new Timer();
    Keyboard.get().notify(keyDown, keyUp);
    setupShip();
  }

  public function render(framebuffer: Framebuffer): Void {
    if (!initialized) {
      return;
    }

    var g = backbuffer.g2;

    // clear and draw to our backbuffer
    g.begin(bgColor);
    ship.render(g);
    g.end();

    // draw our backbuffer onto the active framebuffer
    framebuffer.g2.begin();
    Scaler.scale(backbuffer, framebuffer, System.screenRotation);
    framebuffer.g2.end();

    update();
  }

  private function setupShip() {
    ship = new Ship(Std.int(screenWidth / 2) - Std.int(ship.width / 2), 
      Std.int(screenHeight / 2) - Std.int(ship.height / 2), 
      Assets.images.playerShip);
    ship.attachGun(new Gun(gunSpeed, Assets.images.bullet, Assets.sounds.bulletShoot));
  }

  private function update() {
    timer.update();
    updateShip();
  }

  private function updateShip() {
    ship.update(controls, timer.deltaTime);

    // limit the ship to the width of the screen
    if (ship.x < 0) {
      ship.x = 0;
    } else if (ship.x + ship.width > screenWidth) {
      ship.x = screenWidth - ship.width;
    }

    // limit the ship to the height of the screen
    if (ship.y < 0) {
      ship.y = 0;
    } else if (ship.y + ship.height > screenHeight) {
      ship.y = screenHeight - ship.height;
    }
  }

  private function keyDown(key: Key, value: String): Void {
    controls.keyDown(key, value);
  }

  private function keyUp(key: Key, value: String): Void {
    controls.keyUp(key, value);
  }
}

Almost the exact same as before, aside from the instantiation of the Gun and attaching it to the Ship. Also we grab the bullet.wav sound out of the assets. For that you should go to bfxr.net, create a sound you enjoy, then export it as a wav file to your Assets folder, and adjust the name to “bulletShoot.wav”.

Running the Application

You’ll now be able to move the ship, and when you hit the z button you’ll shoot a bullet out.

Lasers

If you don’t see this, check out my part-4 branch:

https://github.com/jamiltron/KhaShmup/tree/part-4

In Summary

We’re starting to see a game form piece-by-piece. We learned how to shoot, how to play a sound, and we learned a little bit about object pooling to delay initialization/garbage-collection costs. In part 5 we’ll be adding some targets for the player to shoot.