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. |
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.
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 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 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.
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.
this.boundKeyDown.
That matters because removeEventListener() only works correctly if it receives the exact
same function reference that was passed to addEventListener().
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.
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.
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.
PeppaBattleLevelBase controls the main battle level system.PeppaBossEnemy controls the boss enemy behavior.PeppaPowerUpMenu controls the between-level power-up screen.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.
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.
initialize() starts a level or menu.destroy() cleans up the current level or menu.handleKeyDown(event) uses the event parameter to check which key was pressed.takeDamage(amount) uses the amount parameter to subtract health from the boss.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.
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.
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.
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.
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.