MEKAI Games Devlog

How we built a roguelite in Godot

A practical Godot architecture for short roguelite runs: scenes, Resources, signals, run state, deterministic randomness, saves, and content that remains easy to expand.

ยท MEKAI Games

Fred faces a demon in the Godot-built roguelite Rock, Paper, Scissors, SHOOT!

Start with the run, not the screens

A roguelite is easier to maintain when the run is treated as its own model. The interface should display cash, streak, cards and current opponent, but it should not own those values. Keep a RunState object responsible for the seed, progression, inventory and rewards, then let scenes read from it.

This makes a failed duel, a shop purchase and a boss reward part of the same predictable flow. It also stops menu code from becoming the place where game rules accidentally live.

  • RunState: seed, cash, streak, owned upgrades and unlocked actions.
  • Duel scene: input, opponent decision, result and feedback.
  • Shop scene: available offers, prices and purchase validation.
  • Content data: opponents, cards, bosses and scenario definitions.

Use scenes for behavior and Resources for data

Godot scenes are good at composing behavior and visuals. Custom Resources are better for definitions that designers need to edit repeatedly. An OpponentDefinition resource can hold an ID, portrait, scenario, decision weights, reward and mechanic parameters without duplicating a full scene for every opponent.

Keep runtime state separate from the resource. A shared resource describes what a card is; a runtime object records whether that card has already triggered this round. This avoids mutations leaking between runs.

Fred faces Death with the complete duel interface visible
A stable duel scene can load different opponent data, art and rules without changing its core layout.

Signals keep the flow readable

Use signals for events that several systems need to observe: duel_resolved, cash_changed, card_triggered and streak_changed. The run model emits the fact; UI, audio and animation respond independently.

Avoid turning one global event bus into a list of every action in the project. Local signals are easier to trace. Reserve an Autoload for state that genuinely survives scene changes, and keep scene-specific behavior inside the scene.

  • Emit facts after validation, not commands before validation.
  • Pass small typed payloads such as IDs and result objects.
  • Disconnect temporary listeners when a scene exits.
  • Log the seed and resolved event order when debugging a run.

Make randomness reproducible

Create a RandomNumberGenerator for the run and store its seed. Use it for opponent selection, shop offers and weighted rewards. A recorded seed lets you replay a broken run and test whether the same sequence produces the same content.

Do not mix visual randomness with gameplay randomness. Particles can use their own generator; balance-critical choices should use the run generator so cosmetic effects never alter outcomes.

Save IDs, then rebuild the run

A compact save should contain primitive values and stable content IDs: seed, streak, cash, current stage and owned card IDs. Load definitions from the project, then rebuild runtime objects from those IDs.

Add a save version from day one. When a card is renamed or removed, a migration can replace its old ID instead of breaking every existing save. Test a save made before each public update.

  • Never serialize active scene nodes as the save format.
  • Write to a temporary file before replacing the last valid save.
  • Validate IDs and clamp values when loading untrusted data.
  • Keep a small automated test that saves, reloads and compares the run.

Sources and further reading

Play on Steam