Skip to main content

Platformer Game

Main menu showing Code Warrior title with Start Game button Platformer game started with player character on platforms Player character near yellow intermediate checkpoint Victory screen showing congratulations for reaching final checkpoint

The Project

Platformer game developed with intermediate object-oriented programming in JavaScript. An application that demonstrates fluid animations, physics engine, collision detection, and advanced state management to create a complete platform game.

Source Code

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Learn Intermediate OOP by Building a Platformer Game</title>
</head>
<body>
<div class="start-screen">
<h1 class="main-title">freeCodeCamp Code Warrior</h1>
<p class="instructions">
Help the main player navigate to the yellow checkpoints.
</p>
<p class="instructions">
Use the keyboard arrows to move the player around.
</p>
<p class="instructions">You can also use the spacebar to jump.</p>
<div class="btn-container">
<button class="btn" id="start-btn">Start Game</button>
</div>
</div>
<div class="checkpoint-screen">
<h2>Congrats!</h2>
<p>You reached the last checkpoint.</p>
</div>
<canvas id="canvas"></canvas>
<script src="./script.js"></script>
</body>
</html>

The Three Revelations

There are 3 concepts that struck me in this project: APIs, requestAnimationFrame, and Infinity.

APIs: Standing on the Shoulders of Giants

I find it incredible how a simple API call gives us as end users the feeling of ease, but behind the scenes there are countless lines of code that compose it.
I now recognize the great work of backend developers: whether they're software engineers from various browsers or developers contributing to open source. I recognize that I'm literally standing on the shoulders of giants.
Every time I write requestAnimationFrame() or canvas.getContext('2d'), I'm using the result of years of optimizations, technical discussions, and iterations.

requestAnimationFrame: Intelligent Efficiency

Take the requestAnimationFrame() API as an example. Thanks to this simple call, the browser knows exactly when it's about to redraw the screen (refresh cycle).
It goes from 60 to 0 fps depending on whether there's activity or not, resulting in energy and memory savings. When the tab button isn't active, the browser simply stops calling the function. Zero wasted cycles.
It's clever because instead of an infinite loop that always runs (even when not needed), the browser synchronizes the code with its native rendering cycle.

Infinity: The "Disable, Don't Destroy" Pattern

Checkpoints in this game are set to Infinity when obtained. But why not destroy the object? Why not remove it from the array entirely?

With claim() we have 4 levels of defense:

claim() {
this.width = 0; // 1. Impossible geometry
this.height = 0; // 2. Zero dimensions
this.position.y = Infinity; // 3. Out of the world
this.claimed = true; // 4. Already used
}

It's clear there's no intention to remove them from the array. I wondered why: removing them would also free up precious memory, I thought. Well, that's true but the price to pay would be very high.

Each layer protects against a different type of access. In fact, setting width and height to zero protects with mathematics: when the code performs collision detection, it calculates if two rectangles overlap, but with null dimensions the overlap becomes impossible. Even if the code executes, the geometric calculations automatically fail.

Setting position.y to Infinity protects with space. Any player position (100, 500, even 9999) will always be less than Infinity. The checkpoint has literally fallen out of the game world, and any vertical position comparison fails.

The claimed = true protects with logic and is the most efficient check. It's a guard clause, meaning a check that exits the function immediately, that tells the code "this checkpoint has already been used, don't waste time checking collisions". It prevents useless calculations allowing a quick exit from the check. In short, if one of the checks fails due to a future bug, the others save you.

The saved memory isn't worth the problems created:

Removing with checkpoints.splice(index, 1) would create more problems than it solves. At 60fps, the array indices change while you're iterating, potentially causing visible bugs. Each removal also requires memory reallocation and activates the garbage collector (the system that automatically cleans memory), not to mention that modifying the array while others are reading it causes race conditions (situations where multiple parts of the code access the same resource simultaneously creating unpredictable bugs).
With claim() there's no allocation, no garbage collection, zero pauses. Therefore, "turning off/on" existing objects is decidedly better.

What I Learned

Intermediate OOP:

  • Inheritance: Extending base classes to create object variants
  • Composition: Combining simpler objects to create complex behaviors
  • Encapsulation: Hiding implementation details behind clean interfaces
  • Object Pooling: Reusing objects instead of creating/destroying them continuously

Game Development Patterns:

  • Game Loop: Main update → render cycle synchronized with requestAnimationFrame()
  • Delta Time: Frame-rate independent handling for smooth movement
  • State Management: Managing game states (menu, playing, paused, game over)
  • Collision Detection: AABB (Axis-Aligned Bounding Box) to detect collisions

Canvas API:

  • getContext('2d') to get the drawing context
  • clearRect() to clear the canvas every frame
  • fillRect() and strokeRect() to draw shapes
  • drawImage() for sprites and textures

Physics Simulation:

  • Gravity as constant downward acceleration
  • Velocity and acceleration as 2D vectors
  • Friction for natural slowdown
  • Jump mechanics with vertical impulse

Performance Optimization:

  • requestAnimationFrame() for synchronization with refresh rate
  • Reduced DOM calls
  • Spatial partitioning for efficient collision detection
  • Boolean flags for early exit (guard clauses)

Event Handling:

  • keydown and keyup for keyboard input
  • Simultaneous multiple input handling (jump + movement)
  • Preventing default browser behaviors

Defensive Programming:

  • Intentional redundancy for robustness
  • Guard clauses to prevent useless calculations
  • Boundary checking to avoid out-of-bounds
  • Fallback values for edge case situations

Ready to start the next project!


Next Project: Review Algorithmic Thinking by Building a Dice Game