2025/10/21

- Type
- Learning Resource
- Format
- Study Guide
- Version
- Godot 4.x
- Subject Tags
- Downloadable Demo
- FAQ/Troubleshooting
- Created
- Updated
- 2025/10/07
- 2025/10/21
Split-screen co-op games let multiple players enjoy a game together on the same screen, each with their own view of the game world.
In split-screen games, we use multiple cameras to render different views of the same game world. Each camera follows a player and renders its view to a separate viewport. These viewports are then arranged on the screen to create the split-screen effect.
In this study guide, you'll learn:
This section is a quick reference for the video above, which was made with Godot 3. Here's what changed in Godot 4:
Camera2D.make_current()
from code. The same applies to . Camera3DAlso, the ViewportContainer
node was renamed to . SubViewportContainer
For this demo specifically, we also made these two improvements:
The rest of this guide uses Godot 4 syntax and conventions.
Setting up split-screen co-op in Godot is surprisingly simple. You add a for each player, and inside each container, you place a SubViewportContainer and a SubViewport. Camera2D
Then you attach the cameras to the player nodes, set up input mappings for each player, and make sure the game world is shared between the viewports. We'll go over these steps in the next sections.
In this demo, we split the screen evenly using a . Each player gets a HBoxContainer that fills half the screen. SubViewportContainer
We also added a in between as a visual divider. ColorRect
Notice how we added the Level2D scene as a child of one . In the 2D editor, you can see the game world rendered in one of the viewports only. SubViewport
To make the two viewports share the same game world, we assign the LeftSubViewport's world_2d
property to the RightSubViewport's world_2d
property from code.
Finally, we assign a to each player by making them children of their respective Camera2D nodes. SubViewport
You'll see how we share the world between the nodes and attach the cameras to the players in the code section coming up. SubViewport
First, you need to define the input map for each player. You can use any input scheme or controllers you prefer, but in this demo, we went with the following:
Next, we need to connect these input actions to each player node. To do this, we create a custom Resource
class called PlayerControls
. This class stores the input action names for a player:
class_name PlayerControls extends Resource
@export var player_index := 0
@export var move_left := "p1_move_left"
@export var move_right := "p1_move_right"
@export var jump := "p1_jump"
Now we create the actual PlayerControls
resources. We make two of them, one for each player, and save them as res://character/player_1_controls.tres and res://character/player_2_controls.tres.
The default values are for player 1
. For player 2
, we change the player_index
to 1
and update the input action names in the Inspector.
Let's put everything together with some code in the main scene's script.
We start by defining an array of dictionaries to hold references to each player's , SubViewport, and player node: Camera2D
extends Control
# We need to:
# 1. Share the world of the first viewport with the second viewport.
# 2. Create a remote transform attached to each player that pushes their position to the camera.
# This data structure helps us to do that conveniently. See the _ready() function below.
@onready var players: Array[Dictionary] = [
{
sub_viewport = %LeftSubViewport,
camera = %LeftCamera2D,
player = %Level2D/Player2D1,
},
{
sub_viewport = %RightSubViewport,
camera = %RightCamera2D,
player = %Level2D/Player2D2,
},
]
This makes our _ready()
function clean and straightforward. We loop through each player's info to set up the shared world and attach the cameras to their players using remote transforms:
func _ready() -> void:
# The `world_2d` object of the Viewport class contains information about
# what to render. Here, it's our game level. We need to pass it
# from the first to the second SubViewport for both of them to render
# the same level.
players[1].sub_viewport.world_2d = players[0].sub_viewport.world_2d
# For each player, we create a remote transform that pushes the character's
# position to the corresponding camera.
for info in players:
var remote_transform := RemoteTransform2D.new()
remote_transform.remote_path = info.camera.get_path()
info.player.add_child(remote_transform)
Now let's see how we use our custom PlayerControls
resource.
These are the relevant parts of the res://character/player_2d.gd script:
extends CharacterBody2D
extends CharacterBody2D
# ...
# ...
@export var controls: PlayerControls = null
@export var controls: PlayerControls = null
func _ready() -> void:
# Disable the player if we don't assign a `PlayerControls` resource to
# the `controls` variable to prevent a crash.
if controls == null:
set_physics_process(false)
func _physics_process(delta: float) -> void:
var horizontal_direction := Input.get_axis(
controls.move_left, controls.move_right
)
velocity.x = horizontal_direction * speed
velocity.y += gravity * delta
var is_jumping := Input.is_action_just_pressed(controls.jump)
var is_jump_cancelled := (
Input.is_action_just_released(controls.jump) and velocity.y < 0.0
)
# ...
By using the controls
variable instead of hard-coding input action names, our player script can work with any player's control scheme.
Make sure you enabled the 's SubViewportContainerStretch
We could have added the nodes directly in the editor, but since we created the level as a separate scene, it's cleaner to add them through code. RemoteTransform2D
This prevents us from expanding the Level2D node by using the Editable Children feature in the Scene dock as in the following image:
As a general rule, I recommend against using Editable Children. It can cause you to lose changes on the expanded children if you update the parent scene later on.
Import the zip file in Godot 4.5 or later to explore the code and see how everything works together.
Don't stop here. Step-by-step tutorials are fun but they only take you so far.
Try one of our proven study programs to become an independent Gamedev truly capable of realizing the games you’ve always wanted to make.
Get help from peers and pros on GDQuest's Discord server!
20,000 membersJoin ServerThere are multiple ways you can join our effort to create free and open source gamedev resources that are accessible to everyone!
Sponsor this library by learning gamedev with us onGDSchool
Learn MoreImprove and build on assets or suggest edits onGithub
Contributeshare this page and talk about GDQUest onRedditYoutubeTwitter…