doc 3 of 9

Stage 01 — The Runner

You will build: the project, and a player whose movement feels like an action game — weight, momentum, stop-on-a-dime tuning you chose yourself. You'll learn: project setup · acceleration and friction · the vocabulary of "game feel" Warm-up: none — first stage. (Built the farming game? Predict, before coding: what will velocity = direction * speed feel like at action-game speeds, and why won't it survive this stage?)

Why this exists#

In a calm game, instant velocity is fine. In a fast game it reads as weightless — your brain expects mass, and mass means velocity changes over time. The whole trick is Vector2.move_toward(): walk the current velocity toward a target a fixed amount per tick. Three numbers fall out — top speed, acceleration, friction — and tuning them is your first real act of game design, not programming.

Build it#

A — Project#

Godot 4.7 → New Project → action_rpg, in your projects folder, Mobile renderer. Pixel-art settings (same reasoning as ever — render small, scale by integers, keep pixels square): viewport 640×360, window override 1280×720, stretch mode viewport, scale mode integer, texture filter Nearest. Turn on the typed-GDScript warning (debug/gdscript/warnings/untyped_declaration → Warn) — the house rules apply here too. Then:

git init -b main
git add . && git commit -m "stage 00: empty project, pixel settings"

B — Input map#

move_left/right/up/down on WASD + arrows. (Dash and attack come with their stages — don't stub what you can't test.)

C — The player, with weight (type it, don't paste it)#

player/player.tscn: CharacterBody2D + Sprite2D (placeholder) + CollisionShape2D at the feet. The script — complete, because the pattern is the lesson; the numbers are yours:

class_name Player
extends CharacterBody2D

@export var max_speed: float = 140.0
@export var acceleration: float = 1200.0
@export var friction: float = 1000.0

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 != Vector2.ZERO:
		facing = direction
		# accelerate toward intent
		velocity = velocity.move_toward(direction * max_speed, acceleration * delta)
	else:
		# no intent: friction bleeds speed off
		velocity = velocity.move_toward(Vector2.ZERO, friction * delta)
	move_and_slide()

Before you run — predict: why does this code multiply by delta when the farming game's movement didn't? (Hint: one sets velocity, one changes it. Which operation is per-tick?) Run only after you've committed to an answer.

D — Tune it like you mean it#

This is the stage's real work. Make a scratch arena (Node2D + a few StaticBody2D walls) and spend twenty minutes on the three exported numbers in the Inspector while the game runs:

FeelTry
Ice-skatingfriction 200
Stiff/instant (farming-style)acceleration 99999
Heavy tankacceleration 400, max 100
Snappy action (a starting point)max 140 / accel 1200 / friction 1000

Pick your triple. Write the three numbers and one sentence of why in a comment above the exports. You will re-tune after the dash exists — feel is a system, not a setting.

git add . && git commit -m "stage 01: movement with weight, tuned"

Checkpoint — definition of done#

  • Starting, stopping, and direction changes all take visible (but small) time
  • Diagonals aren't faster (test a corner — and know which line guarantees it)
  • Your three numbers are chosen, committed, and justified in a comment
  • Zero warnings; pixels crisp at any window size
  • You can explain: what would break if friction were applied even while a direction is held? Try it for science, then revert.

Stretch (no instructions)#

A subtle sprite lean: the player tilts a few degrees into their velocity. Two lines and suddenly there's a body under that placeholder.

If you get stuck#

  • Movement feels identical to instant → your acceleration is so high it is instant; drop it by 10× and watch what the parameter actually does before re-raising.
  • Player never quite stops → print velocity.length() when idle; if it hovers near zero without reaching it, what does move_toward do on the final step — and is friction actually being reached in your if?
  • Sliding along walls feels sticky → that's move_and_slide doing its job against acceleration; revisit after the dash, it changes the equation.