Godot 3.0: Splitscreen Demo (Using Viewports)

Tags: godot gamedev tutorial

Introduction

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.

You can watch a video version of this tutorial here:

Game setup

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:

splitscreen_start.zip

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.

Viewports, Cameras, and Worlds

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.

By themselves, Viewport nodes don’t have position information (they don’t inherit from Spatial or CanvasItem). We’re going to use ViewportContainer, a Control node, to hold each viewport. To keep them arranged side-by-side, we’ll use an HBoxContainer.

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 2 and 1 (to 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 container.

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

Adding the World

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

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

Setting up the cameras

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 Main script:

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!

Note: I find it looks best if you disable the Drag Margin properties of the cameras.

Camera limits

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 _ready():

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

Minimap

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 (340, 200) 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.

In the _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.

Conclusion

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.

This demo is only a small example of what’s possible with Viewports. More demos and examples are coming soon.

Please comment below with your questions and suggestions.

Download the code for this tutorial

Comments