preview

Creating a simple, object-oriented platformer game in p5.js

By Squiggles, on January 03, 2024 - My process making a simple platformer in p5.js!

Want to just play the game? Click here!

The Idea

I’m going to create a simple platformer game in p5.js that involves jumping around and fighting enemies. It won’t win any awards, but it will be a fun project to work on and take you through my process of creating a game, especially one that is object-oriented.

To start, let’s lay out some success critera for the project.

Success Criteria

Process

Without further ado, let’s get started!

Creating the World

The first thing we need to do is create the world class. This will be the class that contains all the entities and objects in the world, and will be responsible for updating and drawing them. My first iteration of this class covered the very basics, and looked like this:

class World {
    constructor(name, gravity, tickRate) {
        this.name = name;
        this.gravity = gravity;
        this.tickRate = tickRate;
        this.entities = [];
    }
    getName() {
        return this.name;
    }
    getGravity() {
        return this.gravity;
    }
    getTickRate() {
        return this.tickRate;
    }
    getEntities() {
        return this.entities;
    }
    setName(name) {
        this.name = name;
    }
    setGravity(gravity) {
        this.gravity = gravity;
    }
    setTickRate(tickRate) {
        this.tickRate = tickRate;
    }
    toString() {
        return this.name + " " + this.gravity + " " + this.tickRate;
    }
    addEntity(entity) {
        this.entities.push(entity);
    }
    tick() {
        for (let i = 0; i < this.entities.length; i++) {
            this.entities[i].tick();
        }
        sleep(1/this.tickRate);
    }
}

This code mostly consists of getters and setters for the attributes of the world, and that’s about it. The only other thing of note is the tick() method, which calls the tick() method of each entity in the world, and then waits for 1/tickRate seconds. This is so that the game runs at a consistent speed, and doesn’t run faster on faster computers.

Here you’ll notice that I’ve left out the logic for handling gravity acting on the entities. This is because I wanted entities to handle their own gravity, so collisions would be easier to handle.

Implementing gravity

As I mentioned before, I wanted entities to handle their own gravity. So, let’s create an entity class that will handle the gravity and other general movement for us. This class will be abstract, and will be inherited by the player and enemies. To preface, as I’m sure we all know:

F=maF = ma

Where F (N)F\ (N) is the force, m (kg)m\ (kg) is the mass, and a (ms2)a\ (ms^{-2}) is the acceleration due to gravity. Therefore, to calculate the weight force on the object, we can use that formula.

So, I implemented the class like this:

class Entity {
    constructor (world, id, name, position, velocity, mass, maxHealth) {
        this.world = world;
        this.id = id;
        this.name = name;
        this.position = position;
        this.velocity = velocity;
        this.mass = mass;
        this.maxHealth = maxHealth;
        this.health = maxHealth;
    }
    getId() {
        return this.id;
    }
    getName() {
        return this.name;
    }
    getPosition() {
        return this.position;
    }
    getMass() {
        return this.mass;
    }
    getMaxHealth() {
        return this.maxHealth;
    }
    getHealth() {
        return this.health;
    }
    setId(id) {
        this.id = id;
    }
    setName(name) {
        this.name = name;
    }
    setPosition(position) {
        this.position = position;
    }
    setMass(mass) {
        this.mass = mass;
    }
    setMaxHealth(maxHealth) {
        this.maxHealth = maxHealth;
    }
    setHealth(health) {
        this.health = health;
    }
    toString() {
        return this.id + " " + this.name + " " + this.position + " " + this.mass + " " + this.maxHealth 
            + " " + this.health;
    }
    handleGravity(gravity, t) {
        let force = this.mass * gravity;
        this.position.y += t * (this.velocity.y + t * force / 2);
        this.velocity.y += force * t;
    }
    handleCollision(entities) {
        if (this.position.y + 25 > height) {
            this.position.y = height - 25;
        }
    }
    draw() {
        circle(this.position.x, this.position.y, 50);
    }
    tick() {
        this.handleCollision(this.world.getEntities());
        this.handleGravity(this.world.getGravity(), 1/this.world.getTickRate());
        this.handleCollision(this.world.getEntities());
        this.draw();
    }
}

As you can see, this class is also very basic to start off with. Getters and setters again, and a tick() method that gets called by the world. What we’re interested in here is the handleGravity() method. I’ve used a physical force based approach for the gravity, hence force = mass * gravity, like I mentioned earlier.

I’ve also added a very basic handleCollision() method that just stops the entity from falling off the screen. This will be expanded on later.

First test

Now that we have the world and entity classes, let’s test them out!

Let’s set some testing criteria:

Results

TestResultProof
The program should run.Passed!No errors appeared in the console.
The world should be able to be created with a name, gravity, and tick rate.Passed!Returned: Test 9.8 20
The world should be able to add entities.Passed!No errors appeared in the console.
The world should be able to tick.Passed!No errors appeared in the console.
The entity should be able to be created with a name, position, velocity, mass, and max health.Passed!Returned: 0 Test p5.Vector Object : [956, 0.1, 0] 10 100 100
The entity should fall due to gravity.Passed!See video below!
The entity shouldn’t fall off the screen.Passed!

Creating the player

Now that we have the world and entity classes, let’s create the player class. We want it to have the features of the entity class, whilst also being able to have its own unique features. So to do this, we’ll inherit from the entity class. Here we go:

class PlayerEntity extends Entity {
    constructor (world, id, name, position, velocity, mass, maxHealth, strafingForce, jumpingForce,
        crouchingForce) {
        super(world, id, name, position, velocity, mass, maxHealth, strafingForce,
            jumpingForce, crouchingForce);
    }
    handleInput() {
        if (keyIsDown(65)) {
            this.strafingLeft = true;
        }
        else if (keyIsDown(68)) {
            this.strafingRight = true;
        }
        else if (keyIsDown(32) || keyIsDown(87)) {
            this.jumping = true;
        }
        else if (keyIsDown(83)) {
            this.crouching = true;
        }
        else {
            this.strafingLeft = false;
            this.strafingRight = false;
            this.jumping = false;
            this.crouching = false;
        }
    }
    tick() {
        this.handleCollision(this.world.getEntities());
        this.handleInput();
        this.handleMovement(1/this.world.getTickRate());
        this.handleGravity(this.world.getGravity(), 1/this.world.getTickRate());
        this.handleCollision(this.world.getEntities());
    }
    toString() {
        return "PlayerEntity[" + this.id + ", " + this.name + ", " + this.position + ", " + this.velocity +
            ", " + this.mass + ", " + this.maxHealth + ", " + this.health + "]";
    }
}

As you can see, this class mostly sends its data to the superclass, apart from it has a handleInput() method that handles the keyboard input from the player, to actually call the movement methods created in the superclass. It also overrides the tick() method to include the call of the handleInput() method.

Adding platforms

Now that we have the player class, let’s add some platforms to the world. This will be useful as it will allow the player to jump around and test the gravity and collision detection.

class Platform {
    constructor(world, id, position, width, height, color, slippery) {
        this.world = world;
        this.id = id;
        this.position = position;
        this.width  = width;
        this.height = height;
        this.color = color;
        this.slippery = slippery;
    }
    getId() {
        return this.id;
    }
    getPosition() {
        return this.position;
    }
    getWidth() {
        return this.width;
    }
    getHeight() {
        return this.height;
    }
    getColor() {
        return this.color;
    }
    getSlippery() {
        return this.slippery;
    }
    getBounds() {
        //return the top, left, bottom and rightmost coordinates of the platform
        return {
            top: this.position.y - this.height/2,
            left: this.position.x - this.width/2,
            bottom: this.position.y + this.height/2,
            right: this.position.x + this.width/2
        };
    }
    setId(id) {
        this.id = id;
    }
    setPosition(position) {
        this.position = position;
    }
    setWidth(width) {
        this.width = width;
    }
    setHeight(height) {
        this.height = height;
    }
    setColor(color) {
        this.color = color;
    }
    setSlippery(slippery) {
        this.slippery = slippery;
    }
    toString() {
        return "Platform[" + this.id + ", " + this.position + ", " + this.width + ", " + this.height + ", "
            + this.color + ", " + this.slippery + "]";
    }
    draw() {
        push();
        rectMode(CENTER);
        fill(this.color);
        rect(this.position.x, this.position.y, this.width, this.height);
        pop();
    }
}

Again, this class is mostly comprised of getters and setters, and a draw() method. The only thing of note is the getBounds() method, which returns the top, left, right, and bottommost coordinates of the platform. This will be useful for collision detection.

Adding proper collisions

Speaking of, now that we have platforms, let’s make entities collide with them properly! We’ll need to update the handleCollision() method in the entity class to handle collisions with other objects.

handleCollision() {
    let radius = 25;
    if (this.position.y + 25 > height) {
        this.position.y = height - 25;
        this.grounded = true;
    }
    for (let i = 0; i < this.world.getPlatforms().length; i++) {
        let top = this.world.getPlatforms()[i].getBounds().top;
        let aboveTop = this.position.y - radius < top;

        let left = this.world.getPlatforms()[i].getBounds().left;
        let withinLeft = this.position.x - radius > left;

        let bottom = this.world.getPlatforms()[i].getBounds().bottom;
        let belowBottom = this.position.y - radius > bottom;

        let right = this.world.getPlatforms()[i].getBounds().right;
        let withinRight = this.position.x - radius < right;

        if (!aboveTop && withinLeft && belowBottom && withinRight) {
            this.position.y = top - radius;
        }
    }
}

This method loops through all the the platforms in the world, and checks if the entity is within the bounds of the platform. If it is, it moves the entity out of the bounds of the platform.

Second test

Now that we have the player class and platforms, it’s time to test!

Criteria:

Results

TestResultProof
The program should run.Passed!No errors appeared in the console.
The player should be able to be created with a name, position, velocity, mass, and max health.Passed!Returned: PlayerEntity[0, Test, p5.Vector Object : [956, 0.1, 0], 10, 100, 100]
The player should fall due to gravity.Passed!See video below!
The player should be able to move left and right.Passed!See video below!
Platforms should be able to be created with a position, width, height, color, and slipperiness.Passed!Returned: Platform[0, Test, p5.Vector Object : [956, 540, 0]], 200, 50, "#FF00FF", false
The player should collide with the platforms.Failed.

As you can see from the video, the player can move left and right, but gets stuck in some quantum entanglement nightmare when it collides with the platform. We’re gonna have to fix that.

Fixing collisions

This was a real head-scratcher. I couldn’t figure out why the player was getting stuck in the platform. I tried everything, from changing the order of the checks, to changing the way the player was moved out of the platform. I ended up fixing it (after 9 days!) thanks to this amazing video on creating a platformer in Pygame, by DaFluffyPotato.

The solution was simple - instead of handling collision before or after the entity moves, we handle it during the movement, for each axis.

This little animation shows how it works:

Essenstially, we check for collisions for each axis. For this reason, we mix it in with the movement code, so that the first check is after x-axis movement, and the second check is after y-axis movement. This way, we only have to worry about each axis at a time, and can get the direction of the velocity of the entity, to tell where it came from, and snap it back:

handleMovement(platforms, gravity) {
    if (isNaN(this.velocity.y) || isNaN(this.velocity.x) || isNaN(deltaTime) || isNaN(this.strafingForce)
            || isNaN(this.jumpingForce) || isNaN(gravity)) {
        return;
    }

    this.velocity.y = min(100, this.velocity.y + gravity * deltaTime);
    this.airTimer ++;

    if (this.strafingLeft || this.strafingRight) {
        if (this.strafingLeft) {
            this.velocity.x = max(-2, this.velocity.x - this.strafingForce * deltaTime);
        }
        if (this.strafingRight) {
            this.velocity.x = min(2, this.velocity.x + this.strafingForce * deltaTime);
        }
    } else {
        this.velocity.x = this.velocity.x * 0.95;
    }
    this.jumpCooldown -= deltaTime;

    if (this.jumping && this.jumps > 0) {
        this.jumping = false;
        this.jumpCooldown = 0.25;   
        this.velocity.y = -5;
        this.jumps -= 1;
    }

    this.position.x += this.velocity.x;

    platforms.forEach(platform => {
        const platformBounds = platform.getBounds();
        if (this.collides(this.getBounds(), platformBounds)) {
            if (this.velocity.x > 0) {
                this.position.x = platformBounds.left - 25.1;
                this.velocity.y = this.velocity.y * 0.95;
                if (this.jumps == 0) {
                    this.jumps = 1;
                }
            } if (this.velocity.x < 0) {
                this.position.x = platformBounds.right + 25.1;
                // drag
                this.velocity.y = this.velocity.y * 0.95;
                if (this.jumps == 0) {
                    this.jumps = 1;
                }
            }
        } else {
            this.world.setGravity(9.8);
        }
    });

    this.position.y += this.velocity.y;

    platforms.forEach(platform => {
        const platformBounds = platform.getBounds();
        if (this.collides(this.getBounds(), platformBounds)) {
            if (this.velocity.y > 0) {
                this.position.y = platformBounds.top - 25.1;
                this.velocity.y = 0;
                this.jumps = 2;
                this.airTimer = 0;
            } if (this.velocity.y < 0) {
                this.position.y = platformBounds.bottom + 25.1;
                this.velocity.y = 0;
            }
        }
    });
}

Like I described earlier, we run the collision check after moving on each axis, and if there is a collision, we snap the entity back to the edge of the platform, and set its velocity to 0.

Such a simple solution to such an annoying problem!

Third test

Let’s run the same test critera as test 2, and see if it works now.

Results

TestResultProof
The program should run.Passed!No errors appeared in the console.
The player should be able to be created with a name, position, velocity, mass, and max health.Passed!Returned: PlayerEntity[0, Test, p5.Vector Object : [956, 0.1, 0], 10, 100, 100]
The player should fall due to gravity.Passed!See video below!
The player should be able to move left and right.Passed!See video below!
Platforms should be able to be created with a position, width, height, color, and slipperiness.Passed!Returned: Platform[0, Test, p5.Vector Object : [956, 540, 0]], 200, 50, "#FF00FF", false
The player should collide with the platforms.Passed!

Yes, the player is square now. And the background is blue. I think it looks nicer for now!

Adding enemies

Enemies! These will finally give the game some life. Let’s create an enemy class that inherits from the entity class.

class EnemyEntity extends Entity {
    constructor (world, id, name, position, velocity, mass, maxHealth, width, height, strafingForce,
            jumpingForce, patrolPoints) {
        super(world, id, name, position, velocity, mass, maxHealth, width, height, strafingForce,
            jumpingForce);

        this.patrolPoints = patrolPoints;
        this.patrolIndex = 0;
        this.goals = {
            patrol: this.patrolGoal.bind(this),
            chase: this.chaseGoal.bind(this),
            attack: this.attackGoal.bind(this)
        }
        this.flipped = false;
        this.goalOrder = [];
        this.SPRITE;
        this.enemyFall = enemyFall;
        this.enemyJump = enemyJump
    }

    draw() {
        push();
        fill(255, 0, 0);
        imageMode(CENTER);
        if (this.flipped) {
            push();
            scale(-1, 1);
            image(this.SPRITE, -this.position.x, this.position.y, this.width, this.height);
            pop();
        } else {
            image(this.SPRITE, this.position.x, this.position.y, this.width, this.height);
        }
        

        fill(100, 100, 100);
        rect(this.position.x - this.width/6, this.position.y - 100, 50, 10);
        fill(255, 0, 0);
        rect(this.position.x - this.width/6, this.position.y - 100, this.health * 5, 10);
        pop();
    }

    tick() {
        this.runGoals(this.goalOrder);
        this.handleMovement(this.world.getPlatforms(), this.world.getGravity());
        this.handleHealth();
        this.animation();
    }

    setGoalOrder(goalList) {
        this.goalOrder = goalList;
    }
    

    runGoals(goalList) {
        for (const goal of goalList) {
            if (goal()) {
                break;
            }
        }
    }


    patrolGoal() {
        const currentWaypoint = this.patrolPoints[this.patrolIndex];
        const distanceToWaypoint = currentWaypoint.x - this.position.x;
        const direction = currentWaypoint.copy().sub(this.position).normalize();
        if (Math.abs(distanceToWaypoint) > 5) {
            if (direction.x > 0) {
                this.strafingRight = true;
                this.strafingLeft = false;
                this.maxSpeed = 2;
            } else if (direction.x < 0) {
                this.strafingLeft = true;
                this.strafingRight = false;
                this.maxSpeed = 2;
            }
        } else {
            this.patrolIndex = (this.patrolIndex + 1) % this.patrolPoints.length;
        }

        return true;
    }

    chaseGoal() {
        const player = this.world.getPlayer();
        if (player == null) {
            return false;
        }
        const distanceToPlayer = player.getPosition().x - this.position.x;
        const direction = player.getPosition().copy().sub(this.position).normalize();

        if (Math.abs(distanceToPlayer) > 750) {
            return false;
        }
        if (this.strafingLeft && direction.x > 0) {
            return false;
        }
        else if (this.strafingRight && direction.x < 0) {
            return false;
        }
        else {
            if (direction.x > 0) {
                this.strafingRight = true;
                this.strafingLeft = false;
                this.maxSpeed = 4;
            } else if (direction.x < 0) {
                this.strafingLeft = true;
                this.strafingRight = false;
                this.maxSpeed = 4;
            }
        }

        return true;
    }
    attackGoal() {
        const player = this.world.getPlayer();

        if (player == null) {
            return false;
        }
        if (dist(this.position.x, this.position.y, player.position.x, player.position.y) < 70) {
            this.strafingLeft = false;
            this.strafingRight = false;
            this.attack();
            return true;
        } else {
            return false;
        }
    }
    dummyGoal() {
        return true;
    }

    animation() {

        if (this.wallTimer > 0.05) {
            this.enemyFall = enemyOldFall;
            this.enemyJump = enemyOldJump;
        }
    
        if (this.velocity.y > 0.1) {
            this.SPRITE = this.enemyFall;
            if (this.strafingLeft) {
                this.flipped = false;
            } else if (this.strafingRight) {
                this.flipped = true;
            }
        } else if (this.velocity.y < 0) {
            this.SPRITE = this.enemyJump;
            if (this.strafingLeft) {
                this.flipped = false;
            } else if (this.strafingRight) {
                this.flipped = true;
            } 
            if (this.enemyFall == enemyWallFall) {
                this.SPRITE = enemyWallJump;
            }
        } else {
            if ((this.strafingLeft || this.strafingRight) && animationFrame == 1) {
                this.SPRITE = enemyWalk1;
                if (this.strafingLeft) {
                    this.flipped = false;
                } else if (this.strafingRight) {
                    this.flipped = true;
                }
            } else if ((this.strafingLeft || this.strafingRight) && animationFrame == 2) {
                this.SPRITE = enemyWalk2;
                if (this.strafingLeft) {
                    this.flipped = false;
                } else if (this.strafingRight) {
                    this.flipped = true;
                }
            } else if ((this.strafingLeft || this.strafingRight) && animationFrame == 3) {
                this.SPRITE = enemyWalk3;
                if (this.strafingLeft) {
                    this.flipped = false;
                } else if (this.strafingRight) {
                    this.flipped = true;
                }
            } else if ((this.strafingLeft || this.strafingRight) && animationFrame == 4) {
                this.SPRITE = enemyWalk4;
                if (this.strafingLeft) {
                    this.flipped = false;
                } else if (this.strafingRight) {
                    this.flipped = true;
                }
            } else {
                if (animationFrame % 4 == 0) {
                    this.SPRITE = enemyIdle1;
                } else {
                    this.SPRITE = enemyIdle2;
                }
            }
        }
    }
    
}

As you can see, this class is huge! It has a lot of methods, and a lot of attributes. Let’s go through them.

Animation

The first thing of note is that my very good friend mewchu_idk created some sprites for me! They are very simple and cute, and I love them. I’ve added them to the game, and implemented a very basic animation system for them.

AI Goals

The next thing of note is the goals attribute. For this I took inspiration from the way Minecraft handles its entity AI. It is a dictionary of goal functions to choose from. The functions chosen are then called by the runGoals() method:

runGoals(goalList) {
    for (const goal of goalList) {
        if (goal()) {
            break;
        }
    }
}

This simply loops through the list of provided goals, and and checks if they are “possible” (return true). If they are, it runs them, and then breaks out of the loop. This allows us to have a list of goals that are run in order, with the highest priority at the top, and if one is possible, it is run, and the rest (lower priority) are ignored.

One type of goal is the chaseGoal(). This goal is “possible” if the player is within 750 pixels of the enemy. If it is, the enemy will chase the player. This is done by setting the strafingLeft and strafingRight attributes to true, depending on the direction of the player:

chaseGoal() {
    const player = this.world.getPlayer();
    if (player == null) {
        return false;
    }
    const distanceToPlayer = player.getPosition().x - this.position.x;
    const direction = player.getPosition().copy().sub(this.position).normalize();

    if (Math.abs(distanceToPlayer) > 750) {
        return false;
    }
    if (this.strafingLeft && direction.x > 0) {
        return false;
    }
    else if (this.strafingRight && direction.x < 0) {
        return false;
    }
    else {
        if (direction.x > 0) {
            this.strafingRight = true;
            this.strafingLeft = false;
            this.maxSpeed = 4;
        } else if (direction.x < 0) {
            this.strafingLeft = true;
            this.strafingRight = false;
            this.maxSpeed = 4;
        }
    }
    return true;
}

Fourth test

It’s testing time once again!

Criteria:

Results

TestResultProof
The program should run.Passed!No errors appeared in the console.
The enemy should be able to be created with a name, position, velocity, mass, and max health.Passed!Returned: EnemyEntity[0, Test, p5.Vector Object : [956, 0.1, 0], 10, 100, 100]
The enemy should fall due to gravity.Passed!See video below!
The enemy should be able to move left and right.Passed!See video below!
The enemy should collide with platforms.Passed!See video below!
The enemy should take a list of goals, and run them in priority order.Passed!

Final touches

Now that all of our systems are in place, let’s add some UI and “juice” to the game. I’m going to add a health bar, a parallax background, a tutorial, and some screen shake.

Health bar

This is super simple! I just added a rectangle to the main draw() method, and made it change size depending on the player’s health. I also added mini health bars to the enemies.

Parallax background

This one is a bit more complex. I created a Camera class that handles the position of a fake camera, and then translates the world to that position. Then, in the world class, I added a drawBackground() method:

    drawBackground() {
    background(0);	
    for (let x = -bgWidth; x < width * 8; x += bgWidth - 1) {
        for (let y = -bgHeight; y < height; y += bgHeight - 1) {
            image(bg, x - this.camera.x * 0.5, y - this.camera.y * 0.5, bgWidth, bgHeight);
        }
    }
}

This method tiles the image across the screen, and translates it by half of the camera’s position, so that it appears to move slower than the player.

Screen shake

In our new Camera class, I added a shake() and performShake() method:

shake(strength, duration){
    this.shakeStrength = strength;
    this.shakeDuration = duration;
}
performShake(strength) {
    this.x += random(-strength, strength);
    this.y += random(-strength, strength);
}

The shake() method sets the strength and duration of the shake, and the performShake() method actually performs the shake. These methods can be called whenever, such as when the player takes damage, or dies.

Tutorial

This was by far the simplest thing to add. I just added some text fields around the world. However! I couldn’t help but make the text overly sarcastic. So I collaborated with ChatGPT, and created these messages:

Ah, the abyss welcomes you! Revel in the thrill of using the groundbreaking A and D keys for a riveting horizontal journey.

Behold the majestic space and W keys– gatekeepers to the world of vertical enlightenment. Press them to ascend to new heights.

Double-jump, because clearly, one jump is for beginners. Show off your gravity-defying skills!

Walls aren’t just for holding up ceilings. Grab them, jump off them, become the parkour prodigy you were always meant to be.

Congratulations, you navigated a straight line. I’m sure Nobel Prize committees are taking note.

Meet our resident dummy – a real Shakespearean tragedy in the making. Left click to end its miserable existence, if you fancy.

Oh, and you probably shouldn’t fall in the lava. It’s hot.

Meet your new best friend – alive, kicking, and ready to spice up your life. But beware, it’s got a knack for the whole ‘murdering you’ thing. Proceed with caution, or not. Your call.

Those were super fun to make, and I think they add a lot of character to the game.

Final test

Let’s run the final test!

Criteria:

Results

TestResultProof
The program should run.Passed!No errors appeared in the console.
The player should be able to be created with a name, position, velocity, mass, and max health.Passed!Returned: PlayerEntity[0, Test, p5.Vector Object : [956, 0.1, 0], 10, 100, 100]
The player should fall due to gravity.Passed!See video below!
The player should be able to move left and right.Passed!See video below!
Platforms should be able to be created with a position, width, height, color, and slipperiness.Passed!Returned: Platform[0, Test, p5.Vector Object : [956, 540, 0]], 200, 50, "#FF00FF", false
The player should collide with the platforms.Passed!See video below!
Enemies should be able to be created with a name, position, velocity, mass, and max health.Passed!Returned: EnemyEntity[0, Test, p5.Vector Object : [956, 0.1, 0], 10, 100, 100]
Enemies should fall due to gravity.Passed!See video below!
Enemies should be able to move left and right.Passed!See video below!
Enemies should collide with platforms.Passed!See video below!
Enemies should take a list of goals, and run them in priority order.Passed!See video below!
The game should have health bars.Passed!See video below!
The game should have a parallax background.Passed!See video below!
The game should have a tutorial.Passed!See video below!
The game should have screen shake.Passed!

Analysis

Let’s compare our final product to our original criteria:

CriteriaResult
A base entity class that the player and enemies will inherit from.This criteria has 100% been met by the Entity class.
A world class that will contain the player and enemies, as well as the platforms and other objects in the world.This criteria has 100% been met by the World class.
The ability of enemies to pathfind to the player and attack them.“Pathfinding” is a bit of a stretch, but the enemies have functional AI and do chase the player and attack them if told to.
A friendly user interface that allows the player to see their health and other stats.This criteria has been met by the health bars, but also by the Camera class, the tutorial text, and the handleInput() method in the PlayerEntity class, as user input is definitely a form of user interface.

Conclusion

I’m very happy with the result of this project. I think it’s a great example of how to use object-oriented programming to create a game, and it strengthened my understanding of the concepts of OOP. I also think it’s a great example of how to use the p5.js library to create a game. I’m very happy with the result, and I hope you are too, and you can check the game out here!

Thanks for reading!