Voltar para o Blog
Quest Log

Godot: Multiplayer Local com Tela Dividida (Split Screen)

Tela dividida com dois jogadores em multiplayer local no Godot

Multiplayer local com tela dividida no Godot 4: monte um split screen para 2 jogadores no mesmo aparelho com SubViewport, Camera2D e controles separados.

Godot: Multiplayer Local com Tela Dividida (Split Screen)

Montar multiplayer local com tela dividida no Godot e um daqueles recursos que parece complicado e nao e. Voce nao precisa de rede, servidor, nem da High-Level Multiplayer API. Split screen e puro problema de renderizacao e de input: dois jogadores no mesmo aparelho, cada um com sua camera e seus controles, olhando para o mesmo mundo por metades da tela. Neste tutorial eu monto isso do zero no Godot 4, com os nodes certos, codigo tipado, e os cuidados que ninguem conta antes de a performance cair.

Multiplayer local com tela dividida (split screen) nao e multiplayer online

Vale separar os dois porque a confusao custa dias de trabalho. Multiplayer online significa maquinas diferentes conversando por rede. Voce lida com latencia, sincronizacao de estado, autoridade de node e RPC. E o territorio da High-Level Multiplayer API, e a gente cobre isso em multiplayer online no Godot como proximo passo.

Multiplayer local (couch co-op) e outra coisa. Tudo roda no mesmo processo, na mesma maquina. Nao existe pacote, nao existe ping, nao existe servidor. O jogo ja sabe o estado dos dois jogadores porque os dois vivem na mesma cena. O unico desafio e mostrar dois pontos de vista ao mesmo tempo e ler dois conjuntos de controle sem que um atrapalhe o outro. Por isso split screen e um bom primeiro passo antes de encarar netcode: voce treina game feel de co-op sem a dor de rede.

A estrutura de nodes do split screen

O truque do Godot 4 e o SubViewport. Ele renderiza uma cena inteira dentro de uma textura, e o SubViewportContainer desenha essa textura na interface. Um viewport por jogador, cada um com sua camera, e voce tem duas visoes lado a lado.

A arvore fica assim:

SplitScreen (Control)
└── HBoxContainer
    ├── SubViewportContainer  (stretch = true)
    │   └── SubViewport
    │       └── Camera2D  (do jogador 1)
    └── SubViewportContainer  (stretch = true)
        └── SubViewport
            └── Camera2D  (do jogador 2)

O HBoxContainer coloca as duas metades lado a lado (divisao vertical, cada jogador ocupa metade da largura). Trocar por VBoxContainer empilha uma em cima da outra (divisao horizontal). Cada SubViewportContainer precisa ter o stretch ligado para o conteudo do viewport preencher o espaco, e ambos com size_flags_horizontal em SIZE_EXPAND_FILL para dividir a tela em partes iguais.

Aqui esta o ponto que derruba a maioria: por padrao cada SubViewport cria o proprio World2D. Se voce deixar assim, os dois viewports renderizam mundos vazios e separados, nao o seu jogo. Os jogadores precisam existir no mesmo mundo, entao os viewports tem que compartilhar um unico World2D.

Compartilhando o mesmo mundo entre os dois viewports

A ideia e ter uma cena de mundo (o nivel, os players, os inimigos) e faze-la aparecer nos dois viewports. Voce instancia o mundo dentro do primeiro SubViewport e manda o segundo usar o mesmo World2D do primeiro.

extends Control

@export var cena_mundo: PackedScene

@onready var viewport_p1: SubViewport = $HBoxContainer/ViewportP1/SubViewport
@onready var viewport_p2: SubViewport = $HBoxContainer/ViewportP2/SubViewport

func _ready() -> void:
    var mundo: Node2D = cena_mundo.instantiate()
    viewport_p1.add_child(mundo)
    viewport_p2.world_2d = viewport_p1.world_2d

A primeira parte instancia o mundo e o coloca no viewport do jogador 1. A linha que resolve tudo e a ultima: viewport_p2.world_2d = viewport_p1.world_2d. Ela faz o segundo viewport apontar para o mesmo World2D, entao ambos enxergam os mesmos nodes fisicos, os mesmos sprites, a mesma simulacao. Voce instancia o mundo uma vez so; a segunda tela e apenas outra janela para ele.

Para 3D o padrao e identico, so troca os tipos: Camera3D em vez de Camera2D e world_3d em vez de world_2d. Toda a logica de compartilhamento vale igual.

Agora cada camera precisa achar seu jogador e seguir so ele. Como os dois players estao no mesmo mundo, uso grupos para localiza-los e reparento cada Camera2D para o jogador correto.

func _configurar_cameras() -> void:
    var players: Array[Node] = get_tree().get_nodes_in_group("players")
    if players.size() < 2:
        push_warning("Split screen espera 2 players no grupo 'players'.")
        return
    var cam_p1: Camera2D = viewport_p1.get_node("Camera2D")
    var cam_p2: Camera2D = viewport_p2.get_node("Camera2D")
    cam_p1.reparent(players[0])
    cam_p1.position = Vector2.ZERO
    cam_p1.make_current()
    cam_p2.reparent(players[1])
    cam_p2.position = Vector2.ZERO
    cam_p2.make_current()

O reparent move a camera para dentro do node do jogador sem tira-la do mundo compartilhado, entao ela passa a acompanhar aquele player automaticamente. O position = Vector2.ZERO centraliza a camera no jogador. O make_current() e obrigatorio: como cada camera esta em seu proprio SubViewport, cada uma vira a camera ativa do seu viewport sem competir com a outra. Chame _configurar_cameras() no fim do _ready(), depois que o mundo ja foi instanciado.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

Controles separados por jogador via Input Map

Renderizar duas telas nao adianta se os dois jogadores movem o mesmo boneco. A regra de ouro do multiplayer local e: cada jogador tem seu proprio conjunto de acoes. Nao existe acao generica move_left compartilhada; existe p1_left e p2_left, mapeadas para teclas ou botoes diferentes.

No Input Map voce cria as acoes com prefixo por jogador. Por exemplo, p1_left, p1_right, p1_jump para o teclado (A, D, W ou espaco) e p2_left, p2_right, p2_jump para outro trecho do teclado ou para um segundo joystick. Se voce nunca configurou acoes no editor, vale ler antes o guia sobre configurar o Input Map e controles no Godot, porque o resto deste tutorial assume que essas acoes existem.

Com as acoes prontas, o segredo e nao repetir codigo de player. Escreva um script de jogador que recebe seu prefixo e monta os nomes das acoes a partir dele.

extends CharacterBody2D

@export var id_jogador: int = 1
@export var velocidade: float = 220.0
@export var forca_pulo: float = 400.0

var prefixo: String = ""
const GRAVIDADE: float = 980.0

func _ready() -> void:
    prefixo = "p%d_" % id_jogador
    add_to_group("players")

func _physics_process(delta: float) -> void:
    var direcao: float = Input.get_axis(prefixo + "left", prefixo + "right")
    velocity.x = direcao * velocidade
    if not is_on_floor():
        velocity.y += GRAVIDADE * delta
    if Input.is_action_just_pressed(prefixo + "jump") and is_on_floor():
        velocity.y = -forca_pulo
    move_and_slide()

Cada instancia do player recebe um id_jogador diferente no inspetor (1 e 2). No _ready() isso vira o prefixo "p1_" ou "p2_", e todo Input.get_axis e Input.is_action_just_pressed monta o nome da acao concatenando o prefixo. O player 1 le p1_left/p1_right, o player 2 le p2_left/p2_right, e os dois nunca se cruzam. Um script, dois jogadores, controles isolados.

Lendo o device de cada joystick

Teclado dividido funciona, mas couch co-op de verdade quer um controle na mao de cada um. No Godot, todo evento de joystick carrega o device, o indice do controle conectado. Voce associa cada device a um jogador e filtra o input por ele.

extends Node

var device_por_jogador: Dictionary = {}

func atribuir_controle(id_jogador: int, device: int) -> void:
    device_por_jogador[id_jogador] = device

func _unhandled_input(evento: InputEvent) -> void:
    if evento is InputEventJoypadButton or evento is InputEventJoypadMotion:
        var device: int = evento.device
        for id_jogador: int in device_por_jogador:
            if device_por_jogador[id_jogador] == device:
                pass  # roteie o evento para o player desse id_jogador

O evento.device diz de qual controle veio o input. Com um dicionario ligando id_jogador ao device, voce sabe a quem entregar cada evento. Na pratica voce ainda precisa mapear os botoes do joystick nas acoes p1_ e p2_ no Input Map, mas o filtro por device e o que impede o controle do jogador 2 de mexer no jogador 1.

Ajustando a divisao e a responsividade

A divisao vertical (lado a lado) que montamos com HBoxContainer costuma ser a escolha padrao porque preserva altura, boa para plataforma e arena. Divisao horizontal, com VBoxContainer, preserva largura e combina com corrida ou jogos de movimento lateral longo. Nao trate isso como decisao fixa; deixe ajustavel.

Para a tela dividida acompanhar a janela quando o jogador redimensiona, ancore o Control raiz no retangulo cheio e deixe os SubViewportContainer com SIZE_EXPAND_FILL. Ainda assim, o SubViewport nao redimensiona sozinho junto do container: force o tamanho quando a janela mudar.

func _atualizar_tamanho_viewports() -> void:
    var meia_largura: Vector2i = Vector2i(int(size.x / 2.0), int(size.y))
    viewport_p1.size = meia_largura
    viewport_p2.size = meia_largura

func _ready() -> void:
    get_viewport().size_changed.connect(_atualizar_tamanho_viewports)
    _atualizar_tamanho_viewports()

Aqui cada SubViewport recebe metade da largura e a altura cheia, e o metodo reroda sempre que a janela muda de tamanho pelo sinal size_changed. Sem isso, a textura do viewport pode ficar esticada ou serrilhada ao redimensionar. Para divisao horizontal, inverta a conta: largura cheia e metade da altura.

Cuidados com performance e UI

Dois viewports significam desenhar a cena praticamente duas vezes. Em 2D isso raramente e problema, mas o custo cresce com geometria, luzes 2D e shaders pesados, e some rapido em 3D. Meca em hardware fraco, nao so na sua maquina. Se o framerate cair, o primeiro corte e simplificar efeitos por viewport: uma tela pode rodar com menos particulas ou sem certos pos-processamentos sem que o jogador note na correria do co-op.

Um cuidado que passa batido: fisica e logica rodam uma vez so, no mundo compartilhado, nao por viewport. Isso e bom, e barato. O que dobra e a renderizacao. Entao otimizar split screen e quase sempre otimizar o que a GPU desenha, nao o que a CPU calcula.

Por fim, a UI. Cada jogador quer sua propria HUD (vida, pontos, municao) presa a metade dele. Coloque um CanvasLayer com a HUD dentro de cada SubViewport. Assim a interface do jogador 1 fica confinada ao viewport 1 e nao invade a tela do jogador 2. HUD posta fora dos viewports, no Control raiz, apareceria por cima das duas metades, o que raramente e o que voce quer em co-op.

Proximos passos

Com SubViewportContainer mais SubViewport compartilhando um World2D, uma Camera2D por jogador e acoes p1_/p2_ no Input Map, voce tem couch co-op de verdade rodando em uma maquina so. O caminho natural a partir daqui e um destes dois. Se o objetivo e polir os controles e o game feel local, aprofunde em configurar o Input Map e controles no Godot. Se o proximo salto e levar o co-op para a internet, entao e hora de estudar multiplayer online no Godot como proximo passo, onde entram rede, sincronizacao e a High-Level Multiplayer API. E se voce ainda esta montando a base para chegar confiante ate aqui, comece pela trilha completa de Godot para iniciante e volte para o split screen com o alicerce firme.

Perguntas frequentes

Qual a diferenca entre multiplayer local e online no Godot?

Multiplayer local roda tudo na mesma maquina, sem rede, com dois jogadores compartilhando o aparelho e a tela dividida. Online usa a High-Level Multiplayer API para sincronizar estado entre maquinas diferentes pela rede. Split screen nao precisa de RPC nem servidor.

Preciso de dois World2D para tela dividida no Godot?

Nao. Para os dois jogadores existirem no mesmo mundo, os SubViewports precisam compartilhar o mesmo World2D. Voce cria o mundo em um viewport e passa a referencia com world_2d para o outro. Dois World2D separados renderizariam duas cenas independentes.

Como leio dois controles diferentes no mesmo teclado ou joystick?

Crie acoes separadas no Input Map, como p1_left e p2_left, e mapeie cada uma para teclas ou botoes distintos. Para joysticks, o InputEventJoypadMotion carrega o device, entao voce filtra por indice do controle e associa cada device a um jogador.

Tela dividida no Godot custa muito de performance?

Renderizar dois viewports aproxima o custo de desenhar a cena duas vezes. Em 2D isso costuma ser tranquilo, mas some geometria, luzes e shaders. Vigie o custo em hardware fraco e simplifique efeitos por viewport se o framerate cair.

Dividir a tela na horizontal ou na vertical, qual escolher?

Depende do jogo. Divisao vertical (lado a lado) da mais altura util e combina com plataformas e arenas. Divisao horizontal (uma tela em cima da outra) preserva a largura, boa para corrida e jogos com movimento lateral longo. Deixe isso ajustavel.

Cada jogador pode ter sua propria HUD na tela dividida?

Sim. Coloque a HUD como CanvasLayer dentro de cada SubViewport. Assim vida, pontos e municao de cada jogador ficam presos ao viewport dele e nao vazam para a outra metade da tela.