Superpowers Game Development Series #5

SUPER PACMAN

Chapter 12 : Game Source Reference

Project structure

  • Global (script)
  • Game (folder)
    • Game (script)
    • Scene (scene)
  • Menu (folder)
    • Menu (script)
    • Scene (scene)
    • Screens (folder)
      • End (sprite)
      • Start (sprite)
      • Levels (sprite)
    • Buttons (folder)
      • Buttons (sprite)
      • Levels (sprite)
  • Level (folder)
    • Tileset (tile set)
    • Template (folder)
      • Tilemap (tile map)
    • Level1 (folder)
      • Tilemap (tile map)
    • Level2 (folder)
      • Tilemap (tile map)
    • Level3 (folder)
      • Tilemap (tile map)
    • Level4 (folder)
      • Tilemap (tile map)
    • Level5 (folder)
      • Tilemap (tile map)
    • Level6 (folder) // if modding
      • Tilemap (tile map)
  • Pacman (folder)
    • Pacman (script)
    • Move (sprite)
    • Life (sprite)
    • Death (sprite)
  • Ghosts (folder)
    • Ghost (script)
    • Ghost1 (sprite)
    • Ghost2 (sprite)
    • Ghost3 (sprite)
    • Ghost4 (sprite)
    • Vulnerable (sprite)
  • Items (folder)
    • Coins (folder)
      • Small (sprite)
      • Big (sprite)
    • Fruits (folder)
      • Fruits (script)
      • Sprite (sprite)
  • Sounds (folder)
    • Music
    • MenuButton
    • EatCoin
    • EatGhost
    • EatFruit
    • PacmanDeath
  • Font (font)

Source Assets

  • ghosts
    • ghost1.png
    • ghost2.png
    • ghost3.png
    • ghost4.png
    • vulnerable.png
  • items
    • bigcoin.png
    • fruits.png
    • smallcoin.png
  • menu
    • button.png
    • endscreen.png
    • levels.png
    • levelscreen.png
    • startscreen.png
  • pacman
    • death.png
    • life.png
    • move.png
  • scripts
    • fruit.ts
    • ghost.ts
    • game.ts
    • global.ts
    • menu.ts
    • pacman.ts
  • sounds
    • eatcoin.mp3
    • eatfruit.mp3
    • eatghost.mp3
    • menu.mp3
    • music.mp3
    • pacmandeath.mp3
  • font.png
  • tileset.png

Source Code

global.ts
// Super Pacman - Game Development Tutorial #5
// Pax Fabrica - Learn creative development while revisiting video game history
// @MichaelSeyne - mseyne.github.io
// Peer Production Licence - Free and open for peers
// Release Version Finale

// This variable will be used to open a web url in a new window, it is not directly related to the game logic
declare var window;

// A global module
namespace Global {

  // points for each objects the pacman can eat
  export enum points {
        coin = 10,
        bigcoin = 50,
        fruit = 100,
        ghost = 200
       }

  // keyboard's keys the player will use in the game
  export const keys = {
        left: "LEFT",
        right: "RIGHT",
        up: "UP",
        down: "DOWN",
        space: "SPACE",
        exit: "ESCAPE"
       }

  // name of the menu screens
  export const menuScreens = {
    start: "Start",
    levels: "Levels",
    end: "End"
  }

  // number of coins small and big
  export let coins = {
        small: 0,
        big: 0
       }

  // list of all the coins in game, small and big
  export let coinsList = {
        small: [],
        big: []
       }

  // starting life
  export const lifesMax:number = 3;
  // life order indicator for HUD
  export const lifesOrder:boolean[][] = [[false, false, false], [true, false, false], [true, true, false]]

  // the game behavior
  export let game:GameBehavior;
  // the game HUD (game information displayed)
  export let HUD:Sup.Actor;
  // the pacman behavior
  export let pacman:PacmanBehavior;
  // the ghosts behavior in a list
  export let ghosts:GhostBehavior[];
  // the fruits behavior in a list
  export let fruits:FruitsBehavior[];
  // the game time
  export let time:string;
  // the game score
  export let score: number;
  // level chosen to play
  export let currentLevel:string;
  // boolean flag if the game is won or not
  export let won:boolean;
  // number of frame the game stay blocked before continue
  export let freeze: number;

  // the current pacman lifes
  export let pacmanLifes: number;
  // number of coins eaten
  export let coinsEatens: number;
  // number of ghosts eaten
  export let ghostsEaten:number;
  // number of fruits eaten
  export let fruitsEaten:number;
  // fruits status, each position in list is a fruit, if false the fruit is not eaten
  export let fruitsEatenByIndex: boolean[] = [false, false, false, false, false];


  // fruits positions in the level
  export let fruitsRandomPositions:Sup.Math.Vector2[];
  // current available fruits positions in the level
  export let fruitsAvailablePositions:Sup.Math.Vector2[];

  export function startNewGame(){
    // Set datas to default    
    won = undefined;
    ghosts = [];
    coins.small = 0;
    coins.big = 0;
    coinsList.small = [];
    coinsList.big = [];
    fruits = [];
    fruitsRandomPositions = [];
    fruitsAvailablePositions = [];
    fruitsEatenByIndex = [false, false, false, false, false];
    score = 0;
    fruitsEaten = 0;
    ghostsEaten = 0;
    coinsEatens = 0;
    // Start life
    pacmanLifes = lifesMax;

    // Freeze the game for 300 frames before to start
    freeze = 300;

    Sup.Audio.playSound("Sounds/Music");

    // Load the game scene (leave the menu scene)
    Sup.loadScene("Game/Scene");
    // Get the HUD actor for global access
    HUD = Sup.getActor("HUD");
    // Set the current level Tile Map
    Sup.getActor("Level").tileMapRenderer.setTileMap("Levels/"+currentLevel+"/Tile Map");
  }
}


namespace Level {
  // All the different level names  
  export const levels = {
               1:"Level1",
               2:"Level2",
               3:"Level3",
               4:"Level4",
               5:"Level5",
               6:"Level6"
              }

  // The differents layers of the tile map for each level
  export enum layers {
        positions = 0,
        backgroung = 1,
        walls = 2
       }

  // Size of level per unit of 16 pixels
  export enum size {
        width = 26,
        height = 32 
       }

  // Tile set references of the game objects
  export enum tiles {
        smallcoin = 63,
        bigcoin = 64,
        fruits = 65,
        ghost = 66,
        pacman = 67
       }

  // End statistics of the game
  export const endStats = ["Score", "Time", "Ghosts", "Coins", "Fruits", "Lifes"];

  export function set(){
    // Call all the Level functions once 
    setPacman();
    setGhosts();
    setCoins();
    getFruitsRandomPositions();
  }

  // function that check all the tile from the tile map and return the position of the searched tile
  function checkMap(layer:number, tile:number){
    // Loop the number of unit there is on width size
    for(let x = 0; x < size.width; x++){
      // Loop the number of unit there is on height size
      for(let y = 0; y < size.height; y++){
        // Check if the tile in x and y is the tiled looked for
        if(Global.game.tileMap.getTileAt(layer, x, y) === tile){
          // if yes, return the position as a new Vector2
          return new Sup.Math.Vector2(x, y);
        }
      }
    }
  }

  // function that get the start position of pacman on the map
  function setPacman(){
    // Call the checkMap function and set the returned position to the variable
    let spawnPosition = checkMap(layers.positions, tiles.pacman);
    // Give to the pacman actor the spawnposition of this level (constant)
    Global.pacman.spawnPosition = spawnPosition;
    // Copy this position for the current position (variable)
    Global.pacman.position = spawnPosition.clone();
  }

  // function that get the start ghost positions on the map
  function setGhosts(){
    // Index of ghosts, start with the first
    let ghostIndex: number = 0;
    // Loop through all the tile set
    for(let x = 0; x < size.width; x++){
      for(let y = 0; y < size.height; y++){
        // If the current tile x, y is a ghost start position tile
        if(Global.game.tileMap.getTileAt(layers.positions, x, y) === tiles.ghost){
            // If yes, create a new Vector 2 with the position of the tile
            let spawnPosition = new Sup.Math.Vector2(x, y)
            // Give the position to the current ghost Actor as the spawnPosition and the current position
            Global.ghosts[ghostIndex].spawnPosition = spawnPosition;
            Global.ghosts[ghostIndex].position = spawnPosition.clone();
            // Change ghost index to prepare the next one
            ghostIndex++
        }
      }
    }
  }

  function setCoins(){
    // Loop to check all the tile positions from the Tile Map
    for(let x = 0; x < size.width; x++){
      for(let y = 0; y < size.height; y++){
        // If the tile is a fruit or a small coin, add a small coin actor
        if(Global.game.tileMap.getTileAt(layers.positions, x, y) === tiles.smallcoin ||
        Global.game.tileMap.getTileAt(layers.positions, x, y) === tiles.fruits){
          // Add a coin to the total count
          Global.coins.small++
          // Create a new actor
          let coin = new Sup.Actor("smallCoin");
          // Set X and Y position to the actor and a Z position to 10
          coin.setPosition(x, y, 10);
          // Set a new component Sprite renderer to the actor with the smallCoin sprite
          new Sup.SpriteRenderer(coin, "Items/Coins/Small");
          // Add the actor to the list of small coins
          Global.coinsList.small.push(coin);
        }
        // If the tile is a big coin, add a big coin actor
        else if(Global.game.tileMap.getTileAt(layers.positions, x, y) === tiles.bigcoin){
          // Add a coin to the total count
          Global.coins.big++
          // Create a new actor
          let coin = new Sup.Actor("bigCoin");
          // Set X and Y position to the actor and a Z position to 10
          coin.setPosition(x, y, 10);
          // Set a new component Sprite renderer to the actor with the bigCoin sprite
          new Sup.SpriteRenderer(coin, "Items/Coins/Big");
          // Add the actor to the list of big coins
          Global.coinsList.big.push(coin);
        }
      }
    }
  }


  function getFruitsRandomPositions(){
    // Check all the tile positions from the tile map
    for(let x = 0; x < size.width; x++){
      for(let y = 0; y < size.height; y++){
       // if the tile checked is a fruit tile, add it to the fruitsRandomPositions list
       if(Global.game.tileMap.getTileAt(layers.positions, x, y) === tiles.fruits){
         Global.fruitsRandomPositions.push(new Sup.Math.Vector2(x, y));
       }
      }
    }
  }
}
game.ts
class GameBehavior extends Sup.Behavior {
  // We initialize the tile map
  public tileMap: Sup.TileMap;
  // We initialize locally the timer variable
  private time: number; private second: number; private minute: number;

  start() {
    // We set the game behavior globally
    Global.game = this;
    //  We reset the game timer globally and locally
    Global.time = "0"; this.time = 0; this.second = 0; this.minute = 0;
    // We set the tilemap to the current game level
    this.tileMap = Sup.getActor("Level").tileMapRenderer.getTileMap();
    // We prepare the level with the function Level.set()
    Level.set();
  }

  updateScore(points:number){
        // Add the points to the game score
        Global.score += points;
        // Update the HUD score display
        Global.HUD.getChild("Score").textRenderer.setText("SCORE:"+Global.score.toString());
  }

  displayNewScore(position:Sup.Math.Vector3, points:number){
      // Create a new actor score
      let score = new Sup.Actor("score");
      // Add a new component text renderer to the actor with the points as text
      new Sup.TextRenderer(score, points.toString());
      // Add the font Font to the component text renderer
      score.textRenderer.setFont("Font");
      // Give the current position to the score (+0.5 to adapt to the centered origin)
      score.setPosition(position.x+0.5, position.y+0.5, position.z);
      // Destroy the actor score after 1 second
      Sup.setTimeout(1000, function(){score.destroy();});
    }

  updateLife(){
      // Loop the number of maximum lifes the pacman got (3 times) and give the current value to index
      for (let index = 0; index < Global.lifesMax; index++){
        // Get the sprite Renderer component from HUD/Lifes/index actor
        let sprite = Global.HUD.getChild("Lifes").getChild(index.toString()).spriteRenderer;
        // Check the boolean flag from the lifesOrder pattern of the pacman current lifes and current index
        if(Global.lifesOrder[Global.pacmanLifes][index] === true){
          // If the flag is true, set the sprite animation to full
          sprite.setAnimation("full", false);
        }
        else{
          // If the flag is false, set the sprite animation to empty
          sprite.setAnimation("empty", false);
        }
      }
    }

  updateTimer(){
    // convert minute and second to string and set them to variables
    let minute = this.minute.toString(); let second = this.second.toString();
    // If the minutes or seconds are inferior to 10, then add a 0 to the string to keep display consistency 
    if (this.minute < 10){
      minute = "0"+minute;
    }
    if (this.second < 10){
      second = "0"+second;
    }
    // Build the complete string for the current time
    Global.time = minute+':'+second;
    // Display it with the HUD/Timer text renderer
    Global.HUD.getChild('Timer').textRenderer.setText("TIME:"+Global.time);
  }  

  update() {
    // If the freeze counter is on, decrease it from 1 and return to pass the block and repeat 
    if(Global.freeze > 0){
      Global.freeze--
      return;
    }

    // Check if the game is won or not (when the Global.won is not undefined anymore)
    if(Global.won === false || Global.won === true){
      // Load the menu scene and destroy the game scene
      Sup.loadScene("Menu/Scene");
      // Call the function that will load the victory or gameover end screen
      Sup.getActor("Menu").getBehavior(MenuBehavior).setEndscreen();
    }

    // Increase the game timer by one
    this.time++;
    // When the time got 60 frames add 1 second (the game is default set as 60 frames = 1 second)
    if(this.time%60 === 0){
      this.second++;
      // When the second is 60, add 1 minute and reset second to 0 
      if(this.second%60 === 0){
        this.minute++; this.second = 0;
      }
      // Call the updateTimer method every second
      this.updateTimer();
    }

    // Check if the exit key is pressed
    if(Sup.Input.wasKeyJustPressed(Global.keys.exit)){
      // If yes, load the menu scene and destroy the game scene
      Sup.loadScene("Menu/Scene");
    }

    // Check if there is still coins left, if not, the game is won
    if(Global.coins.small === 0 && Global.coins.big === 0){
      Sup.Audio.playSound("Sounds/Music");
      // Set the won flag to true
      Global.won = true;
      // Set frames number freeze counter
      Global.freeze = 100;
    }

    // Check if there is still lifes for pacman, if not, the game is lost
    if (Global.pacmanLifes === 0){
      // Set the won flag to false
      Global.won = false;
    }
  }
}
Sup.registerBehavior(GameBehavior);
class MenuBehavior extends Sup.Behavior {
  // Create a ray caster used to check collision between the mouse and objects of the screen
  private ray = new Sup.Math.Ray;
  // The current screen displayed
  private screen:string;
  // Init The button actor
  private button:Sup.Actor;
  // Init The level buttons actors in a list
  private levelsList:Sup.Actor[];

  awake() {
    // Set the current screen with the start screen
    this.updateScreen(Global.menuScreens.start);
    // Set the button actor to local button variable
    this.button = Sup.getActor("Button");
    // Set the level button actors to an empty list
    this.levelsList = [];
    // Call the function that add all levels buttons actors to the list
    this.getLevelsList();
    // Set the opacity of the levels buttons to default
    this.setLevelOpacityDefault();
  }

  updateScreen(screenName:string){
    // Set the screenName parameter as the current screen
    this.screen = screenName;
    // Get the Screens actor in a variable
    let menuScreens = Sup.getActor("Screens");
    // Loop through all the menu Screen
    for ( let screen in Global.menuScreens){
      // Check if the screen from loop is the same than the current screen
      if (Global.menuScreens[screen] === this.screen){
        // if yes, set the screen visibility to true
        Sup.getActor("Screens").getChild(Global.menuScreens[screen]).setVisible(true);
      }
      else{
        // else, set the screen visibility to false
        Sup.getActor("Screens").getChild(Global.menuScreens[screen]).setVisible(false);
      }
    }
  }

  getLevelsList(){
    // Loop through all the levels name
    for(let level in Level.levels){
      // Get the actor of the level from the loop and add it to the levelsList 
      this.levelsList.push(Sup.getActor("Screens").getChild("Levels").getChild(Level.levels[level]));
    }
  }

  setLevelOpacityDefault(){
    // Loop through all the levels actors from the list
    for (let level of this.levelsList){
      // Set the opacity of the sprite to half
      level.spriteRenderer.setOpacity(0.5);
    }
  }

  setLevelOpacityBright(actor:Sup.Actor){
    // Loop through all the levels actors from the list
    for (let level of this.levelsList){
      // if the actor name is the same than the level name from the loop
      if(actor.getName() === level.getName()){
        // Set the opacity of this level button to full
        level.spriteRenderer.setOpacity(1);
      }
      else{
        // Else, set the opacity of this level button to half
        level.spriteRenderer.setOpacity(0.5);
      }
    }
  }

  setEndscreen(){
    // Update the screen display with the end screen
    this.updateScreen(Global.menuScreens.end);
    // Get the actor of the end screen
    let endScreen: Sup.Actor = Sup.getActor('Screens').getChild("End");
    // If the game won flag is true
    if(Global.won){
      // Set the animation victory of the sprite (without looping the animation)
      endScreen.spriteRenderer.setAnimation("victory", false);
    }
    else{
      // Set the animation game over of the sprite (without looping the animation)
      endScreen.spriteRenderer.setAnimation("gameover", false);
    }

    // Set the statistic datas to update the text display in the end screen
    endScreen.getChild("Score").textRenderer.setText("Score:"+Global.score);
    endScreen.getChild("Time").textRenderer.setText("Time:"+Global.time);
    endScreen.getChild("Ghosts").textRenderer.setText("Ghosts eaten:"+Global.ghostsEaten);
    endScreen.getChild("Coins").textRenderer.setText("Coins eaten:"+Global.coinsEatens);
    endScreen.getChild("Fruits").textRenderer.setText("Fruits eaten:"+Global.fruitsEaten);
    endScreen.getChild("Lifes").textRenderer.setText("Lifes left:"+Global.pacmanLifes);
  }

  update() {
    // Update the position of the raycaster of the mouse inside the camera zone
    this.ray.setFromCamera(Sup.getActor("Camera").camera, Sup.Input.getMousePosition());
    // Give data related to the collision between the mouse and the button to a variable
    let hitButton = this.ray.intersectActor(Sup.getActor("Button"));

    // if the hitButton variable got datas, it is hovered
    if(hitButton.length > 0){
      // If the current screen is the start screen
      if(this.screen === Global.menuScreens.start){
        // Change the animation of the button to hoverPlay
        this.button.spriteRenderer.setAnimation("hoverPlay", false);
        // Change the animation of the start screen
        Sup.getActor("Screens").getChild("Start").spriteRenderer.setAnimation("hover", false);
      }
      // If the current screen is the level selection or the end screen
      else {
        // Change the animation of the button to hoverReturn
        this.button.spriteRenderer.setAnimation("hoverReturn", false);
      }
      // If the button is pressed while hovering it
      if(Sup.Input.wasMouseButtonJustPressed(0)){
        Sup.Audio.playSound("Sounds/MenuButton");
        // call the updateScreen() function with a parameter depending of the current screen
        switch(this.screen){
          case "Start":
            // If the current screen was start screen, go to levels screen
            this.updateScreen(Global.menuScreens.levels);
            break;
          case "Levels":
            // If the current screen was levels screen, go to start screen
            this.updateScreen(Global.menuScreens.start);
            break;
          case "End":
            // If the current screen was end screen, go to start screen
            this.updateScreen(Global.menuScreens.start);
            break;
        }
      }
    }
    else{
      // If the button is not hovered, reset animation to unhoverPlay is the current screen is Start
      if(this.screen === "Start"){
        this.button.spriteRenderer.setAnimation("unhoverPlay");
        // Change the animation of the start screen
        Sup.getActor("Screens").getChild("Start").spriteRenderer.setAnimation("unhover", false);
      }
      // Else reset animation to unhoverReturn for the others screens
      else {
        this.button.spriteRenderer.setAnimation("unhoverReturn");
      }
    }

    // If the current screen is levels screen
    if (this.screen === "Levels"){
    // Give data related to the collision between the mouse and all the levels buttons to a variable
      let hitLevel = this.ray.intersectActors(this.levelsList);
      if (hitLevel.length > 0){
        // if there is hitLevel data, a button level is hovered, call funtion to set a light opacity
        this.setLevelOpacityBright(hitLevel[0].actor);
        // if the current button is clicked
        if(Sup.Input.wasMouseButtonJustPressed(0)){
          // If the level clicked is the level 6 (which is a tutorial link than we will change later)
          if (hitLevel[0].actor.getName() === "Level6"){
            // Open a new window with the url
            window.open("http://mseyne.github.io/");
          }
          // Else, for all others levels
          else{
            // Set the current level to the level which have been clicked
            Global.currentLevel =  hitLevel[0].actor.getName();
            // Start a new game
            Global.startNewGame();
          }
        }
      }
      // if no one is hovered set opacity false to all levels buttons
      else{
        this.setLevelOpacityDefault();
      }
    }
  }
}
Sup.registerBehavior(MenuBehavior);
pacman.ts
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;

  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);
  }

  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){
        Sup.Audio.playSound("Sounds/EatCoin");
        // 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));
        }
      }
    }

  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) {
        Sup.Audio.playSound("Sounds/EatGhost");
        // 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();
        }
      }
    }
  }

  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){
        Sup.Audio.playSound("Sounds/EatFruit");
        // Remove the fruit from the list
        Global.fruits.splice(Global.fruits.indexOf(fruit), 1);
        // Call a local function of the fruit script
        fruit.eaten();
      }
    }
  }

  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;
  }

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

  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);
    // Timer before respawn
    this.respawnCooldown = 10;
  }

  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.spriteRenderer.stopAnimation();
    // Set the pacman actor visible
    this.actor.setVisible(true);
  }

  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);
ghost.ts
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;

  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);
  }

  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());
  }

  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(){
    // 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();
  }

  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() {
    // 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){
        Sup.Audio.playSound("Sounds/EatGhost");
        // The ghost is eaten
        this.eaten();
        // The game freeze for 20 frames
        Global.freeze = 20;
      }
      // if the ghost is not vulnerable
      if(!this.vulnerable){
        Sup.Audio.playSound("Sounds/PacmanDeath");
        Sup.setTimeout(1000, function(){Sup.Audio.playSound("Sounds/Music")});
        // 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.ts
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;

  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();
  }

  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);
  }

  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
    Global.fruitsEaten++;
    // Destroy this actor
    this.actor.destroy();
  }

  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);

<-- go to chapter 11 -- go back to summary -->