Kha Shmup Tutorial Part 10

We wrapped up the last post by adding a main menu and introducing game states, and we left off one more state - the game over state.

We want to make sure that we appropriately handle the collision of the player and enemies, the displaying of a game over screen, and that we allow the player to restart the game if they wish. Let’s start by making some changes to our main game class:

KhaShmup.hx

package;

// ...

class KhaShmup {
  // ...

  private function renderGameOver(g: Graphics): Void {
    uiManager.renderGameOver(g);
  }

  public function render(framebuffer: Framebuffer): Void {
    if (Type.enumEq(gameState, GameState.Initializing)) {
      return;
    }

    var g = backbuffer.g2;

    // clear and draw to our backbuffer
    g.begin(bgColor);
    switch(gameState) {
    case GameState.MainMenu:
      renderMainMenu(g);
      updateMainMenu();
    case GameState.Playing:
      renderPlaying(g);
      updatePlaying();
    case GameState.Over:
      renderPlaying(g);
      renderGameOver(g);
      updateGameOver();
    default:
      // no-op
    }
    g.end();

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

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

    CollisionHandler.handleGroupCollisions(bullets, enemies, uiManager.scoreUp);
    CollisionHandler.handleSingleToGroupCollisions(ship, enemies, setGameOver);
  }

  private function setGameOver() {
    gameState = GameState.Over;
    controls.reset();
  }

  private function updateGameOver() {
    timer.update();
    enemySpawner.update(timer.deltaTime);

    if (controls.shoot) {
      reset();
      gameState = GameState.Playing;
    }
  }

  // ...
}

We add in the render and update portions for when the game is in the Over state. The new render method just lets the ui manager render some text on screen as we’ll see in a bit, and the update method allows the player to press the shoot button, which resets the game.

There’s also a new call to the collision handler to check for collisions between the player ship and the enemies, supplying the setGameOver method if there are matches. We also add in a new sound (made from bfxr), so that the player will have their own explosion sound upond collision.

Looking at these changes we see that we need to update our player Ship class, as well as the CollisionHandler.

Ship.hx

package;

// ...
import kha.Sound;
import kha.audio1.Audio;
// ...

class Ship implements Hitboxed {
  // ...
  private var isActive = true;

  public var explosionSound: Sound;
  public var hitbox: Hitbox;
  public var x(default, set): Int;
  public var y(default, set): Int;

  private function set_x(x: Int) {
    hitbox.updatePosition(x, y);
    return this.x = x;
  }

  private function set_y(y: Int) {
    hitbox.updatePosition(x, y);
    return this.y = y;
  }

  public function new(x: Int, y: Int, image: Image, explosionSound: Sound) {
    hitbox = new Hitbox(x, y, 0, 0, image.width, image.height);
    this.x = x;
    this.y = y;
    this.image = image;
    this.explosionSound = explosionSound;
  }

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

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

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

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

    if (gun != null) {
      gun.reset();
    }
  }

  public function update(controls: Controls, deltaTime: Float) {
    if (!isActive) {
      return;
    }
    // ...
  }

  // ...
}

We’ve changed Ship so that it implements Hitboxed, which requires us to supply a public Hitbox, as well as a hit method. We’ve also added in an isActive boolean so that the player can essentially be “turned off” during the game over screen.

We also take in a sound upon construction that gets played when the player is hit, which is also where they become inactive.

We take advantage of accessors to make sure that whenever the Ship’s x or y member variables are updated that the hitbox’s position is updated as well.

Now let’s look at the small change we need to make to the CollisionHandler to account for our changes:

CollisionHandler.hx

package;

// ...

class CollisionHandler {
  // ...

  // convenience method for comparing a single entity vs many for collisions
  public static function handleSingleToGroupCollisions(single: Hitboxed, group: Array<Hitboxed>,
                                                       ?callback: Void->Void = null): Void {
    for (g in group) {
      handleBiCollision(single, g, callback);
    }
  }
  // ...
}

Nothing too fancy - just adding in a convenience method for checking collisions between one entity and several.

Finally we need to make some adjustments to our UIManager:

UIManager.hx

package;

// ...

class UIManager {
  // ...
  private static inline var gameOverStr = "Game Over";
  private static inline var restartStr = "Press Z to restart.";

  // ...

  public function renderGameOver(g: Graphics): Void {
    g.font = font;
    g.fontSize = titleSize;
    
    var x = viewport.x + Math.round(viewport.width / 2);
    var gameOverW = g.font.width(titleSize, gameOverStr);
    g.drawString(gameOverStr, x - Math.round(gameOverW / 2), viewport.y + titleY);

    g.fontSize = instructionSize;
    var restartW = g.font.width(instructionSize, restartStr);
    g.drawString(restartStr, x - Math.round(restartW / 2), viewport.height - startY);
  }

  // ...
}

Running the Application

Now when the player collides with an enemy they should blow up, the player should be displayed a “game over” message, and they should be able to restart the game by pressing the shoot button.

KhaShmup10

Make sure to look at my part 10 branch if you’re having problems: https://github.com/jamiltron/KhaShmup/tree/part-10

That kind of wraps up the game, hitting all of the goals we set out to do. In the next post we will be wrapping up, learning how to distribute a Kha game on the web, and discussing further enhancements that you can add to make this game more fun.