Mini-Spiel-Zustandsmaschine: Vom Startbildschirm über Kampf bis zur Abrechnung

"80 % der Bugs bei Kartenspielen stammen aus schlechtem Zustandsdesign; Zustandsmaschinen lösen das Problem verstreuter Zustandsprüfungen effektiv."
"Verschachtelte Zustandsmaschinen sind zum Mainstream-Designmuster komplexer Spiele geworden; Battle State kann BeginBattle, HeroTurn, EnemyTurn, EndBattle als vier Unterzustände enthalten."
Ihr Mini-Spiel ist live – und Spieler melden: Die Abrechnungsansicht hängt. Das Spiel ist vorbei, die Oberfläche zeigt noch den Kampf. Sie öffnen den Code und sehen überall if (isPlaying && !isPaused && !isGameOver && hasPlayerLeft). Wer hat das geschrieben? Ach ja – Sie selbst.
Bei einem Kartenspiel bin ich in dieselbe Falle getappt. Zustandsprüfungen als Boolean-Variablen, verstreut über dutzende Dateien – eine Funktion ändern hieß ein Dutzend Files anfassen. Im Mehrspieler war es am schlimmsten: Der Server war in der Abrechnung, der Client spielte noch Kampfanimationen ab – Zustandsdrift auf beiden Seiten. Nach einem Refactoring mit Zustandsmaschine wurde der Code deutlich übersichtlicher.
Dieser Artikel zeigt, wie Sie eine Mini-Spiel-Zustandsmaschine entwerfen. Vom Startbildschirm über Kampf bis zur Abrechnung – aufgeteilt in drei Ebenen: Spielfluss, Kampffluss und Kampf-Details. Dazu Praxiserfahrung: Zustandssperren, serverseitige Validierung, Timeout-Mechanismen – das sind die Dinge, die im Alltag wirklich zählen.
Warum brauchen Mini-Spiele eine Zustandsmaschine?
Kurz gesagt: Ein Spiel ist ein ständiges Hin und Her zwischen Zuständen.
Startbildschirm, Kampf, Abrechnung, Pause, Zuschauen – hinter jeder Ansicht steckt ein Zustand. Der Spieler tippt auf „Spiel starten“, der Startbildschirm verschwindet, die Kampfoberfläche erscheint. Kampfende, Abrechnung poppt auf. Klingt simpel – im Code wird es schnell unübersichtlich.
Die drei Sünden der if-else-Kaskade
Typischer Code, den Sie sicher schon gesehen haben:
// Zustandsprüfungen verstreut überall
if (isPlaying && !isPaused && !isGameOver) {
// Spieler kann handeln
}
if (isGameOver && !hasShownResult) {
// Abrechnungsansicht anzeigen
}
if (isPlaying && currentPlayer === 'player1') {
// Spieler 1 ist dran
}
Drei Probleme:
Schwer lesbar: Um den aktuellen Zustand zu erkennen, müssen Sie die ganze Datei nach Boolean-Variablen durchsuchen. isPlaying, isPaused, isGameOver – verstreut wie Puzzleteile, die Sie selbst zusammensetzen müssen.
Schwer änderbar: Einen „Pause“-Zustand hinzufügen? Dutzende if-Prüfungen anpassen. Eine vergessen – Bug. Ich habe einmal einen „Zuschauen“-Zustand ergänzt, zwei Tage Code geändert – und nach dem Release lief eine Animation weiter, obwohl pausiert war.
Schwer debuggbar: Wechselt die Reihenfolge durcheinander, finden Sie die Ursache nicht. isGameOver ist true, hasShownResult noch false – welche Prüfung fehlt?
Ein Sohu-Artikel nennt die Zahl: 80 % der Bugs bei Kartenspielen kommen von schlechtem Zustandsdesign. Klingt drastisch – wer es erlebt hat, weiß: stimmt.
Wie löst die Zustandsmaschine das?
Sie bündelt die verstreuten Prüfungen an einem Ort.
Jeder Zustand hat eigenes Verhalten: Was passiert beim Eintritt, während des Laufs, beim Verlassen. Wechselregeln sind klar: Vom Startbildschirm nur zum Kampf – nicht direkt zur Abrechnung.
Zum Vergleich:
// Zustandsmaschinen-Ansatz
class HomeState {
enter() { showHomeUI(); }
exit() { hideHomeUI(); }
handleEvent(event) {
if (event === 'START_BATTLE') {
manager.changeState(new BattleState());
}
}
}
class BattleState {
enter() { showBattleUI(); startBattle(); }
exit() { hideBattleUI(); cleanupBattle(); }
}
Zustände sind gekapselt. HomeState ändern – BattleState bleibt unberührt. Neuer Zustand: neue State-Klasse plus eine Wechselregel.
Better Programming schreibt: Verschachtelte Zustandsmaschinen sind zum Mainstream bei komplexen Spielen geworden. Mini-Spiele ausgenommen? Nein – klare Zustände, klarer Code.
Dreischichtige Zustandsmaschinen-Architektur
Mini-Spiel-Zustandsmaschinen sollten geschichtet sein. Ohne Schichten landet Kampflogik in der Spielfluss-Ebene – der Code wuchert.
Ich teile in drei Ebenen: Spielfluss, Kampffluss, Kampf-Details.
Erste Ebene: Spielfluss
Das Rückgrat vom Start bis zur Abrechnung:
BootState: Beim Start – Ressourcen laden, Engine initialisieren. Cocos Creators Boot-Szene macht genau das: erst Boot, dann Home.
HomeState: Startmenü. Level wählen, Charakter wählen, Bestenliste. Relativ einfach: UI anzeigen, Button-Klicks verarbeiten.
BattleState: Kampfszene. Kein Endpunkt, sondern Container – innen läuft eine Unter-Zustandsmaschine.
SettlementState: Abrechnung. Sieg/Niederlage, Statistik, Kampf-Ressourcen aufräumen.
Ablauf: Boot → Home → Battle → Settlement → zurück zu Home (oder Beenden).
Skizze:
Spielstart → BootState
↓ (Laden fertig)
HomeState
↓ (Start tippen)
BattleState ← Kampf-Unterzustandsmaschine hier
↓ (Kampfende)
SettlementState
↓ (Zurück tippen)
HomeState
Zweite Ebene: Kampffluss
In BattleState läuft eine eigene Unter-Zustandsmaschine. Better Programming: Battle State kann BeginBattle, HeroTurn, EnemyTurn, EndBattle als vier Unterzustände enthalten.
BeginBattleState: Kampf initialisieren. Level-Parameter, Charakterdaten, Spieler synchronisieren. Im Mehrspieler hier sperren – kein Beitritt/Austritt mitten drin.
PlayerTurnState: Spielerzug. Karten legen, angreifen, verteidigen – alles in diesem Zustand. Zugende → nächster Zug oder Abrechnung.
EnemyTurnState (optional): KI-Aktion. Einzelspieler kann weglassen; Mehrspieler = Gegnerzug.
EndBattleState: Abrechnungslogik. Sieg/Niederlage, Belohnungen, Abrechnungsanimation.
Ablauf:
BattleState betreten → BeginBattleState (Lock, Sync)
↓ (Bereit)
PlayerTurnState
↓ (Zugende)
EnemyTurnState (optional)
↓ (Kampfende)
EndBattleState
↓ (Abrechnung fertig)
BattleState verlassen → SettlementState
Dritte Ebene: Kampf-Details
Manche Zustände brauchen weitere Unterteilung. In PlayerTurnState zum Beispiel:
Animations-Zustandsmaschine: Angriff → Angriffsanimation, Treffer → Trefferanimation. Unity und Cocos Creator haben eingebaute Animations-Zustandsmaschinen – hier nur kurz erwähnt.
Runden-Zustandsmaschine: Karte legen → angreifen → verteidigen → Zug beenden. Jede Aktion ein Unterzustand.
Beast Card Clash kapselt Setup, Scoring und Ergebnisbildschirm in eigene State-Klassen. Vorteil: Logik einer Phase ändern, ohne andere Phasen anzufassen.
Dreischichtig heißt klare Verantwortung. Startbildschirm ändern – Kampflogik bleibt intakt. Rundendetails ändern – Abrechnungsfluss nicht. Teamarbeit wird einfacher: eine Person Kampffluss, eine andere Details.
Kern-Schnittstellen der Zustandsmaschine
Architektur erklärt – jetzt Code.
Ein Zhihu-Artikel zu Unity nennt die einheitliche Schnittstelle: OnEnter, OnUpdate, OnExit, OnHandleEvent. Davon abgeleitet, hier TypeScript:
IGameState-Schnittstelle
Jeder Zustand implementiert:
interface IGameState {
name: string; // Zustandsname – hilfreich beim Debuggen
enter(params?: any): void; // Initialisierung beim Eintritt
update(dt: number): void; // Frame-Update (optional)
exit(): void; // Aufräumen beim Verlassen
handleEvent(event: GameEvent): void; // Events verarbeiten, Zustandswechsel auslösen
}
Die fünf Methoden:
name: Debugging. Logs zeigen „HomeState“ oder „BattleState“ – klarer als Booleans.
enter: Beim Eintritt. HomeState: Start-UI anzeigen, Spielerdaten laden. BattleState: Kampfparameter, Unter-Zustandsmaschine starten.
update: Pro Frame. Im Kampf oft – Countdown, Eingabe prüfen. Startbildschirm meist unnötig.
exit: Beim Verlassen. Ressourcen freigeben, UI verbergen, Listener entfernen.
handleEvent: Event-getriebener Wechsel. „Spiel starten“ → START_BATTLE → HomeState wechselt zu BattleState.
GameStateManager
Die Zustandsmaschine braucht einen Manager für Wechsel. Stack Exchange beschreibt CGameEngine mit Init, Cleanup, ChangeState, PushState, PopState.
Vereinfacht:
class GameStateManager {
private currentState: IGameState | null = null;
private stateStack: IGameState[] = []; // Überlagernde Zustände (Pause über Kampf)
// Initialisierung
init(firstState: IGameState) {
this.currentState = firstState;
this.currentState.enter();
}
// Zustand wechseln
changeState(newState: IGameState, params?: any) {
if (this.currentState) {
this.currentState.exit();
}
this.currentState = newState;
this.currentState.enter(params);
}
// Überlagernden Zustand pushen (Pause, Dialog)
pushState(state: IGameState) {
if (this.currentState) {
// Aktueller Zustand bleibt, wird nur pausiert
this.stateStack.push(this.currentState);
}
this.currentState = state;
state.enter();
}
// Überlagernden Zustand poppen
popState() {
if (this.currentState) {
this.currentState.exit();
}
this.currentState = this.stateStack.pop();
// Kein erneutes enter – vorher nur pausiert
}
// Frame-Update
update(dt: number) {
if (this.currentState) {
this.currentState.update(dt);
}
}
// Event verarbeiten
handleEvent(event: GameEvent) {
if (this.currentState) {
this.currentState.handleEvent(event);
}
}
}
changeState: Wechsel – alter Zustand exit, neuer enter. Start → Kampf.
pushState / popState: Überlagerung. Pause während Kampf: PauseState auf den Stack. Pause beenden: pop, BattleState läuft weiter.
Wichtig für Pause, Dialoge, Bestätigungsboxen – überlagern ohne den darunterliegenden Zustand zu zerstören.
Praxis-Fallstricke und Lösungen
Theorie reicht – jetzt typische Probleme. Ein Sohu-Artikel fasst Kartenspiel-Erfahrungen zusammen; hier im Detail:
Zustandsdrift: Server und Client nicht synchron
Im Mehrspieler der häufigste Fall.
Szenario: Server in Abrechnung, ein Client spielt noch Kampfanimation – Netzwerkverzögerung, Broadcast verpasst. Anzeige „Im Kampf“, obwohl schon vorbei.
Ursache: Wechsel-Nachricht verloren oder zu spät.
Lösung:
-
Synchronisation: Server broadcastet bei jedem Wechsel; Client synchronisiert sofort.
-
Heartbeat: Client sendet regelmäßig aktuellen Zustand; bei Abweichung erzwingt der Server Sync.
// Client-Heartbeat
class BattleState {
enter() {
this.startHeartbeat();
}
startHeartbeat() {
setInterval(() => {
socket.send({
type: 'HEARTBEAT',
state: this.manager.currentState.name,
roomId: this.roomId
});
}, 3000); // alle 3 Sekunden
}
}
Parallelitätskonflikt: Gleichzeitige Aktionen
Am Tisch legen zwei Spieler gleichzeitig Karten – Wechselreihenfolge durcheinander.
Ursache: Kein Lock, parallele Operationen ohne Warteschlange.
Lösung: Lock plus „aktueller Bediener“-Mechanismus.
class DealingState implements IGameState {
private lock: boolean = true;
private currentOperator: string = '';
enter() {
this.lock = true; // Kartenausgabe gesperrt
// Beitritt/Austritt verboten
// Initiale Daten für alle synchronisieren
setTimeout(() => this.unlock(), 3000); // nach 3 s entsperren
}
unlock() {
this.lock = false;
this.currentOperator = this.getFirstPlayerId();
this.manager.changeState(new PlayerTurnState());
}
handleEvent(event: GameEvent) {
if (this.lock) {
// Gesperrt – Aktion ablehnen
return;
}
// Nur aktueller Bediener darf Events auslösen
if (event.playerId === this.currentOperator) {
// Aktion verarbeiten
}
}
}
Locks sichern: In kritischen Phasen (Kartenausgabe, Abrechnung) läuft nur ein Ablauf – keine Unterbrechung durch Spielereingaben.
Abrechnungssicherheit: Gefälschte Punkte vom Client
Client sendet Punkte und Sieg/Niederlage – ein Crack ändert die Werte. Wie erkennt der Server Betrug?
Ursache: Server vertraut Client-Daten.
Lösung: Server rechnet unabhängig.
// Server-Abrechnungslogik
class EndBattleState {
enter() {
// Keine Client-Punkte akzeptieren
// Server berechnet aus Kampfprotokoll
const result = this.calculateResultFromLog(battleLog);
// Ergebnis an alle Clients
this.broadcastResult(result);
}
calculateResultFromLog(log: BattleLog) {
// Aus Kampfprotokoll (Kartenreihenfolge, Angriffsdaten) berechnen
// Client-Logs können gefälscht sein – kritische Daten serverseitig prüfen
}
}
Kernprinzip: Server vertraut dem Client nicht. Client zeigt an – Server entscheidet.
Timeout-Deadlock: Zustand ohne Ausstieg
Ein Zustand hängt – Wechsel zum nächsten kommt nie.
Ursache: Kein Timeout, erwartetes Event tritt nicht ein.
Lösung: Timeout pro Zustand.
class PlayerTurnState implements IGameState {
private timeoutTimer: number;
enter() {
this.timeoutTimer = setTimeout(() => {
// Timeout – automatisch überspringen
this.manager.handleEvent({
type: 'TIMEOUT_SKIP',
playerId: this.currentPlayer
});
}, 30000); // 30 Sekunden
}
exit() {
clearTimeout(this.timeoutTimer); // Timeout beim Verlassen aufräumen
}
}
Wichtig bei Disconnect oder Netzwerk-Hänger – der Zustand darf nicht ewig warten.
Vier Fallstricke – Praxis schlägt Theorie. Zustandsdrift kostete mich zwei Tage Debug-Zeit. Heartbeat danach – Problem weg.
Cocos Creator in der Praxis
Theorie, Schnittstellen, Fallstricke – jetzt Cocos Creator.
Im vorherigen Artikel „Cocos Creator: Mini-Spiel-Projektstruktur“ ging es um Boot, Szenen, Abrechnungsseite. Hier: Wie die Zustandsmaschine mit Layern zusammenspielt.
Zustandsmaschine bei Ein-Szene-Architektur
Cocos Creator empfiehlt für Mini-Spiele: eine Szene, mehrere Layer ein-/ausblenden. Passt zur Zustandsmaschine.
BootLayer → HomeLayer → BattleLayer → SettlementLayer
Jeder Layer ein Zustand:
import { director, Node } from 'cc';
// HomeState
class HomeState implements IGameState {
name = 'Home';
private homeLayer: Node | null = null;
enter() {
// HomeLayer anzeigen
const scene = director.getScene();
this.homeLayer = scene?.getChildByName('HomeLayer') ?? null;
if (this.homeLayer) {
this.homeLayer.active = true;
this.initHomeUI();
}
}
exit() {
// HomeLayer verbergen
if (this.homeLayer) {
this.homeLayer.active = false;
this.cleanupHome();
}
}
handleEvent(event: GameEvent) {
if (event.type === 'START_BATTLE') {
// zu BattleState wechseln
this.manager.changeState(new BattleState(), event.params);
}
}
initHomeUI() {
// Spielerdaten laden, Button-Events setzen
}
cleanupHome() {
// Start-Ressourcen freigeben
}
}
// BattleState
class BattleState implements IGameState {
name = 'Battle';
private battleLayer: Node | null = null;
enter(params?: any) {
const scene = director.getScene();
this.battleLayer = scene?.getChildByName('BattleLayer') ?? null;
if (this.battleLayer) {
this.battleLayer.active = true;
this.startBattle(params);
}
}
exit() {
if (this.battleLayer) {
this.battleLayer.active = false;
this.cleanupBattle();
}
}
startBattle(params: any) {
// Kampf-Unterzustandsmaschine starten
this.battleStateManager.init(new BeginBattleState(params));
}
cleanupBattle() {
// Kampf-Ressourcen aufräumen
}
}
Layer ein-/ausblenden = visueller Zustandswechsel. enter zeigt, exit verbirgt.
Datenübergabe beim Zustandswechsel
Parameter mitgeben – z. B. gewähltes Level an BattleState:
// HomeState löst Wechsel aus
handleEvent(event: GameEvent) {
if (event.type === 'START_BATTLE') {
// Level-Parameter übergeben
this.manager.changeState(new BattleState(), {
levelId: event.levelId,
difficulty: event.difficulty
});
}
}
// BattleState empfängt Parameter
enter(params?: any) {
const levelId = params?.levelId ?? 'default';
const difficulty = params?.difficulty ?? 'normal';
this.loadLevel(levelId, difficulty);
}
Fortschritt in localStorage – Kampf unterbrochen, beim nächsten Mal weiter:
// Fortschritt speichern
exit() {
localStorage.setItem('battle_progress', JSON.stringify({
levelId: this.levelId,
round: this.currentRound,
score: this.score
}));
}
// Fortschritt wiederherstellen
enter() {
const saved = localStorage.getItem('battle_progress');
if (saved) {
const progress = JSON.parse(saved);
this.resumeFromProgress(progress);
}
}
WeChat-Mini-Spiel: Besonderheiten
Ein CSDN-Artikel empfiehlt: Zustandsmaschine in einer globalen Datei – einfacher zu pflegen.
Paketgröße: WeChat-Hauptpaket 4 MB. Zustandsmaschinen-Code schlank halten.
Subpackages: Kern (Boot, Home, Battle) im Hauptpaket; Erweiterungen (Speziallevel, Events) in Subpackages.
Globale State-Datei: In game.js initialisieren, andere Module greifen global zu.
// game.js
import { GameStateManager } from './states/GameStateManager';
import { BootState } from './states/BootState';
// Globaler State-Manager
window.gameStateManager = new GameStateManager();
window.gameStateManager.init(new BootState());
WeChat-API über globale Variable:
// WeChat-Login-Callback
wx.login({
success: (res) => {
const state = window.gameStateManager.currentState;
if (state.name === 'HomeState') {
state.handleEvent({ type: 'LOGIN_SUCCESS', code: res.code });
}
}
});
Vorteil: WeChat-API braucht keinen Import der Zustandsmaschine – Zugriff über window.
Diesen Artikel zusammen mit „Cocos Creator: Mini-Spiel-Projektstruktur“ lesen – dann wird das Gesamtbild klar. Projektstruktur ist der Container, die Zustandsmaschine das Verhalten.
Fazit
Eine Zustandsmaschine zerlegt den Spielfluss in klare Knoten.
Dreischichtig – Spielfluss, Kampffluss, Kampf-Details – skaliert vom Mini-Spiel bis zum großen Projekt. Eine Ebene ändern, andere bleiben stabil; Teamarbeit wird einfacher.
Praxis schlägt Theorie. Zustandsdrift, Parallelitätskonflikte, Abrechnungssicherheit, Timeout-Deadlocks – alle vier habe ich erlebt, Debug war mühsam. Locks, Heartbeat, serverseitige Validierung, Timeouts – danach war Ruhe. Richtiger Code, weniger Bugs.
Zum Ausprobieren: vollständiges Code-Beispiel (GitHub-Link). Zuerst „Cocos Creator: Mini-Spiel-Projektstruktur“ – Layer-Aufteilung verstehen, dann diesen Artikel – wie die Zustandsmaschine läuft. Beide zusammen, Architektur klar.
Als Nächstes: KI-gestütztes Testen der Zustandsmaschine – Testfälle für Wechselpfade generieren lassen. Automatisierte Tests senken die Fallstricke-Wahrscheinlichkeit.
Dreischichtige Mini-Spiel-Zustandsmaschine entwerfen
Vollständiger Implementierungsablauf für Zustandsverwaltung vom Startbildschirm über Kampf bis zur Abrechnung
⏱️ Estimated time: 60 min
- 1
Step 1: Zustände der Spielfluss-Ebene definieren
Haupt-Zustände auflisten: BootState, HomeState, BattleState, SettlementState. Jeder Zustand entspricht einem Layer oder einer Szene; Wechselregeln festlegen. - 2
Step 2: Unterzustände der Kampffluss-Ebene aufteilen
In BattleState eine Unter-Zustandsmaschine: BeginBattleState (Initialisierung mit Lock), PlayerTurnState (Spielerzug), EnemyTurnState (optional KI-Zug), EndBattleState (Abrechnungsbewertung). - 3
Step 3: Zustands-Schnittstellen und Manager implementieren
IGameState-Schnittstelle (name, enter, exit, update, handleEvent) erstellen, GameStateManager für changeState sowie pushState/popState implementieren. - 4
Step 4: Schutzmechanismen gegen typische Fallstricke
An kritischen Zuständen (Kartenausgabe, Abrechnung) Locks gegen Parallelitätskonflikte, Timeouts gegen Deadlocks, Heartbeat gegen Zustandsdrift, serverseitige Berechnung gegen Abrechnungsbetrug. - 5
Step 5: An Cocos Creator Layer anbinden
In enter den passenden Layer anzeigen und Daten initialisieren; in exit Layer verbergen und Ressourcen freigeben. Zustandsmaschine steuert UI-Wechsel – Logik und Darstellung entkoppelt.
FAQ
Brauchen Mini-Spiele unbedingt eine Zustandsmaschine?
Ist eine dreischichtige Zustandsmaschine Over-Engineering?
Was ist der Unterschied zwischen Zustandsmaschine und Szenenwechsel?
Wie löst man Zustandsdrift im Mehrspieler?
Wie schützt man Abrechnungsdaten vor Betrug?
10 Min. Lesezeit · Veröffentlicht am: 19. Mai 2026 · Aktualisiert am: 14. Juli 2026
AI-gestützte Cocos Mini-Game-Entwicklung
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
Cocos Creator Mini-Spiel-Projektstruktur: Boot, Szenen und Settlement-Seite sauber trennen
Best Practices für die Cocos Creator Mini-Spiel-Projektstruktur: Single-Scene-Architektur mit vier Layer-Ebenen, vollständiger Aufteilung von Boot-Startseite, Hauptszene und Settlement-Seite – inklusive Codebeispielen.
Teil 3 von 21
Nächster
Mini-Spiel-Ideen mit KI in PRD und Aufgabenliste zerlegen
So zerlegen Sie in 30 Minuten eine Mini-Spiel-Idee in ein vollständiges PRD und eine Entwicklungs-Aufgabenliste mit KI. Inklusive Prompt-Vorlagen, PRD-Struktur für Mini-Spiele und Praxisbeispiel – für Indie-Entwickler und kleine Teams.
Teil 5 von 21
Ähnliche Beiträge
Mini-Spiel-Produktexperiment: Günstiger Validierungspfad für Gameplay und Monetarisierung

Mini-Spiel-Produktexperiment: Günstiger Validierungspfad für Gameplay und Monetarisierung
Indie-Spieleentwicklung: Erst Gameplay validieren, dann Systeme ausbauen (MVP-Praxisleitfaden)


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen