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#
| System | Pattern | Why |
|---|---|---|
| Player movement | CharacterBody2D, velocity from Input.get_vector() in _physics_process | Engine-native kinematics; physics-rate correctness (docs) |
| Player states | State 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 tiles | The tilemap is a view; savable truth never lives in visuals. Storage choice = decision 2 |
| Planting / crops | CropData 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 cycle | For now: a debug "next day" key. Later: a TimeSystem singleton emitting day_ended | Globals must earn their scope; a key press teaches the same growth logic without global state. Growth wiring = decision 3 |
| Inventory | Inventory extends Resource holding {ItemData: count}, emits changed | Truth in the data layer; any UI is an interchangeable observer. First version: harvest → inventory → debug output |
| Inventory UI | Later module — hotbar/panel listening to changed | Built after the core loop works |
| Tools / interaction | Facing-arithmetic cell lookup now; acting = the state machine's USE_TOOL state. Interactor (Area2D) + ToolData resources are the deferred upgrade | One 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 |
| Saving | Later 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
.tresfiles):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}· theInventorycontents · the day counter. - Standing rule: never mutate a loaded
.tresat runtime. Ten crops referencing oneCropDataare one shared object — change it and all ten change.
Signals#
| Signal | Emitted by | Listened to by |
|---|---|---|
day_advanced | debug action (later: TimeSystem) | plot map — grows planted crops |
crop_matured(cell) | plot map / Crop | visual stage swap (later: effects) |
harvested(item, amount) | interaction logic | Inventory.add() |
changed | Inventory | debug 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#
- State machine style — make this call before stage 05.
(A) plain
ifchecks: least code today, tends to sprout contradictory flags; (B)enum State+matchwith 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: ______ - 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: ______ - 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.