SUPERPOWERS TUTORIAL #4

SUPER ASTEROIDS and SUPER SPACEWAR, Chapter 5

Building the basic program structure

Global datas

We can now start programming our game, we first need to set differents datas to be accessible globally at any moment in our scripts. Often when we develop a game for the first time, the data are added, modified, removed or changed as we write the rest of our program, but for the tutorial we write the datas first and will explain them again when we met them in our code.

We will create different namespace for each of our game object, alien, ship, asteroid.

First we create in our Global script a main namespace for the Game and add differents datas that we export to be able to access them from our others scripts.

// Main game datas and functions
namespace Game{

  // Game timer max value
  export const timeMax: number = 7200; //7200 2 minutes

  // The game index of the current played game, 0 for Asteroids, 1 for Spacewar
  export let nameIndex: number;

  // The screen limits in width and height + outside border
  export let bounds : {
    width:number,
    height:number
  }

  // set a border of 2 x 1.5 units (invisible border of 24 pixels)
  let border: number = 3;

  // Game points won when target shot
  export enum points {
    asteroidBig = 10,
    asteroidMedium = 15,
    asteroidSmall = 20,
    alien = 50,
    ship = 100 ,
    death = -50,
     }

  // Life hearts positions
  export const hearts = [
    ["empty", "empty", "empty", "empty", "empty"],
    ["full", "empty", "empty", "empty", "empty"],
    ["full", "full", "empty", "empty", "empty"],
    ["full", "full", "full", "empty", "empty"],
    ["full", "full", "full", "full", "empty"],
    ["full", "full", "full", "full", "full"]
  ]

  // Flags to check HUD changes
  export var checkLifeHUD: boolean;
  export var checkScoreHUD: boolean;
}

We now write a Menu namespace and add datas related:

// Menu datas
namespace Menu{

  // Different menu screen index
  export const screens = {
      main : "Main",
      asteroids : "Asteroids",
      spacewar : "Spacewar",
      gameover : "GameOver",
     }

  // Game names index
  export enum names {
        Asteroids = 0,
        Spacewar = 1,
       }
}

We write the ship datas :

// Ship datas
namespace Ships{
  // Default ship size
  export const size: number = 0.5;
  // Default ship collision radius
  export const amplitude: number = 1.5;
  // Starting ship score
  export const startScore: number = 0;
  // Starting ship life
  export const startLife: number = 3;

  // Starting spawn positions
  export const spawns: Sup.Math.Vector3[] = [
    new Sup.Math.Vector3(0, 0, 14), // ship1 for asteroids game
    new Sup.Math.Vector3(-4, -12, 2), // ship1 for spacewar game
    new Sup.Math.Vector3(4, 12, 4) // ship1 for spacewar game
  ] 

  // ship index
  export enum index {
    ship1 = 0,
    ship2 = 1
    };

  // Starting time before next shoot
  export const shootingTimer: number = 30;
  // Starting time before respawn
  export const respawnTimer: number = 180;
  // Starting time before vulnerability
  export const invincibleTimer: number = 200;

  // Linear speed
  export const linearAcceleration: number = 0.005;
  // Linear slowing down
  export const linearDamping: number = 0.97;

  // Rotation speed
  export const angularAcceleration: number = 0.02;
  // Rotation slowing down
  export const angularDamping: number = 0.75;

  // Commands for each ship by index
  export const commands = [
    {left:"LEFT", right:"RIGHT", forward:"UP", shoot:"CONTROL", boost:"SHIFT"}, // commands[0]
    {left:"A", right:"D", forward:"W", shoot:"SPACE", boost:"C"} // commands[1]
  ];

  // Missiles list for each ship by index
  export let missiles: Sup.Actor[][];
  // Starting missile life before destruction (frames)
  export const missileLife: number = 60;
  // Missile speed (unit/frame)
  export const missileSpeed: number = 0.30;
}

We write the datas for the alien ship :

// Alien datas
namespace Alien{
  // Flag for alien alive
  export let alive: boolean = true;

  // Starting alien life
  export const startLife: number = 5;
  // Current alien life
  export let lifes: number;

  // Alien z position
  export const zPosition: number = 12;

  // Different alien sizes
  export const sizes: number[] = [1.7, 1.3, 1];
  // Different collision amplitude related to size
  export const amplitudes: number[] = [2.5, 2.2, 2];

  // Linear and rotation speed of alien ship
  export let linearSpeed: number = 0.05;
  export let rotationSpeed: number = 0.01;

  // Starting time before alien ship respawn
  export const respawnTime: number = 300;
  // Current time befer alien ship respawn
  export let spawnTimer: number = 0;

  // Starting time before alien ship shoot again
  export const shootTime: number = 200;

  // Alien missile list
  export let missiles: Sup.Actor[];
  // Alien missile speed (unit/frame)
  export const missileSpeed: number = 0.05;
}

We write the asteroids datas :

// Asteroids datas
namespace Asteroids{
  // List of all current asteroids
  export let list: Sup.Actor[];

  // Differents size of asteroids
  export const sizes = {"big":1.5, "medium":1, "small":0.5};
  // Different collision amplitude related to size
  export const amplitudes = {"big":2.5, "medium":2, "small":1}

  // Range for Z positions of asteroids
  export enum zPositions {min = -28, max = 10};

  // Starting asteroids number
  export const startCount: number = 5;
  // Current asteroids number
  export let currentCount: number;
  // Maximum asteroids number
  export const maxCount: number = 10;

  // Range of linear and rotation speed of asteroids
  export enum linearSpeed {min = -0.05, max = 0.05};
  export enum rotationSpeed {min = -0.01, max = 0.01};

  // Starting time before asteroids spawning
  export const respawnTime: number = 180;
  // Current time before asteroids spawning
  export let spawnTimer: number = 0;
}

We now have all our datas ready to use in our program.

Init game behavior

The GameBehavior class in ou Game script will be used as the main process running when our game start after we launch it from the menu. There will be differents branch in this behavior depending which game is running. By example, if the game is Game.nameIndex is 0 (Asteroids), then we can spawn the alien and the asteroids, else we don't.

Let's start to write some process of the game. (we use the start method and not the awake method to init the behavior)

class GameBehavior extends Sup.Behavior {
  timer: number;

  start() {
    // When GameBehavior awake, if game is Asteroids then spawn asteroids and alien
    if(Game.nameIndex === 0){
      // Give to this game instance, the starting timer
      this.timer = Game.timeMax;
      // And update the HUD timer
      Game.updateTimer(this.timer);

      // Spawn an Alien ship
      Game.spawnAlien();

      // Spawn as much asteroids than we have set as a Starting number
      for(let i = 0; i < Asteroids.startCount; i++){
        // Spawn an asteroid
        Game.spawnAsteroid();
      }
    }
  }

  update() {
    // If the game is Asteroids then spawn alien and asteroids
    if(Game.nameIndex === 0){
      //If alien destroyed, spawn a new alien after a certain time
      if(!Alien.alive){
        // If spawnTimer not finished, decrease by one
        if(Alien.spawnTimer > 0){
          Alien.spawnTimer--;
        // If spawnTimer finished, spawn an alien ship
        }
        else {
          Game.spawnAlien();
        }
      }

      // Spawn a new asteroid after a certain time until the overall number reach maximum
      // If asteroid current count is less than the max number
      if (Asteroids.currentCount < Asteroids.maxCount) {
        // If spawnTimer is not finished, decrease by one
        if (Asteroids.spawnTimer > 0) {
          Asteroids.spawnTimer--;
        }
        // If spawnTimer is finished, create a big asteroid
        else {
          Game.spawnAsteroid();
          // Reset timer for next asteroid
          Asteroids.spawnTimer = Asteroids.respawnTime; 
        }
      }

      // Timer and game over screen decrease to each frame
      if (this.timer > 0) {
        this.timer--
        // If 60 frames passed (which mean one second when converted), update
        if(this.timer % 60 === 0) {
          Game.updateTimer(this.timer);
        }
      // If timer at 0, then the game is finished.
      } 
      else {
        // The game is over, return ship1 score
        Game.gameOver("ship1");
      }
    }
  }
}
Sup.registerBehavior(GameBehavior);

We now have four new functions than we haven't wrote yet :

  • Game.spawnAsteroid() which create an asteroid in the game
  • Game.spawnAlien() which create an alien ship in the game
  • Game.updateTimer() which update the game HUD timer
  • Game.gameOver() which display the game over screen

We return in the Global script to write them now.

Global first functions

We also need to start an important function, the Game.start() that will be used to initialize a new game.

In the Game namespace we add the five functions than we export globally.

namespace Game{
  [...]
  // Start the game
  export function start(){

  }

  // Create an alien in the Game scene              
  export function spawnAlien(){

  }

  // Create an asteroid in the Game scene
  export function spawnAsteroid(size: string){

  }

  // Update HUD timer in the Game scene
  export function updateTimer(time: number){

  }

  // Close game scene and return menu game over screen
  export function gameOver(winner?: string){

  }
}
Game.start

For now, we just need to set with this function the bounds of our game screen.

namespace Game{
  [...]  
  // Start the game
  export function start(){
    // get the Camera actor the the game Scene
    let screen = Sup.getActor("Camera").camera;
    // We get the game screen bounds and add the invisible border
    bounds = {
      width: screen.getOrthographicScale() + border,
      height: screen.getOrthographicScale() + border
    }
  }
  [...]
}
[...]

Because this function will change many time and will be called from the menu, we still need to launch our game and do tests before that. We then call the function at the begining of the awake method of our Game script.

  [...]
  awake() {
    Game.start() // Temporary Function Call
[...]
Game.spawnAlien
  // Create an alien in the Game scene              
  export function spawnAlien(){
    // Load the alien prefab and get the Alien actor (index 0) as a new alien variable
    let alien = Sup.appendScene("Alien/Prefab")[0];

    // Choose a random position somewhere at the vertical edges of the game screen
    // Create the three axis positions x, y, z
    let x: number = 0, y: number = 0, z: number = 0;

     // Choose randomly a position on one of the vertical edges
    x = Sup.Math.Random.sample([-bounds.width / 2, bounds.width / 2]);
    // Get the height without the invisible border
    let height = bounds.height - border
    // Choose randomly a position on y axis
    y = Sup.Math.Random.integer(-height / 2 , height / 2);
    // Get alien default Z position
    z = Alien.zPosition;

    // Set the randomly chosen position to the Alien actor
    alien.setLocalPosition(x, y, z);
  }
Game.spawnAsteroid
  // Create an asteroid in the Game scene
  export function spawnAsteroid(){
    // Load the asteroid prefab and get the Asteroid actor (index 0) as a new asteroid variable
    let asteroid = Sup.appendScene("Asteroid/Prefab")[0];

    // Choose a random position somewhere at the edges of the game screen
    // Create the three axis positions x, y, z
    let x: number = 0, y: number = 0, z: number = 0;

    // Choose randomly if the asteroids come from the horizontal or vertical edges
    if(Sup.Math.Random.sample([0, 1]) === 0){
      x = Sup.Math.Random.sample([-bounds.width / 2, bounds.width / 2]);
      y = Sup.Math.Random.integer(-bounds.height / 2, bounds.height / 2);
    }
    else {
      x = Sup.Math.Random.integer(-bounds.height / 2, bounds.height / 2);
      y = Sup.Math.Random.sample([-bounds.width / 2, bounds.width / 2]);
    }
    // Choose randomly the z position on the scene in the range of Asteroids.zPoistions
    z = Sup.Math.Random.integer(Asteroids.zPositions.min, Asteroids.zPositions.max);

    // Set the randomly chosen position to the Asteroid actor
    asteroid.setLocalPosition(x, y, z);

    // Report the asteroid size
    asteroid.getBehavior(AsteroidBehavior).sizeClass = "big";
  }

To make it work without bug we need to init a variable in the Asteroid script, inside the class AsteroidBehavior :

class AsteroidBehavior extends Sup.Behavior {
  sizeClass: string;
[...]
Game.updateTimer

The updateTimer function use a certain amount of time in frames and convert it to second and minutes. We kept the default setting of Sueprpowers, 60 frames per seconds.

  // Update HUD timer in the Game scene
  export function updateTimer(time: number){
    Sup.log("one second (60 frames) of", time, "frame left."); // Debug log
    // We convert frames in minutes and seconds
    let minutes = Math.floor((time / 60) / 60);
    let seconds = Math.floor((time / 60) % 60);
    // We get The Timer actor from the Game scene than we store in a timer variable
    let timer = Sup.getActor("HUD").getChild("Timer");
    // Update the text Renderer with the new time converted automatically as a string
    // For the last 10 seconds we need to add a 0 to keep 2 numbers in the timer
    if (seconds < 10) {
      timer.textRenderer.setText(minutes + ":0" + seconds);
    }
    else {
      timer.textRenderer.setText(minutes + ":" + seconds);
    }
  }

Note : To try to launch the game and see the behavior of the timer and the HUD, we need to set the game nameIndex to 0.

  export let nameIndex: number = 0;  // Temporary allocation
Game.gameOver

We will comme back to this function when we will work on the menu, for now we can just log a string that will loop in the console to say the timer is out.

Note : the parameter with ? mean that it is an optional parameter.

  // Close game scene and return menu game over screen
  export function gameOver(winner?: string){
    Sup.log("Game Over");
  }

Ok, we now have a basic game structure, if we do test and launch the game we have a timer working and the asteroids and ship on the game, but apart for the timer and the displaying of alien ship and asteroids nothing happen yet, for animation, we need to write individual behavior of this objects. We do that in the next chapter.

<-- back to chapter 4 -- go to chapter 6 -->