Prima lo stack, poi il gioco

La prima domanda non era "che gioco" ma "con cosa". Per un 2D semplice o medio le opzioni pratiche sono poche: canvas nativo con JavaScript vanilla per il massimo controllo e zero dipendenze, oppure un framework come Phaser.js se serve fisica avanzata e si vuole risparmiare tempo su collisioni e sprite. Per un'interfaccia complessa (carte, puzzle, quiz) spesso basta HTML/CSS classico, senza game engine.

Dato che il resto del sito è già costruito con pagine HTML single-file senza build step, la scelta naturale è stata restare coerente: canvas nativo, un file autonomo, facile da capire e da deployare — lo stesso approccio già usato altrove per progetti personali con lo stesso stile "un file, zero dipendenze".

I requisiti minimi per farlo funzionare davvero come PWA installabile sono tre: un manifest.json con nome, icone da almeno 192px e 512px e display: "standalone"; un service worker per il caching offline e l'installabilità; e HTTPS, obbligatorio ma già garantito gratuitamente dall'hosting in uso. Il salvataggio dei progressi, in questa prima fase, resta volutamente semplice: localStorage, niente backend.

Perché Breakout, tra tutte le idee possibili

Tra le opzioni valutate — puzzle a griglia, memory a tema, quiz a tempo, Wordle-like in italiano, un gestionale idle — Breakout ha vinto per il rapporto tra tempo di sviluppo e soddisfazione immediata: logica di collisione semplice, canvas nativo senza sprite complessi, e un genere che si presta bene a diventare installabile come app arcade standalone.

Le scelte di base sono state deliberatamente minime per partire in fretta: controlli via drag touch/mouse sulla paletta (funziona uguale su desktop e mobile), livelli con difficoltà crescente, nessun power-up nella prima versione, high score in localStorage, stile visivo retro/neon. Tutto il resto — livelli fissi, mattoncini speciali, grafica più curata, audio, power-up — è arrivato in iterazioni successive, ognuna innestata sulla precedente senza riscrivere da capo.

IterazioneCosa aggiunge
1 — ScheletroCanvas, paletta, palla, mattoncini random, PWA installabile
2 — Livelli10 poi 15 livelli fissi, mattoncini multi-hit e indistruttibili, selettore livello
3 — Estetica e audioTrail, particelle, sfondo synthwave, suoni via Web Audio API
4 — Game feelScreen shake, 4 power-up, bilanciamento probabilità di drop

Da livelli casuali a 15 livelli disegnati a mano

La prima versione generava i mattoncini con una griglia semi-random che cresceva di riga in riga: funzionale, ma senza personalità. Il passo successivo è stato sostituirla con pattern disegnati a mano — piramidi, scacchiere, diamanti, corridoi, una "fortezza" — ognuno con una progressione di difficoltà pensata, non solo più righe e palla più veloce.

A quel punto sono entrati in gioco due tipi di mattoncino speciale: multi-hit (richiedono due colpi, cambiano colore dopo il primo e danno più punti) e indistruttibili (rimbalzano la palla ma non si rompono mai e non contano per il completamento del livello — altrimenti quel livello non finirebbe mai). Il controllo di "livello completato" ha dovuto ignorarli esplicitamente nel conteggio dei mattoncini rimanenti.

💡

Un dettaglio facile da dimenticare: se aggiungi mattoncini indistruttibili, ogni punto del codice che controlla "quanti mattoncini restano per finire il livello" deve escluderli esplicitamente, altrimenti il livello diventa impossibile da completare.

Con 10 livelli fissi è arrivato anche il resto della progressione: high score e livello massimo raggiunto salvati insieme in localStorage, e un selettore livello nel menu che sblocca solo quelli già visti almeno una volta. In un secondo momento la progressione si è allungata a 15 livelli, mantenendo lo stesso principio — pattern più densi di mattoncini blindati verso la fine, fino a un ultimo livello quasi completamente indistruttibile tranne una fila d'apertura.

Grafica neon e suoni senza un solo file audio

La versione base funzionava ma era visivamente spoglia. Il passo successivo ha aggiunto: una scia luminosa dietro la palla, particelle colorate quando un mattoncino esplode (colore diverso per multi-hit e indistruttibile), uno sfondo con griglia e scanline in stile synthwave, un glow pulsante sulla paletta e un flash al contatto con la palla.

Per l'audio, l'obiettivo era zero file da scaricare: la Web Audio API permette di generare oscillatori direttamente in JavaScript, modulando frequenza e volume nel tempo per ottenere bip, click ed effetti percussivi — un suono diverso per rimbalzo sul muro, sulla paletta, per mattoncino normale, multi-hit e indistruttibile, più piccole melodie per vita persa, game over, livello completato.

web audio api · bip sintetizzato, nessun file esterno
function beep(freq, duration, type = 'square', volume = 0.15) {
  if (!audioCtx || muted) return;
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();
  osc.type = type;
  osc.frequency.value = freq;
  gain.gain.value = volume;
  gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
  osc.connect(gain).connect(audioCtx.destination);
  osc.start();
  osc.stop(audioCtx.currentTime + duration);
}

Un dettaglio tecnico non negoziabile: i browser richiedono un'interazione dell'utente per avviare il contesto audio, quindi il primo suono parte solo al primo tocco o click — non c'è modo di anticiparlo, e va gestito come normale, non come bug.

Screen shake e power-up: dal "funziona" al "si sente vivo"

Lo screen shake, quando si perde una vita, è più semplice di quanto sembri: basta applicare una translate() casuale e decrescente al contesto del canvas per pochi frame prima di disegnare la scena, senza toccare la posizione reale di palla, paletta o mattoncini. Nessuna libreria fisica coinvolta, solo un'offset temporaneo sul disegno.

Per i power-up, la parte più delicata non è stata l'idea (4 tipi: paletta larga, palla lenta, multi-ball a 3 palline, vita extra) ma la ristrutturazione della logica di aggiornamento. La palla principale e le eventuali palline extra del multi-ball dovevano condividere lo stesso comportamento — collisioni, rimbalzi, effetti attivi — invece di duplicare il codice. La soluzione più robusta è stata estrarre una funzione updateSingleBall() riusabile, chiamata in loop su tutte le palline in campo.

⚠️

Il primo tentativo di implementare la "palla lenta" scalava dx/dy a ogni frame con un fattore moltiplicativo, con il rischio di divisioni per zero al ripristino della velocità. La versione più semplice e robusta applica lo scaling una sola volta, all'attivazione e alla disattivazione dell'effetto, non a ogni frame.

Ogni modifica strutturale di questo tipo è stata verificata con un controllo di sintassi JS automatico e con un browser headless che simula una partita per qualche secondo, per intercettare errori runtime che un semplice "sembra funzionare a occhio" non avrebbe rivelato.

Bilanciare le probabilità di drop

La prima versione dei power-up usava un 28% di probabilità totale di drop per ogni mattoncino normale distrutto — con centinaia di mattoncini per partita, il campo si affollava troppo in fretta. Il bilanciamento non è stato un calcolo teorico ma una stima pratica: quante volte un evento compare durante una partita reale, con quella densità di mattoncini.

Power-upPrimaDopo
🟢 Paletta larga9%5%
🔵 Palla lenta9%4%
🟡 Multi-ball7%3.5%
🔴 Vita extra3%1.5%

Il principio generale: gli aiuti minori (paletta larga, palla lenta) possono restare relativamente frequenti perché il loro impatto è limitato e temporaneo, mentre gli aiuti forti (vita extra) devono restare rari — altrimenti il gioco perde la tensione che lo rende interessante. I power-up droppano solo da mattoncini normali, mai da multi-hit o indistruttibili, per non premiare troppo generosamente i mattoncini già più difficili da rompere.

Dov'è arrivato, e cosa resta apribile

Il gioco oggi vive nella sezione giochi come PWA installabile a tutti gli effetti: 15 livelli fissi con mattoncini multi-hit e indistruttibili, selettore livello, effetti visivi e sonori generati senza asset esterni, screen shake e 4 power-up bilanciati. Il service worker cachea tutti gli asset per il funzionamento offline, e a ogni deploy la versione della cache viene incrementata per evitare mismatch tra versione servita e versione cachata.

Resta apertamente rimandata l'idea di power-up più avanzati e di una sincronizzazione dei punteggi tra dispositivi via backend, invece del solo localStorage attuale — una scelta consapevole per ora, da rivedere se il gioco continuerà a crescere.

Domande frequenti

Serve un game engine per fare un gioco 2D semplice come Breakout?

No. Per un arcade 2D semplice o medio, canvas nativo con JavaScript vanilla dà il massimo controllo senza dipendenze e senza build step, mentre un framework come Phaser.js conviene solo se serve fisica avanzata o si vogliono risparmiare settimane su collisioni e sprite. Per giochi con UI complessa (carte, puzzle, quiz) spesso basta HTML/CSS classico.

Come si generano effetti sonori senza usare file audio esterni?

Con la Web Audio API si possono creare oscillatori direttamente in JavaScript, modulandone frequenza e volume nel tempo per ottenere bip, click ed effetti percussivi. Zero file da scaricare e peso della pagina praticamente invariato; il limite è che restano suoni sintetici, adatti a un arcade retro ma non a musica complessa.

Cosa serve per rendere un gioco HTML installabile come app (PWA)?

Tre elementi minimi: un manifest.json con nome, icone da almeno 192px e 512px e display standalone; un service worker che cachi gli asset per l'offline; e HTTPS, obbligatorio ma spesso gratuito con l'hosting in uso. Con questi tre elementi il browser mostra il prompt di installazione.

Come si implementa lo screen shake su un gioco canvas?

Applicando una translate() casuale e decrescente al contesto 2D per pochi frame prima di disegnare la scena, e riportandola a zero subito dopo. Non serve muovere gli elementi di gioco: si sposta il sistema di coordinate del disegno per un istante, senza toccare la logica di collisioni.

Come si bilanciano le probabilità di drop dei power-up in un gioco arcade?

Partendo da una percentuale generosa per verificare che il sistema funzioni, poi riducendola osservando quante volte l'evento si presenta in una partita reale. Conviene differenziare le probabilità per effetto: gli aiuti più forti restano più rari di quelli minori.