Stage 03 — The soil grid: tiles you can till
You will build: a tile-painted field and the act of tilling — pressing a key turns the grass cell in front of the farmer into soil, in both pixels and data. You'll learn: TileMapLayer + TileSet · keeping game truth out of visuals · typed dictionaries — and you'll make design decision 2 from the architecture doc. Warm-up — from memory:
- Layers vs masks — which one is "what I am" and which is "what I look for"?
- Write from memory: the full signature of a typed function taking a
Vector2icell and returning whether tilling succeeded. - From stage 01: what does
Input.get_vectorclamp its result to, and why did that matter?
Why this exists#
A TileMapLayer paints a grid efficiently — one node, thousands of cells
(Using TileMaps;
note it's TileMapLayer, one node per layer — the single-TileMap-with-layers workflow died
in Godot 4.3, and most online tutorials predate that). But tiles are pixels, not state:
"is this plot tilled? watered? what's planted?" is savable game truth, and the architecture
splits truth from presentation on purpose.
Design decision 2 — make the call now, record it (and your reason) in the architecture doc:
- (A)
Dictionary[Vector2i, PlotState]beside the tilemap — tilemap purely visual; trivially savable; one lookup per interaction. Recommended, and the build steps below assume it. - (B) TileSet custom data layers — state rides the tiles; fewer structures; save/load and growth iteration get awkward. If you choose B, the build steps are yours to derive from the TileSet docs — a deliberate spec-level exercise. Record your choice and your reason either way.
Build it#
A — A TileSet with two tiles#
Add Ground (TileMapLayer) under World (above the bodies in the tree so it draws behind).
Inspector → Tile Set → New TileSet, tile size 32×32. In the TileSet panel add an atlas
with your placeholder art — two 32×32 tiles are enough: grass, tilled. (Placeholder
squares were the plan; real art swaps in later without touching logic.) Paint a field.
B — The farmer faces somewhere#
Tilling needs "the cell in front of me." In player.gd, keep a facing direction — a few
pieces are blanked below; those decisions are yours to make:
var facing: Vector2 = Vector2.DOWN
func _physics_process(_delta: float) -> void:
var direction: Vector2 = Input.get_vector("move_left", "move_right", "move_up", "move_down")
if direction != ______: # only update facing while actually moving — why?
facing = direction.______() # snap to the dominant axis; find it in the Vector2 class ref
velocity = direction * speed
move_and_slide()
One of those blanks has three plausible answers in the class reference — pick by testing diagonals and watching which feels right for a 4-directional farmer.
C — The FarmGrid: truth lives here#
New child of World: plain Node named FarmGrid, script farm_grid.gd. The signatures
and intent comments are given; the bodies are yours to write:
class_name FarmGrid
extends Node
## One plot's runtime state. Grows in stage 04.
class PlotState:
var tilled: bool = false
@export var ground: TileMapLayer
var _plots: Dictionary[Vector2i, PlotState] = {}
## Turn a world position into the cell it lands in.
func cell_at(world_pos: Vector2) -> Vector2i:
# ground.local_to_map + ground.to_local — mind which space each expects
pass
## Till the cell if it's grass. Returns whether anything changed.
func till(cell: Vector2i) -> bool:
# 1. already tilled? refuse. 2. record truth in _plots 3. repaint via ground.set_cell
pass
set_cell's signature (coords, source_id, atlas_coords) trips everyone once — read it in the
TileMapLayer class reference
before writing, not after failing.
D — The act action#
Input Map: add act (E and/or Space). In player.gd, on act just-pressed, ask the grid to
till cell_at(global_position + facing * 32.0). Getting the FarmGrid reference is a design
decision you've already met in the architecture — the parent injects it (@export), the child
never reaches out.
Predict before you run: stand at the field's left edge facing left and press act. What cell gets computed — and should
tillrefuse it? Does your code?
E — Commit#
git add . && git commit -m "stage 03: tileset, facing, FarmGrid tilling"
Checkpoint — definition of done#
- Grass cell in front of the farmer becomes tilled soil on act — visually AND in
_plots(prove the second half with one temporaryprint) - Tilling the same cell twice does nothing the second time
- Facing works in all four directions (till above, below, left, right)
- Zero warnings; committed; decision 2 recorded in the architecture doc, with a reason
- You can explain: if you deleted the Ground node entirely, what tilling information survives — and why is that the design's whole point?
Stretch (no instructions)#
Hoe range: tilling works from one tile away but not two — and something visual (a modulate flash, anything) tells the player which cell act would hit right now.
If you get stuck#
- Tilled tile paints in the wrong place → you're mixing world/local space; print both the input position and the mapped cell, walk one tile, watch how each changes.
- Nothing paints → is
groundactually assigned in the Inspector? (@exportnodes are null until wired — the error message will have told you exactly this.) set_cellerrors → count its arguments against the 4.x class reference; a Godot-3 or early-4 snippet has a different shape.