Lesson 07

Player control: move and soft/hard drop

Falling on its own isn't a game. Now we wire input actions the proper Godot way, let the player slide the piece left and right (blocked by walls and stacks), and add soft drop and hard drop.

▸ By the end of this lesson: Full horizontal control plus soft and hard drop, all collision-checked

7.1Define input actions (don't hardcode keys)

Godot lets you name abstract actions and bind keys to them. This is how you support rebinding and multiple keys per action later. Set them up once:

  1. Project → Project SettingsInput Map tab.
  2. In the "Add New Action" box, type move_left and click Add. Repeat for move_right, soft_drop, hard_drop, and rotate_cw.
  3. Click the + next to move_left, press the Left Arrow (and optionally A). Bind move_right to Right/D, soft_drop to Down/S, hard_drop to Space, rotate_cw to Up/W.
  4. Close settings.

Why actions beat raw keys

With actions you check Input.is_action_pressed("move_left") instead of a specific key. Rebinding, controller support, and touch buttons all plug into the same action names without touching game logic.

7.2Horizontal movement with a repeat delay

Holding left should slide the piece smoothly, not teleport once or move impossibly fast. We add a small repeat timer for horizontal moves. Update main.gd's variables and _process:

main.gd (_process)gdscript
# add with the other vars in main.gd
var _move_accum: float = 0.0
const MOVE_REPEAT: float = 0.09   # seconds between held-key steps

func _process(delta: float) -> void:
	if active_piece == null:
		return
	_handle_horizontal(delta)
	_handle_soft_drop(delta)
	# gravity
	_fall_accum += delta
	if _fall_accum >= fall_interval:
		_fall_accum = 0.0
		_step_down()
main.gdgdscript
func _handle_horizontal(delta: float) -> void:
	var dir: int = 0
	if Input.is_action_pressed("move_left"):
		dir -= 1
	if Input.is_action_pressed("move_right"):
		dir += 1
	if dir == 0:
		_move_accum = MOVE_REPEAT   # so next press moves immediately
		return
	_move_accum += delta
	if _move_accum >= MOVE_REPEAT:
		_move_accum = 0.0
		if _can_move(active_piece, Vector2i(dir, 0)):
			active_piece.move_by(Vector2i(dir, 0))

Immediate first step

Setting _move_accum = MOVE_REPEAT when no key is held means the very next press moves the piece instantly, then repeats at the delay. Without this the first tap feels laggy.

7.3Soft drop (hold down to fall faster)

main.gdgdscript
func _handle_soft_drop(delta: float) -> void:
	if Input.is_action_pressed("soft_drop"):
		# Speed up gravity while held by advancing the accumulator faster.
		_fall_accum += delta * 8.0

Soft drop is just faster gravity

Rather than a separate movement path, soft drop simply feeds the gravity accumulator faster. Fewer code paths = fewer bugs. The piece still locks through the normal _step_down route.

7.4Hard drop (slam to the bottom instantly)

Hard drop belongs in _input, not _process, because it's a one-shot event, not a held state. Add:

main.gdgdscript
func _input(event: InputEvent) -> void:
	if active_piece == null:
		return
	if event.is_action_pressed("hard_drop"):
		_hard_drop()

func _hard_drop() -> void:
	# Move down until blocked, then lock.
	while _can_move(active_piece, Vector2i(0, 1)):
		active_piece.move_by(Vector2i(0, 1))
	_lock_piece()

is_action_pressed vs is_action_just_pressed

In _input, use event.is_action_pressed(name) — it's already edge-triggered per event. In _process, use Input.is_action_pressed for held state and Input.is_action_just_pressed for one-shots. Mixing these up causes hard drop to fire many times or not at all.

  1. Run with F5. Slide the piece with Left/Right — it stops at walls and against stacked cells.
  2. Hold Down to soft drop; press Space to hard drop and lock instantly.

Checkpoint — expected state