← Home    Full Blog

Object-Oriented Programming

Writing Classes · Methods & Parameters · Instantiation · Inheritance · Method Overriding · Constructor Chaining

📋 Table of Contents

  1. Inheritance (Basic)
  2. Writing Classes
  3. Constructor Chaining
  4. Method Overriding
  5. Instantiation & Objects
  6. Methods & Parameters

📐 Inheritance (Basic) — Class Hierarchy

Class Hierarchy (2+ levels deep)

Inheritance lets one class extend another, so it automatically gets all of the parent's properties and methods without rewriting them. The game has two inheritance chains. One controls the boss enemy, one controls the levels. Without inheritance, every level would need its own copy of the laser, HUD, and collision code — that's hundreds of duplicate lines.

GameObject ← engine base (provided) └── Enemy ← sprite, canvas, position (provided) └── PeppaBossEnemy ← MY CLASS: health, movement, damage
PeppaBattleLevelBase ← MY BASE CLASS: laser system, HUD, collisions ├── PeppaLevel1 ← George Pig config ├── PeppaLevel2 ← Peppa Pig config └── PeppaLevel3 ← Daddy Pig (final boss) config

Try It — Inheritance

Run this to see how a child class automatically gets the parent's methods. Try adding your own method to PeppaBossEnemy.

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

✏️ Writing Classes

Writing Custom Classes

A class is a blueprint. You define it once, and then you can create as many objects from it as you want. Each object gets its own copy of all the properties. In the game, PeppaBossEnemy is the blueprint for the boss — it stores the boss's health, speed, and defeat state. The constructor runs once when the boss is created and sets all the starting values.

What a class looks like

class PeppaBossEnemy extends Enemy {

  constructor(data, gameEnv) {
    super(data, gameEnv);           // run the parent's setup first

    this.health      = data?.health    ?? 3;   // how many hits the boss can take
    this.maxHealth   = this.health;             // saved so the HUD can show "3/3"
    this.moveSpeed   = data?.moveSpeed ?? 0.8; // pixels per frame toward the player
    this.isDefeated  = false;                  // turns true when health hits 0
    this.isVulnerable = true;                  // false during the 300ms cooldown after a hit
  }

}
data?.health ?? 3 means: "use data.health if it exists, otherwise default to 3." This lets George Pig and Daddy Pig both use the same class but with different health values — you just pass a different data object when creating each one.

Try It — Writing Classes

Run this to see how a class acts as a blueprint. Try creating a third boss with different values.

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

🔗 Constructor Chaining

When a child class has its own constructor, the very first thing it must do is call super(). super() runs the parent's constructor — it passes the arguments up the chain so the parent can do its setup before the child adds anything on top. Without calling super() first, this doesn't exist yet and JavaScript throws a ReferenceError.

Boss chain — PeppaBossEnemy → Enemy → GameObject

super(data, gameEnv) here means: "run Enemy's constructor with these same two arguments." Enemy then calls super() again, which runs GameObject's constructor. By the time that chain finishes, the boss has a canvas element, a loaded sprite image, and a position on screen — all set up by the parent classes. Only after that does PeppaBossEnemy's own constructor run and add this.health, this.moveSpeed, etc.

class PeppaBossEnemy extends Enemy {
  constructor(data, gameEnv) {
    super(data, gameEnv);  // runs Enemy's constructor → which runs GameObject's constructor
                           // after this line: canvas, sprite image, and position all exist
    this.health    = data?.health    ?? 3;
    this.moveSpeed = data?.moveSpeed ?? 0.8;
    this.isDefeated = false;
  }
}

Level chain — PeppaLevel1/3 → PeppaBattleLevelBase

Each level passes a different config object to super(). The base class constructor reads those values and sets up the laser system, HUD, and collisions once. PeppaLevel1 and PeppaLevel3 are completely different fights but their entire constructor is just one line.

class PeppaLevel1 extends PeppaBattleLevelBase {
  constructor(gameEnv) {
    super(gameEnv, { enemyName: 'George Pig', enemyHealth: 3, enemySpeed: 0.8 });
    // super() passes this config up — PeppaBattleLevelBase reads it and builds the level
  }
}

class PeppaLevel3 extends PeppaBattleLevelBase {
  constructor(gameEnv) {
    super(gameEnv, { enemyName: 'Daddy Pig', enemyHealth: 12, enemySpeed: 1.4 });
    // same base class, different numbers = completely different fight
  }
}
All the laser, HUD, and collision code lives once in PeppaBattleLevelBase. The child classes don't repeat any of it — they just pass different numbers through super() to get a different fight.

Code Runner — Constructor Chaining Practice

Run this to see constructor chaining with super(). Try changing the name or adding a new property in the child constructor.

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

🔄 Method Overriding

Overriding Parent Methods

Overriding means writing a method in the child class with the same name as one in the parent. When that method is called, the child's version runs instead of the parent's. The game engine calls update() on every object 60 times per second. The parent's update() just draws the sprite. By overriding it, I add boss-specific behavior — chasing the player, stopping when defeated, and drawing the hitbox overlay.

// OVERRIDES Enemy.update() — custom chase + hitbox draw behavior
update() {
  this.draw();                       // inherited from GameObject

  if (this.isDefeated) return;        // early exit if dead

  const player = this.findPlayer();
  if (!player) return;

  const moved = this.moveTowardPlayer(player);
  if (moved) this.stayWithinCanvas();   // inherited method

  if (this.showHitbox) this.drawHitbox();
}

// OVERRIDES Enemy.handleCollisionEvent() — delegated to level logic
handleCollisionEvent() {
  // Collision managed by PeppaBattleLevelBase, not the enemy itself
}
update() runs 60 times per second. The override adds boss-specific behavior each frame — draw sprite, stop if defeated, chase the player. Without it, the boss just stands still. handleCollisionEvent() is intentionally empty to prevent the engine's default collision from interfering with the custom damage system.

Try It — Method Overriding

Run this to see how the child's update() replaces the parent's. Try setting isDefeated = true before calling update.

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

🏗️ Instantiation & Objects

Creating Objects from a Class

Instantiation means creating an actual object from a class blueprint using new. Until you write new PeppaBossEnemy(data), the class is just a description — nothing exists in the game yet. The engine reads the this.classes array at the start of each level and calls new on every entry. If PeppaBossEnemy isn't in that array, there is no boss.

// Inside PeppaBattleLevelBase constructor — instantiates all game objects
this.classes = [
  { class: GamEnvBackground, data: image_data_background },  // Object literal
  { class: Player,           data: sprite_data_ishan    },  // Object literal
  { class: PeppaBossEnemy,   data: sprite_data_enemy    }   // Object literal
];

// sprite_data_enemy is a JSON config object — all required properties
const sprite_data_enemy = {
  id:             config.enemyName,
  greeting:       config.enemyGreeting,       // String
  src:            `${path}/images/gamify/${config.enemyImage}`,
  SCALE_FACTOR:   config.enemyScale ?? 4,    // Number
  ANIMATION_RATE: 18,                         // Number
  INIT_POSITION:  { x: width * 0.72, y: height * 0.66 },  // Object
  health:         config.enemyHealth,          // Number
  moveSpeed:      config.enemySpeed,           // Number
  hitbox:         { widthPercentage: 0.45, heightPercentage: 0.6 }
};
The config object is passed to the constructor as data. The constructor reads data.health, data.moveSpeed etc. Changing these numbers is all it takes to make a different boss — no other code changes needed.

Try It — Instantiation

Run this to see how new creates separate objects from one class. Notice changing george's HP doesn't affect daddy's.

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

📚 Related Homework: Classes and Methods

Classes and Methods Homework - Demonstrates object-oriented programming fundamentals, including class design, method implementation, and inheritance — the foundation of the game's class hierarchy (PeppaBossEnemy, PeppaBattleLevelBase, etc.).


⚙️ Methods & Parameters

A method is a function inside a class. Parameters are its inputs. Below are five methods from the game — each takes specific inputs, does one job, and returns a value other code uses.

moveTowardPlayer(target) — moves the boss one step toward the player each frame

// param: target — the player object (has .position, .width, .height)
// returns: true if the boss moved, false if it's already on top of the player
moveTowardPlayer(target) {
  const dx = (target.position.x + target.width  / 2) - (this.position.x + this.width  / 2);
  const dy = (target.position.y + target.height / 2) - (this.position.y + this.height / 2);
  const distance = Math.hypot(dx, dy);
  if (distance > 1) {
    this.position.x += (dx / distance) * this.moveSpeed;  // divide by distance = normalize
    this.position.y += (dy / distance) * this.moveSpeed;
    return true;
  }
  return false;
}
Dividing by distance before multiplying by moveSpeed keeps the boss at a constant speed regardless of how far away the player is. The return value tells update() whether to call stayWithinCanvas() — no point doing that if the boss didn't move.

takeDamage(amount) — called when a laser hits the boss

// param: amount — how much HP to subtract (default 1)
// returns: true if this hit caused the boss to be defeated
takeDamage(amount = 1) {
  if (this.isDefeated)    { console.log('[Boss] Already defeated.');    return true;  }
  if (!this.isVulnerable) { console.log('[Boss] Invulnerable, ignoring.'); return false; }
  this.health       = Math.max(0, this.health - amount);
  this.isVulnerable = false;   // start 300ms cooldown window
  console.log(`[Boss] Hit! HP: ${this.health}/${this.maxHealth}`);
  if (this.health === 0) this.isDefeated = true;
  return this.isDefeated;
}
Without the invulnerability check, a single laser crossing the boss hitbox would call takeDamage on every frame it overlapped — that's up to 10 hits per laser. The 300ms window means each laser can only land one hit.

spawnLaser(fromX, fromY, targetX, targetY, isPlayerLaser) — adds a new laser to the array

// isPlayerLaser: true = player fired it (damages boss), false = boss fired it (damages player)
spawnLaser(fromX, fromY, targetX, targetY, isPlayerLaser) {
  const dx = targetX - fromX,  dy = targetY - fromY;
  const len = Math.hypot(dx, dy) || 1;
  this.lasers.push({
    x: fromX, y: fromY,
    vx: (dx / len) * this.laserSpeed,   // velocity — added to x each frame
    vy: (dy / len) * this.laserSpeed,   // velocity — added to y each frame
    isPlayerLaser, life: 60
  });
}
isPlayerLaser is what tells the collision system which side takes damage. Without it, every laser would hit both the player and the boss.

drawHitbox() — draws a red rectangle showing the collision boundary

drawHitbox() {
  const ctx = this.gameEnv?.ctx;
  if (!ctx) return;
  ctx.save();
  ctx.strokeStyle = 'rgba(255, 0, 0, 0.85)';
  ctx.strokeRect(this.position.x, this.position.y, this.width, this.height);
  ctx.restore();
  console.log(`[Hitbox] pos=(${Math.round(this.position.x)},${Math.round(this.position.y)})`);
}
Used during testing to make the collision box visible. I found the box was too large — lasers registered hits that visually missed. Tuning widthPercentage: 0.45 fixed it.

toggleHitbox() — flips the debug overlay on or off

toggleHitbox() {
  this.showHitbox = !this.showHitbox;  // ! flips true→false or false→true
  console.log(`[Hitbox] now ${this.showHitbox ? 'ON' : 'OFF'}`);
  return this.showHitbox;  // caller can read the new state
}
update() checks showHitbox every frame. When true, the overlay draws. Toggling without restarting the game was essential for tuning hitbox sizes during playtesting.

Try It — Methods & Parameters

Run this to see takeDamage(amount) in action. Try passing different amounts or setting isVulnerable = false to see the blocked path.

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

📚 Related Homework: Classes and Methods

Classes and Methods Homework - Demonstrates object-oriented programming fundamentals, including class design, method implementation, and inheritance — the foundation of the game's class hierarchy (PeppaBossEnemy, PeppaBattleLevelBase, etc.).