Classes

Classes are blueprints for creating objects. They encapsulate data for the object and methods to manipulate that data. In JavaScript, classes are defined using the class keyword.

Example:

Here I wrote a new class for an interactive object which are Trees which were used in my jungle level:

class Tree extends Character {
    constructor(data) {
        super(data);
        
        // Basic properties
        this.isChopped = false;
        this.frameIndex = 0;
        this.frameCounter = 0;
        this.spriteData = data;
        
        // Create two separate canvases - one for each animation
        this.idleCanvas = this.canvas;
        this.chopCanvas = document.createElement('canvas');
        this.chopCtx = this.chopCanvas.getContext('2d');
        
        // Load sprite sheet
        this.spriteSheet = new Image();
        this.spriteSheet.src = data.src;
        
        // Set up canvases
        this.spriteSheet.onload = () => {
            const frameWidth = this.spriteData.pixels.width / this.spriteData.orientation.columns;
            const frameHeight = this.spriteData.pixels.height / this.spriteData.orientation.rows;
            
            // Setup idle canvas
            this.idleCanvas.width = frameWidth;
            this.idleCanvas.height = frameHeight;
            this.idleCanvas.style.width = `${this.width}px`;
            this.idleCanvas.style.height = `${this.height}px`;
            this.idleCanvas.style.position = 'absolute';
            this.idleCanvas.style.left = `${this.position.x}px`;
            this.idleCanvas.style.top = `${GameEnv.top + this.position.y}px`;
            
            // Setup chop canvas with same dimensions but initially hidden
            this.chopCanvas.width = frameWidth;
            this.chopCanvas.height = frameHeight;
            this.chopCanvas.style.width = `${this.width}px`;
            this.chopCanvas.style.height = `${this.height}px`;
            this.chopCanvas.style.position = 'absolute';
            this.chopCanvas.style.left = `${this.position.x}px`;
            this.chopCanvas.style.top = `${GameEnv.top + this.position.y}px`;
            this.chopCanvas.style.display = 'none';
            
            // Add chop canvas to the game container
            const container = this.idleCanvas.parentNode;
            if (container) {
                container.appendChild(this.chopCanvas);
            }
            
            // Add to game objects
            if (!GameEnv.gameObjects.includes(this)) {
                GameEnv.gameObjects.push(this);
            }
        };
    }

    // Handle tree chopping
    chopTree() {
        if (!this.isChopped) {
            this.isChopped = true;
            this.frameIndex = 0;
            this.frameCounter = 0;
            
            // Hide idle canvas and show chop canvas
            this.idleCanvas.style.display = 'none';
            this.chopCanvas.style.display = 'block';
        }
    }

    // Draw the tree
    draw() {
        if (this.spriteSheet && this.spriteSheet.complete) {
            const frameWidth = this.spriteData.pixels.width / this.spriteData.orientation.columns;
            const frameHeight = this.spriteData.pixels.height / this.spriteData.orientation.rows;

            // Get animation data
            const animationData = this.isChopped ? this.spriteData.chop : this.spriteData.idle;
            
            // Calculate position in sprite sheet
            const frameX = (animationData.start + this.frameIndex) * frameWidth;
            const frameY = animationData.row * frameHeight;

            // Clear and draw on appropriate canvas
            const currentCanvas = this.isChopped ? this.chopCanvas : this.idleCanvas;
            const currentCtx = this.isChopped ? this.chopCtx : this.ctx;
            
            currentCtx.clearRect(0, 0, currentCanvas.width, currentCanvas.height);
            currentCtx.drawImage(
                this.spriteSheet,
                frameX, frameY, frameWidth, frameHeight,
                0, 0, currentCanvas.width, currentCanvas.height
            );

            // Update animation frame based on animation rate from sprite data
            this.frameCounter++;
            if (this.frameCounter >= this.spriteData.ANIMATION_RATE) {
                if (this.isChopped) {
                    this.frameIndex = Math.min(this.frameIndex + 1, animationData.columns - 1);
                } else {
                    this.frameIndex = (this.frameIndex + 1) % animationData.columns;
                }
                this.frameCounter = 0;
            }
        }
    }

    update() {
        this.draw();
    }
}

export default Tree;

Methods

Methods are functions defined within a class. They describe the behaviors of the objects created from the class. Methods can manipulate the object’s data and perform actions.

Example:

Here I create a method to handle the player’s idle animation when a key is not pressed

handleIdleAnimation() {
    // Update the idle frame index for animation at a slower rate
    this.idleFrameCounter++;
    if (this.idleFrameCounter % (this.animationRate * 1) === 0) { // Adjust the rate as needed
        this.idleFrameIndex = (this.idleFrameIndex + 1) % this.spriteData.idle.columns;
    }

    // Draw the idle frame
    const frameWidth = this.spriteData.pixels.width / this.spriteData.orientation.columns;
    const frameHeight = this.spriteData.pixels.height / this.spriteData.orientation.rows;
    const frameX = this.idleFrameIndex * frameWidth;
    const frameY = this.spriteData.idle.row * frameHeight;

    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    this.ctx.drawImage(
        this.spriteSheet,
        frameX, frameY, frameWidth, frameHeight, // Source rectangle
        0, 0, this.canvas.width, this.canvas.height // Destination rectangle
    );
}

Instantiate Objects

To create an instance of a class, you use the new keyword followed by the class name and parentheses. This process is called instantiation.

Example:

Here I create an array that stores instances of different gameobjects which is an demonstration of instantiating objects

this.objects = [
    { class: Background, data: image_data_jungle },
    { class: Player, data: sprite_data_explorer },
    { class: Npc, data: sprite_data_lumberjack },
    { class: Tree, data: sprite_data_tree }
];

Use Objects to Interact with Data/Methods

Once you have instantiated an object, you can interact with its data and methods using dot notation. This allows you to access and modify the object’s properties and call its methods.

Example:

Below you can see two snippets, one is the data being stored in “sprite_data_lumberjack” , and it’s properties being stored under a class (same snippest from above)

const sprite_src_lumberjack = path + "/images/gamify/lumberjack.png";
        const LUMBERJACK_SCALE_FACTOR = 2.8;
        const sprite_data_lumberjack = {
            id: 'Lumberjack',
            greeting: "Hi I am Lumberjack, I love chopping wood and exploring the jungle!",
            src: sprite_src_lumberjack,
            SCALE_FACTOR: LUMBERJACK_SCALE_FACTOR,
            STEP_FACTOR: 1000,
            ANIMATION_RATE: 25,
            INIT_POSITION: { x: (width / 2) - (348 / LUMBERJACK_SCALE_FACTOR), y: height - (height / LUMBERJACK_SCALE_FACTOR) },
            pixels: { height: 348, width: 348 },
            orientation: { rows: 6, columns: 6 },
            idle: { row: 0, start: 0, columns: 4 },
            down: { row: 0, start: 0, columns: 3 },
            left: { row: 2, start: 0, columns: 6 },
            right: { row: 2, start: 0, columns: 6 },
            up: { row: 4, start: 0, columns: 6 },
            hitbox: { widthPercentage: 0.45, heightPercentage: 0.2 },
            keypress: { up: 87, left: 65, down: 83, right: 68 }, // W, A, S, D
            quiz: {
                title: "What should I do?",
                questions: [
                    "Should I chop this tree?\n1. Yes\n2. No",
                ]
            }
this.objects = [
    { class: Background, data: image_data_jungle },
    { class: Player, data: sprite_data_explorer },
    { class: Npc, data: sprite_data_lumberjack },
    { class: Tree, data: sprite_data_tree }
];

Call Methods with Parameters and Handle Return Values

Methods can accept parameters and return values. Parameters allow you to pass data into the method, and return values allow the method to send data back to the caller.

Example:

Here can you see the “handleLumberjackQuiz” method is called with a paramter which si the quiz question. The prompt function returns the player’s input which is stored in the “answer” variable which is used to determine the next action

const answer = prompt(`${lumberjack.data.quiz.title}\n${question}`);
if (answer === '1') {
    // If the player answers "1", chop the tree
    alert("The tree was chopped down!");
    tree.chopTree();  // Call chopTree on the actual tree instance
} else if (answer === '2') {
    // If the player answers "2", do not chop the tree
    alert("The tree was not chopped down.");
} else {
    // If the player provides an invalid response, show an error message
    alert("Invalid response. Please answer with 1 or 2.");
}

Implement Basic Inheritance for Code Reuse

Inheritance allows you to create a new class based on an existing class. The new class inherits the properties and methods of the existing class, allowing for code reuse and extension. In JavaScript, inheritance is implemented using the extends keyword.

Example:

An example of inheritance being used is my Tree class, which inherits properties from an existing class (Character) to reuse it’s code which simplifies the Tree.js file, making it easier to work with. Below is a snippet of the Tree class inheriting the Character class’s property, and also a link to the a .drawio file displaying inheritance being used in the adventuregame

drawio file

class Tree extends Character