Lesson 05

Pieces: shapes, spawning, and drawing them

A piece is a small set of cells with a shape and a color. This lesson defines the seven classic shapes as data, builds a Piece that draws itself from live Cell scenes, and spawns one at the top of the well.

▸ By the end of this lesson: A visible piece spawning at the top of the well, drawn from the data grid

5.1Shapes as offset data

Each shape is a list of (col, row) offsets from the piece's origin. This is all a shape is — no images required.

shapes.gdgdscript
# Seven shapes, each a list of Vector2i offsets from the piece origin,
# plus a color id (1..7). We keep these in one place: shapes.gd.
extends RefCounted
class_name Shapes

# Color id -> actual Color, used when drawing.
const COLORS: Array[Color] = [
	Color.BLACK,                 # 0 unused (empty)
	Color("#4a8fe7"),            # 1 I - blue
	Color("#e8b24a"),            # 2 O - amber
	Color("#a988e0"),            # 3 T - violet
	Color("#5cc98b"),            # 4 S - green
	Color("#e56b6f"),            # 5 Z - red
	Color("#e0863f"),            # 6 L - orange
	Color("#6bd4c4"),            # 7 J - teal
]

# Each entry: { "id": int, "cells": Array[Vector2i] }
const SHAPES: Array = [
	{ "id": 1, "cells": [Vector2i(-1,0), Vector2i(0,0), Vector2i(1,0), Vector2i(2,0)] }, # I
	{ "id": 2, "cells": [Vector2i(0,0),  Vector2i(1,0), Vector2i(0,1), Vector2i(1,1)] }, # O
	{ "id": 3, "cells": [Vector2i(-1,0), Vector2i(0,0), Vector2i(1,0), Vector2i(0,1)] }, # T
	{ "id": 4, "cells": [Vector2i(0,0),  Vector2i(1,0), Vector2i(-1,1),Vector2i(0,1)] }, # S
	{ "id": 5, "cells": [Vector2i(-1,0), Vector2i(0,0), Vector2i(0,1), Vector2i(1,1)] }, # Z
	{ "id": 6, "cells": [Vector2i(-1,0), Vector2i(0,0), Vector2i(1,0), Vector2i(1,1)] }, # L
	{ "id": 7, "cells": [Vector2i(-1,0), Vector2i(0,0), Vector2i(1,0), Vector2i(-1,1)] },# J
]

static func random_shape() -> Dictionary:
	return SHAPES[randi() % SHAPES.size()]

static func

static func belongs to the class, not an instance — call it as Shapes.random_shape() without creating a Shapes object. randi() % n gives a random int in 0..n-1.

5.2Build the Piece scene

  1. Scene → New Scene → "Other Node" → Node2D root, rename Piece.
  2. Save as piece.tscn.
  3. Attach a script piece.gd to it (code below).

The Piece holds its grid origin, its list of cell offsets, and the live Cell nodes it spawns to draw itself.

piece.gdgdscript
extends Node2D
class_name Piece

const CELL_SIZE: int = 32
const CellScene: PackedScene = preload("res://cell.tscn")

var color_id: int = 1
var offsets: Array = []            # Array[Vector2i], relative to origin
var origin: Vector2i = Vector2i.ZERO   # grid position of the piece
var _cell_nodes: Array = []        # the visual Cell instances

# Configure from a Shapes dictionary and a starting origin.
func setup(shape: Dictionary, start_origin: Vector2i) -> void:
	color_id = shape["id"]
	offsets = shape["cells"].duplicate()
	origin = start_origin
	_build_visuals()
	_redraw()

# Absolute grid cells this piece currently occupies.
func get_cells() -> Array:
	var result: Array = []
	for off in offsets:
		result.append(origin + off)
	return result

# Spawn one Cell node per offset (once).
func _build_visuals() -> void:
	for off in offsets:
		var c: Cell = CellScene.instantiate()
		add_child(c)
		c.set_color(Shapes.COLORS[color_id])
		_cell_nodes.append(c)

# Move the visual cells to match origin + offsets.
func _redraw() -> void:
	for i in range(offsets.size()):
		var grid_pos: Vector2i = origin + offsets[i]
		_cell_nodes[i].set_grid_position(grid_pos)

Visuals built once, moved often

We instantiate the Cell nodes a single time in _build_visuals(), then just reposition them in _redraw(). Re-instantiating every frame would leak nodes and tank performance. Build once, move many.

5.3Spawn a piece from Main

Replace any leftover temporary code in main.gd. Add a spawn method and call it once at start:

main.gdgdscript
# add near the top of main.gd, with the other consts/vars:
const PieceScene: PackedScene = preload("res://piece.tscn")
var active_piece: Piece = null

# The column where new pieces appear (middle-ish).
const SPAWN_ORIGIN: Vector2i = Vector2i(4, 1)

func spawn_piece() -> void:
	active_piece = PieceScene.instantiate()
	add_child(active_piece)
	active_piece.setup(Shapes.random_shape(), SPAWN_ORIGIN)

Then call spawn_piece() at the end of _ready():

main.gd (_ready)gdscript
func _ready() -> void:
	_draw_border()
	spawn_piece()
  1. Run with F5. A random colored shape appears near the top-center of the well.
  2. Run several times — you'll get different shapes and colors.

Nothing falls yet

The piece just sits there — we haven't added the fall timer or movement. That's Lessons 6 and 7. Right now we're only confirming that shapes spawn and draw correctly.

Checkpoint — expected state