Table of Contents

Writing Classes

Object-Oriented Programming begins with creating custom classes. A class is a blueprint for creating objects that share common properties and behaviors. Below is a basic class definition for a custom character:

class Character {
    constructor(name, health, position) {
        this.name = name;
        this.health = health;
        this.position = position;
    }

    draw() {
        // Logic to render character on screen
    }
}

Classes are the foundation of OOP. They allow us to organize code logically and reuse functionality across our applications.

Inheritance (Basic)

Inheritance allows us to create a hierarchy of classes where child classes inherit properties and methods from parent classes. This promotes code reuse and logical organization.

In my game projects, I created custom enemy classes that extend the base Enemy class:

import Enemy from '@assets/js/GameEnginev1/essentials/Enemy.js';
import Player from '@assets/js/GameEnginev1/essentials/Player.js';

// Guard inherits from Enemy
class Guard extends Enemy {
    constructor(data = null, gameEnv = null) {
        super(data, gameEnv);
        this.velocity.y = -3; // Vertical patrolling behavior
    }
}

// HorizontalGuard also inherits from Enemy
class HorizontalGuard extends Enemy {
    constructor(data = null, gameEnv = null) {
        super(data, gameEnv);
        this.velocity.x = -3; // Horizontal patrolling behavior
    }
}

Both Guard and HorizontalGuard extend the Enemy class, inheriting its properties and methods while adding their own specialized behavior. This is a 2-level class hierarchy: GameObjectEnemyGuard.

Class Hierarchy Diagram

classDiagram
    class GameObject {
        position
        velocity
        gameEnv
        draw()
        update()
        collisionChecks()
    }
    
    class Enemy {
        playerDestroyed
        handleCollisionEvent()
        stayWithinCanvas()
    }
    
    class Guard {
        velocity.y = -3
        update()
        stayWithinCanvas()
        handleCollisionEvent()
    }
    
    class HorizontalGuard {
        velocity.x = -3
        update()
        stayWithinCanvas()
        handleCollisionEvent()
    }
    
    GameObject --|> Enemy
    Enemy --|> Guard
    Enemy --|> HorizontalGuard

Reading the Diagram:

  • Attributes (top portion): Data stored in each object
    • position, velocity, gameEnv are inherited by all child classes
  • Methods (bottom portion): Functions that each class can perform
    • Methods with the same name in child classes are overridden to create specialized behavior

Constructor Chaining with super()

When a child class extends a parent class, the child’s constructor must call super() to invoke the parent’s constructor. This initializes inherited properties and ensures proper class hierarchy setup.

In the examples above, both Guard and HorizontalGuard use super(data, gameEnv) to initialize the parent Enemy class before setting their own velocity properties:

class Guard extends Enemy {
    constructor(data = null, gameEnv = null) {
        super(data, gameEnv);  // Call parent constructor
        this.velocity.y = -3;  // Set child-specific property
    }
}

This pattern ensures that all inherited properties are properly initialized before the child class adds its own customizations.

Method Overriding

Method overriding allows child classes to replace or extend the functionality of parent class methods. This is a key feature of polymorphism in OOP.

In my game projects, I overrode the update() and stayWithinCanvas() methods from the Enemy class to implement unique patrolling behavior:

Example: Guard Class with Method Overriding

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

    /**
     * OVERRIDDEN METHOD: update()
     * The parent Enemy class has an update() method, but Guard provides its own implementation.
     * This override adds vertical patrol behavior to the guard.
     */
    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();
    }

    /**
     * OVERRIDDEN METHOD: stayWithinCanvas()
     * The parent Enemy class has this method, but Guard overrides it to add bouncing behavior
     * when the guard hits the top or bottom canvas boundaries.
     */
    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;
        }
    }

    /**
     * OVERRIDDEN METHOD: handleCollisionEvent()
     * Custom collision handling specific to the Guard class.
     */
    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;

Challenge

Vertical patrolling enemy with Guard.js

Lines: 1 Characters: 0
Game Status: Not Started

Example: HorizontalGuard Class with Method Overriding

import Enemy from '@assets/js/GameEnginev1/essentials/Enemy.js';
import Player from '@assets/js/GameEnginev1/essentials/Player.js';

class HorizontalGuard extends Enemy {
    constructor(data = null, gameEnv = null) {
        super(data, gameEnv);
        this.velocity.x = -3 // Set an initial horizontal velocity for the guard in the constructor. Because it is in the constructor, this velocity will be set as soon as the level starts.
    }

    /**
     * OVERRIDDEN METHOD: update()
     * Similar to Guard, but implements horizontal patrol behavior instead of vertical.
     */
    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.x += this.velocity.x; // update position

        // Ensure the object stays within the canvas boundaries
        this.stayWithinCanvas();
    }

    /**
     * OVERRIDDEN METHOD: stayWithinCanvas()
     * This override bounces horizontally instead of vertically.
     */
    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 = 0
        }
        // Top of the canvas
        if (this.position.y < 0) {
            this.position.y = 0;
            this.velocity.y = 0;
        }
        // Right of the canvas
        if (this.position.x + this.width > this.gameEnv.innerWidth) {
            this.position.x = this.gameEnv.innerWidth - this.width;
            this.velocity.x *= -1; // Reverse horizontal velocity to create a "bounce" effect
            console.log(this.velocity.x);
        }
        // Left of the canvas
        if (this.position.x < 0) {
            this.position.x = 0;
            this.velocity.x *= -1; // Reverse horizontal velocity to create a "bounce" effect
            console.log(this.velocity.x);
        }
    }

    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 HorizontalGuard;

Challenge

Horizontal patrolling enemy with HorizontalGuard.js

Lines: 1 Characters: 0
Game Status: Not Started

Methods and Parameters

Methods are functions defined within a class that perform actions on the object’s data. Methods often accept parameters (inputs) that allow the method to behave differently based on the data passed to it.

Methods with Multiple Parameters

One of the key benefits of OOP is the ability to create methods with multiple parameters that work with different types of data. Below is an example of a method with 2 parameters:

Code Runner Challenge

Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

Real-World Game Example: collisionHandler()

In game development, methods with multiple parameters are essential. For example, a collision handler might take parameters for the colliding object and the direction of collision:

/**
 * Handle collision with another object
 * @param {GameObject} other - The object we collided with
 * @param {String} direction - The direction of collision (e.g., "top", "bottom", "left", "right")
 */
collisionHandler(other, direction) {
    if (other instanceof Enemy) {
        console.log(`Collision from ${direction}!`);
        this.takeDamage(10);
    }
}

This method receives two parameters and uses both to determine how the collision should be handled.

Instantiation and Objects

Instantiation is the process of creating an object from a class. When we instantiate an object, we create a specific instance with its own properties and methods based on the class definition.

Object Instantiation in Game Levels

In my game projects, I instantiate game objects when configuring a level. Each level is initialized with multiple objects (background, player, enemies, etc.) by creating instances of their respective classes:

// Example from a game level configuration
this.classes = [      
    { class: GameEnvBackground, data: bgData },
    { class: Player, data: playerData },
    { class: Guard, data: guardData },
    { class: HorizontalGuard, data: horizontalGuardData }
];

Each entry in this array creates a new instance of that class with the provided data. The game engine iterates through these classes and instantiates each object, populating the level with all necessary game objects.