Wie ich Easter Hop gebaut habe

Ein Flappy-Bird-Klon in einer einzigen HTML-Datei — der komplette Weg vom weißen Rechteck zum fertigen Spiel, mit allem Code und allen Irrwegen.

Pixel-Avatar von Peter Jacob
Peter Jacob — Solo-Entwickler aus Hamburg
Veröffentlicht: · Über mich

Dieser Artikel ist der Text, den ich selbst gern gefunden hätte, bevor ich angefangen habe: eine vollständige, ehrliche Anleitung, wie ein kleines Browserspiel wirklich entsteht. Nicht als Tutorial mit perfekten Schritten, sondern so, wie es tatsächlich gelaufen ist — inklusive der Abende, an denen nichts funktioniert hat. Am Ende steht Easter Hop: ein Flappy-Bird-Klon mit Osterhase, der in einer einzigen HTML-Datei lebt, ohne Framework, ohne Build-Tool, ohne eine einzige Bilddatei.

Jeder Code-Ausschnitt hier stammt unverändert aus dem Spiel. Du kannst alles im Quelltext von play.html nachlesen — das ist Absicht und Teil des Konzepts.

Inhalt
  1. Die Regel: eine Datei, null Abhängigkeiten
  2. Das Grundgerüst: Canvas und Loop
  3. Physik, die sich gut anfühlt
  4. Ein Sprite ohne Bilddatei
  5. Hindernisse und die Schwierigkeitskurve
  6. Eine Welt, die die Jahreszeit wechselt
  7. Polish: Partikel, Screenshake, Squash
  8. Mobile: wo die echten Probleme wohnen
  9. Eine Bestenliste ohne eigenen Server
  10. Vom Maulwurf zum Osterhasen
  11. Veröffentlichen für 0 Euro
  12. Was ich beim nächsten Spiel anders mache

1. Die Regel: eine Datei, null Abhängigkeiten

Bevor die erste Zeile Code entstand, stand eine Entscheidung: Das ganze Spiel kommt in eine HTML-Datei. Kein npm, kein Bundler, keine Engine, keine Assets. Diese Regel klingt nach Verzicht, ist aber das Gegenteil — sie ist der Grund, warum das Projekt fertig wurde. Jede Abhängigkeit, die man nicht hat, kann nicht kaputtgehen, nicht veralten und nicht konfiguriert werden müssen. Und ein Spiel, das man als eine Datei verschicken kann, hat null Reibung: Link anklicken, spielen. Warum mir das so wichtig ist, habe ich in einem eigenen Essay aufgeschrieben.

Die praktische Konsequenz: alles wird auf einem HTML5-Canvas mit der 2D-API gezeichnet, alle Grafiken sind Code, und die einzige Ausnahme von der Regel ist inzwischen eine Musikdatei und — seit dem Ausbau dieser Website — ein gemeinsames Stylesheet für die Inhaltsseiten. Das Spiel selbst läuft nach wie vor autark.

2. Das Grundgerüst: Canvas und Loop

Tag eins war unglamourös: ein 400×600-Pixel-Canvas und ein weißes Rechteck, das fällt und bei Leertaste springt. Aber unter diesem Rechteck steckt bereits die wichtigste Architektur-Entscheidung des ganzen Projekts — der Fixed-Timestep-Game-Loop:

const FIXED_DT = 1000 / 60;
let lastTime   = 0;
let accumulator = 0;

function gameLoop(timestamp) {
  if (lastTime === 0) lastTime = timestamp;
  let delta = timestamp - lastTime;
  lastTime = timestamp;
  if (delta > 200) delta = 200;
  accumulator += delta;
  while (accumulator >= FIXED_DT) {
    update();
    accumulator -= FIXED_DT;
  }
  render();
  requestAnimationFrame(gameLoop);
}

Warum der Aufwand statt eines simplen setInterval? Weil requestAnimationFrame so oft feuert, wie das Display aktualisiert — auf meinem Monitor 60-mal pro Sekunde, auf einem modernen Smartphone gern 120-mal. Ohne Entkopplung liefe das Spiel auf dem Handy doppelt so schnell. Der Akkumulator sammelt die echte verstrichene Zeit und arbeitet sie in exakten 16,67-ms-Schritten ab: Die Simulation rechnet überall gleich, nur gezeichnet wird unterschiedlich oft.

Das Math-lose Detail if (delta > 200) delta = 200; habe ich erst nach einem Bug ergänzt: Wechselt man den Tab und kommt nach einer Minute zurück, will der Loop 3.600 verpasste Schritte nachholen — der Browser friert ein. Der Deckel wirft überschüssige Zeit einfach weg. Solche Kleinigkeiten stehen in keinem Tutorial-Titel, kosten aber echte Debugging-Abende.

3. Physik, die sich gut anfühlt

Die komplette Flugphysik sind drei Konstanten und drei Zeilen pro Simulationsschritt:

const GRAVITY           = 0.45;
const FLAP_IMPULSE      = -7.5;
const TERMINAL_VELOCITY = 10;

moleVY += GRAVITY;
if (moleVY > TERMINAL_VELOCITY) moleVY = TERMINAL_VELOCITY;
moleY += moleVY;

Die Zahlen sehen banal aus, aber sie sind das Ergebnis mehrerer Abende reinen Playtestings. Die erste Version hatte Schwerkraft 0,9 und Impuls −7: Die Figur fiel wie ein Stein und verlangte panisches Dauertippen. Nicht schwer — unangenehm. Der Unterschied zwischen beidem ist die eigentliche Kunst bei einem Ein-Knopf-Spiel.

Zwei Design-Entscheidungen waren wichtiger als jede Zahl. Erstens: Ein Tap setzt die Geschwindigkeit (moleVY = FLAP_IMPULSE), er addiert nicht. Dadurch wirkt jeder Hüpfer identisch, egal wie schnell man gerade fällt — der Spieler lernt genau einen Bogen. Zweitens: ein Cooldown von 5 Frames zwischen zwei Hüpfern, damit hektisches Getrommel nicht in unkontrolliertes Dauersteigen ausartet. Was beides für die Spielstrategie bedeutet, steht ausführlich im Tipps-Guide.

4. Ein Sprite ohne Bilddatei

Der Osterhase ist keine PNG. Er ist ein Array aus Zeichenketten — jedes Zeichen ein Pixel, jede Farbe ein Eintrag in einer Zuordnungstabelle:

const BUNNY_SPRITE = [
  '00000WW000WW00000',
  '0000WWPW0WWPW0000',
  '0000WPPW0WPPW0000',
  '000WWPPWWWPPWW000',
  '00WWWWWWWWWWWWD00',
  '0WWWWETWWWTEWWWD0',
  // … 14 Zeilen insgesamt
];
const BUNNY_COLORS = {
  'W': [240, 235, 225],  // Fell
  'P': [255, 160, 180],  // Ohren innen
  'E': [ 40,  30,  60],  // Augen
  // …
};

Gezeichnet wird mit zwei verschachtelten Schleifen und fillRect, jedes Zeichen als 3×3-Pixel-Block. Das klingt primitiv, hat aber zwei enorme Vorteile: Das Sprite ist im Texteditor direkt bearbeitbar (ich habe die Ohren buchstäblich durch Buchstaben-Verschieben geformt), und es braucht keine einzige externe Datei. Der Nachteil — man wird kein Michelangelo mit 17×14 Zeichen — ist bei diesem Spielstil verschmerzbar.

5. Hindernisse und die Schwierigkeitskurve

Die Zäune sind Rechtecke mit Deko: Pfosten, Querriegel, Plankenlinien und spitze Zaunlatten, die in die Lücke ragen. Interessanter als das Zeichnen ist die Kurve dahinter:

function getGapSize(score)     { return Math.max(100, 150 - score * 1.5); }
function getScrollSpeed(score) { return Math.min(5.0, 2.5 + score * 0.04); }

Zwei Einzeiler steuern die gesamte Dramaturgie: Die Lücke schrumpft von 150 auf minimal 100 Pixel (erreicht bei Punkt 33), das Tempo verdoppelt sich von 2,5 auf 5,0 Pixel pro Frame (erreicht bei Punkt 63). Keine Level, keine Stufen — nur zwei stetige Funktionen. Beim Testen habe ich gelernt: Spieler spüren Knicke in der Schwierigkeit sofort und empfinden sie als unfair; stetige Kurven fühlen sich dagegen wie die eigene Ermüdung an.

Die Kollision ist schlichtes AABB (Rechteck-gegen-Rechteck) mit einem Trick: Die Hitbox ist rundum 3 Pixel kleiner als die sichtbare Grafik. Pixelgenaue Kollision wäre „korrekter", fühlt sich aber willkürlich an — das Auge bewertet Beinahe-Treffer großzügiger als der Rechner. Die unsichtbare Gnade lässt knappe Durchflüge wie Können wirken.

6. Eine Welt, die die Jahreszeit wechselt

Das Feature, auf das ich am stolzesten bin, war ursprünglich ein Experiment: Die Welt färbt sich mit steigendem Score stufenlos von Frühling zu Sommer. Die Umsetzung ist verblüffend wenig Code — zwei komplette Farbpaletten und eine lineare Interpolation:

function getPalette(score) {
  const t = Math.min(score / 30, 1.0);
  const p = {};
  for (const key in SPRING) {
    p[key] = lerpColor(SPRING[key], SUMMER[key], t);
  }
  return p;
}

Der Kniff ist nicht die Interpolation, sondern die Disziplin: Keine Zeichenfunktion darf eine Farbe hart codieren. Alles fragt seine Rolle ab — palette.ground, palette.particle, palette.obstacleLED — und weiß nichts von Jahreszeiten. Dadurch wandert buchstäblich jedes Element gleichzeitig durch den Farbraum, vom Himmelsverlauf bis zu den Blütenblättern der Hintergrundblumen, ohne eine Zeile Animationscode. Die komplette technische Aufschlüsselung inklusive des Slot-Hash-Tricks für die endlos scrollenden Wolken und Ostereier steht auf der Technik-Seite.

7. Polish: Partikel, Screenshake, Squash

Der Unterschied zwischen „funktioniert" und „fühlt sich gut an" liegt fast vollständig in Dingen, die spielmechanisch nichts tun:

Ich habe alle drei testweise deaktiviert, um das zu überprüfen: Das Spiel spielt sich identisch — und fühlt sich tot an. Wer nur eine Lektion aus diesem Artikel mitnimmt, dann diese: Plane für Polish genauso viel Zeit ein wie für Mechanik.

8. Mobile: wo die echten Probleme wohnen

Auf dem Desktop war das Spiel nach zwei Wochenenden im Wesentlichen fertig. Die restliche Zeit ging fast komplett in Mobile — und zwar in Probleme, die man vorher nicht erraten kann:

Der 300-ms-Tap-Delay. Viele Mobile-Browser verzögern Klick-Events um bis zu 300 Millisekunden, ein Erbe der Doppeltipp-Zoom-Ära. Für ein Timing-Spiel ist das tödlich. Die Lösung: touch-action: none auf dem Canvas plus preventDefault() im touchstart-Handler.

Die iOS-Tastatur. Für die Namenseingabe der Bestenliste braucht man eine Bildschirmtastatur — aber ein Canvas kann keinen Fokus bekommen. Der Standard-Workaround ist ein unsichtbares <input>-Feld, das programmatisch fokussiert wird. Auf Android reicht das; iOS Safari verlangt zusätzlich, dass der focus()-Aufruf synchron aus einem Touch-Event stammt. Diese eine Zeile an die richtige Stelle zu verschieben hat mich einen Abend gekostet.

Audio. Mein erster Versuch mit programmatisch erzeugten Web-Audio-Soundeffekten scheiterte an iOS-Autoplay-Sperren, die sich nur zu 70 % zuverlässig umgehen ließen — und unzuverlässiger Sound ist schlimmer als keiner. Die finale Lösung ist bewusst simpel: eine Musikschleife über ein normales <audio>-Element, gestartet beim ersten Tap, mit Stumm-Schalter. Die ganze Leidensgeschichte steht im Devlog.

9. Eine Bestenliste ohne eigenen Server

Ein lokaler Highscore in localStorage ist sechs Zeilen Code. Aber ein Highscore, den niemand sieht, ist einsam — also wollte ich eine globale Top 3. Ohne Server. Die Lösung: Supabase, eine gehostete Postgres-Datenbank mit automatischer REST-API. Das Spiel spricht sie direkt aus dem Browser an:

const res = await fetch(
  SB_URL + '/rest/v1/scores?select=name,score&order=score.desc&limit=3',
  { headers: SB_HEADERS }
);

Geschrieben wird nur, wenn ein Score tatsächlich in die Top 3 gehört — der Schreib-Traffic ist praktisch null, und der kostenlose Supabase-Tarif reicht auf Jahre. Gespeichert werden ausschließlich ein Spitzname (max. 10 Zeichen) und die Punktzahl, bewusst keine IP-Adressen oder Gerätedaten — Details dazu in der Datenschutzerklärung. Die Bestenliste wird zusätzlich in localStorage gecacht, damit sie auch ohne Netz sofort angezeigt wird.

10. Vom Maulwurf zum Osterhasen

Easter Hop hieß die ersten Wochen „Molt the Mole": ein kupferner Roboter-Maulwurf, unterirdische Server-Racks, kaltes Neonlicht. Technisch solide, atmosphärisch grau. Als Ostern näher rückte, habe ich ein Experiment gewagt: gleiche Physik, gleiche Architektur, komplett neue Haut.

Das Rebranding war der beste Test der Architektur, den ich mir hätte ausdenken können. Weil alle Farben im Paletten-System steckten und das Sprite ein Zeichen-Array war, bestand die Verwandlung fast nur aus Datenänderungen: neue Paletten, neues Sprite, Zäune statt Racks. Der Code, der sich bewegt, blieb unangetastet. Wenn deine Grafik sich nicht ohne Logik-Änderungen austauschen lässt, sind Darstellung und Simulation zu eng verheiratet — das habe ich hier zum ersten Mal wirklich verinnerlicht. Die alte Maulwurf-Version existiert übrigens noch als Git-Tag; weggeworfen wird nichts.

11. Veröffentlichen für 0 Euro

Das Spiel liegt in einem GitHub-Repository, und GitHub Pages serviert es direkt aus dem master-Branch — kostenlos, mit HTTPS, ohne Server-Wartung. Der komplette Deploy-Prozess ist git push. Dazu kommt eine eigene Domain (hopzy.de, ein paar Euro im Jahr), die per CNAME-Eintrag auf GitHub zeigt.

Drei Stolperfallen, die ich unterwegs mitgenommen habe: Erstens, relative Pfade verwenden (./assets/… statt /assets/…), sonst brechen Ressourcen, sobald die Seite nicht im Domain-Root liegt. Zweitens, GitHub Pages kann keine echten 301-Redirects — wer URLs umbenennt, behilft sich mit Meta-Refresh-Stubs und rel="canonical". Drittens, der Pages-Build hängt gelegentlich einfach fest; nicht panisch werden, neu anstoßen hilft.

12. Was ich beim nächsten Spiel anders mache

Nach einem halben Jahr mit diesem Projekt ist meine Liste kurz, aber ehrlich:

Und die eine Sache, die ich exakt wieder so machen würde: die Eine-Datei-Regel. Sie hat aus einem Projekt, das auf dem Festplatten-Friedhof hätte enden können, ein fertiges Spiel gemacht, das jeder sofort spielen kann. Mehr über den Menschen dahinter steht auf der Über-mich-Seite — und wenn du selbst etwas baust und Fragen hast: Schreib mir. Über Post zu diesem Artikel freue ich mich wirklich.

Über den Autor

Ich bin Peter Jacob, Webentwickler aus Hamburg. Easter Hop ist mein Feierabend-Projekt — die Entstehung Schritt für Schritt steht im Devlog, die Systeme im Detail auf der Technik-Seite, die Spielstrategie im Tipps-Guide. Feedback gern per E-Mail.