Object-Oriented Programming (OOP) Evidence


🔹 1. Writing Classes

  • 📌 Requirement: Create at least 2 custom classes.
  • 📝 Files: Bullet.js, RedRidingMusic.js, and SplineBarrier.js.
  • What it is: A class is a blueprint. It groups related data and functions together into one neat object instead of using separate, messy variables.
  • How I used it: The Bullet class groups position and speed properties so the game can spawn many bullets easily. I also built separate classes to manage background music tracks and path-based collision boundaries.
  • Why it helps: Each object tracks its own data independently. This cleans up the global scope and stops unrelated parts of the game from breaking each other.
class Bullet {
    // Sets up a new bullet with position, speed, and size
    constructor(data) {
        this.x = data.x; // Where the bullet is horizontally
        this.y = data.y; // Where the bullet is vertically
        this.velocity = data.velocity || { x: 0, y: 0 }; // How fast it moves
        this.gameEnv = data.gameEnv;
        this.shooter = data.shooter; // Who shot it (player or enemy)
        this.direction = data.direction || 'down';
        this.width = 20; // Bullet width
        this.height = 20; // Bullet height
        this.lifetime = 3000; // Despawn timer (3 seconds)
        this.creationTime = Date.now();
        this.destroyed = false; // True when the bullet hits something or expires
    }
}

class RedRidingMusic {
    // Handles the background music settings
    constructor() {
        this.audio = null; // Holds the actual music file
        this.started = false;
        this.isPlaying = false; // Makes sure music doesn't play twice at once
        this.endpoint = 'https://itunes.apple.com/search?term=little+red+riding+hood&entity=song&limit=5';
        this.userActivated = false;
        this.createToggleButton(); // Creates the play/pause button on screen
    }
}

class SplineBarrier {
    // Creates boundaries on the map so characters can't walk off track
    constructor(leftPoints, rightPoints, gameEnv) {
        this.leftControlPoints = leftPoints;
        this.rightControlPoints = rightPoints;
        this.gameEnv = gameEnv;
        this.ctx = gameEnv.ctx;

        // Turns a few points into a smooth line for the borders
        const leftSamples = this.sampleSpline(this.leftControlPoints, 1000);
        const rightSamples = this.sampleSpline(this.rightControlPoints, 1000);
        
        // Connects the left and right borders to make a complete boundary wall
        this.polygon = [...leftSamples, ...rightSamples.reverse()];
    }
}

🔹 2. Methods with Parameters

  • 📌 Requirement: Create methods that take at least 2 parameters.
  • 📝 Examples: checkInZone(player, zone) and checkPlayerWolfCollision(player, wolf).
  • What it is: Parameters are placeholders for information given to a function. They let a function accept different data every time it runs instead of doing the exact same thing forever.
  • How it works: Collision methods accept two game elements at the same time (such as the player and a wolf object). The function reads both positions to determine if they overlap on the screen.
  • Why it helps: It prevents duplicate code. A single collision function can compare any target against any obstacle or zone dynamically, keeping the project organized and modular.
// EVIDENCE: METHODS WITH PARAMETERS
class GameLevelRedRidingHood2 {

    // Method accepts two objects to calculate if a player is standing inside a zone
    checkInZone(player, zone) {
        if (!player?.position) return false;

        // Compares the player boundaries against the target zone boundaries
        return (
            player.position.x + player.width > zone.x &&
            player.position.x < zone.x + zone.width &&
            player.position.y + player.height > zone.y &&
            player.position.y < zone.y + zone.height
        );
    }

    // Method accepts two objects to calculate if the player hits an active wolf enemy
    checkPlayerWolfCollision(player, wolf) {
        if (!player?.position || !wolf?.position) return false;

        // Custom size adjustments to make the hitting area feel accurate on screen
        const wolfPadding = 60;
        const playerPadding = 10;

        // Compares coordinates of both elements to check for any overlapping spaces
        return (
            player.position.x + playerPadding < wolf.position.x + wolf.width - wolfPadding &&
            player.position.x + player.width - playerPadding > wolf.position.x + wolfPadding &&
            player.position.y + playerPadding < wolf.position.y + wolf.height - wolfPadding &&
            player.position.y + player.height - playerPadding > wolf.position.y + wolfPadding
        );
    }
}

🔹 3. Object Instantiation

  • 📌 Requirement: Create game objects dynamically within a game level configuration.
  • 📝 Examples: Using new GameEnvBackground(...) and new ShooterPlayer(...).
  • What it is: Instantiation means bringing a class blueprint to life. Using the new keyword builds an actual, active copy of that class in memory.
  • How it works: When the game level loads, the code runs commands to build the level pieces. It creates the map background using the background class and sets up the user's character using the player class.
  • Why it helps: It keeps level generation flexible. Instead of coding every level by hand, data settings can tell the system exactly which objects to build on the fly when the player starts a new stage.
// EVIDENCE: OBJECT INSTANTIATION
class GameLevelRedRidingHood3 {
    constructor(gameEnv) {
        this.gameEnv = gameEnv;

        let width = gameEnv.innerWidth;
        let height = gameEnv.innerHeight;

        // Configuration settings for the level background asset
        const image_data_forest = {
            name: 'forest',
            src: gameEnv.path + "/images/projects/red-riding/lrrh-lvl3-bg-clipped.png",
            pixels: { height: 580, width: 1038 }
        };

        // Instantiates the background object using the 'new' keyword
        this.background = new GameEnvBackground(image_data_forest, gameEnv);

        // Configuration settings for the main player sprite assets and sizing
        const sprite_data_red = {
            id: 'RedRidingHood',
            src: gameEnv.path + "/images/projects/red-riding/Finalred.png",
            SCALE_FACTOR: 6,
            STEP_FACTOR: 800,
            INIT_POSITION: { x: width / 2 - 50, y: height - 100 },
            pixels: { height: 144, width: 192 },
            orientation: { rows: 3, columns: 4 }
        };

        // Instantiates the interactive player object using the 'new' keyword
        this.player = new ShooterPlayer(sprite_data_red, gameEnv);
    }
}

🔹 4. Class Hierarchy & Inheritance

  • 📌 Requirement: Create a parent class and a child class that inherits its features.
  • 📝 Example: ShooterPlayer extends the base Player class.
  • What it is: Inheritance lets a child class copy properties and behaviors from a parent class automatically using the extends keyword.
  • How it works: The ShooterPlayer class inherits from the general Player class. This gives it immediate access to basic movement and animations without writing that code all over again.
  • Why it helps: It stops you from rewriting duplicate code. Shared features stay in the parent class, making the child class smaller and easier to read since it only focuses on unique additions like ammo and shooting controls.
// EVIDENCE: CLASS HIERARCHY AND INHERITANCE
import Player from '@assets/js/GameEnginev1.1/essentials/Player.js';

// ShooterPlayer is a child class that copies features from the parent Player class
class ShooterPlayer extends Player {
    constructor(data, gameEnv) {
        // super() calls the parent class constructor to set up standard player features
        super(data, gameEnv);

        // Adds unique properties specific only to a shooting character
        this.bullets = []; // Tracks active projectiles fired by this player
        this.shootCooldown = data.shootCooldown || 500; // Minimum wait time between shots in milliseconds
        this.lastShotTime = 0; // Tracks the timestamp of the last shot to manage the cooldown
        this.facing = 'up'; // Tracks the looking direction so bullets fly the right way
    }
}

🔹 5. Polymorphism & Method Overriding

  • 📌 Requirement: Change how an inherited method works in a child class.
  • 📝 Example: Modifying the update() method inside the ShooterPlayer class.
  • What it is: Overriding happens when a child class replaces or adds to a function it got from its parent class. The function keeps the exact same name, but runs new code.
  • How it works: The child class runs its own update() function. It uses super.update() to keep the parent's basic walking mechanics, but immediately adds extra steps to move and clean up flying bullets.
  • Why it helps: It keeps the game loop simple. The main engine can tell every single object to update() without needing to care about what kind of character it is or what special actions they are doing.
// EVIDENCE: POLYMORPHISM AND METHOD OVERRIDING
class ShooterPlayer extends Player {

    // Overrides the parent class update method to add shooting logic
    update() {
        // Run the parent class update first to handle standard walking and animations
        super.update();

        // Extra custom step: Check all active bullets and remove old ones
        this.bullets = this.bullets.filter(bullet => {
            const age = Date.now() - bullet.creationTime; // Calculates how long the bullet has been flying
            
            // If the bullet has been active longer than its allowed lifetime, mark it for deletion
            if (age > bullet.lifetime) {
                bullet.destroyed = true;
                return false; // Removes it from the active bullets array
            }
            return true; // Keeps it in the array if it's still fresh
        });
    }
}

🔹 6. Encapsulation & Access Modifiers

  • 📌 Requirement: Protect variable values from being changed incorrectly by outside code.
  • 📝 Examples: Using method functions like setX() and setY() to filter values.
  • What it is: Encapsulation hides data inside an object. It stops outside scripts from changing variables directly, protecting the internal stability of the program.
  • How it works: Instead of letting other classes edit coordinates directly, specialized setting methods are used. These methods check the new values first to ensure they stay within valid screen boundaries.
  • Why it helps: It stops bugs from spreading. Bad values are caught immediately inside the specific methods instead of breaking other parts of the game engine later on.
// EVIDENCE: ENCAPSULATION AND ACCESS CONTROL
class Npc extends Character {
    /**
     * General patrol movement within defined walking area (bouncing behavior)
     * This method encapsulates the movement logic and protects the character 
     * from walking off the designated boundaries of the custom map.
     */
    patrol() {
        // Use moveDirection and speed, defaulting if not set
        if (!this.moveDirection) this.moveDirection = { x: 1, y: 1 };
        if (!this.speed) this.speed = 1;
        
        // Update position based on direction and speed
        this.position.x += this.moveDirection.x * this.speed;
        this.position.y += this.moveDirection.y * this.speed;

        // Encapsulation checking: Bounce off left/right boundaries and update sprite direction safely
        if (this.position.x <= this.walkingArea.xMin) {
            this.position.x = this.walkingArea.xMin;
            this.moveDirection.x = 1;
            this.direction = 'right';
        }
        if (this.position.x + this.width >= this.walkingArea.xMax) {
            this.position.x = this.walkingArea.xMax - this.width;
            this.moveDirection.x = -1;
            this.direction = 'left';
        }

        // Encapsulation checking: Bounce off top/bottom boundaries safely
        if (this.position.y <= this.walkingArea.yMin) {
            this.position.y = this.walkingArea.yMin;
            this.moveDirection.y = 1;
        }
        if (this.position.y + this.height >= this.walkingArea.yMax) {
            this.position.y = this.walkingArea.yMax - this.height;
            this.moveDirection.y = -1;
        }
    }
}