Superpowers Game Development Series #5

SUPER PACMAN

Chapter 9 : Scripting pacman behavior

We write the pacman script.

Pacman behavior

We need to define all the methods our pacman will need to interact with the world objects, moving inside the walls and eating ghosts, coins and fruits. It define also the behavior when the pacman is eaten and respawn.

Datas

We initialize the variables with their types.

class PacmanBehavior extends Sup.Behavior {
  // Spawn position in the current level
  public spawnPosition: Sup.Math.Vector2;
  // Current position in the maze
  public position: Sup.Math.Vector2;
  // Next direction the pacman will follow
  private nextDirection: string;
  // Current direction of the pacman
  private currentDirection: string;
  // Time in frame before respawn
  private respawnCooldown: number;
[...]
Awakening and starting

We set the datas of the pacman when the game start for the first time.

[...]
  awake() {
    // Give global access to this actor behavior
    Global.pacman = this;
  }

  start(){
    // Set the current position to the actor in the scene
    this.actor.setPosition(this.position);
    // Multiply the position by ten to work only with integer numbers
    this.position.multiplyScalar(10);
  }
[...]
Get small coin

This method check if there is a coin when the pacman reach the center a tile. We also need a method that check if this position is one of the random position for the fruit to appear, if it is the case, we give the information as now the position is available for a fruit.

[...]
  getCoinAt(column:number, row:number){
    // Loop through all the small coins of the level
    for(let coin of Global.coinsList.small){
      // If the position in x and the position in y of the small coin is the same than the position of the pacman
      if(coin.getX() === column && coin.getY() === row){
        // Get the index of this actor from the coin list
        let coinIndex = Global.coinsList.small.indexOf(coin);
        // Remove this index from the coin list
        Global.coinsList.small.splice(coinIndex, 1);
        // Remove one coin from the total coin counter
        Global.coins.small--;
        // Call the function getFruitAvailablePosition to check if it is also a fruit position
        this.getFruitAvailablePosition(column, row);
        // Then destroy the coin actor (and sprite renderer)
        coin.destroy();
        // Add score for this coin
        Global.game.updateScore(Global.points.coin);
        // Add a coin to the statistic of coin eaten
        Global.coinsEatens++;
      }
    }
  }

  getFruitAvailablePosition(column:number, row:number){
      // Loop through all the random positions for the fruits to appear
      for(let position of Global.fruitsRandomPositions){
        // If the current pacman position is one of them
        if(position.x === column && position.y === row){
          // Then add this position as a new vector in the available position list
          Global.fruitsAvailablePositions.push(new Sup.Math.Vector2(column, row));
        }
      }
    }
[...]
Get big coin

This method is mostly the same than the previous one, except than when a big coin is taken, the vulnerability of the ghosts switch.

[...]
  getBigcoinAt(column:number, row:number){
    // Loop through all the big coins of the level
    for (let bigcoin of Global.coinsList.big) {
      // If the position in x and the position in y of the big coin is the same than the position of the pacman
      if (bigcoin.getX() === column && bigcoin.getY() === row) {
        // Get the index of this big coin from the list
        let bigcoinIndex = Global.coinsList.big.indexOf(bigcoin);
        // Remove the big coin from the list of big coins
        Global.coinsList.big.splice(bigcoinIndex, 1);
        // Decrease by one the number of total coin counter in the level
        Global.coins.big--;
        // Destroy the bigcoin actor (and sprite renderer)
        bigcoin.destroy();
        // Add score for this big coin
        Global.game.updateScore(Global.points.bigcoin);
        // Add a coin to the statistic of coin eaten
        Global.coinsEatens++;
        // Loop through all the ghosts from the list
        for(let ghost of Global.ghosts){
          // Call a method from the ghost behavior to set their vulnerability
          ghost.setVulnerabilityOn();
        }
      }
    }
  }
[...]

To avoid an error we can set an empty function in the ghost script, we will complete it in the next chapter.

class GhostBehavior extends Sup.Behavior {
[...]
  setVulnerabilityOn(){}
[...]
Get fruit

This method check if there is a fruit to take in the pacman current position.

[...]
  getFruitAt(column:number, row:number){
    // Loop through all the fruits from the list
    for (let fruit of Global.fruits){
      // If the fruit position are the same than the current pacman position
      if(fruit.position.x === column && fruit.position.y === row){
        // Remove the fruit from the list
        Global.fruits.splice(Global.fruits.indexOf(fruit), 1);
        // Call a local function of the fruit script
        fruit.eaten();
      }
    }
  }
[...]

To avoid error we will need to initialize the fruit script and the die function.

class FruitsBehavior extends Sup.Behavior {
  public position: Sup.Math.Vector2;
[...]
Movement check

This method check a tile and return true only if the pacman is aligned on the grid and can

[...]
  canMove(moveX:number, moveY:number){
    // If the pacman position is not centered on the grid, don't check the tile, return false
    if (moveX !== 0 && this.position.y%10 !== 0 || moveY !== 0 && this.position.x%10 !==0) return false;

    // Initialize cursor coordinates on tile to check   
    let tileX:number; let tileY:number;
    // The cursor origin we check on a tile is left, down of a tile, we change the cursor position :
    if(moveX === -1){
      // When going to left move the origin cursor to right position and move it to the next tile on the left
      tileX = Math.floor((this.position.x+9)/10)+moveX;
    }
    else {
      // Move the cursor to the next tile on the right
      tileX = Math.floor(this.position.x/10)+moveX;
    }
    if(moveY === -1){
      // When going to left move the origin cursor to up position and move it to the next tile up
      tileY = Math.floor((this.position.y+9)/10)+moveY;
    }
    else {
      // Move the cursor to the next tile up
      tileY = Math.floor(this.position.y/10)+moveY;
    }
    // If the tile checked is empty in tileMap, then return true, else return false
    if (Global.game.tileMap.getTileAt(Level.layers.walls, tileX, tileY) === -1) return true;
    else return false;
  }
[...]
Set direction animation

This method update the current direction and the moving sprite of the pacman.

[...]
  setDirection(){
    // Next direction become current direction
    this.currentDirection = this.nextDirection;
    // Change the animation with the current direction
    this.actor.getChild("Move").spriteRenderer.setAnimation("go"+this.currentDirection);
  }
[...]
death

This method update the life of the pacman, hide the actor and create a new actor which play the death animation.

[...]
  die(){
    // Decrease by one the pacman lifes
    Global.pacmanLifes--
    // Update life from HUD
    Global.game.updateLife();
    // Set the pacman actor invisible
    this.actor.setVisible(false);
    // Create a new actor called Death
    let death = new Sup.Actor("Death");
    // Set the current position to the death actor
    death.setPosition(this.actor.getPosition());
    // Add a sprite renderer component and play once the death animation
    new Sup.SpriteRenderer(death).setSprite("Pacman/Death").setAnimation("death", false);
    // Set the timer before respawn
    this.respawnCooldown = 10;
  }
[...]
respawn

This method reset the pacman to the spawn position.

[...]
  respawn(){
    // Reset current position to the spawn position
    this.position = this.spawnPosition.clone();
    // Set the current position to the actor in the scene
    this.actor.setPosition(this.position);
    // Multiply the position to work with integer
    this.position.multiplyScalar(10);
    // Reset the movement current and next direction
    this.nextDirection = "";
    this.currentDirection = "";
    // Stop the movement animation
    this.actor.getChild("Move").spriteRenderer.stopAnimation();
    // Set the pacman actor visible
    this.actor.setVisible(true);
  }
[...]
Update loop

The loop check the input of the player, check the tilemap by calling methods for collisions/eating and possibility of movements.

[...]
  update() {
    // If the game is freezed, return to the beginning of the loop
    if(Global.freeze > 0){
      return;
    }

    // If the respawnCooldown is on, decrease it by one each frame
    if(this.respawnCooldown > 0){
      this.respawnCooldown--;
      // When the timer reach 1, call the respawn function
      if(this.respawnCooldown === 1){
        this.respawn();
      }
      return;
    }

    // When the player press a directional key, store it as the next direction.
    if(Sup.Input.wasKeyJustPressed(Global.keys.left)){
      this.nextDirection = "LEFT";
    }

    if(Sup.Input.wasKeyJustPressed(Global.keys.right)){
      this.nextDirection = "RIGHT";
    }

    if(Sup.Input.wasKeyJustPressed(Global.keys.up)){
      this.nextDirection = "UP";
    }

    if(Sup.Input.wasKeyJustPressed(Global.keys.down)){
      this.nextDirection = "DOWN";
    }

    // Check if it is possible to go in the next direction and if yes, set it as the current direction
    if( this.nextDirection === "LEFT" && this.canMove(-1, 0)){    
      this.setDirection();
    }
    else if( this.nextDirection === "RIGHT" && this.canMove(1, 0)){       
      this.setDirection();
    }
    else if( this.nextDirection === "UP" && this.canMove(0, 1)){    
      this.setDirection();
    }
    else if( this.nextDirection === "DOWN" && this.canMove(0, -1)){    
      this.setDirection();
    }

    // Check if it is possible to go in the current direction and if yes keep moving
    if( this.currentDirection === "LEFT" && this.canMove(-1, 0)){    
      this.position.add(-1, 0);
    }
    else if( this.currentDirection === "RIGHT" && this.canMove(1, 0)){    
      this.position.add(1, 0);
    }
    else if( this.currentDirection === "UP" && this.canMove(0, 1)){    
      this.position.add(0, 1);
    }
    else if( this.currentDirection === "DOWN" && this.canMove(0, -1)){    
      this.position.add(0, -1);
    }
    else{
      // Stop the animation movement if the pacman is blocked
      this.actor.spriteRenderer.stopAnimation();
    }

    // Update the position actor with the new position, (divide by ten to get the real unit)
    this.actor.setPosition(this.position.x/10, this.position.y/10);

    // Keep pacman in the game screen on the x axis (tunnel)
    if (this.position.x < 0){
      this.position.x = (Level.size.width * 10)-10;
    }
    if (this.position.x > (Level.size.width * 10)-10){
      this.position.x = 0;
    }

    // If the pacman is centered in the unit gride, check if there is something to eat
    if (this.position.x%10 === 0 && this.position.y%10 === 0){
      // Get the current x and y position, divise by ten, to work with the real unit
      let posX: number = this.position.x/10; 
      let posY: number = this.position.y/10;
       // Check if there is a coin in current position and take it if yes
      this.getCoinAt(posX, posY);
      // Check if there is a bigCoin in current position and take it if yes
      this.getBigcoinAt(posX, posY);
      // Check if there is a fruit in current position and take it if yes
      this.getFruitAt(posX, posY);
    }
  }
}
Sup.registerBehavior(PacmanBehavior);

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

<-- go to chapter 8 -- go to chapter 10 -->