Collision Mechanics
Learn how collision mechanics work and how to use it.
Run the cell below and move the astronaut into the UFO.
Notice how the astronaut(the player) is destroyed when you collide with the npc? This is done with collision mechanics.
Collision mechanics dictate when and what happens between two objects in a game. They can be broken down into two parts:
- Detecting collisions between two objects(whether or not they are touching)
- Responding to that collision(Take damage, kill player, update score, etc.)
Enemy.js
In our game engine, there is a file called Enemy.js that we use for collision mechanics. It is a class with multiple methods that draws the enemy and checks for collisions with the player. However, it isn’t enough for our games. So we have to create new classes ourselves that extend the Enemy class and change it’s code to fit our needs.
Case Study 1: Astronaut-killing UFO
When making your game with collision-mechanics, create a new file that will handle the collisions. You can organize it with the rest of your game files, or in a subdirectory if you have other custom-assets. It can be named whatever, just make sure to follow PascalCase(no spaces, first letter capitalized). For this example, we will call ours ExampleEnemy.js.
%%js
import Enemy from '@assets/js/GameEnginev1/essentials/Enemy.js';
import Player from '@assets/js/GameEnginev1/essentials/Player.js';
class ExampleEnemy extends Enemy {
constructor(data = null, gameEnv = null) {
super(data, gameEnv);
}
// Bc we are extending Enemy.js, all of it's methods can/will be used.
// However, whenever handleCollisionEvent() is called it will run what we want it to do rather than the default.
handleCollisionEvent() {
var player = this.gameEnv.gameObjects.find(obj => obj instanceof Player); // searches for the 'Player' object in the game so it can kill it
console.log("Collision has occurred, player has been destroyed."); // console message for logging purposes
player.destroy(); // A method that will destroy the player. Because we are calling it here, the player will be destroyed whenever it collides with this enemy.
this.playerDestroyed = true; // A variable to check if the player has been destroyed. Used by other methods to check whether or not the player has been killed yet
}
}
export default ExampleEnemy;
Now in your level code, create your NPCs the way you would normally do it, but instead of using the Npc class, use your new ExampleEnemy class.
this.classes = [
{ class: GameEnvBackground, data: bgData },
{ class: Player, data: playerData },
{ class: ExampleEnemy, data: npcData } // Normally, it would be { class: Npc, data: npcData }, but we are changing it to use collision mechanics.
];
Thats it! Now run your level, and it should operate similar to the one below.
Case Study 2: Patrolling Guard
Now, a stationary enemy has it’s uses, but we can do more. If we were able to change the position of the enemy in a loop, we could make it move. But how do we make it move continuously? We can use the update() method. This method is called continuously in the game loop, so we can change the position of the enemy in this method. We can change it by a set velocity that we can define in the contstructor. And this child of Enemy.js can be named Guard.js. Now, whenever adding this file to your levels, all the places where ExampleEnemy was used can be replaced with Guard.
%%js
import Enemy from '@assets/js/GameEnginev1/essentials/Enemy.js';
import Player from '@assets/js/GameEnginev1/essentials/Player.js';
class Guard extends Enemy {
constructor(data = null, gameEnv = null) {
super(data, gameEnv);
this.velocity.y = -3 // Set an initial vertical velocity for the guard in the constructor. Because it is in the constructor, this velocity will be set as soon as the level starts.
}
/**
* Override the update method to handle collision detection and update the vertical velocity
*/
update() {
// Update begins by drawing the object
this.draw();
if (this.spriteData && typeof this.spriteData.update === 'function') {
this.spriteData.update.call(this);
}
// Check for collision with the player
if (!this.playerDestroyed && this.collisionChecks()) {
this.handleCollisionEvent();
}
this.position.y += this.velocity.y; // Change position based on the velocity. Bc this is in update(), this will be called continuously.
// Ensure the object stays within the canvas boundaries
this.stayWithinCanvas();
}
handleCollisionEvent() {
var player = this.gameEnv.gameObjects.find(obj => obj instanceof Player);
console.log("Collision has occurred, player has been destroyed.");
player.destroy();
this.playerDestroyed = true;
}
}
export default Guard;
But why isn’t it working? Well, the guard is moving up, but we forgot something. We never added any logic to reverse the velocity when it hits the top of the screen.
But even if we forgot that, why doesn’t the guard move up off screen? It is because of the stayWithinCanvas() method. This method keeps the object within the canvas boundaries. But this method can solve our previous problem as well. If it can track when the object hits the edge of the screen, it knows when we need to reverse the velocity. So we can add that logic in the stayWithinCanvas() method by adding it to Guard.js and overriding it the way we did for update() and handleCollisionEvent().
import Enemy from '@assets/js/GameEnginev1/essentials/Enemy.js';
import Player from '@assets/js/GameEnginev1/essentials/Player.js';
class Guard extends Enemy {
constructor(data = null, gameEnv = null) {
super(data, gameEnv);
this.velocity.y = -3 // Set an initial vertical velocity for the guard in the constructor. Because it is in the constructor, this velocity will be set as soon as the level starts.
}
/**
* Override the update method to handle collision detection and update the vertical velocity
*/
update() {
// Update begins by drawing the object
this.draw();
if (this.spriteData && typeof this.spriteData.update === 'function') {
this.spriteData.update.call(this);
}
// Check for collision with the player
if (!this.playerDestroyed && this.collisionChecks()) {
this.handleCollisionEvent();
}
this.position.y += this.velocity.y; // update position
// Ensure the object stays within the canvas boundaries
this.stayWithinCanvas();
}
/**
* stayWithinCanvas method ensures that the object stays within the boundaries of the canvas.
* With this override, we can also reverse the velocity whenever the guard hits the edges of the canvas.
*/
stayWithinCanvas() {
// Bottom of the canvas
if (this.position.y + this.height > this.gameEnv.innerHeight) {
this.position.y = this.gameEnv.innerHeight - this.height;
this.velocity.y *= -1; // Reverse vertical velocity to create a "bounce" effect
console.log(this.velocity.y);
}
// Top of the canvas
if (this.position.y < 0) {
this.position.y = 1;
this.velocity.y *= -1; // Reverse vertical velocity to create a "bounce" effects
console.log(this.velocity.y);
}
// Right of the canvas
if (this.position.x + this.width > this.gameEnv.innerWidth) {
this.position.x = this.gameEnv.innerWidth - this.width;
this.velocity.x = 0;
}
// Left of the canvas
if (this.position.x < 0) {
this.position.x = 0;
this.velocity.x = 0;
}
}
handleCollisionEvent() {
var player = this.gameEnv.gameObjects.find(obj => obj instanceof Player);
console.log("Collision has occurred, player has been destroyed.");
player.destroy();
this.playerDestroyed = true;
}
}
export default Guard;
Similar to how we added those two features above, you can tinker with the existing code yourself and tailor it to your needs. Maybe you want to add a visual when the player dies, make the enemy move left to right instead of up and down, or whatever else your game needs. There are many different ways to use collision mechanics, and it all boils down to what you want in your game.