Introduction and Tech Stack
Underroot is a 2D idle/incremental survival-crafting game built in Godot 4. Below: what it is, why Godot and GDScript, and a map of the systems the rest of these docs cover.
What Underroot is
You play a lone digger. Tunnel down for materials, grow a village on the surface, hold back the Maw — a creature that advances from the right and eats the world one wall at a time. Progress is depth reached, days survived, and village population. It persists across sessions: resources keep draining and threats keep advancing while you're away.
The project name and tagline are set in project.godot:
config/name="Underroot"
config/description="Every layer buys time."
config/version="1.0"
The publisher/company is Swavvy AB (export_presets.cfg, application/company_name).
Engine and language
- Engine: Godot 4, targeting engine version 4.6 —
project.godotdeclaresconfig/features=PackedStringArray("4.6", "Forward Plus"). CI pins the exact toolchain to 4.6.2 (.github/workflows/ci.yml,GODOT_VERSION: 4.6.2). - Renderer: Forward+ (
"Forward Plus"inconfig/features). - Language: GDScript only — there is no .NET/C# in the project.
- Typing: Strict static typing throughout. Scripts use typed variables, typed parameters, and explicit return types; the parse-check gate (
tools/parse_check.ps1) fails the build on any type or identifier error.
Rendering approach
Underroot draws almost all of its world and UI procedurally rather than with sprites. World nodes and UI panels implement _draw() and paint shapes directly (see scenes/world/World.gd, whose _draw() renders the underground layer bands, depth fog, surface strip, ambient day/night tint, and the pond). Static image textures are reserved for story art and cutscenes — for example the intro slideshow images loaded in scenes/ui/IntroStory.gd.
Because the UI is procedural, nearly every control is built in code inside _ready() via add_child(); there are no .tscn files for UI nodes apart from the entry scenes (IntroStory.tscn, TitleScreen.tscn).
Display and viewport
The default viewport is 1280×720 (project.godot):
window/size/viewport_width=1280
window/size/viewport_height=720
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
The stretch aspect is expand, so overlays that must fill the screen should read the live viewport size (get_viewport_rect().size) rather than assume a fixed 1280×720. The default clear color is a dark violet, Color(0.07, 0.05, 0.15, 1).
World coordinate convention
Underroot's world is Y-down: the ground surface is world Y 0, and underground is positive Y (tiles descend as Y increases). The tile size is TILE_SIZE = 32 pixels, defined once as GameConstants.TILE_SIZE in scripts/core/GameConstants.gd; controller and display scripts source it from there. World bounds and zone landmarks live in scenes/world/World.gd (WORLD_LEFT := -3200.0, WORLD_RIGHT := 1000.0, the forest to the left of 0, defenses near DEFENSE_LEFT := 250.0, and the Maw front at MAW_FRONT_X := 700.0).
High-level system map
Underroot is organized around a large set of autoload singletons (33 of them, listed in project.godot). They divide roughly into these areas:
| Area | Representative autoloads | Role |
|---|---|---|
| Core runtime | EventBus, GameManager, SaveManager, TimeManager |
Signals, run state, persistence, the day clock |
| World and digging | WorldManager, RockManager, DiscoveryManager, BuildManager |
Tile grid, walls, surface rock, discovery pockets, wall stacking |
| The threat | MawController, MawAdaptation, WeatherManager, AstrolabeManager |
Multi-front Maw, per-material familiarity, storms, ritual escalation |
| Village and survival | SurvivalManager, VillageManager, ProjectManager, SpecialistManager |
Food/water/fuel drain, population, villager tasks, specialists |
| Economy and content | DataRegistry, Inventory, ToolState, MachineManager, CraftingUnlockManager |
JSON data, resources, tools, machines, recipe unlocks |
| Idle/offline | TaskQueue, OfflineSimulator |
Queued tasks and catch-up simulation on resume |
| Meta and cosmetics | CosmeticManager, CodeManager, ChallengeManager, HarrowManager, SessionTelemetry |
Account-wide unlocks, redemption codes, challenge modifiers, local telemetry |
All cross-system communication flows through the EventBus autoload (scripts/core/EventBus.gd); scene nodes do not connect signals directly to one another.
Two scripts run before anything else in the autoload order and exist purely to prepare the environment: FontSetup (attaches embedded fallback fonts, since Web builds have no OS fonts) and PerfSetup (applies Low Performance Mode at startup before any renderer reads PerformanceMode.enabled).
Persistence at a glance
Saves are plain JSON flat files. There are 12 save slots (user://saves/slot_N.json; SaveManager.MAX_SLOTS = 12), plus account-level config.json and settings.json. The save format is versioned — SaveManager.SAVE_VERSION = 3 — with in-place _migrate() upgrades and corrupt-slot quarantine so a bad file never blocks boot. On Windows the user data directory is %APPDATA%\Godot\app_userdata\Underroot\.
Key files
project.godot— engine config: name, version, features (4.6 / Forward+), viewport, autoload list.export_presets.cfg— Web, Windows Desktop, and Linux export targets.scripts/core/GameConstants.gd— canonical constants, includingTILE_SIZE.scenes/world/World.gd— the world root; procedural_draw()and the per-frame heartbeat.scripts/core/EventBus.gd— the single hub for cross-system signals.