Superpowers Game Development Series #5

SUPER PACMAN

Chapter 10 : Scripting ghost and fruit behaviors

Ghost Behavior

We can now write the ghost script. The script is the behavior of one ghost. Each ghost actor have one instance of this behavior for itself.

Datas

We initialize the differents data specific to the ghost owner of this behavior.

class GhostBehavior extends Sup.Behavior {
  // The starting position of the ghost (in jail)
  public spawnPosition: Sup.Math.Vector2;
  // The current position of this ghost
  public position: Sup.Math.Vector2;
  // Timer before being free to leave the jail
  private freedomCoolDown: number;
  // The current moving direction of this ghost
  private moveDirection: string;
  // Flag checking if the jail door is open for this ghost
  private doorOpen: boolean;
  // If the ghost have vulnerability
  private vulnerable: boolean;
  // Timer before the ghost loose vulnerability
  private vulnerabilityCooldown: number;
[...]
Awakening and start

We set the datas of this ghost behavior, the difference between awake and start is related to the loading of other scripts of the game, per example the ghosts list need to be complet before the start to be used in the level setting. Then the actor position can be set only after, in start method, when the level finished to be set.

[...]  
  awake() {
    // add this behavior to the ghosts list
    Global.ghosts.push(this);
    // close the door of the jail
    this.doorOpen = false;
    // Choose a random time before to be free from jail
    this.freedomCoolDown = Sup.Math.Random.integer(50, 300);
    // We start not vulnerable
    this.vulnerable = false;
  }

  start(){
    // Set the current position of this actor
    this.actor.setPosition(this.position);
    // Multiply the current position by 100 to work only with integer an not float number
    this.position.multiplyScalar(100);
  }
[...]
Vulnerability

This methods switch vulnerability flag of the ghost, it telling us if the ghost can be eaten by the pacman or not. The player know visually with the sprite changement.

[...]
  setVulnerabilityOn(){
    this.vulnerable = true;
    // Timer before to loose vulnerability
    this.vulnerabilityCooldown = 500;
    // Set the ghost sprite of vulnerability
    this.actor.spriteRenderer.setSprite("Ghosts/Vulnerable");
  }

  setVulnerabilityOff(){
    this.vulnerable = false;
    // Change back sprite to normal
    this.actor.spriteRenderer.setSprite("Ghosts/Ghost"+this.actor.getName());
  }
[...]
Jail

This methods set the ghost to leave or return in the jail.

[...]
  leaveJail(){
      // Check the tile Up and tile Down of the current position
      let tileUp = this.position.y/100 + 1;
      let tileDown = this.position.y/100 - 1;
      // If the tile up or down of the ghost is a door, then go on this tile and get out
      if(Global.game.tileMap.getTileAt(Level.layers.walls, this.position.x / 100, tileUp) === 54) this.moveDirection = "UP";
      else if(Global.game.tileMap.getTileAt(Level.layers.walls, this.position.x / 100, tileDown) === 58) this.moveDirection = "DOWN";
  }

  goToJail(){
    // Set the position of the ghost to the starting position
    this.position = this.spawnPosition.clone();
    // Then set the new position to the ghost actor
    this.actor.setPosition(this.position);
    // Stop the movement direction
    this.moveDirection = "";
    // Close the door of the jail
    this.doorOpen = false;
    // Multiply the new position to 100 to use them anew in the script
    this.position.multiplyScalar(100);
  }
[...]
Eaten

This method is called when the ghost is eaten by the pacman.

[...]  
  die(){
    // Set the timer before to be free again
    this.freedomCoolDown = 600;
    // Update the score of the player with ghost points
    Global.game.updateScore(Global.points.ghost);
    // Display the new points to the current ghost position
    Global.game.displayNewScore(this.actor.getPosition(), Global.points.ghost);
    // Add statistic, number of ghost eaten in total
    Global.ghostsEaten++;
    // Return this ghost to jail
    this.goToJail();
  }
[...]
Movement

The method canMove check if this ghost can move on the next tile, the method chooseDirection take all possible directions from the current position and add them in a list of possible directions. Then from this list, choose randomly one.

[...]
  canMove(moveX:number, moveY:number){
    // get the tiles x and y for the next tile of the current position and direction
    let tileX = this.position.x/100 + moveX;
    let tileY = this.position.y/100 + moveY;
    // If the next tile is not a wall, return true 
    if(Global.game.tileMap.getTileAt(Level.layers.walls,tileX, tileY) === -1){
      return true;
    }
    // else return false
    return false;
  }

  chooseDirection(){
    // Initialize a new list of possible directions
    let availableDirections:string[] = [];

    /*
    - Check if the next tile is free to go in all chooseDirection
    - Return false, if the direction checked is the one from which the ghost come from
    - If it is possible to move on the next tile and the ghost don't come from this direction, add the direction to the list.
    */
    if(this.canMove(1, 0) && this.moveDirection !== "LEFT"){
      availableDirections.push("RIGHT");
    }
    if(this.canMove(-1, 0) && this.moveDirection !== "RIGHT"){
      availableDirections.push("LEFT");
    }
    if(this.canMove(0, 1) && this.moveDirection !== "DOWN"){
      availableDirections.push("UP");
    }
    if(this.canMove(0, -1) && this.moveDirection !== "UP"){
      availableDirections.push("DOWN");
    }
    // then choose randomly a new direction from the list
    this.moveDirection = Sup.Math.Random.sample(availableDirections);
    // Don't change animation if movement is undefined (in the case the ghost is stuck and have to go back)
    if (this.moveDirection === undefined) return;
    // Set the new walk animation now related to the new direction
    this.actor.spriteRenderer.setAnimation("go"+this.moveDirection);
  }
[...]
Update loop

The update loop contain all the checking used for the ghost to go out from jail and navigate in the maze, calling the related methods when needed.

[...]
  update() {
    // Skip the loop if the freeze timer is on
    if(Global.freeze > 0){
      return;
    }

    // Stay in jail as long as the freedom timer is on
    if(this.freedomCoolDown > 0){
      // When the timer reach 1
      if(this.freedomCoolDown === 1){
        // Open the door and set off vulnerability
        this.doorOpen = true;
        this.setVulnerabilityOff();
      }
      // Decrease by one each frame
      this.freedomCoolDown--
    }

    // Keep moving in the current direction
    if(this.moveDirection === "RIGHT"){
      this.position.x += 5;
    }
    if(this.moveDirection === "LEFT"){
      this.position.x -= 5;
    }
    if(this.moveDirection === "UP"){
      this.position.y += 5;
    }
    if(this.moveDirection === "DOWN"){
      this.position.y -= 5;
    }

    // Check if the ghost change direction when centered in the grid
    if(this.position.x%100 === 0 && this.position.y%100 === 0){
      this.chooseDirection();
      // If the door is open, leave the jail
      if(this.doorOpen){
        this.leaveJail();
      }
    }

    /*
    - check if there is a contact with the pacman    
    - if contact, something different happen according to the ghost vulnerability
    - if the distance between the pacman and the ghost is inferior to half the size of a case
    */
    if (Math.abs(this.position.x/10 - Global.pacman.position.x) < 5 && Math.abs(this.position.y/10 - Global.pacman.position.y) < 5){
      // if the ghost is vulnerable
      if(this.vulnerable){
        // The ghost is eaten
        this.eaten();
        // The game freeze for 20 frames
        Global.freeze = 20;
      }
      // if the ghost is not vulnerable
      if(!this.vulnerable){
        // the pacman die
        Global.pacman.die();
        // All the ghosts return to jail
        // Loop through the ghosts list
        for(let ghost of Global.ghosts){
          // Call the method to send the ghost in jail
          ghost.goToJail();
          // Set a timer before being free from jail
          ghost.freedomCoolDown = Sup.Math.Random.integer(200, 400);
        }
        // The game freeze for 200 frames
        Global.freeze = 200;
      }
    }

    // If vulnerable, decrease by one cooldown timer
    if (this.vulnerable){
      this.vulnerabilityCooldown--;
      // If the timer is inferior to 150 frames, start actor blinking
      if (this.vulnerabilityCooldown < 150){
        // blinking magic :)
        if (this.vulnerabilityCooldown % 40 < 8 && this.vulnerabilityCooldown % 40 > -8){
          // Set the sprite animation blink
          this.actor.spriteRenderer.setAnimation("blink");
        }
      }
      // If the timer reach 1, set off the vulnerability
      if (this.vulnerabilityCooldown === 1) {
        this.setVulnerabilityOff();
      }
    }

    // Stay in the maze when go out of the screen
    if(this.position.x < 0) {
      this.position.x = (Level.size.width-1) * 100;
    }
    if(this.position.x > (Level.size.width-1) * 100) {
      this.position.x = 0;
    }

    // Update ghost actor position
    this.actor.setPosition(this.position.x / 100, this.position.y / 100);
  }
}
Sup.registerBehavior(GhostBehavior);

Fruit Behavior

We write the Fruit script.

Datas

Initialize this specific fruit datas.

class FruitsBehavior extends Sup.Behavior {
  // Position of the fruit
  public position: Sup.Math.Vector2;
  // Flag check if fruit have spawned or not
  private spawn: boolean;
  // Timer before to spawn
  private spawnCooldown: number;
  // Fruit number and name
  private name: string;
  // Previous fruit number and name
  private previousFruit: number;
[...]
Awakening

Set the datas for this specific fruit when awake the first time.

[...]
  awake() {
    // Add this fruit to the fruits list
    Global.fruits.push(this);
    // Set this fruit as not spawned
    this.spawn = false;
    // Store the name of this fruit (a number)
    this.name = this.actor.getName();
    // Get the name of the previous fruit before this one (integral number - 1)
    this.previousFruit = parseInt(this.name) - 1;
    // Get the position of the actor
    this.position = this.actor.getPosition().toVector2();
  }
[...]
start spawn and set position

Methods setting the spawn timer and choosing a random position.

[...]
  startSpawn(){
    // Set spawn flag as true
    this.spawn = true;
    // Set timer before spawn
    this.spawnCooldown = 500;
  }

  setPosition(){
    // Get a random position from available positions
    this.position = Sup.Math.Random.sample(Global.fruitsAvailablePositions);
    // Set the position to the actor
    this.actor.setPosition(this.position);
  }
[...]
death

Method giving point to the player when fruit is eaten and destroy the actor.

[...]
  eaten(){
    // Add points to the player
    Global.game.updateScore(Global.points.fruit);
    // Display the point to the current position
    Global.game.displayNewScore(this.actor.getPosition(), Global.points.fruit);
    // Set true, as the fruit eaten status
    Global.fruitsEatenByIndex[parseInt(this.name)] = true;
    // Add 1 to total fruits eaten statistic
    Global.fruitsEaten++;
    // Destroy this actor
    this.actor.destroy();
  }
[...]
update loop

A loop that check if the previous fruit have been eaten and if there is space in the maze to spawn. When it is time (free space and previous fruit is eaten) then spawn.

[...]  
update() {
    // if this fruits have not spawn and if there is an available positions
    if (this.spawn === false && Global.fruitsAvailablePositions.length > 0 ){
      // If there is no previous fruit before this one
      if (this.previousFruit === -1){
        // Call the method to start to spawn
        this.startSpawn();
      }
      // Else, check if the previous fruit have already been eaten
      else if(Global.fruitsEatenByIndex[this.previousFruit] === true){
        // If yes, call the method to start to spawn
        this.startSpawn();
      }
    }

    // If the spawn flag is true
    if (this.spawn === true){
      // Decrease spawn cooldown by one
      this.spawnCooldown--
      if(this.spawnCooldown === 1){
      // when timer is 1, spawn actor to an available position in the maze
      this.setPosition();
      }
    }
  }
}
Sup.registerBehavior(FruitsBehavior);

We can download the superpowers project v10 from this chapter here.

<-- go to chapter 9 -- go to chapter 11 -->