Data-driven levels
A level manager defines each level as data: score goals, cat-level goals, or endless mode. Adding a level means adding an entry, not a scene.
// Personal Project · Game · Case Study
Drop, merge and grow the roundest cats. A cozy physics merge puzzle I designed and programmed in Godot, from the merge logic to the level system, and shipped to itch.io as a game you play straight in the browser.
Drop cats into the mug. When two cats of the same kind touch, they merge into a bigger,
rounder cat, all the way up to level 8. If cats stay piled above the rim for too long,
it's game over. Move with A / D and drop with Space.
Runs in your browser via itch.io. Nothing downloads until you press Play.
Beyond the endless mug, the game has a level-select map styled as a recipe book: a tutorial, four goal-based levels and an Endless mode. Each level has its own goal, like reaching a score or creating a certain cat.
When the mug gets crowded there are two power-ups: Delete removes a single cat, and the Rat runs through the mug and shuffles everything around. Progress saves automatically, and there are volume and fullscreen settings.
Merges are detected on RigidBody2D contacts. That sounds simple, but both cats
in a pair get the contact signal, often in the same frame, so without guards the same pair
would merge twice and spawn two big cats. Each cat flags itself (and its partner) as merged
the moment a merge starts, and freshly spawned cats ignore contacts for a short moment.
The second problem was an error from the physics engine on every merge. The contact callback runs in the middle of the physics step, where new bodies can't be added. Spawning the merged cat with a deferred call moves it to just after the step, and the error went away.
func _on_body_entered(body): if has_merged or freeze or not can_merge: return if body is RigidBody2D and body.has_method("get_cat_level"): if body.get_cat_level() == cat_level and not body.has_merged and not body.freeze: merge_with(body) func merge_with(other: RigidBody2D): has_merged = true other.has_merged = true var merge_pos = (global_position + other.global_position) / 2 game.spawn_merged_cat.call_deferred(cat_level + 1, merge_pos) queue_free() other.queue_free()
Lightly trimmed from the real cat.gd: game stands in for the game scene node, and the level-cap check and scoring are left out.
A level manager defines each level as data: score goals, cat-level goals, or endless mode. Adding a level means adding an entry, not a scene.
Custom shaders for the cat-removal effect and the menu, and heart-shaped particle bursts generated from a parametric heart curve.
Autoloads handle game state and saving, music that carries across scenes, and an options panel available from anywhere.
Exported to HTML5 / WebAssembly with threads turned off, so it runs on itch.io without SharedArrayBuffer. Also exports to Windows.
Getting from "works on my machine" to a public release was its own chunk of work: