Lesson 03
GridFall is played on a grid, so we need two vocabularies: pixel coordinates (where things draw) and grid coordinates (row, column). This lesson builds the reusable Cell scene and the math that converts between the two.
In Godot 2D, position is measured in pixels, with (0,0) at the top-left and y increasing downward. GridFall also thinks in grid cells: column 0..9, row 0..17. We convert with one constant — the cell size in pixels.
# grid (col, row) -> pixels (x, y)
# pixel_x = col * CELL_SIZE
# pixel_y = row * CELL_SIZE
#
# Example with CELL_SIZE = 32:
# cell (3, 5) -> pixel (96, 160)Vector2 and Vector2i
Godot bundles an (x, y) pair into a Vector2 (floats) or Vector2i (integers). Grid coordinates are whole numbers, so we use Vector2i for them and Vector2 for pixel positions.
A cell is one colored square. We'll make it once and reuse it hundreds of times.
Cell.Cell → Add Child Node → ColorRect. This is the visible square.ColorRect. In the Inspector set Size to (30, 30) (leaving a 2px gap inside a 32px cell), and pick any Color for now.cell.tscn.Now give the Cell a script so it can recolor itself on command. Attach a script to the root Cell node:
extends Node2D
class_name Cell
# Grid size in pixels. One source of truth for the whole game.
const CELL_SIZE: int = 32
@onready var rect: ColorRect = $ColorRect
# Place this cell at a grid coordinate.
func set_grid_position(grid_pos: Vector2i) -> void:
position = Vector2(grid_pos.x * CELL_SIZE, grid_pos.y * CELL_SIZE)
# Recolor the visible square.
func set_color(c: Color) -> void:
# Guard in case this is called before _ready wires up the node.
if rect:
rect.color = cclass_name and @onready
class_name Cell registers this scene as a type you can reference from other scripts. @onready delays a variable's assignment until the node is in the tree, so $ColorRect actually exists. $ColorRect means "the child named ColorRect".
Main.main.tscn.main.tscn. Now ▶ (F5) launches GridFall.Attach a script to Main and define the well's dimensions. These constants drive everything downstream.
extends Node2D
# --- Well dimensions (the play field) ---
const COLS: int = 10
const ROWS: int = 18
const CELL_SIZE: int = 32
# Pixel size of the whole well, handy for centering / borders.
const WELL_WIDTH: int = COLS * CELL_SIZE # 320
const WELL_HEIGHT: int = ROWS * CELL_SIZE # 576
func _ready() -> void:
print("Well is %d x %d cells (%d x %d px)" % [COLS, ROWS, WELL_WIDTH, WELL_HEIGHT])
_draw_border()
# Draw a simple frame so we can see the play field while building.
func _draw_border() -> void:
var frame := ColorRect.new()
frame.color = Color(0.15, 0.18, 0.22) # dark slate
frame.size = Vector2(WELL_WIDTH, WELL_HEIGHT)
frame.position = Vector2.ZERO
frame.z_index = -1 # behind everything else
add_child(frame)String format operator
"%d x %d" % [a, b] substitutes values into a string. %d = integer, %s = anything, %.2f = float with 2 decimals. Get the placeholder count wrong and Godot errors at runtime — count them.
Temporarily add this to the bottom of Main._ready() to confirm the Cell scene loads and positions correctly:
# --- temporary test, remove after Lesson 3 ---
var cell_scene: PackedScene = preload("res://cell.tscn")
var c: Cell = cell_scene.instantiate()
add_child(c)
c.set_grid_position(Vector2i(4, 2)) # column 4, row 2
c.set_color(Color.ORANGE)res:// paths
res:// is the project root. preload() loads a scene at compile time and is the standard way to reference other scenes. If the path is wrong, Godot underlines it in the editor.
Vector2i(4, 2) numbers and re-run to feel how grid coordinates map to the screen.Checkpoint — expected state
cell.tscn with set_grid_position() and set_color().main.tscn set as the project's main scene, drawing the well border.