2 Know your Enemies



Send in the ghosts

Ghostly objects

In the last lecture we have already learned about arrays to store numbers. We used them to store our level design.

Wouldn`t it be great if we could store our in-game enemies – in our case the ghosts – also in an array? Then we could loop thorugh the ghost array and update all ghosts positions etc.


Programming with objects

In order to make ghost storable in an array we need to define a "ghost object". This object holds all attributes of our ghosts:

var ghost = {
    posX: 5,
    posY: 3,
    speedX: 1,
    speedY: 0  
  }
  

Arrays: storing objects (and other data)

Then we can create an empty array and append our new ghost object to the array:

  var allGhosts = [];
  allGhosts.push(ghost);
  

Looping through arrays

Now we can easily iterate over all ghosts and update their positions. Of course in this first example, for now just one ghost lives in our array.

  
    allGhosts.forEach((g) => {
      g.posX += g.speedX;
      g.posY += g.speedY;
      ellipse(g.posX, g.posY, 30, 30);
    });
    
// A simple first version of a Pac Man clone

// Player (Pac-Man) variables
xPos = 8;                           // Grid position X (not pixels!)
yPos = 5;                           // Grid position Y (not pixels!)
diameter = 40;                      // Pac-Man size in pixels
stepSize = 40;                      // Size of one grid cell in pixels
openMouth = true;                   // Mouth animation state
direction = "right";                // Current facing direction

// Level layout (1 = wall, 0 = empty path)
// 15x10 grid represented as 1D array
var levelMap = [
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
  1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1,
  1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
  0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0,
  0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0,
  1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
  1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1,
  1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
]

// Ghost object with properties
var ghost = {
  posX: 5,                          // Grid position X
  posY: 3,                          // Grid position Y  
  speedX: 1,                        // Movement direction X (-1 or 1)
  speedY: 0                         // Movement direction Y (-1 or 1)
}

// Array to hold multiple ghosts
var allGhosts = [];
allGhosts.push(ghost);              // Add first ghost to array

function setup() {
  createCanvas(600, 400);
  background(255);
  ghostIMG = loadImage('ghost-small.png');  // Load ghost sprite
  drawMap();                                 // Draw maze walls
  drawAvatar(xPos, yPos, direction);         // Draw Pac-Man
  frameRate(20);                             // Slow down animation
}

function draw() {
  background(255);                  // Clear screen
  drawMap();                        // Redraw maze
  drawAvatar(xPos, yPos, direction); // Redraw Pac-Man
  fill(0);
  
  // Update each ghost
  allGhosts.forEach((g) => {
    // Bounce off walls (simple AI)
    if (checkObstacle("left", g.posX, g.posY) == true || checkObstacle("right", g.posX, g.posY) == true) {
        g.speedX = -g.speedX;       // Reverse direction
    }
    g.posX += g.speedX;             // Move ghost
    g.posY += g.speedY;
    
    // Draw ghost at grid position converted to pixels
    image(ghostIMG, g.posX * stepSize, g.posY * stepSize, 40, 40);
    
    // Check collision with player
    if (g.posX == xPos && g.posY == yPos){
      noLoop();                     // Game over - stop animation
    }
  });
}

function keyPressed() {
  openMouth = !openMouth;           // Toggle mouth animation
  
  // WASD controls
  if (key == "a") {                 // Move left
    if (checkObstacle("left", xPos, yPos) == false) {
      xPos -= 1;
      direction = "left";
    }
  }
  if (key == "d") {                 // Move right
    if (checkObstacle("right", xPos, yPos) == false) {
      xPos += 1;
      direction = "right";
    }
  }
  if (key == "w") {                 // Move up
    if (checkObstacle("up", xPos, yPos) == false) {
      yPos -= 1;
      direction = "up";
    }
  }
  if (key == "s") {                 // Move down
    if (checkObstacle("down", xPos, yPos) == false) {
      yPos += 1;
      direction = "down";
    }
  }
}

// Check if there's a wall in given direction
function checkObstacle(dir, x, y) {
  obstacle = false;
  linearPos = y * 15 + x;           // Convert 2D to 1D array index
  
  if (dir == "left") {
    if (levelMap[linearPos - 1] == 1) {     // Check left cell
      obstacle = true;
    }
  }
  if (dir == "right") {
    if (levelMap[linearPos + 1] == 1) {     // Check right cell
      obstacle = true;
    }
  }
  if (dir == "up") {
    if (levelMap[linearPos - 15] == 1) {    // Check cell above (15 = row width)
      obstacle = true;
    }
  }
  if (dir == "down") {
    if (levelMap[linearPos + 15] == 1) {    // Check cell below
      obstacle = true;
    }
  }
  return obstacle;
}

// Draw the maze from levelMap array
function drawMap() {
  fill(255, 127, 80);               // Orange color for walls
  stroke(255);
  strokeWeight(3);
  rectMode(CENTER);
  
  for (var x = 0; x < 15; x++) {
    for (var y = 0; y < 10; y++) {
      var tst = levelMap[y * 15 + x];       // Get cell value
      if (tst == 1) {                       // 1 = wall
        rect(x * stepSize + (stepSize / 2), y * stepSize + (stepSize / 2), stepSize, stepSize);
      } else if (tst == 2) {                // 2 = dot (not used yet)
        ellipse(x * stepSize + (stepSize / 2), y * stepSize + (stepSize / 2), 10, 10);
      }
    }
  }
}

// Draw Pac-Man at grid position facing given direction
function drawAvatar(x, y, direction) {
  // Convert grid to pixel coordinates
  x = x * stepSize + (stepSize / 2);
  y = y * stepSize + (stepSize / 2);
  
  fill(240, 240, 0);                // Yellow
  noStroke();
  
  // Mouth animation
  if (openMouth == true) {
    openAngle = 1;                  // Wide open (radians)
  } else {
    openAngle = 0.1;                // Almost closed
  }
  
  ellipse(x, y, diameter, diameter); // Draw body
  fill(255);                         // White for mouth
  
  // Draw mouth as pie slice facing correct direction
  if (direction == "right") {
    arc(x, y, diameter + 1, diameter + 1, -openAngle, openAngle, PIE);
  } else if (direction == "left") {
    arc(x, y, diameter + 1, diameter + 1, -openAngle - PI, openAngle - PI, PIE);
  } else if (direction == "up") {
    arc(x, y, diameter + 1, diameter + 1, -openAngle - PI / 2, openAngle - PI / 2, PIE);
  } else if (direction == "down") {
    arc(x, y, diameter + 1, diameter + 1, -openAngle + PI / 2, openAngle + PI / 2, PIE);
  }
}
    

Ghost town
Can you fill the array with 4 ghost objects on different positions moving to different directions?




Detecting collisions

A Simple Way to Detect Collisions

As we know the coordinates of all ghosts and our avatar in the matrix of the 2D game world, we can simply compare them every frame. Just like we did with the avatar and the walls in the last chapter.
If the coordinates of at least one ghost are the same as our avatar, there must be a collision as they sit on the same square.

Inside the loop iterating over all ghost objects we can write:

    if (g.posX == xPos && g.posY == yPos){ // test if x/y coordinates of ghosts are the same as avatar
        noLoop();   // stop the game
    }
    

Here`s a version of our little Pac Man clone that detects the collisions with four ghosts.




Using external libraries to make our lifes easier

However, checking coordinates of objects in a matrix is not enough for every type of game. Sometimes more precision or flexibility is needed.

Collision detecton in JS

p5.play library
Examples

Upload and load the library in HTML:

 <script src="p5.play.js" type="text/javascript"> </script >" 

Define Sprites:

s = createSprite(300, 350, 50, 50);

Move Sprites:

s.velocity.x = -10;
      s.velocity.y = 0;

Load images in sprites:

let img = loadImage('spaceship.png');
    s.addImage(img);

Check collision of sprites.