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.