Overview
This is your first "real" 2D game in Godot: a character you move around the screen, objects you collect, and a clear win condition. It introduces the two node types behind most 2D games — CharacterBody2D for things that move and collide, and Area2D for things that detect overlaps — plus the signal that connects them.
You move with the arrow keys (or WASD after a small tweak), touch coins to collect them, and watch the counter in the corner. When the last coin is picked up the game shows "You win!" with your time, and pressing Enter restarts the level. No custom art is needed: every sprite uses the icon.svg image that Godot adds to every new project.
- A
Playerscene usingCharacterBody2D,Sprite2DandCollisionShape2D. - Smooth 8-direction movement with
Input.get_vector()andmove_and_slide(). - A reusable
Coinscene built onArea2Dwith abody_enteredsignal and a customcollectedsignal. - A
Mainscene that counts coins with groups and updates a HUDLabel. - A win state with elapsed time and a restart key using
reload_current_scene().
Prerequisites
- Nodes, scenes and instancing — lesson 5.
_physics_process()and@onready— lesson 7.- Signals, including custom signals — lesson 8.
Vector2and built-in types — lesson 10.@exportvariables and the Inspector — lesson 11.
Project Structure
res://├── icon.svg # created automatically with every project├── main.tscn / main.gd├── player.tscn / player.gd└── coin.tscn / coin.gdEach scene owns one job. Player only knows how to move. Coin only knows that something touched it — it announces this with its own collected signal and removes itself. Main listens to every coin's signal, keeps score and decides when the game is won. Because coins never talk to the HUD directly, you can drop as many coins into the level as you like without changing any code.
The code on this page is indented with spaces. If Godot reports "Mixed use of tabs and spaces for indentation" after you paste, use the script editor's Edit > Indentation > Convert Indent to Tabs.
Step 1: The Player Scene
Create a new scene with a CharacterBody2D root named Player. Add a Sprite2D and drag icon.svg into its Texture property, then set its Scale to 0.5. Add a CollisionShape2D with a new CircleShape2D (radius about 28). Save as player.tscn and attach player.gd to the root.
Player (CharacterBody2D) ← player.gd├── Sprite2D ← Texture: icon.svg, Scale: 0.5└── CollisionShape2D ← Shape: CircleShape2D (radius 28)Step 2: Player Movement
Input.get_vector() reads four actions at once and returns a Vector2 already normalized, so moving diagonally is not faster than moving straight. The built-in ui_left/ui_right/ui_up/ui_down actions map to the arrow keys out of the box. Setting velocity and calling move_and_slide() lets the physics engine handle collisions; clamp keeps the player on screen.
extends CharacterBody2D
@export var speed: float = 300.0
func _physics_process(_delta: float) -> void: var direction := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") velocity = direction * speed move_and_slide()
# Keep the player inside the visible window. var screen := get_viewport_rect().size position = position.clamp(Vector2.ZERO, screen)Open Project > Project Settings > Input Map, add actions such as move_left/move_right/move_up/move_down bound to A/D/W/S (and the arrow keys), then use those names in Input.get_vector().
Step 3: The Coin Scene
Create a scene with an Area2D root named Coin, a Sprite2D using icon.svg at Scale 0.25 with a gold Modulate colour (for example #ffd34d), and a CollisionShape2D with a small CircleShape2D. The script adds the coin to the coins group so Main can count coins without a hard-coded list. When the player's body enters the area, the coin emits collected and frees itself with queue_free().
extends Area2D
signal collected(value: int)
@export var value: int = 1
func _ready() -> void: add_to_group("coins") body_entered.connect(_on_body_entered)
func _process(delta: float) -> void: rotation += 2.0 * delta # a gentle spin so coins stand out
func _on_body_entered(body: Node2D) -> void: if body is CharacterBody2D: collected.emit(value) queue_free()Step 4: The Main Scene & HUD
Create main.tscn with a Node2D root named Main. Instance player.tscn once and coin.tscn eight or more times (Ctrl+D duplicates), spreading the coins around the screen. Add a CanvasLayer named HUD with a Label called ScoreLabel in the top-left corner and a centred Label called MessageLabel. Set main.tscn as the main scene.
Main (Node2D) ← main.gd├── Player (player.tscn)├── Coin (coin.tscn) ← duplicate 8+ times├── Coin2 (coin.tscn)├── ...└── HUD (CanvasLayer) ├── ScoreLabel (Label) └── MessageLabel (Label)get_tree().get_nodes_in_group("coins") returns every coin in the level, so Main connects to each one's collected signal and knows how many there are in total. Time.get_ticks_msec() measures how long the player took.
Complete Code
player.gd and coin.gd are shown in full in Steps 2 and 3. The last script ties the level together:
extends Node2D
var collected_count: int = 0var total_coins: int = 0var start_time_ms: int = 0var game_won: bool = false
@onready var score_label: Label = $HUD/ScoreLabel@onready var message_label: Label = $HUD/MessageLabel
func _ready() -> void: var coins := get_tree().get_nodes_in_group("coins") total_coins = coins.size() for coin in coins: coin.collected.connect(_on_coin_collected) start_time_ms = Time.get_ticks_msec() message_label.text = "Collect all the coins!" update_score()
func _on_coin_collected(value: int) -> void: collected_count += value update_score() if collected_count >= total_coins: win()
func update_score() -> void: score_label.text = "Coins: %d / %d" % [collected_count, total_coins]
func win() -> void: game_won = true var seconds := (Time.get_ticks_msec() - start_time_ms) / 1000.0 message_label.text = "You win! Time: %.1f s\nPress Enter to play again" % seconds $Player.set_physics_process(false) # stop movement
func _unhandled_input(event: InputEvent) -> void: if game_won and event.is_action_pressed("ui_accept"): get_tree().reload_current_scene()Testing the Game
- Run the project (F5). The HUD shows "Coins: 0 / 8" (or however many coins you placed).
- Move with the arrow keys; diagonal movement should be the same speed as straight movement.
- Touch a coin: it disappears and the counter increases by one.
- Try to leave the window: the player stops at the edge.
- Collect the last coin: the message shows "You win!" with your time and the player stops moving.
- Press Enter: the level reloads with all coins back in place.
Click Run to see what this code prints.
If touching a coin does nothing, check that the Coin's CollisionShape2D has a shape assigned, that Monitoring is enabled on the Area2D (it is by default), and that the Player and Coin share at least one collision layer/mask (both use layer 1 by default).
Extend This Project
- Spawn coins at random positions from code with
preload("res://coin.tscn").instantiate()andrandf_range(). - Add an
@export var valueof 5 on a few special coins with a different Modulate colour. - Add walls using
StaticBody2Dnodes somove_and_slide()has something to slide along. - Add a countdown
Timerso the player must collect every coin before time runs out. - Play a sound with an
AudioStreamPlayerwhen a coin is collected (play it fromMain, since the coin frees itself).
Summary
You built a complete 2D game from three small scenes: a CharacterBody2D player moved with Input.get_vector() and move_and_slide(), Area2D coins that detect the player and announce it through a custom signal, and a Main scene that uses groups to wire everything together and track the win condition. Small scenes that communicate through signals are the heart of Godot's design.