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
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.