If you completed the challenges in the previous chapter, you likely noticed a issue: to make multiple squares, you had to duplicate your variables and code.
Complete games don't just have one or two objects. Even a simple level can contain hundreds of entities! Map tiles, players, enemies, NPCs, UI elements, weapons, and pickups are all individual entities. Imagine having to manually declare playerX, playerY, enemyX, enemyY for every single one of them. That is a nightmare.
This is where Object-Oriented Programming (OOP) and Classes come to the rescue.
What is a Class in Game Development?
Think of a class as a blueprint. If you want to build a house, you don't invent a new architecture from scratch every time; you use a blueprint, and build as many houses as you need from it.
In game development, we create a blueprint (a Class) for a generic game object. From that single blueprint, we can generate thousands of individual objects, each with their own unique coordinates, colors, and behaviors.
The Base Sprite Class
In modern JavaScript (ES6+), we use the Class keyword to create our blueprints.
For a 2D game, nearly every visual object needs the same basic properties: an X/Y position, a width, a height, and a method to draw itself on the canvas. Let's create a generic Sprite class that provides this foundation.
const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");
class Sprite {
// The constructor fires immediately when a new object is created
constructor(x, y, width, height, color = "black") {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.color = color;
}
// Every sprite should know how to draw itself
draw(ctx) {
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.width, this.height);
}
// Placeholder for movement and physics logics
update(dt) {
// We will expand it later!
}
}
Now, instead of writing dozens of variables, creating a new object is a single line of code using the new keyword:
// Instantiating our class
const player = new Sprite(50, 50, 20, 20, "blue");
const enemy1 = new Sprite(100, 100, 20, 20, "red");
const enemy2 = new Sprite(150, 100, 20, 20, "red");
// Drawing them
player.draw(context);
enemy1.draw(context);
enemy2.draw(context);
Inheritance: Classes Extending Classes
A generic Sprite is great, but a Player and an Enemy behave very differently. We don't want to rewrite the X/Y coordinates and drawing logic for every specific type of character.
Instead, we use Inheritance. We can create a specific Enemy class that extends (inherits from) our base Sprite class. It gets all the drawing and coordinate logic for free, but we can add unique properties like health or attackPower.
// Enemy inherits everything from Sprite
class Enemy extends Sprite {
constructor(x, y, name, strength) {
// 'super' calls the constructor of the parent (Sprite) class
super(x, y, 20, 20, "red");
// Unique Enemy properties
this.name = name;
this.strength = strength;
}
// Unique Enemy methods
attack() {
console.log(`\({this.name} attacks for \){this.strength} damage!`);
}
}
// Creating an Enemy
const boss = new Enemy(200, 50, "Bug", 500);
boss.draw(context); // Inherited from Sprite!
boss.attack(); // Specific to Enemy!
Managing the Chaos with Arrays
We solved the blueprint problem, but manually calling enemy1.draw(), enemy2.draw(), and boss.draw() is still tedious.
To manage large groups of objects, we use Arrays. By pushing all our game objects into an array, we can use a loop to update and draw hundreds of enemies in just three lines of code.
const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");
// Create empty arrays to hold our entities
const players = [];
const enemies = [];
// Populate the arrays
players.push(new Sprite(10, 50, 20, 20, "green"));
// Let's spawn a row of 5 enemies using a loop!
for(let i = 0; i < 5; i++) {
let startX = i * 40; // Spaces them out horizontally
enemies.push(new Enemy(startX, 100, "Bug", 10));
}
// --- Inside your Game Loop ---
// Using loop to update/draw everything
for (const player of players) {
player.draw(context);
}
for (const enemy of enemies) {
enemy.draw(context);
}
By combining Classes, Inheritance, and Arrays, you now have the exact architectural foundation used by professional game engines to manage massive, complex game states efficiently!
Next Chapter: User Input & Interactivity — Breathing Life into Our Sprites
Spawning arrays of sprites is satisfying, but right now, our game world is completely static. Our player is just sitting there staring at a row of unmoving enemies.
In the next chapter, we are going to hook up our keyboard to the game loop. We will learn how to capture user input state efficiently, map keys to velocities, and finally utilize that update(dt) function we left behind to make our player zip smoothly across the screen! See you there.
