LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
GDScriptBeginner~1.5 hours

Top-Down Coin Collector

Build a 2D game where a player moves with the keyboard, collects coins with Area2D signals and wins when all coins are gone.

CharacterBody2DInputArea2DGroups

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.

What You'll Build
  • A Player scene using CharacterBody2D, Sprite2D and CollisionShape2D.
  • Smooth 8-direction movement with Input.get_vector() and move_and_slide().
  • A reusable Coin scene built on Area2D with a body_entered signal and a custom collected signal.
  • A Main scene that counts coins with groups and updates a HUD Label.
  • 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.
  • Vector2 and built-in types — lesson 10.
  • @export variables 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.gd

Each 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.

Pasting Code into Godot

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.

player.gd
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)
Want WASD?

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().

coin.gd
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:

main.gd
extends Node2D
var collected_count: int = 0
var total_coins: int = 0
var start_time_ms: int = 0
var 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

  1. Run the project (F5). The HUD shows "Coins: 0 / 8" (or however many coins you placed).
  2. Move with the arrow keys; diagonal movement should be the same speed as straight movement.
  3. Touch a coin: it disappears and the counter increases by one.
  4. Try to leave the window: the player stops at the edge.
  5. Collect the last coin: the message shows "You win!" with your time and the player stops moving.
  6. Press Enter: the level reloads with all coins back in place.
HUD after collecting every coin

Click Run to see what this code prints.

Coin Not Collected?

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() and randf_range().
  • Add an @export var value of 5 on a few special coins with a different Modulate colour.
  • Add walls using StaticBody2D nodes so move_and_slide() has something to slide along.
  • Add a countdown Timer so the player must collect every coin before time runs out.
  • Play a sound with an AudioStreamPlayer when a coin is collected (play it from Main, 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.