Lesson 12

Next-piece preview and a little juice

Two touches that make GridFall feel finished: a preview of the upcoming piece so players can plan, and a brief flash when lines clear so the payoff reads. Both reuse systems you already built.

▸ By the end of this lesson: A next-piece preview panel and a satisfying line-clear flash

12.1Decide the next piece in advance

To preview, we pick the next shape ahead of time and hold it. Update main.gd to keep a next_shape:

main.gd (replace _ready)gdscript
var next_shape: Dictionary = {}

func _ready() -> void:
	board = Board.new(COLS, ROWS)
	_draw_border()
	game_over_panel.visible = false
	restart_button.pressed.connect(_on_restart_pressed)
	next_shape = Shapes.random_shape()   # prime the pump
	_refresh_hud()
	spawn_piece()

In spawn_piece(), use next_shape and then roll a new one:

main.gd (replace spawn_piece)gdscript
func spawn_piece() -> void:
	if game_over:
		return
	var shape: Dictionary = next_shape
	next_shape = Shapes.random_shape()
	_draw_preview()
	if not _shape_fits_at(shape, SPAWN_ORIGIN):
		_trigger_game_over()
		return
	active_piece = PieceScene.instantiate()
	add_child(active_piece)
	active_piece.setup(shape, SPAWN_ORIGIN)
	_fall_accum = 0.0

12.2Draw the preview

  1. In main.tscn, under Main (not the HUD, so it uses world space), add a Node2D named Preview. Position it to the right of the well, e.g. (360, 160).

Render the next shape into it, reusing the Cell scene:

main.gdgdscript
@onready var preview_root: Node2D = $Preview

func _draw_preview() -> void:
	for child in preview_root.get_children():
		child.queue_free()
	if next_shape.is_empty():
		return
	for off in next_shape["cells"]:
		var c: Cell = CellScene.instantiate()
		preview_root.add_child(c)
		# Offsets can be negative; nudge by (1,1) so the preview sits nicely.
		var p: Vector2i = off + Vector2i(1, 1)
		c.set_grid_position(p)
		c.set_color(Shapes.COLORS[next_shape["id"]])

queue_free the old preview

Clear the preview's children before drawing the new shape or old cells pile up and overlap. Same pattern as _render_board(): wipe, then draw.

12.3A line-clear flash with a Tween

A tiny flash sells the clear. We create a short white overlay across the cleared rows and fade it out with a Tween — Godot's built-in animation tool. Add to main.gd and call it from _award_score:

main.gdgdscript
func _flash_clear() -> void:
	var flash := ColorRect.new()
	flash.color = Color(1, 1, 1, 0.6)
	flash.size = Vector2(WELL_WIDTH, WELL_HEIGHT)
	flash.position = Vector2.ZERO
	flash.z_index = 5
	add_child(flash)
	# Fade alpha to 0 over 0.25s, then remove the node.
	var tw: Tween = create_tween()
	tw.tween_property(flash, "modulate:a", 0.0, 0.25)
	tw.tween_callback(flash.queue_free)
main.gd (replace _award_score)gdscript
func _award_score(cleared: int) -> void:
	cleared = clampi(cleared, 0, 4)
	score += LINE_SCORES[cleared]
	lines_cleared_total += cleared
	_update_level()
	_refresh_hud()
	_flash_clear()

Tweens in one line

create_tween() makes a throwaway animator. tween_property(target, "modulate:a", 0.0, 0.25) animates the alpha to 0 over a quarter second. tween_callback then frees the node. No AnimationPlayer setup needed for simple effects.

  1. Run with F5. The next piece shows to the right and updates as you play.
  2. Clear a line — the well flashes white briefly, then fades. Multi-clears feel punchier.

Checkpoint — expected state