Alien Farming Game — Architecture
3D top-down farming prototype, 640×360 logical viewport (window 1280×720), Godot 4.7, Forward+ with d3d12, Jolt Physics. Placeholder primitive meshes throughout — the systems are real, the art is not — which is a feature: every visual can be reasoned about because every visual is a shape you can count the vertices of. This document is the map of the whole design, written before the first line of code, updated whenever a decision lands.
The 3D delta, in one paragraph#
The 2D farming game put the world on the XY plane with Y down. This one puts it on the XZ
plane with Y up: "left/right" is X, "forward/back" is Z, and Y is the only axis gravity
touches. Input.get_vector still gives you a 2D result (x = strafe, y = forward), and the
first thing every 3D movement script does is lift it: Vector3(raw.x, 0.0, raw.y). Everything
else — the state machine, the data split, the grid — transfers, and the stages are ordered so
you feel each transfer land.
Systems map#
| System | Pattern | Why |
|---|---|---|
| Player movement | CharacterBody3D, per-axis move_toward on velocity.x/velocity.z in _physics_process, gravity from get_gravity() when airborne | Per-axis steering keeps acceleration honest on turns (velocity is steered, not replaced); get_gravity() is the 4.3+ way to ask the world for the pull instead of hardcoding Vector3.DOWN * 9.8 (docs) |
| Player states | Node-based, nestable FSM — StateMachine extends State, one child State per state, travel() by name | The 2D game used the enum rung; here per-state files pay for themselves (five states, each with enter/exit/tick behaviour) and nesting is the reserved upgrade — shapeshift becomes a machine inside the player machine. The state machine shelf is the general treatment; this project is its part 03 with a twist |
| Facing | A facing: Vector3 the states maintain; skin rotation atan2(-direction.x, -direction.z) | The model's front is −Z; the math converts a movement vector into the one yaw that points it. Facing is data (savable, queryable) and rotation is a view of that data |
| Camera | Plain Camera3D child of the player first; Phantom Camera (addon) with damped follow in the shipped scene | Decision 1. A child camera follows with zero code; the Phantom layer exists so other systems (cutscenes, planting close-ups) can borrow the camera later without a new node |
| Farm grid | Farm Node3D owning Dictionary[Vector2i, Plot] (_plots) + Dictionary[Vector2i, Crop] (_views); 1 m cells; world_to_cell via floori | The dictionary is the authority; the Crop nodes are projections that can be deleted and rebuilt. Decision 3. floori — not int() — because the grid crosses the origin and int() truncates toward zero |
| Crops | CropData extends Resource (.tres: id, name, days_per_stage, stage_meshes: Array[Mesh]) + Plot extends RefCounted (per-cell state) + Crop scene (the view) | The definition/state split from the 2D game, with Plot as a RefCounted because a plot is data with a lifetime, not a node. Adding crop #3 = one file |
| Growth | Farm.advance_day() ticks every plot: days_grown += 1, stage = mini(days_grown / days_per_stage, last) | Central tick (the 2D game's decision 3B): save/load and "skip N days" stay one loop; the stage math is integer division on paper |
| Debug | Debug autoload: a keycode→Callable registry, set_process_unhandled_input(OS.is_debug_build()), every binding release-gated | Debug actions are not input-map actions: they never pollute the player's control scheme and they vanish from an exported build by construction, not by remembering |
| Day cycle | For now: the F1 debug action. Later: a TimeSystem emitting day_advanced | Same deferral as the 2D game — a key press exercises the identical growth path without global state |
Scene tree#
Game (Node3D)
├── DirectionalLight3D ← shadow-casting sun; angle is a design number
├── WorldEnvironment ← ProceduralSky + glow (the 3D "background")
└── World (Node3D)
├── Farm (Node3D) ← crop_catalogue export; owns _plots and _views
│ └── (Crop nodes instanced here at runtime, one per planted cell)
├── Player (CharacterBody3D) ← player.tscn, below
├── Ground (CSGBox3D 15×1×15) ← use_collision on; the field's bounds for free
└── GameCamera (Camera3D, top_level, fov 45)
└── PhantomCameraHost → PhantomCamera3D (follows Player, damped)
Player (CharacterBody3D)
├── Skin (Node3D) → Mesh (capsule) → Face (box) ← placeholder "character"
├── WorldCollisionShape (CollisionShape3D, capsule)
├── StateMachine (Node, StateMachine script)
│ ├── IdleState · WalkState · RunState · UseItemState · ShapeshiftState
└── Interfaces (CanvasLayer) → Inventory (Control) ← reserved, empty
The states are children of the machine, keyed by node name — the name is the API
(travel(&"WalkState")), and the reference doc records the two traps this project hit: a
state node with no script is silently skipped at registration, and renaming a state node
renames its transition key.
Data model#
- Definitions (immutable
.tres):CropData—id: StringName,display_name,days_per_stage: int,stage_meshes: Array[Mesh]. One per crop kind. - Runtime state (mutable, savable):
Plot(RefCounted) — whichCropData, currentstage,days_grown. One per planted cell, held inFarm._plots. - Views (rebuildable, unsavable):
Cropscene — aMeshInstance3Dshowingstage_meshes[stage]. Held inFarm._views, deleted and rebuilt without loss. - The rule that keeps them honest: if two plants of the same kind could ever disagree
about it, it belongs on
Plot, notCropData.days_grownis per-plant;days_per_stageis per-kind. - Standing rule from the 2D game, unchanged: never mutate a loaded
.tresat runtime.
Signals#
| Signal | Emitted by | Listened to by |
|---|---|---|
finished(next_state) | any State | its StateMachine (→ travel) |
day_advanced | the debug action (later: TimeSystem) | Farm.advance_day |
crop_planted(cell) / crop_removed(cell) | Farm | reserved — the inventory and the UI will listen; nothing does yet, and that's the audit |
The state machine's finished signal is the one live wire: a state that completes
(UseItemState emits finished(&"IdleState") on entry) announces its return rather than
the machine polling it. Parent calls child, child announces up — the same direction as every
other signal in the almanac.
Saving (designed now, built later)#
Serialize Farm._plots (cell → {crop id, stage, days_grown}), the player's position and
facing, and the day counter — with a version field from day one. _views is never saved; it
is rebuilt by walking _plots. The 2D game's honesty test applies verbatim: deleting
every visual node must lose no information. Stage 07's requirements section is where this
becomes a build.
The three design decisions — yours to make#
- The camera — make this call in stage 01.
(A) a plain
Camera3Dparented to the player with a fixed offset: zero dependencies, instant follow, but every camera need is a new node and a new fight; (B) the Phantom Camera addon: one real camera, a layer of phantoms that request control, damped follow built in — and an external dependency to own. One-line trade-off: A is free today; B is the upgrade path the game will want. Your call and your reason: ______ - The state machine's rung — stage 02's call.
(A) the
enum+matchmachine from the state machine shelf's part 02: one file, four states, done; (B) node-based states with a genericStateMachine: one file per state, a registration step, and nesting available for free. One-line trade-off: A is less ceremony; B is where shapeshift lives. Your call and your reason: ______ - Where a plot's truth lives — stage 04's call, once the grid exists.
(A) a
Dictionary[Vector2i, Plot]onFarm, withCropnodes as disposable views: saving is a walk, the scene is a projection; (B) one node per cell holding its own state: the tree is the map, fewer structures, and saving means walking the tree and trusting it. One-line trade-off: A is the 2D game's answer and it transfers; B is the temptation. Your call and your reason: ______
Build order#
First milestone — the walking skeleton: the player walks a lit field under a camera, state machine driving Idle/Walk, one cell plantable by facing, one crop growing through two meshes on a day-tick. No inventory, no saving, no real clock — the smallest complete loop in three dimensions, end to end, before any system gets deep.
Then: facing & run & gravity → the grid's math → crops as data → the day tick → the requirements stage. Later modules, in whatever order the itch strikes: harvest · inventory · TimeSystem · saving · shapeshift (the nested machine) · synthesize · audio.