Build a top-down game where a character collects three coins, the score reaches 30, and a message invites you to restart. This guide gives you the node names, Inspector settings, and three scripts that belong together. You can use the icon from a new Godot project for both the player and coins; no asset download is required.
The finished loop and what you need
Use a Godot 4 project with GDScript. You will make Player.tscn, Coin.tscn, and Main.tscn. This is top-down movement: there is no jump, floor, or gravity. If you have just finished our movement exercise, the new idea is combining two input axes into one direction vector.
- Move with WASD or the arrow keys.
- Collect three coins worth 10 points each. Each coin can score only once.
- Keep the player inside the window. Show a completion message and restart with R.
1. Create the project and input actions
Create a new project using the Compatibility renderer. In Project Settings → Display → Window → Size, set Viewport Width to 640 and Viewport Height to 360. Leave the game at that size while following this guide. Keep all three scenes and scripts in the project root for now so their paths are easy to find.
Open Project Settings → Input Map. Add the following action names exactly, then add their key events. The strings in the script must match these names, including underscores. Each action may have more than one key.
move_left: A and Left Arrow.move_right: D and Right Arrow.move_up: W and Up Arrow.move_down: S and Down Arrow.restart: R.
2. Give the player an image and a collision shape
Select a node to inspect it.
A body controlled by your script. It supplies movement and collision methods for a 2D character.
- Script
- player.gd
- Motion mode
- Floating
- Scale
- (1, 1)
- Collision layer
- 1
Reads input and moves
Displays a texture. It makes the character or coin visible, but does not define its collision boundary.
- Texture
- icon.svg
- Position
- (0, 0)
Displays the player image
Child of Player
Gives its parent body or area a collision boundary. Assign a Shape resource to define that boundary.
- Shape
- CircleShape2D
- Radius
- 16
- Position
- (0, 0)
The player boundary
Child of Player
Attach player.gd to the root. Keep both children at local position (0, 0).
Create a scene using Other Node → CharacterBody2D. Rename its root Player and add the two children shown above. Select Sprite2D and drag the project’s icon.svg from FileSystem onto its Texture property. Scale only Sprite2D until the image is about 32 pixels wide; for a 128-pixel image, use Scale (0.25, 0.25). Keep the Player root at Scale (1, 1).
Select CollisionShape2D. Its Shape starts empty: choose New CircleShape2D, open that resource, and set Radius to 16. The node existing in the tree is not enough; the assigned Shape resource defines the collision boundary. On Player, set Motion Mode to Floating for top-down movement. Leave Collision Layer 1 enabled. Save as Player.tscn.
Attach a script named player.gd to Player, replacing any movement template with this entire file. The player adds itself to the player group when it becomes ready, so pickups can identify it even if you later rename its node.
extends CharacterBody2D
@export var speed: float = 220.0
func _ready() -> void:
add_to_group("player")
func _physics_process(_delta: float) -> void:
var direction := Input.get_vector(
"move_left", "move_right", "move_up", "move_down"
)
velocity = direction * speed
move_and_slide()
# Main is untransformed; this example has no camera.
position = position.clamp(
Vector2(16, 64), get_viewport_rect().size - Vector2(16, 16)
)Input.get_vector limits the direction’s length to one, which prevents diagonal keyboard movement from being faster. Velocity is pixels per second. Do not multiply it by delta before move_and_slide: that method handles the timestep. The final clamp supplies a simple screen boundary, not a physical wall; the top margin leaves room for the score.
3. Build a coin that can be collected once
Select a node to inspect it.
Detects overlapping bodies or areas. Use its signals for pickups and triggers; it does not act as a solid wall.
- Script
- coin.gd
- Monitoring
- On
- Collision mask
- 1
Attach coin.gd
Displays a texture. It makes the character or coin visible, but does not define its collision boundary.
Small, gold-tinted project icon
Child of Coin
Gives its parent body or area a collision boundary. Assign a Shape resource to define that boundary.
- Shape
- CircleShape2D
- Radius
- 12
- Position
- (0, 0)
The pickup boundary
Child of Coin
The CoinPickup class name below lets Main connect to coin instances safely.
Create an Area2D root named Coin. Give Sprite2D the same icon, scaled to about 24 pixels wide, and change its Self Modulate color to gold so it differs from Player. Assign a CircleShape2D with Radius 12 to its CollisionShape2D child. Leave the children centered at (0, 0).
On Coin, keep Monitoring enabled. Under Collision → Mask, enable layer 1 so the area detects Player’s collision layer. An Area2D detects overlaps; it does not stop the player like a wall. Attach coin.gd below and save Coin.tscn. Do not also connect body_entered through the editor: this script already connects it.
class_name CoinPickup
extends Area2D
signal collected(value: int)
@export var value: int = 10
var taken: bool = false
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
if taken or not body.is_in_group("player"):
return
taken = true
collected.emit(value)
queue_free()The guard has two jobs. A body outside the player group is ignored, and taken prevents another overlap from scoring again before the coin is freed at the end of the frame. The coin emits its value without finding a label or changing Main’s score. You will connect its listener next; without that listener, a disappearing coin cannot update the UI. Try this failure deliberately in the coin signal exercise.
4. Arrange the level and score labels
Select a node to inspect it.
Groups objects in 2D space. Its children inherit its transform.
Attach main.gd
An instance of a saved scene. It brings that scene’s root and children into this level.
Position (96, 180)
Child of Main
Groups objects in 2D space. Its children inherit its transform.
Container for the three instances
Child of Main
An instance of a saved scene. It brings that scene’s root and children into this level.
Position (256, 96)
Child of Main/Coins
An instance of a saved scene. It brings that scene’s root and children into this level.
Position (416, 180)
Child of Main/Coins
An instance of a saved scene. It brings that scene’s root and children into this level.
Position (544, 272)
Child of Main/Coins
Places its canvas items on a separate drawing layer, useful for a screen-space interface.
Keeps labels in screen space
Child of Main
Displays text, such as a score or a hint. A script can update its text property.
Position (16, 12)
Child of Main/HUD
Displays text, such as a score or a hint. A script can update its text property.
Position (16, 36)
Child of Main/HUD
Keep Main and Coins at position (0, 0) and scale (1, 1). Node names are used by main.gd.
Create a 2D scene, rename the Node2D root Main, and save Main.tscn. Drag Player.tscn from FileSystem into Main. Add a Node2D child named Coins; select it before dragging Coin.tscn into the scene. Duplicate that coin twice and set the three positions shown in the figure. All coins must be direct children of Coins, because the script only looks in that container.
Add a CanvasLayer named HUD under Main. Add two Label children named ScoreLabel and HintLabel. Move them to the positions in the figure and give them temporary Text values such as Score: 0 and Move with WASD. Set a readable font size, such as 18, using Theme Overrides → Font Sizes. Main will replace their text when it runs.
5. Connect score, completion, and restart
Attach main.gd to Main and paste the file below. Main’s children become ready before Main._ready, so Player has joined its group and the coin instances exist when we connect them. There is no need to create a global coins group: the Coins container gives this level a local list.
extends Node2D
var score: int = 0
var remaining: int = 0
@onready var score_label: Label = $HUD/ScoreLabel
@onready var hint_label: Label = $HUD/HintLabel
func _ready() -> void:
for child in $Coins.get_children():
var coin := child as CoinPickup
if coin:
remaining += 1
coin.collected.connect(_on_coin_collected)
score_label.text = "Score: 0"
hint_label.text = "Move with WASD or arrows. R restarts."
if remaining == 0:
hint_label.text = "Add Coin.tscn instances under Coins."
func _on_coin_collected(value: int) -> void:
score += value
remaining -= 1
score_label.text = "Score: %d" % score
if remaining == 0:
hint_label.text = "All coins collected! Press R to play again."
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("restart"):
get_tree().reload_current_scene()CoinPickup is the class declared in coin.gd. The cast skips unrelated children of Coins. The score counts point values while remaining counts coin instances: changing one coin’s Value to 25 must not make the win condition trigger early. Restart reloads the scene, creating fresh coins and a fresh Main with score zero.
If your result is different
- Only the player appears: you probably ran Player.tscn. Open Main and use Run Current Scene, or set Main as the project’s main scene.
- The player crosses a coin without collecting it: check both assigned Shape resources, Coin’s Monitoring, and whether its mask includes Player’s layer 1. Turn on Debug → Visible Collision Shapes to inspect the boundaries.
- The coin disappears but the score stays zero: confirm it is a Coin.tscn instance directly under Coins and that main.gd is attached to Main. A listener connection is missing or its setup failed.
- A node path error mentions ScoreLabel: compare HUD/ScoreLabel with the exact names and nesting in Main. The path is relative to the node holding main.gd.
- CoinPickup is unknown: save coin.gd with class_name CoinPickup at the top and resolve its first parser error before editing Main.
- Movement is very slow: remove any extra multiplication by delta from velocity. Compare against the complete player.gd above.
Make one change and prove you understand it
Ready to change the rules yourself? Try adding a score target to this game. The exercise gives you a brief, hints, and checks for both winning and running out of points.
Change the Value of one coin instance to 25. With the other two still worth 10, predict a final score of 45, then run the scene and check it. Next duplicate a fourth coin under Coins: the completion message should wait for four pickups without changing Main’s code. These checks exercise the difference between a point total and a count of objects.
For the architecture behind these files, read how the player’s nodes work together. For more event-flow examples, continue with the signals tutorial. If a setup step is still unfamiliar, return to Build a player and test that part on its own.
Frequently asked questions
- Do I need art assets or plugins for this first Godot game?
- No. The project icon can supply both images. The tutorial uses built-in Godot 4 nodes and GDScript, with no plugins or asset downloads.
- Why does the coin disappear without adding points?
- The coin can emit and free itself even with no listener. Check that Main has its script, that the coin is a CoinPickup instance under Coins, and that the connection is made before collection.
- Why does this character not fall or jump?
- This is a top-down game. CharacterBody2D does not add gravity automatically, and this movement script intentionally has no gravity or jump code.
- What should happen when the game is finished?
- Three default 10-point coins produce Score: 30 and an All coins collected message. Pressing R reloads Main, returns the coins, and resets the score.
Sources and revision context
Updated September 8, 2026. Selected checks passed in Godot 4.7.2, standard macOS build, headless, on September 8, 2026. Checked initial score and coin count, movement, stopping, pickup overlaps, and completion text. Restart was not tested. These checks used the article scripts with supplied scene fixtures; they are not a full tutorial playtest. Added a link to the score-target follow-up exercise; the scripts are unchanged.