doc 2 of 10

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#

SystemPatternWhy
Player movementCharacterBody3D, per-axis move_toward on velocity.x/velocity.z in _physics_process, gravity from get_gravity() when airbornePer-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 statesNode-based, nestable FSMStateMachine extends State, one child State per state, travel() by nameThe 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
FacingA 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
CameraPlain Camera3D child of the player first; Phantom Camera (addon) with damped follow in the shipped sceneDecision 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 gridFarm Node3D owning Dictionary[Vector2i, Plot] (_plots) + Dictionary[Vector2i, Crop] (_views); 1 m cells; world_to_cell via flooriThe 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
CropsCropData 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
GrowthFarm.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
DebugDebug autoload: a keycode→Callable registry, set_process_unhandled_input(OS.is_debug_build()), every binding release-gatedDebug 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 cycleFor now: the F1 debug action. Later: a TimeSystem emitting day_advancedSame 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): CropDataid: StringName, display_name, days_per_stage: int, stage_meshes: Array[Mesh]. One per crop kind.
  • Runtime state (mutable, savable): Plot (RefCounted) — which CropData, current stage, days_grown. One per planted cell, held in Farm._plots.
  • Views (rebuildable, unsavable): Crop scene — a MeshInstance3D showing stage_meshes[stage]. Held in Farm._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, not CropData. days_grown is per-plant; days_per_stage is per-kind.
  • Standing rule from the 2D game, unchanged: never mutate a loaded .tres at runtime.

Signals#

SignalEmitted byListened to by
finished(next_state)any Stateits StateMachine (→ travel)
day_advancedthe debug action (later: TimeSystem)Farm.advance_day
crop_planted(cell) / crop_removed(cell)Farmreserved — 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#

  1. The camera — make this call in stage 01. (A) a plain Camera3D parented 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: ______
  2. The state machine's rung — stage 02's call. (A) the enum + match machine from the state machine shelf's part 02: one file, four states, done; (B) node-based states with a generic StateMachine: 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: ______
  3. Where a plot's truth lives — stage 04's call, once the grid exists. (A) a Dictionary[Vector2i, Plot] on Farm, with Crop nodes 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.