📐

Object-Oriented Programming

This page focuses on CS111 object-oriented programming requirements: writing custom classes, using methods with parameters, creating objects, inheritance, method overriding, constructor chaining, and organizing game logic into reusable class-based systems.

Learning Objective Project Evidence Required Assessment Method
Writing Classes Section 1 explains custom classes for the battle level, boss enemy, and power-up menu. Code review of class files.
Methods & Parameters Section 2 explains methods such as initialize(), destroy(), handleKeyDown(event), and takeDamage(amount). Review method signatures and how parameters are used.
Instantiation & Objects Section 3 explains object instantiation with new and object literals for boss settings and power-up data. Review object literals and class setup.
Inheritance Section 4 explains how PeppaBossEnemy extends the parent Enemy class. Code review of extends usage.
Method Overriding Section 5 explains how PeppaBossEnemy overrides the parent update() behavior. Code review of polymorphic methods.
Constructor Chaining Section 6 explains constructor chaining with super(data, gameEnv). Review constructor setup.
🧠

Object-Oriented Programming Overview


How the Level Class Organizes the Fight

This project uses object-oriented programming because the game is built around classes and methods. The PeppaBattleLevelBase class is responsible for controlling one level of the game. Instead of putting all code in one giant script, the game groups related behavior into methods like initialize(), destroy(), update(), and createHud().

class PeppaBattleLevelBase {
    constructor(gameEnv, config) {
        this.gameEnv = gameEnv;
        this.config = config;
        this.playerHealth = this.playerMaxHealth;
        this.levelScore = 0;
        this.leaderboard = [];
    }

    initialize() {
        this.createHud();
        this.updateHud(
            `${this.config.enemyName}: "${this.config.enemyGreeting}" — Fight!`
        );
    }

    destroy() {
        document.removeEventListener('keydown', this.boundKeyDown);
        if (this.hud) {
            this.hud.remove();
        }
    }
}

A specific example is the initialize() method, which starts the HUD and shows a message telling the player how to fight. Another example is destroy(), which removes event listeners and HUD elements so old level data does not stay on the screen.

This is useful because each level can reuse the same class structure while changing values in config, such as the enemy name, enemy health, level title, and intro message.

Custom Boss Enemy Class

The PeppaBossEnemy class is one of the clearest examples of object-oriented programming in the project. It extends the engine's Enemy class, so it inherits basic sprite drawing, position, and canvas behavior, then adds custom boss-specific state like health, maxHealth, moveSpeed, and isDefeated.

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

class PeppaBossEnemy extends Enemy {
    constructor(data = null, gameEnv = null) {
        super(data, gameEnv);
        this.health = data?.health ?? 3;
        this.maxHealth = this.health;
        this.moveSpeed = data?.moveSpeed ?? 0.8;
        this.isDefeated = false;
    }

    update() {
        this.draw();
        if (this.isDefeated) return;

        const player = this.gameEnv.gameObjects.find(obj => obj?.constructor?.name === 'Player');
        if (!player) return;

        const myCenterX = this.position.x + this.width / 2;
        const myCenterY = this.position.y + this.height / 2;
        const playerCenterX = player.position.x + player.width / 2;
        const playerCenterY = player.position.y + player.height / 2;

        const dx = playerCenterX - myCenterX;
        const dy = playerCenterY - myCenterY;
        const distance = Math.hypot(dx, dy);

        if (distance > 1) {
            this.position.x += (dx / distance) * this.moveSpeed;
            this.position.y += (dy / distance) * this.moveSpeed;
        }

        this.stayWithinCanvas();
    }

    handleCollisionEvent() {
        // Collision handling is managed by the level logic.
    }

    takeDamage(amount = 1) {
        if (this.isDefeated) return true;

        this.health = Math.max(0, this.health - amount);
        if (this.health === 0) {
            this.isDefeated = true;
            this.canvas.style.filter = 'grayscale(1) brightness(0.8)';
            this.canvas.style.opacity = '0.6';
        }

        return this.isDefeated;
    }
}

export default PeppaBossEnemy;

The important OOP idea is constructor chaining. The line super(data, gameEnv) runs the parent Enemy constructor first, which lets the boss appear as a real game object. After that, my custom constructor adds the health system and movement speed.

The update() method overrides the enemy behavior so the boss redraws every frame, finds the player, calculates distance using Math.hypot(), and moves toward the player with normalized movement. The takeDamage(amount) method gives the boss its battle logic.

Method Overriding in the Boss Enemy

Method overriding happens when a child class creates its own version of a method that already exists in the parent class. In this project, PeppaBossEnemy extends the engine's Enemy class, but it writes its own update() method. That means the boss does not only use the default enemy behavior. Instead, it uses custom logic to redraw itself, find the player, calculate distance, move toward the player, and stay inside the canvas.

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

class PeppaBossEnemy extends Enemy {
    constructor(data = null, gameEnv = null) {
        super(data, gameEnv);
        this.health = data?.health ?? 3;
        this.moveSpeed = data?.moveSpeed ?? 0.8;
        this.isDefeated = false;
    }

    // This overrides the parent Enemy update method.
    update() {
        this.draw();

        if (this.isDefeated) {
            return;
        }

        const player = this.gameEnv.gameObjects.find(
            obj => obj?.constructor?.name === 'Player'
        );

        if (!player) {
            return;
        }

        const dx = player.position.x - this.position.x;
        const dy = player.position.y - this.position.y;
        const distance = Math.hypot(dx, dy);

        if (distance > 1) {
            this.position.x += (dx / distance) * this.moveSpeed;
            this.position.y += (dy / distance) * this.moveSpeed;
        }

        this.stayWithinCanvas();
    }
}

This counts as method overriding because the child class keeps the same method name, update(), but changes what the method does. The parent Enemy class gives the boss a basic game-object structure, while the child PeppaBossEnemy class customizes the frame-by-frame behavior for a boss fight.

Instantiation and Objects in the Level Setup

Instantiation means creating an object from a class. In this project, each Peppa level can be made from a class using the new keyword. The class is the blueprint, and the object is the actual level or boss that appears while the game is running.

const levelOne = new PeppaLevel1(gameEnv);
const boss = new PeppaBossEnemy({
    name: 'George Pig',
    health: 5,
    moveSpeed: 1.0
}, gameEnv);

The example above shows two objects being created. levelOne is an object made from PeppaLevel1, and boss is an object made from PeppaBossEnemy. The boss also receives a configuration object with properties like name, health, and moveSpeed.

Objects are also used in the power-up menu. The options array stores multiple object literals, where each object has a name and description. This makes each power-up easy to display and select.

this.options = [
    { name: 'Extra Speed', description: 'Move faster in battle' },
    { name: 'Extra Health', description: 'Start with more health' },
    { name: 'Extra Damage', description: 'Deal more damage per laser' }
];

This proves instantiation and objects because the game creates actual playable objects from classes, and it also uses object literals to store organized game data.

Power-Up Menu Class

The PeppaPowerUpMenu class adds another example of a custom class that is not a normal battle level. It creates a menu between levels where the player can choose Extra Speed, Extra Health, or Extra Damage. This shows arrays, DOM output, keyboard input, and game-state persistence through gameControl.chosenPower.

class PeppaPowerUpMenu {
    constructor(gameEnv) {
        this.gameEnv = gameEnv;
        this.selectedOption = 0;
        this.options = [
            { name: 'Extra Speed', description: 'Move faster in battle' },
            { name: 'Extra Health', description: 'Start with more health' },
            { name: 'Extra Damage', description: 'Deal more damage per laser' }
        ];
        this.chosenPower = null;
        this.classes = [];
    }

    initialize() {
        if (window.speechSynthesis) {
            window.speechSynthesis.speak = () => {};
            window.speechSynthesis.cancel();
        }

        this.createMenu();
        this.boundKeyDown = this.handleKeyDown.bind(this);
        document.addEventListener('keydown', this.boundKeyDown);
        this.gameEnv.gameControl.removeExitKeyListener();
    }

    destroy() {
        document.removeEventListener('keydown', this.boundKeyDown);
        const menu = document.getElementById('peppa-powerup-menu');
        if (menu) menu.remove();
        this.gameEnv.gameControl.addExitKeyListener();
    }

    selectPower() {
        this.chosenPower = this.options[this.selectedOption].name;
        this.gameEnv.gameControl.chosenPower = this.chosenPower;
        this.gameEnv.gameControl.currentLevel.continue = false;
    }

    update() {
        // No-op for compatibility with game loop
    }
}

export default PeppaPowerUpMenu;

The menu stores the choices in an array of objects called options. Each object has a name and description, which makes the menu easy to expand later. The selectedOption number tracks which power-up is currently highlighted, and selectPower() saves the final choice to this.gameEnv.gameControl.chosenPower.

Code review note: I changed the event-listener pattern in the blog version to store this.boundKeyDown. That matters because removeEventListener() only works correctly if it receives the exact same function reference that was passed to addEventListener().

⚙️

Methods, Parameters, and Game Logic


Setup Logic, Persistence, and Input Handling

The updated level code uses methods to keep each job separated. The initialize() method starts the level, loads saved data, creates coins, builds the HUD, creates the laser canvas, and loads the leaderboard. This keeps setup logic organized instead of mixing it into the game loop.

initialize() {
    if (window.speechSynthesis) {
        window.speechSynthesis.speak = () => {};
        window.speechSynthesis.cancel();
    }

    this.ensurePlayerName();
    this.generateCoins();
    this.createHud();

    if (this.gameMode === 'twoPlayer') {
        this.updateHud('2P Mode: Collect coins and defeat the boss!');
    } else {
        this.updateHud('Fight! Use WASD to move, SPACE to fire lasers, and collect coins.');
    }

    document.addEventListener('keydown', this.boundKeyDown);
    this.createLaserLayer();

    this.loadLeaderboard().then(() => {
        this.renderLeaderboard();
    });
}

The handleKeyDown(event) method also supports both players. Player 1 attacks with Space, and Player 2 attacks with Enter only when the game is in two-player mode.

handleKeyDown(event) {
    if (event.code === 'Space') {
        this.attackRequested = true;
        event.preventDefault();
    }

    if (this.gameMode === 'twoPlayer' && event.code === 'Enter') {
        this.player2AttackRequested = true;
        event.preventDefault();
    }
}

This is a good example of methods working together: input methods set request flags, the update loop reads those flags, and separate spawn methods create the lasers.

Return Values in Battle Methods

The boss and menu code also show methods with useful return values and state changes. takeDamage(amount) returns a boolean, so the level can know if the boss has been defeated after a hit. selectPower() does not return a value, but it changes shared game state by saving the chosen power-up to gameControl.chosenPower and then moving the game to the next level.

takeDamage(amount = 1) {
    if (this.isDefeated) return true;

    this.health = Math.max(0, this.health - amount);

    if (this.health === 0) {
        this.isDefeated = true;
        this.canvas.style.filter = 'grayscale(1) brightness(0.8)';
        this.canvas.style.opacity = '0.6';
    }

    return this.isDefeated;
}

This method connects directly to the gameplay because every player laser hit eventually calls boss.takeDamage(). The method handles the boss's health, defeat state, visual defeat effect, and return value in one organized place.


Specific CS111 OOP Requirements


1. Writing Classes

Requirement: Writing custom classes

This project uses custom JavaScript classes to separate the game into organized parts. Instead of putting all gameplay code into one file, the project uses classes for the battle level, boss enemy, and power-up menu. Each class has a different job, which makes the project easier to understand, test, and expand.

class PeppaBattleLevelBase {
    constructor(gameEnv, config) {
        this.gameEnv = gameEnv;
        this.config = config;
    }
}

class PeppaPowerUpMenu {
    constructor(gameEnv) {
        this.gameEnv = gameEnv;
        this.selectedOption = 0;
    }
}

This proves the writing classes requirement because the project creates multiple original classes that organize different parts of the game.

2. Methods and Parameters

Requirement: Methods with parameters

Methods are functions that belong to a class. This project uses methods to keep each action separate. Some methods receive parameters, which are values passed into the method so it can complete a specific task.

handleKeyDown(event) {
    if (event.code === 'Space') {
        this.attackRequested = true;
        event.preventDefault();
    }
}

takeDamage(amount = 1) {
    this.health = Math.max(0, this.health - amount);
    return this.health === 0;
}

This proves the methods and parameters requirement because the project uses class methods to perform specific game actions, and some methods use parameters to customize what happens.

3. Instantiation and Objects

Requirement: Creating objects from classes

Instantiation means creating an object from a class. A class is like a blueprint, and the object is the actual thing created from that blueprint. In this game, levels, bosses, and menus can be created as objects.

const levelOne = new PeppaLevel1(gameEnv);

const boss = new PeppaBossEnemy({
    name: 'George Pig',
    health: 5,
    moveSpeed: 1.0
}, gameEnv);

The project also uses object literals to store structured data. For example, each power-up is stored as an object with a name and description.

this.options = [
    { name: 'Extra Speed', description: 'Move faster in battle' },
    { name: 'Extra Health', description: 'Start with more health' },
    { name: 'Extra Damage', description: 'Deal more damage per laser' }
];

This proves the instantiation and objects requirement because the project creates objects from classes and uses object literals to organize game settings.

4. Inheritance

Requirement: Extending a parent class

Inheritance happens when one class gets behavior from another class. In this project, PeppaBossEnemy inherits from the game engine's Enemy class by using extends. This lets the boss reuse engine features while still adding its own custom boss logic.

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

class PeppaBossEnemy extends Enemy {
    constructor(data = null, gameEnv = null) {
        super(data, gameEnv);
        this.health = data?.health ?? 3;
    }
}

This proves the inheritance requirement because PeppaBossEnemy is a child class that extends the parent Enemy class.

5. Method Overriding

Requirement: Replacing inherited behavior

Method overriding happens when a child class writes its own version of a method that already exists in the parent class. In this project, PeppaBossEnemy overrides update() so the boss can move toward the player instead of only using the default enemy behavior.

class PeppaBossEnemy extends Enemy {
    // This custom update method overrides the parent Enemy update behavior.
    update() {
        this.draw();

        if (this.isDefeated) {
            return;
        }

        const player = this.gameEnv.gameObjects.find(
            obj => obj?.constructor?.name === 'Player'
        );

        if (!player) {
            return;
        }

        const dx = player.position.x - this.position.x;
        const dy = player.position.y - this.position.y;
        const distance = Math.hypot(dx, dy);

        if (distance > 1) {
            this.position.x += (dx / distance) * this.moveSpeed;
            this.position.y += (dy / distance) * this.moveSpeed;
        }

        this.stayWithinCanvas();
    }
}

This proves method overriding because the child class keeps the method name update(), but changes what the method does for the boss enemy.

6. Constructor Chaining

Requirement: Calling the parent constructor with super()

Constructor chaining means the child class calls the parent class constructor before adding its own properties. In JavaScript, this is done with super(). In this project, PeppaBossEnemy calls super(data, gameEnv) so the parent Enemy setup runs first.

class PeppaBossEnemy extends Enemy {
    constructor(data = null, gameEnv = null) {
        super(data, gameEnv);

        this.health = data?.health ?? 3;
        this.maxHealth = this.health;
        this.moveSpeed = data?.moveSpeed ?? 0.8;
        this.isDefeated = false;
    }
}

This proves constructor chaining because the boss constructor first calls the parent constructor with super(data, gameEnv), then adds boss-specific properties like health, speed, and defeated state.

← Back to College Ready Blog