Passa al contenuto principale

Platformer Game

Menu principale che mostra il titolo Code Warrior con il pulsante Avvia gioco Gioco platform avviato con il personaggio del giocatore sulle piattaforme Personaggio del giocatore vicino a un checkpoint intermedio giallo Schermata di vittoria che mostra un messaggio di congratulazioni per aver raggiunto il checkpoint finale

Il Progetto

Platformer game sviluppato con programmazione orientata agli oggetti intermedia in JavaScript. Un'applicazione che dimostra animazioni fluide, physics engine, collision detection e gestione avanzata dello stato per creare un gioco platform completo.

Codice Sorgente

<!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>

Le Tre Rivelazioni

Sono 3 i concetti che mi hanno colpito di questo progetto: API, requestAnimationFrame e Infinity.

API: Sulle Spalle di Giganti

Trovo incredibile come un semplice richiamo ad una API dia a noi utilizzatori finali la sensazione di facilità del comando, ma dietro le quinte ci sono svariate linee di codice che la compongono.
Riconosco ora il grande lavoro dei backend: che siano gli ingegneri software dei vari browser, che siano i developer che contribuiscono con l'opensource. Riconosco di stare letteralmente sulle spalle di giganti.
Ogni volta che scrivo requestAnimationFrame() o canvas.getContext('2d'), sto usando il risultato di anni di ottimizzazioni, discussioni tecniche e iterazioni.

requestAnimationFrame: Efficienza Intelligente

Prendiamo ad esempio l'API requestAnimationFrame(). Grazie a questa semplice chiamata, il browser conosce esattamente quando sta per ridisegnare lo schermo (refresh cycle).
Passa dai 60 agli 0 fps in base a se c'è attività o meno, con conseguente risparmio di energia e memoria allocata. Quando il pulsante tab non è attivo, il browser semplicemente smette di chiamare la funzione. Zero cicli sprecati.
È intelligente perché invece di un loop infinito che gira sempre (anche quando non serve), il browser sincronizza il codice con il suo ciclo di rendering nativo.

Infinity: Il Pattern "Disattiva, Non Distruggere"

I checkpoint in questo gioco sono settati su Infinity quando vengono ottenuti. Ma perché non distruggere l'oggetto? Perché non eliminarlo del tutto dall'array?

Con claim() abbiamo 4 livelli di difesa:

claim() {
this.width = 0; // 1. Geometria impossibile
this.height = 0; // 2. Zero dimensioni
this.position.y = Infinity; // 3. Fuori dal mondo
this.claimed = true; // 4. Già usato
}

Emerge chiaramente che non c'è alcuna intenzione di eliminarli dall'array. Mi sono chiesto il perché: eliminandoli si libererebbe anche memoria preziosa, ho pensato. Ebbene, è vero ma il prezzo da pagare sarebbe molto alto.

Ogni livello protegge da un diverso tipo di accesso. Infatti impostare width e height a zero protegge con la matematica: quando il codice esegue la collision detection, calcola se due rettangoli si sovrappongono, ma con dimensioni nulle la sovrapposizione diventa impossibile. Anche se il codice viene eseguito, i calcoli geometrici falliscono automaticamente.

Impostare position.y a Infinity protegge con lo spazio. Qualsiasi posizione del player (100, 500, persino 9999) sarà sempre minore di Infinity. Il checkpoint è letteralmente caduto fuori dal mondo del gioco, e qualsiasi confronto di posizione verticale fallisce.

Il claimed = true protegge con la logica ed è il controllo più efficiente. È una guard clause, ovvero un controllo che esce subito dalla funzione, che dice al codice "questo checkpoint è già stato usato, non perdere tempo a controllare collisioni". Previene calcoli inutili permettendo un'uscita rapida dal controllo. In poche parole se per un bug futuro uno dei controlli fallisce, gli altri ti salvano.

La memoria risparmiata non vale i problemi creati:

Eliminare con checkpoints.splice(index, 1) creerebbe più problemi di quanti ne risolva. A 60fps, gli indici dell'array cambiano mentre stai iterando, causando possibili bug visivi. Ogni eliminazione inoltre richiede riallocazione di memoria e attiva il garbage collector (il sistema che pulisce automaticamente la memoria), senza contare che modificare l'array mentre altri lo leggono causa race condition (situazioni dove più parti del codice accedono alla stessa risorsa contemporaneamente creando bug imprevedibili).
Con claim() non c'è allocazione, nessuna garbage collection, zero pause. Risulta pertanto decisamente migliore "spegnere/riaccendere" oggetti esistenti.

Cosa Ho Imparato

OOP Intermedia:

  • Inheritance: Estensione di classi base per creare varianti di oggetti
  • Composition: Combinazione di oggetti più semplici per creare comportamenti complessi
  • Encapsulation: Nascondere dettagli implementativi dietro interfacce pulite
  • Object Pooling: Riutilizzo di oggetti invece di crearli/distruggerli continuamente

Game Development Patterns:

  • Game Loop: Ciclo principale update → render sincronizzato con requestAnimationFrame()
  • Delta Time: Gestione frame-rate indipendente per movimento fluido
  • State Management: Gestione stati del gioco (menu, playing, paused, game over)
  • Collision Detection: AABB (Axis-Aligned Bounding Box) per rilevare collisioni

Canvas API:

  • getContext('2d') per ottenere il contesto di disegno
  • clearRect() per pulire il canvas ad ogni frame
  • fillRect() e strokeRect() per disegnare forme
  • drawImage() per sprite e texture

Physics Simulation:

  • Gravità come accelerazione costante verso il basso
  • Velocità e accelerazione come vettori 2D
  • Friction per rallentamento naturale
  • Jump mechanics con impulso verticale

Performance Optimization:

  • requestAnimationFrame() per sincronizzazione con refresh rate
  • Riduzione chiamate al DOM
  • Spatial partitioning per collision detection efficiente
  • Flag booleani per early exit (guard clauses)

Event Handling:

  • keydown e keyup per input tastiera
  • Gestione input multipli simultanei (salto + movimento)
  • Prevenzione comportamenti di default del browser

Defensive Programming:

  • Ridondanza intenzionale per robustezza
  • Guard clauses per prevenire calcoli inutili
  • Boundary checking per evitare out-of-bounds
  • Fallback values per situazioni edge case

Pronto ad iniziare il prossimo progetto!


Prossimo progetto: Ripassare il Pensiero Algoritmico costruendo un Dice Game