How to Build a Co-op Roguelite Prototype in Godot 4 - Weekend Tutorial
Roguelites and co-op are two of the most requested combinations in indie game design. Godot 4's built-in multiplayer and scene system make it possible to get a playable co-op roguelite prototype running in a single weekend. This tutorial walks you through a minimal but complete setup: two players, procedural rooms, shared runs, and the kind of loop that feels good to iterate on.
By the end you'll have a prototype you can expand with more items, enemies, and meta-progression. No prior Godot multiplayer experience required.
What You Will Build
- A 2D top-down game with two playable characters
- Host-and-join multiplayer (one player hosts, the other joins)
- Procedurally generated room-based levels
- Shared run state (one life pool or shared progress)
- Basic combat and pickups so the loop is playable
Prerequisites
- Godot 4.2 or 4.3 installed (download from godotengine.org)
- Basic familiarity with GDScript and the Godot editor
- Two machines or two instances (one host, one client) for testing
If you're new to Godot, our Game Development with Python - Pygame vs Godot comparison gives context on why Godot fits rapid prototyping.
Project Setup and Scene Structure
Step 1: New project
- Create a new Godot 4 project (Renderer: Compatibility or Forward+).
- Use a 2D project template or start from an empty scene.
Step 2: Folder structure
Create folders such as: scenes/, scripts/, resources/, autoload/. Keep player, enemies, and level generation in separate scenes so multiplayer and generation stay clean.
Step 3: Main scene
Your main scene should own the game flow and multiplayer setup. A simple hierarchy:
- Main (Node2D or Node)
- MultiplayerSpawner (or a node that spawns players)
- World (Node2D) – holds the current level
- UI (CanvasLayer) – health, score, lobby
We'll add a script to Main that starts the server or client and spawns players into World.
Enabling Godot 4 Multiplayer
Godot 4 uses the high-level multiplayer API (multiplayer peer). You typically use ENet or WebSocket; ENet is best for desktop co-op.
Step 1: Create a simple lobby
Add a script to your main scene (or a dedicated Lobby scene). Store the peer id of the host and use multiplayer.peer_connected / peer_disconnected to know when the second player joins.
Step 2: Start as host or client
# Example: Main.gd (simplified)
extends Node
@export var port = 9090
var peer: ENetMultiplayerPeer
func _ready():
if "--server" in OS.get_cmdline_args():
host_game()
else:
join_game()
func host_game():
peer = ENetMultiplayerPeer.new()
peer.create_server(port)
multiplayer.multiplayer_peer = peer
multiplayer.peer_connected.connect(_on_peer_connected)
func join_game():
peer = ENetMultiplayerPeer.new()
peer.create_client("127.0.0.1", port)
multiplayer.multiplayer_peer = peer
Step 3: Spawn players only on the server
Use a MultiplayerSpawner or spawn via RPC so that only the server (or host) creates player nodes. Each peer can then control their own character using multiplayer.get_unique_id() and is_multiplayer_authority().
Pro tip: Use a single Player scene with a script that checks is_multiplayer_authority() before processing input. That way the same scene works for both players; authority determines who drives it.
Player Scene and Input Authority
Step 1: Player scene
Create a CharacterBody2D with a CollisionShape2D and Sprite2D (or AnimatedSprite2D). Attach a script that:
- Moves the body with
move_and_slide(). - Reads input only when
is_multiplayer_authority()is true. - Optionally syncs position with
@rpc("any_peer", "call_local")or by making the node replicated (so the engine syncs it).
Step 2: Authority and input
# Player.gd (simplified)
extends CharacterBody2D
@export var speed = 200.0
func _physics_process(_delta):
if not is_multiplayer_authority():
return
var dir = Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = dir * speed
move_and_slide()
When the host spawns two Player instances, assign one to the host's unique id and one to the client's. Then each peer has authority over their own character and the other peer's character is just replicated on their screen.
Procedural Room-Based Level
A weekend-sized approach: pre-made room scenes and a simple grid.
Step 1: Room scenes
Create 3–5 scenes (e.g. Room_01.tscn, Room_02.tscn) as Node2D with:
- Static body or tilemap for walls
- Optional spawn points (Marker2D) for player, enemies, pickups
Step 2: Level generator (server-only)
On the server, build a grid (e.g. 3x3 or 4x4). For each cell, pick a random room scene and instantiate it. Place doors or openings so adjacent rooms connect. Store the layout in a variable so all clients get the same sequence (or generate from a shared seed and run generation only on the server, then sync the result).
Step 3: Spawn players and objects in rooms
When the level is ready, spawn both players in the starting room (e.g. center cell). Spawn enemies and pickups from the room's spawn points. Only the server should instantiate these; Godot will replicate the nodes to the client.
This gives you a different layout each run while keeping the scope small. For more depth, see our How to Build a Roguelike Game from Scratch - Complete Unity Tutorial for procedural and design ideas you can port to Godot.
Shared Run State (Roguelite Feel)
To feel like a co-op roguelite, both players should share something: one life pool, shared gold, or a shared "run" object.
Option A: Shared health
One script (e.g. RunManager, autoload or child of Main) holds current_health and max_health. Only the server modifies it. When any player takes damage, the server subtracts from shared health and updates. If it hits zero, the run ends for both.
Option B: Shared score and run end
Track a single score and a "run active" flag. When both players die (or one dies and you use "last stand" rules), the server sets run active to false and can trigger a summary screen. That way you get a clear win/lose moment for the pair.
Sync this state with @rpc or by making the RunManager a replicated node so both peers see the same values.
Combat and Pickups (Minimal but Playable)
- Enemies: Use Area2D or CharacterBody2D with a small script that only runs on the server. On body entered (player), apply damage to the shared run state and optionally push the player back. Use signals or direct calls to the RunManager.
- Pickups: When a player overlaps a pickup Area2D, only the server handles the overlap: add health or score to the shared state, then queue_free() the pickup. Replication removes it on the client too.
This keeps the loop clear: move, fight, collect, advance rooms, and eventually win or lose together.
Polish and Testing in One Weekend
- Input: Use the Input Map for "move_left", "move_right", "move_up", "move_down" and optionally "attack" / "interact". Same map for both players; authority ensures only the right character moves.
- Camera: A simple approach is one camera that follows the midpoint of both players or the host's player. You can switch to split-screen or a shared view later.
- Testing: Run two instances (e.g. one with
--server, one without) or use two machines on the same LAN. Fix the most obvious sync bugs first (spawn positions, who has authority).
Common mistake: Forgetting that only the server should spawn and remove nodes that affect gameplay. If the client spawns an enemy, the server won't know and you get desyncs.
Where to Go Next
Once the prototype runs smoothly:
- Add more room templates and a seed so you can share "runs" (same seed = same layout).
- Introduce simple items (damage up, speed up) and an inventory or stat block shared between players.
- Add a meta layer: unlock new room types or characters between runs.
For more on game feel and systems, check out Game Level Design - Principles and Best Practices and Building Game AI - Pathfinding, Decision Trees, and Behavior Systems when you're ready to deepen AI and level design.
FAQ
Do both players need the same Godot project?
Yes. Both run the same build; one acts as host (server), the other as client.
Can I use Godot 4's built-in replication without writing RPCs?
Yes. Mark nodes as replicated and change only their state from the server (or from the authority peer). The engine syncs properties. Use RPC when you need one-off actions (e.g. "request respawn").
How do I make the run deterministic for replays or shared seeds?
Run all procedural generation and game logic on the server, and use a fixed seed for the random number generator. Clients only display what the server sends.
Is this suitable for more than two players?
The same pattern scales: one host, multiple clients, one shared run state. You'll need a lobby to choose roles and possibly more room content so the game doesn't feel empty.
Where can I learn more about Godot networking?
The official Godot multiplayer documentation covers peers, RPCs, and replication in depth.
You now have a co-op roguelite prototype in Godot 4 that you can build on over the next few weekends. Focus on one shared run state, server-authoritative spawning, and clear input authority so the rest of your design (items, rooms, enemies) stays easy to add. Found this useful? Share it with your team or bookmark it for your next game jam.