In this demo, we’ll consider a local multiplayer game - a topdown-style maze game with two players (one using arrow keys and the other using WASD controls). This is not a problem if our game world all fits on one screen, but if the map is large, we’ll want to have a “split screen” view tracking the two players separately.
We’ll also look at a quick way to set up a minimap display.
We won’t spend a lot of time on the setup of the game world. The two players are KinematicBody2D objects using no-frills 8-way movement.
If you need help setting up this part, see the following section in the official Godot docs: 2D Movement Overview.
Each player has its input actions set up in the Project Settings -> Input Map section: “right_1” to Right Arrow, “right_2” to D, etc. Note that by naming them this way, we can save time in the code by using:
export var id = 0 func get_input(): velocity = Vector2() if Input.is_action_pressed('right_%s' % id): velocity.x += 1 # etc.
This way both characters can use the same script for movement. Just assign the
appropriate value to
id for each player.
The two players are added to a “World” scene containing a TileMap:
If you like, you can download the starting project, with the world already set up, here:
Note that the map is much larger than the game screen, but aside from that everything works as intended. Setting up your game “world” separately like this will make setting up the viewports much easier and more flexible.
We’re going to start with a new scene that’s going to contain our two viewports.
Create a node to serve as the root. I like to use
Node since it has no properties
of its own - it’s just there to contain the rest of the scene.
Viewport nodes don’t have position information (they don’t
CanvasItem). We’re going to use
Control node, to hold each viewport. To keep them arranged side-by-side, we’ll
Set the HBoxContainer’s Alignment to “Center” and to have a small
gap between the two viewports, set Custom Constants/Separation to
5. In the
“Layout” menu, choose “Full Rect”.
Now add two ViewportContainers as children, naming them with a
match the player they’ll display). Set the Size Flags on both to “Fill, Expand”
so that they will each expand to fill half of the screen. Also, check the Stretch
property so that the Viewport will automatically be set to the size of the
Inside each of these containers add a
Viewport. Note that if you set the viewport’s
Size property, it will be reset by the container.
In order for a Viewport to display anything, we’ll need a
Camera2D which will render
onto the Viewport. Add one to each viewport. Don’t forget to check the Current
property to activate the camera. We can also set each camera’s Zoom to
to get a better view of the area around the player.
Your node setup should look like this:
┖╴Main (Node) ┖╴Viewports (HBoxContainer) ┠╴ViewportContainer2 ┃ ┖╴Viewport2 ┃ ┖╴Camera2D ┖╴ViewportContainer1 ┖╴Viewport1 ┖╴Camera2D
Note that we’ve put
ViewportContainer1 second in the HBoxContainer. This will
place it on the right side since Player 1 uses the arrow keys.
When we run the scene we won’t see anything because the viewports don’t have any
“world” to render. A viewport’s
world (for 3D) or
world_2d property represent
the source for the viewport’s environment and determine what will be rendered by
its camera. The world can be set in code, but for 2D it will also display any
child 2D nodes we add to it.
Let’s instance the “World” scene as a child of
Viewport1. Now when we play the scene
we see the world inside the left viewport.
We also need to add a world to
Viewport2, but we want it to use the same one.
We can handle this in code. Attach a script to
Main and add the following:
extends Node onready var viewport1 = $Viewports/ViewportContainer1/Viewport1 onready var viewport2 = $Viewports/ViewportContainer2/Viewport2 onready var camera1 = $Viewports/ViewportContainer1/Viewport1/Camera2D onready var camera2 = $Viewports/ViewportContainer2/Viewport2/Camera2D onready var world = $Viewports/ViewportContainer1/Viewport1/World func _ready(): viewport2.world_2d = viewport1.world_2d
onready node references are for convenience - we’ll be using them as we move
forward. Remember that when you type “
$” Godot will autosuggest node paths so you
don’t have to type them. You can also drag a node from the scene tree into the
script editor and you’ll get the node’s path.
When we run the scene now, we see the world rendered in both viewports. However, neither camera is moving so we only see a small part of the world.
Attach the following script to each camera:
extends Camera2D var target = null func _physics_process(delta): if target: position = target.position
Now we can assign a target to each camera and it will follow that node’s position.
We’ll do that in the
func _ready(): viewport2.world_2d = viewport1.world_2d camera1.target = world.get_node("Player_1") camera2.target = world.get_node("Player_2")
When we run the scene now, each player is centered in its viewport and our splitscreen setup works!
I find it looks best if you disable the Drag Margin properties of the cameras.
Next, let’s add some limits to the player cameras so that they don’t scroll outside
the bounds of the map. Add this function to the main script and call it in
func set_camera_limits(): var map_limits = world.get_used_rect() var map_cellsize = world.cell_size for cam in [camera1, camera2]: cam.limit_left = map_limits.position.x * map_cellsize.x cam.limit_right = map_limits.end.x * map_cellsize.x cam.limit_top = map_limits.position.y * map_cellsize.y cam.limit_bottom = map_limits.end.y * map_cellsize.y
Let’s add one more fun feature: a minimap showing a zoomed-out view of the entire map so the players can orient themselves.
We’ll need another ViewportContainer, this one a child of
Main. This time, we
don’t want to use Stretch. Add a Viewport and set its Size to
then add a Camera2D. We’ll set the Camera2D’s Position to
(512, 300) to center
it on the screen. We’ll zoom out by setting Zoom to
(9, 9). Don’t forget to
click Current on this camera as well.
_ready(), set the minimap to use the same world as the other two viewports:
$Minimap/Viewport.world_2d = viewport1.world_2d
Use the “Layout” menu to align the Minimap container at “Center Bottom”. Let’s see what it looks like:
We need to get rid of that grey area around the edges. We could find the precise zoom level that matches our desired minimap size, but instead, we’ll check the Transparent Bg on the Viewport. Now our non-map areas aren’t visible and the minimap appears floating directly on top of the main viewports.
Viewports can be very powerful, but also confusing. One way of managing them is to try to keep them separate from the game logic and only use them as displays.