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)
- Coins (folder)
- 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);
menu.ts
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);