Kha Shmup Tutorial Part 5

We can now move and shoot so let’s lay the foundation of providing something to shoot at.

Enemy.hx

package;

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

class Enemy {
  private var image: Image;

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

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

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

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

  public function activate(x: Int, y: Int): Void {
    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);
  }
}

This enemy looks pretty similar - they are pretty much the same as bullets, and they share a lot of resemblance to the Ship. This is ok for our tutorial because we aren’t going to get too complicated, but this is probably the point where you will want to DRY up the code. DRY stands for “Don’t Repeat Yourself”, and means we want to stop duplicating the share it between the classes - you could do this with an base class they inherit from, or a component that these classes hold. I personally would use something like the Ash Entity-Component-System from the start, but that’s somewhat outside of the scope of this tutorial and possibly a topic for a later date.

As I said above this tutorial is pretty simple, and a little duplication won’t kill us, so I am leaving it as is, but this is one of the many “naive” parts of this tutorial I spoke about earlier that you’d want to clean up for a more complex game.

Since we have an Enemy, let’s get a way to get them on screen:

EnemySpawner.hx

package;

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

class EnemySpawner {
  private var minSpawnX: Int;
  private var maxSpawnX: Int;
  private var maxPositionY: Int;
  private var enemyImage: Image;
  private var spawnMinTime: Float;
  private var spawnMaxTime: Float;
  private var spawnNextTime: Float;
  private var currentTime: Float;
  private var enemies: Array<Enemy>;

  public function new(enemyImage: Image, spawnMinTime: Float, spawnMaxTime: Float, 
                      ?minSpawnX: Int = 0, maxSpawnX: Int = 0, maxPositionY: Int = 600) {
    this.enemyImage = enemyImage;
    this.spawnMinTime = spawnMinTime;
    this.spawnMaxTime = spawnMaxTime;
    this.minSpawnX = minSpawnX;
    this.maxSpawnX = maxSpawnX;
    this.maxPositionY = maxPositionY;
    currentTime = 0;
    enemies = new Array<Enemy>();
    spawnNextTime = generateRandomSpawnTime(spawnMinTime, spawnMaxTime);
  }

  private function generateRandomSpawnTime(minTime: Float, maxTime: Float): Float {
    return minTime + Math.random() * (maxTime - minTime);
  }

  private function generateRandomX(minSpawnX, maxSpawnX): Int {
    return Std.random(maxSpawnX - minSpawnX) + minSpawnX;
  }

  private function spawn(x: Int, y: Int): Void {
      for (i in 0...enemies.length) {
        if (!enemies[i].isActive) {
          enemies[i].activate(x, y);
          return;
        }
      }

    enemies.push(new Enemy(x, y, enemyImage));
  }

  public function update(deltaTime: Float) {
    currentTime += deltaTime;

    if (currentTime >= spawnNextTime) {
      currentTime = 0;
      spawnNextTime = generateRandomSpawnTime(spawnMinTime, spawnMaxTime);
      spawn(generateRandomX(minSpawnX, maxSpawnX - enemyImage.width), -enemyImage.height);
    }

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

      if (enemy.isActive && enemy.y > maxPositionY) {
        enemy.isActive = false;
      }
    }
  }

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

Much like the Gun of last lesson - this is an object that basically holds and generates enemies, as well as performing some object pooling and positioning logic.

We use two floats to determine when to spawn enemies - the minimum time we wait to spawn, and the maximum. We randomly select a float between these two, and then once the currentTime is equal to or greater than that, we spawn an enemy just above the screen, and between minSpawnX and maxSpawnX. You may notice two different random methods - Std.random(int) and Math.random(): Std.random(val) will return an integer between 0 and val whereas Math.random returns a float between 0 and 1.0. Because we are generating pure ints in the random position, and floats in the time we use these two different methods, but if you chose to only allow enemies to spawn on whole seconds you could use Std.random in both places.

This spawn logic is pretty simple - as a further exercise you may want to add some additional logic - like maybe you still want to spawn an enemy on a random time, but the time that you generate must always on a quarter second interval.

One more note on code quality - the entire timing logic is a good candidate for being broken out into its own class. You generally want classes to do as little as possible. Again this is an issue of “works for us as is”, but I just thought I should bring it up.

Now let’s tie this spawner back into KhaShmup.hx. I’ll abbreviate this class as its getting somewht big. Make sure to check the part-5 branch if you have any confusion.

KhaShmup.hx

...

class KhaShmup {

  ...
  private var enemySpawner: EnemySpawner;

  public function loadingFinished(): Void {
    ...
    enemySpawner = new EnemySpawner(Assets.images.enemyShip, 1.0, 3.0, 0, screenWidth, screenHeight);
    ...
  }
    
  public function render(framebuffer: Framebuffer): Void {
    ...
    g.begin(bgColor);
    enemySpawner.render(g);
    ship.render(g);
    g.end();
    ...
  }

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

  ...
}

Running the Application

You should now see enemies spawning from the top of the screen and move to the bottom in random intervals. We can’t shoot them just yet, but we’ll be getting to that in part 6.

Lasers

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

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

In Summary

We got enemies on screen, and the next logical step is to allow them to be shot. We’ll be going over some basic collision detection next time.