doc 2 of 9

Farming Game — Architecture

Top-down 2D pixel farming game, 32×32 art, Godot 4.7 (Mobile renderer, desktop target). This document is the map of the whole design: every system, the pattern it uses, and why that pattern — written before the first line of code, updated whenever a decision lands. Reading it before building is how you avoid discovering the architecture by accident.

Systems map#

SystemPatternWhy
Player movementCharacterBody2D, velocity from Input.get_vector() in _physics_processEngine-native kinematics; physics-rate correctness (docs)
Player statesState machine — enum + match, one state var (IDLE / WALK / USE_TOOL)Boolean flags multiply and contradict; one enum makes invalid combinations unrepresentable. A node-based FSM would be overkill at 3 states — see decision 1
Plots (soil)TileMapLayer draws the ground; plot state (tilled/planted) lives in data, not tilesThe tilemap is a view; savable truth never lives in visuals. Storage choice = decision 2
Planting / cropsCropData extends Resource definitions (one .tres per crop: stages, days per stage, textures, yield) + per-plot runtime state (days_grown)Definition/state split: adding a crop = adding a data file, zero code. Definitions stay immutable — Godot shares loaded resources by reference, so mutating one changes every user of it
Day cycleFor now: a debug "next day" key. Later: a TimeSystem singleton emitting day_endedGlobals must earn their scope; a key press teaches the same growth logic without global state. Growth wiring = decision 3
InventoryInventory extends Resource holding {ItemData: count}, emits changedTruth in the data layer; any UI is an interchangeable observer. First version: harvest → inventory → debug output
Inventory UILater module — hotbar/panel listening to changedBuilt after the core loop works
Tools / interactionFacing-arithmetic cell lookup now; acting = the state machine's USE_TOOL state. Interactor (Area2D) + ToolData resources are the deferred upgradeOne tool and one interactable type doesn't need the machinery; when a second arrives, composition means new interactables don't edit the player and a tool swap is a data swap
SavingLater module — the data layer serializes (Inventory, plot map, day counter) to user://Because truth already lives in data objects, saving is just walking the data layer

Scene tree#

Main (Node2D)
├── World (Node2D)
│   ├── Ground (TileMapLayer)          ← grass/soil tiles
│   ├── Crops (Node2D)                 ← Crop scene instances, one per planted plot
│   └── Player (CharacterBody2D)
│       ├── Sprite2D
│       ├── CollisionShape2D
│       └── Camera2D                   ← child of Player: following needs no code at all,
│                                        with limits set to the world bounds

On interaction: this slice reaches the target cell by arithmetic — the player asks the grid for the cell at position + facing * tile_size (stage 03). An Interactor Area2D child is the natural upgrade once there are several kinds of interactable competing for the same keypress, and it's listed under deferred systems below rather than built here. Same for ToolData: with one tool, a Resource for it would be ceremony.

No global singletons in the core build — the day-advance is an input action. TimeSystem becomes the project's first autoload only when shops/NPCs/lighting need to react to time too.

Data model#

  • Definitions (immutable .tres files): CropData (name, growth textures, days_per_stage: Array[int], yield, price) · ItemData (name, icon) · ToolData.
  • Runtime state (mutable, savable): the plot map — see decision 2 — holding per-cell {tilled, crop, days_grown} · the Inventory contents · the day counter.
  • Standing rule: never mutate a loaded .tres at runtime. Ten crops referencing one CropData are one shared object — change it and all ten change.

Signals#

SignalEmitted byListened to by
day_advanceddebug action (later: TimeSystem)plot map — grows planted crops
crop_matured(cell)plot map / Cropvisual stage swap (later: effects)
harvested(item, amount)interaction logicInventory.add()
changedInventorydebug output (later: hotbar UI)

Signals are named in past tense and flow upward — parents call children directly, children announce events without knowing who listens.

Saving (designed now, built later)#

Serialize the plot map, Inventory contents, and day counter — with a version field from day one. Nothing visual is saved; tiles and crop sprites rebuild from data on load. The test that keeps the design honest: deleting every visual node must lose no information.

The three design decisions — yours to make#

  1. State machine style — make this call before stage 05. (A) plain if checks: least code today, tends to sprout contradictory flags; (B) enum State + match with a _set_state() for enter/exit: slightly more ceremony, invalid states impossible, tool timing gets a clean home. One-line trade-off: A is less to type today; B is less to untangle later. Your call and your reason: ______
  2. Where plot truth lives — you'll make this call in stage 03, once you've built your first tilemap. (A) a Dictionary[Vector2i, PlotState] beside the tilemap — visuals stay pure, saving is trivial; (B) TileSet custom data layers — state rides the tiles, fewer structures, awkward saves. Your call and your reason: ______
  3. How crops learn a day passed — stage 04's call. (A) each planted crop listens for the day signal itself — decentralized, logic stays local; (B) the plot map ticks all crops in one loop — central, and "skip N days" or saving stays trivial. Your call and your reason: ______

Build order#

First milestone — the walking skeleton: player walks one screen, one tillable cell, one crop that grows over two manual day-ticks, harvest prints to the console. No UI, no saving, no audio — the smallest complete loop, end to end, before any system gets deep.

Then: collision & camera → soil grid → crops & days → state machine → harvest & inventory → consolidation. Later modules, in whatever order the itch strikes: inventory UI · TimeSystem · audio · saving.