Veröffentlicht am
20.9.2026
UNO als Browserspiel programmieren Teil 1:
Der Techstack
UNO.go nutzt im Frontend Svelte und im Backend Go
Eigentlich kam diese Idee total zufällig: Wir spielten das von Ubisoft entwickelte UNO auf Steam, jedoch kommt es dort immer mal wieder zu regelmäßigen Abstürzen und man weiß nicht wirklich, woran das liegt. “Programmiere doch dein eigenes UNO!”, sagte der Kumpel zu mir und als ich mir diese Idee am Abend durch den Kopf gehen ließ, beschloss ich, diese Idee mithilfe von KI (hier: Google Gemini) und einer guten Bekannten auszuprobieren. An sich ist das Spiel und die dahinter liegenden Regeln auch nicht wirklich schwer, da UNO im Gegensatz zu Schach oder Bridge nicht so kompliziert ist. Was ich mich hingegen früher fragte war, wie Multiplayerspiele im Browser eigentlich genau funktionierten, denn mit einfachen HTTP-Requests - wie man sie z.B. aus Laravel oder Symfony kennt - kommt man nicht weit. Das Zauberwort heißt stattdessen Websocket.
Was ist ein Websocket eigentlich?
Ein Websocket ist im Grunde genommen ein Netzwerkprotokoll, welches eine dauerhafte bidirektionale Verbindung zwischen einem Client und einem Server über eine einzige TCP-Verbindung ermöglicht. Die Verbindung beginnt als ganz normale HTTP-Anfrage, worin der Client den Server mit einem speziellen Header (der so genannte “Upgrade-Header”) bittet, die Verbindung auf das Websocket-Protokoll hochzustufen. Wenn das erfolgreich ist und der Server die Upgrade-Anfrage akzeptiert hat, bleibt die Verbindung offen und wird nicht wie beim klassischen HTTP nach der Antwort geschlossen. Der große Vorteil hierbei ist, dass beide Seiten hierbei Daten senden und empfangen können, ohne das der Client dafür eine Anfrage stellen muss. Es handelt sich hierbei um eine Echtzeitverbindung und der Client muss den Server nicht immer nach neuen Daten fragen - stattdessen schickt der Server die Daten einfach selbst, wenn diese sich aktualisieren. Genau das macht den Websocket für ein UNO-Browserspiel ideal.
Einige ehemalige Arbeitskollegen, die bereits seit den 2000ern dabei sind und schon früher Browserspiele entwickelt hatten, nutzten in den 2000er Jahren Crownjobs in PHP, die im Minuten oder gar Sekundentakt durchliefen. Die Websockets gab es erst seit etwa 2011 und davor musste man eben ziemlich pragmatisch sein.
Die Qual der Wahl - der Techstack
Beim Brainstorming bezüglich des Techstacks kam mir natürlich sofort NodeJS in den Sinn. Diese Laufzeitumgebung ist im Web ohnehin sehr weit verbreitet und in NodeJS kann man ja ohnehin alles mögliche machen, weil es ja für alles ein Paket gibt. Allerdings wollte ich mal etwas Neues ausprobieren und fragte mal im Bekanntenkreis nach, was sie denn dafür nehmen würden. Die Standardantwort war auch hier NodeJS gewesen, doch ich sah mich weiter um. Zunächst wollte ich das in PHP mithilfe von Laravel ausprobieren (Laravel bietet dafür sogar das Paket Laravel Reverb an), aber die Installation von Laravel mit den ganzen Paketen erschien mir dann doch etwas zu “klobig”. Schließlich empfahl mir meine Bekannte, das Backend doch in Go zu programmieren. Go wurde wurde im Jahre 2007 entwickelt und 2009 veröffentlicht; große Pioniere wie Ken Thompson - dem Mitschöpfer der Programmiersprache C - arbeiteten daran und die Sprache kommt mit wenigen Schlüsselwörtern aus. Zudem ist sie aufgrund ihrer speichernahen Architektur sehr schnell und soll von der Syntax her dennoch ziemlich einfach sein. Ich probierte es also aus und entwickelte mithilfe von Gemini meinen ersten Websocket-Server:
func main() {
hub := websocket.NewHub()
go hub.Run()
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
playerID := r.URL.Query().Get("playerId")
roomID := r.URL.Query().Get("roomId")
if playerID == "" || roomID == "" {
http.Error(w, "playerId und roomId sind erforderlich", http.StatusBadRequest)
return
}
websocket.ServeWs(hub, w, r, playerID, roomID)
})
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("UNO Go-Backend läuft!"))
})
port := ":8080"
log.Printf("UNO Backend-Server gestartet auf http://localhost%s\n", port)
log.Printf("WebSocket-Endpunkt: ws://localhost%s/ws?playerId=...&roomId=...\n", port)
if err := http.ListenAndServe(port, nil); err != nil {
log.Fatalf("Server-Fehler: %v", err)
}
}
Die Syntax von Go sieht natürlich etwas anders aus als man es von den gewohnten Klassikern wie Java, C# oder PHP kennt, weil Go eben extrem leicht und dennoch sehr mächtig ist. Das interessante an dieser Sprache ist jedoch, dass man mit nur einem einzigen Paket (in dem Fall gorilla/websocket) und wenigen Dateien bereits einen Backend-Server für das Spiel erstellen kann. Der Speicherverbrauch ist bei einem gut entwickelten Go-Projekt ebenfalls sehr überschaubar. Aus diesem Grund wird es Go im Backend.
Für das Frontend hatte ich mich zunächst ganz klar für Vue entschieden, aber beim Brainstorming ist mir dann auch Svelte vorgeschlagen worden. Svelte arbeitet im Gegensatz zu Vue Virtual DOM, sondern compiliert den Code in reines natives JavaScript und der Browser kann direkt damit arbeiten. Zudem ist der Code erheblich kürzer als bei Vue, wobei in der neuesten Svelte Version 5 einiges geändert und überarbeitet wurde, wodurch der Code noch schlanker geworden ist. Der Code für das Hauptkomponente im Frontend (+page.svelte) sieht z.B. so aus:
<script lang="ts">
import { fly } from 'svelte/transition';
import Lobby from '$lib/components/Lobby.svelte';
import Header from '$lib/components/Header.svelte';
import PlayArea from '$lib/components/PlayArea.svelte';
import Hand from '$lib/components/Hand.svelte';
import { gameState, errorMsg, type Card } from '$lib/gameStore';
let playerId = $state('');
let discardPileRef = $state<HTMLElement | null>(null);
let isAnimatingDiscard = $state(false);
let previousTopCard = $state<Card | null>(null);
let isMyTurn = $derived($gameState.currentTurnPlayerId === playerId);
let topCard = $derived(
$gameState.discardPile.length > 0
? $gameState.discardPile[$gameState.discardPile.length - 1]
: null
);
</script>
<div class="min-h-screen bg-slate-900 text-white font-sans flex flex-col justify-between p-4 select-none">
{#if $errorMsg}
<div transition:fly={{ y: -20, duration: 300 }} class="fixed top-4 right-4 bg-red-600 text-white px-4 py-2 rounded-lg shadow-xl z-50 animate-bounce">
{$errorMsg}
</div>
{/if}
{#if !$gameState.isStarted}
<Lobby bind:playerId />
{:else}
<Header
roomId={$gameState.roomId}
currentTurnPlayerId={$gameState.currentTurnPlayerId}
{isMyTurn}
/>
<PlayArea
bind:discardPileRef
opponents={$gameState.opponents}
{topCard}
{previousTopCard}
{isAnimatingDiscard}
currentColor={$gameState.currentColor}
{isMyTurn}
/>
<Hand
cards={$gameState.myHand}
{isMyTurn}
{topCard}
currentColor={$gameState.currentColor}
{discardPileRef}
onAnimationStart={(currentTop) => {
previousTopCard = currentTop;
isAnimatingDiscard = true;
}}
onAnimationEnd={() => {
isAnimatingDiscard = false;
}}
/>
{/if}
</div>
Man sieht, dass Svelte doch mit weniger Code zurechtkommt als Vue. Für die Gestaltung des Frontends wurde natürlich Tailwind verwendet. Damit steht im Grunde genommen das Skelett von UNO.go und man kann sich der Reihe nach durch das Spiel und seine implementierten Funktionen tasten.