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.
Stack first, game second
The first question wasn't "which game" but "with what". For a simple or medium 2D game the practical options are few: native canvas with vanilla JavaScript for maximum control and zero dependencies, or a framework like Phaser.js if you need advanced physics and want to save time on collisions and sprites. For a complex interface (cards, puzzles, quizzes), plain HTML/CSS is often enough, with no game engine at all.
Since the rest of the site is already built as single-file HTML pages with no build step, the natural choice was to stay consistent: native canvas, one self-contained file, easy to understand and deploy — the same approach already used elsewhere for personal projects with the same "one file, zero dependencies" style.
The minimum requirements to make it actually work as an installable PWA are three: a manifest.json with a name, icons at least 192px and 512px, and display: "standalone"; a service worker for offline caching and installability; and HTTPS, mandatory but already provided free by the hosting in use. Progress saving, at this stage, is deliberately simple: localStorage, no 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.
Why Breakout, among all the possible ideas
Among the options considered — a grid puzzle, a themed memory game, a timed quiz, an Italian Wordle-like, a light idle-management game — Breakout won on the ratio between development time and immediate payoff: simple collision logic, native canvas with no complex sprites, and a genre that lends itself well to becoming an installable standalone arcade app.
The initial choices were deliberately minimal to get started quickly: touch/mouse drag controls for the paddle (works the same on desktop and mobile), levels with increasing difficulty, no power-ups in the first version, high score in localStorage, a retro/neon visual style. Everything else — fixed levels, special bricks, richer graphics, audio, power-ups — arrived in later iterations, each one grafted onto the previous work rather than rewritten from scratch.
| Iterazione | Cosa aggiunge |
|---|---|
| 1 — Scheletro | Canvas, paletta, palla, mattoncini random, PWA installabile |
| 2 — Livelli | 10 poi 15 livelli fissi, mattoncini multi-hit e indistruttibili, selettore livello |
| 3 — Estetica e audio | Trail, particelle, sfondo synthwave, suoni via Web Audio API |
| 4 — Game feel | Screen 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.
From random levels to 15 hand-drawn ones
The first version generated bricks with a semi-random grid that grew row by row: functional, but with no personality. The next step was replacing it with hand-drawn patterns — pyramids, checkerboards, diamonds, corridors, a "fortress" — each with a deliberate difficulty curve, not just more rows and a faster ball.
At that point two kinds of special brick came in: multi-hit (need two hits, change color after the first and award more points) and indestructible (bounce the ball but never break and don't count toward level completion — otherwise that level would never end). The "level complete" check had to explicitly exclude them when counting remaining bricks.
An easy detail to forget: once you add indestructible bricks, every place in the code that checks "how many bricks are left to finish the level" needs to explicitly exclude them, or the level becomes impossible to complete.
With 10 fixed levels came the rest of the progression too: high score and highest level reached saved together in localStorage, and a level selector in the menu that unlocks only levels already seen at least once. Later the progression was extended to 15 levels, keeping the same principle — denser patterns of armored bricks toward the end, up to a final level that's almost entirely indestructible except for one opening row.
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.
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.
Neon visuals and sound without a single audio file
The base version worked but looked visually bare. The next step added: a glowing trail behind the ball, colored particles when a brick explodes (a different color for multi-hit and indestructible), a synthwave-style grid-and-scanline background, a pulsing glow on the paddle, and a flash on contact with the ball.
For audio, the goal was zero files to download: the Web Audio API lets you generate oscillators directly in JavaScript, modulating frequency and volume over time to get beeps, clicks, and percussive effects — a different sound for a wall bounce, a paddle bounce, a normal brick, a multi-hit brick, and an indestructible one, plus short melodies for losing a life, game over, and level complete.
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);
}
One non-negotiable technical detail: browsers require a user interaction to start the audio context, so the first sound only plays on the first tap or click — there's no way around it, and it should be handled as expected behavior, not a 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.
Screen shake and power-ups: from "it works" to "it feels alive"
Screen shake, when you lose a life, is simpler than it sounds: apply a random, decreasing translate() to the canvas context for a few frames before drawing the scene, without touching the real position of the ball, paddle, or bricks. No physics library involved, just a temporary offset on the drawing itself.
For power-ups, the tricky part wasn't the idea (4 types: wide paddle, slow ball, 3-ball multi-ball, extra life) but restructuring the update logic. The main ball and any extra multi-ball balls had to share the same behavior — collisions, bounces, active effects — instead of duplicating code. The more robust fix was extracting a reusable updateSingleBall() function, called in a loop over every ball on screen.
The first attempt at "slow ball" scaled dx/dy every frame with a multiplicative factor, risking divide-by-zero when restoring speed. The simpler, more robust version applies the scaling once, on activation and deactivation of the effect, not every frame.
Every structural change of this kind was checked with an automatic JS syntax check and a headless browser simulating a few seconds of play, to catch runtime errors that a simple "looks fine by eye" wouldn't have revealed.
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-up | Prima | Dopo |
|---|---|---|
| 🟢 Paletta larga | 9% | 5% |
| 🔵 Palla lenta | 9% | 4% |
| 🟡 Multi-ball | 7% | 3.5% |
| 🔴 Vita extra | 3% | 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.
Balancing drop probabilities
The first power-up version used a 28% total drop chance per normal brick destroyed — with hundreds of bricks per playthrough, the field got crowded far too fast. Balancing wasn't a theoretical calculation but a practical estimate: how often an event shows up during a real playthrough, at that brick density.
| Power-up | Before | After |
|---|---|---|
| 🟢 Wide paddle | 9% | 5% |
| 🔵 Slow ball | 9% | 4% |
| 🟡 Multi-ball | 7% | 3.5% |
| 🔴 Extra life | 3% | 1.5% |
The general principle: minor helpers (wide paddle, slow ball) can stay relatively frequent because their impact is limited and temporary, while strong ones (extra life) need to stay rare — otherwise the game loses the tension that makes it interesting. Power-ups only drop from normal bricks, never from multi-hit or indestructible ones, to avoid over-rewarding bricks that are already harder to break.
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.
Where it landed, and what's still open
The game now lives in the games section as a fully installable PWA: 15 fixed levels with multi-hit and indestructible bricks, a level selector, visual and audio effects generated without external assets, screen shake, and 4 balanced power-ups. The service worker caches all assets for offline use, and the cache version gets bumped on every deploy to avoid mismatches between the served and cached versions.
Openly postponed for now: more advanced power-ups and syncing scores across devices via a backend, instead of the current localStorage-only approach — a deliberate choice for now, worth revisiting if the game keeps growing.
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.
Frequently asked questions
Do you need a game engine for a simple 2D game like Breakout?
No. For a simple or medium 2D arcade game, native canvas with vanilla JavaScript gives maximum control with no dependencies and no build step, while a framework like Phaser.js only pays off if you need advanced physics or want to save weeks on collisions and sprites. Complex-UI games (cards, puzzles, quizzes) often just need plain HTML/CSS.
How do you generate sound effects without external audio files?
The Web Audio API lets you create oscillators directly in JavaScript, modulating frequency and volume over time to get beeps, clicks, and percussive effects. Zero files to download and virtually no added page weight; the limitation is that these stay synthetic sounds, fine for a retro arcade but not for complex music.
What do you need to make an HTML game installable as an app (PWA)?
Three minimum pieces: a manifest.json with a name, icons at least 192px and 512px, and standalone display; a service worker caching assets for offline use; and HTTPS, mandatory but often free with the hosting in use. With these three the browser shows the install prompt.
How do you implement screen shake on a canvas game?
By applying a random, decreasing translate() to the 2D context for a few frames before drawing the scene, then resetting it right after. You don't move the game elements: you shift the drawing's coordinate system for an instant, without touching collision logic.
How do you balance power-up drop probabilities in an arcade game?
Start with a generous percentage to verify the system works, then reduce it by observing how often the event shows up in a real playthrough. It helps to differentiate probabilities by effect: stronger helpers stay rarer than minor ones.