Multiplayer no Godot: Tutorial Completo de Networking Para Iniciantes

Diagrama de arquitetura cliente-servidor no Godot Engine

Aprenda a criar jogos multiplayer no Godot Engine. Tutorial passo a passo sobre networking, sincronização, High-Level Multiplayer API e implementação prática.

Multiplayer no Godot: Tutorial Completo de Networking Para Iniciantes

Multiplayer transforma jogos. A experiência de competir ou colaborar com outros humanos adiciona imprevisibilidade, emoção e longevity que single-player raramente alcança. Mas implementar networking é notoriamente complexo - sincronização, latência, segurança e bugs sutis aguardam desenvolvedores inexperientes.

No Godot Engine, a High-Level Multiplayer API torna networking significativamente mais acessível. Com sistemas de RPC (Remote Procedure Calls), sincronização automática e abstrações inteligentes, você pode criar experiências multiplayer funcionais sem se perder em baixo nível de sockets e protocolos.

Neste tutorial completo, vou te guiar desde conceitos fundamentais de networking até implementação prática de jogo multiplayer no Godot. Você aprenderá arquitetura cliente-servidor, sincronização de estado, RPCs, autoridade de nodes, prediction, interpolation e muito mais. Ao final, você terá conhecimento sólido para criar seus próprios jogos multiplayer.

Fundamentos de Networking em Jogos

Antes de código, precisamos entender conceitos fundamentais.

Cliente-Servidor vs Peer-to-Peer

Cliente-Servidor (Recomendado):

  • Um player (ou servidor dedicado) é autoridade
  • Clientes enviam input, servidor processa e retorna estado
  • Servidor previne cheating (valida tudo)
  • Mais complexo mas mais robusto

Peer-to-Peer (P2P):

  • Todos os players conectam entre si
  • Sem servidor central
  • Mais simples para jogos casuais
  • Vulnerável a cheating

Godot suporta ambos, mas cliente-servidor é padrão para jogos sérios.

Latência e Tick Rate

Latência (Ping):

  • Tempo para mensagem ir e voltar
  • <50ms: Excelente
  • 50-100ms: Bom
  • 100-150ms: Aceitável
  • 150ms+: Notável, precisa compensation

Tick Rate:

  • Frequência de updates do servidor
  • 20-30 ticks/segundo: Típico para casual
  • 64+ ticks/segundo: Competitivo (CS:GO usa 64/128)
  • Maior = mais responsivo mas mais bandwidth

Bandwidth:

  • Dados enviados/recebidos por segundo
  • Minimize enviando apenas o necessário
  • Compression ajuda

Sincronização de Estado

Problema: Cada cliente tem versão local do game state. Como mantê-los sincronizados?

Abordagens:

1. State Synchronization:

  • Servidor envia estado completo periodicamente
  • Simples mas bandwidth-intensive
  • Godot: Automatizado via MultiplayerSynchronizer

2. Input Synchronization:

  • Clientes enviam input, servidor simula
  • Menos bandwidth
  • Requer gameplay determinístico

3. Snapshot Interpolation:

  • Servidor envia snapshots, clientes interpolam entre eles
  • Smooth visual mas ligeiramente atrasado
  • Godot 4.x suporta nativamente

Godot combina todas três através da High-Level API.

Descubra Seu Perfil em Game Development

Multiplayer networking é área técnica e desafiadora. Você é o tipo de desenvolvedor que ama resolver problemas complexos de sistemas, ou prefere focar em design e criatividade? Descubra seu perfil ideal.

Fazer Teste Vocacional

Setup Básico: Criando Servidor e Cliente

Vamos começar com exemplo mínimo funcional.

Estrutura de Projeto

Multiplayer_Game/
├── scenes/
│   ├── Player.tscn
│   ├── World.tscn
│   └── UI.tscn
├── scripts/
│   ├── player.gd
│   ├── network.gd
│   └── world.gd
└── main.tscn

Network Manager (Singleton)

Crie

scripts/network.gd
e adicione como Autoload (Project Settings > Autoload):

extends Node

const PORT = 7777
const MAX_PLAYERS = 4

var peer = ENetMultiplayerPeer.new()

func create_server():
    peer.create_server(PORT, MAX_PLAYERS)
    multiplayer.multiplayer_peer = peer
    print("Servidor criado na porta ", PORT)

    # Callbacks
    multiplayer.peer_connected.connect(_on_player_connected)
    multiplayer.peer_disconnected.connect(_on_player_disconnected)

func join_server(address: String):
    peer.create_client(address, PORT)
    multiplayer.multiplayer_peer = peer
    print("Conectando a ", address)

    multiplayer.connected_to_server.connect(_on_connected_to_server)
    multiplayer.connection_failed.connect(_on_connection_failed)

func _on_player_connected(id: int):
    print("Player conectou: ", id)

func _on_player_disconnected(id: int):
    print("Player desconectou: ", id)

func _on_connected_to_server():
    print("Conectado ao servidor com sucesso!")

func _on_connection_failed():
    print("Falha ao conectar ao servidor")

UI Para Criar/Juntar

Crie UI simples com botões:

extends Control

@onready var host_button = $VBoxContainer/HostButton
@onready var join_button = $VBoxContainer/JoinButton
@onready var address_input = $VBoxContainer/AddressInput

func _ready():
    host_button.pressed.connect(_on_host_pressed)
    join_button.pressed.connect(_on_join_pressed)

func _on_host_pressed():
    Network.create_server()
    get_tree().change_scene_to_file("res://scenes/World.tscn")

func _on_join_pressed():
    var address = address_input.text if address_input.text != "" else "127.0.0.1"
    Network.join_server(address)
    get_tree().change_scene_to_file("res://scenes/World.tscn")

Testando:

  1. Run game, clique "Host" (cria servidor)
  2. Run game novamente, digite "127.0.0.1", clique "Join"
  3. Se conectou, você tem networking básico funcionando!

Sincronizando Players

Agora vamos criar players sincronizados entre clientes.

Player Scene

Crie

Player.tscn
:

Player (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
└── Camera2D

Player Script com Multiplayer Authority

extends CharacterBody2D

const SPEED = 300.0

# O ID deste player
@export var player_id: int = 1

func _enter_tree():
    # Define autoridade: apenas o dono controla este node
    set_multiplayer_authority(player_id)

func _physics_process(delta):
    # Apenas o dono processa input
    if not is_multiplayer_authority():
        return

    # Input local
    var direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
    velocity = direction * SPEED

    move_and_slide()

    # Sincroniza posição via RPC
    rpc("sync_position", position)

@rpc("unreliable")
func sync_position(pos: Vector2):
    # Clientes remotos recebem posição
    if not is_multiplayer_authority():
        position = pos

Spawning Players no Servidor

No

World.gd
:

extends Node2D

const PLAYER_SCENE = preload("res://scenes/Player.tscn")

@onready var players_node = $Players

func _ready():
    if multiplayer.is_server():
        multiplayer.peer_connected.connect(_on_player_connected)
        multiplayer.peer_disconnected.connect(_on_player_disconnected)

        # Spawn player do próprio servidor
        spawn_player(1)

func _on_player_connected(id: int):
    print("Spawnando player: ", id)
    spawn_player(id)

func _on_player_disconnected(id: int):
    print("Removendo player: ", id)
    if players_node.has_node(str(id)):
        players_node.get_node(str(id)).queue_free()

func spawn_player(id: int):
    var player = PLAYER_SCENE.instantiate()
    player.player_id = id
    player.name = str(id)
    players_node.add_child(player, true)

Problema: Clientes não vêem outros players spawning!

Spawning Replicado

Use MultiplayerSpawner:

# Em World.tscn, adicione:
MultiplayerSpawner
├── Spawn Path: Players (aponta para node Players)

# Configure no Inspector:
# - Auto Spawn List: Adicione Player.tscn
# - Spawn Function: _spawn_player

Atualização de

World.gd
:

extends Node2D

const PLAYER_SCENE = preload("res://scenes/Player.tscn")

@onready var players_node = $Players
@onready var spawner = $MultiplayerSpawner

func _ready():
    if multiplayer.is_server():
        multiplayer.peer_connected.connect(_on_player_connected)
        multiplayer.peer_disconnected.connect(_on_player_disconnected)
        spawn_player(1)

func _on_player_connected(id: int):
    spawn_player(id)

func _on_player_disconnected(id: int):
    if players_node.has_node(str(id)):
        players_node.get_node(str(id)).queue_free()

func spawn_player(id: int):
    var player = PLAYER_SCENE.instantiate()
    player.player_id = id
    player.name = str(id)

    # Add como child - MultiplayerSpawner sincroniza automaticamente!
    players_node.add_child(player, true)

Agora players aparecem para todos os clientes automaticamente!

RPCs: Remote Procedure Calls

RPCs permitem chamar funções remotamente em outros peers.

Tipos de RPC

@rpc("any_peer"): Qualquer peer pode chamar (default, perigoso!) @rpc("authority"): Apenas autoridade pode chamar (seguro) @rpc("call_local"): Executa localmente também @rpc("reliable"): Garantido chegar (TCP-like) @rpc("unreliable"): Pode perder pacote (UDP-like, mais rápido)

Exemplo: Chat System

extends Control

@onready var chat_log = $VBoxContainer/ChatLog
@onready var input_field = $VBoxContainer/HBoxContainer/InputField
@onready var send_button = $VBoxContainer/HBoxContainer/SendButton

func _ready():
    send_button.pressed.connect(_on_send_pressed)

func _on_send_pressed():
    var message = input_field.text
    if message.strip_edges() == "":
        return

    # Envia mensagem para servidor
    rpc_id(1, "receive_message", multiplayer.get_unique_id(), message)
    input_field.clear()

@rpc("any_peer", "call_local", "reliable")
func receive_message(sender_id: int, message: String):
    # Apenas servidor processa e broadcast
    if multiplayer.is_server():
        # Valida mensagem (anti-spam, profanity filter, etc.)
        var validated_message = validate_message(message)

        # Broadcast para todos
        rpc("display_message", sender_id, validated_message)

@rpc("authority", "reliable")
func display_message(sender_id: int, message: String):
    # Todos os clientes exibem
    chat_log.text += "\n[%d]: %s" % [sender_id, message]

func validate_message(message: String) -> String:
    # Truncate, remove caracteres perigosos, etc.
    return message.substr(0, 200)

Fluxo:

  1. Cliente chama
    rpc_id(1, "receive_message", ...)
    → envia para servidor
  2. Servidor valida em
    receive_message()
  3. Servidor chama
    rpc("display_message", ...)
    → broadcast para todos
  4. Todos exibem em
    display_message()

Segurança: Servidor sempre valida. Nunca confie em input de cliente!

MultiplayerSynchronizer: Sincronização Automática

Para propriedades que mudam frequentemente, use MultiplayerSynchronizer.

Setup

Em

Player.tscn
, adicione:

Player (CharacterBody2D)
├── ...
└── MultiplayerSynchronizer

No Inspector do MultiplayerSynchronizer:

  • Root Path: . (aponta para Player)
  • Replication Config: Crie novo SceneReplicationConfig

No SceneReplicationConfig, adicione propriedades:

  • position
    (Vector2)
  • velocity
    (Vector2)
  • rotation
    (float)

Configurações:

  • Spawn: Sincronizar quando spawna
  • Replication Interval: 0.1 (10 updates/segundo)

Player Script Simplificado

extends CharacterBody2D

const SPEED = 300.0

@export var player_id: int = 1

func _enter_tree():
    set_multiplayer_authority(player_id)

func _physics_process(delta):
    if not is_multiplayer_authority():
        return

    var direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
    velocity = direction * SPEED

    move_and_slide()

    # position e velocity são sincronizados automaticamente!
    # Não precisa de RPC manual

Vantagem: Zero código de networking no script do player. MultiplayerSynchronizer handled everything!

Interpolation Para Movimento Suave

Sincronização automática pode resultar em movimento "jittery" devido a latência.

Client-Side Interpolation

extends CharacterBody2D

const SPEED = 300.0
const INTERPOLATION_SPEED = 10.0

@export var player_id: int = 1

var target_position: Vector2
var target_velocity: Vector2

func _enter_tree():
    set_multiplayer_authority(player_id)

func _ready():
    if not is_multiplayer_authority():
        # Clientes remotos inicializam targets
        target_position = position
        target_velocity = velocity

func _physics_process(delta):
    if is_multiplayer_authority():
        # Owner: movimento normal
        var direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
        velocity = direction * SPEED
        move_and_slide()
    else:
        # Remote: interpola para target
        position = position.lerp(target_position, INTERPOLATION_SPEED * delta)
        velocity = velocity.lerp(target_velocity, INTERPOLATION_SPEED * delta)

# Callback quando MultiplayerSynchronizer atualiza
func _on_position_changed(new_pos: Vector2):
    if not is_multiplayer_authority():
        target_position = new_pos

func _on_velocity_changed(new_vel: Vector2):
    if not is_multiplayer_authority():
        target_velocity = new_vel

Configure callbacks no MultiplayerSynchronizer para chamar

_on_position_changed()
quando propriedade sincroniza.

Lidando com Latência: Prediction e Reconciliation

Para jogos responsivos, clientes precisam prever movimentos.

Client-Side Prediction

extends CharacterBody2D

const SPEED = 300.0

@export var player_id: int = 1

# Histórico de inputs
var input_history: Array = []
var last_processed_input: int = 0

func _physics_process(delta):
    if not is_multiplayer_authority():
        return

    # Captura input
    var direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
    var input_data = {
        "seq": Time.get_ticks_msec(),
        "direction": direction
    }

    # Aplica imediatamente (prediction)
    velocity = direction * SPEED
    move_and_slide()

    # Envia para servidor
    rpc_id(1, "process_input", input_data)

    # Armazena para reconciliation
    input_history.append(input_data)
    if input_history.size() > 100:
        input_history.pop_front()

@rpc("any_peer", "reliable")
func process_input(input_data: Dictionary):
    if not multiplayer.is_server():
        return

    # Servidor processa
    var direction = input_data.direction
    velocity = direction * SPEED
    move_and_slide()

    # Envia estado autoritativo de volta
    rpc_id(input_data.get("sender_id"), "reconcile_state", input_data.seq, position)

@rpc("authority", "reliable")
func reconcile_state(seq: int, server_position: Vector2):
    # Cliente compara posição servidor com prediction
    var position_error = position.distance_to(server_position)

    if position_error > 10.0:  # Threshold
        # Correção: reaplica inputs após o seq recebido
        position = server_position

        for input_data in input_history:
            if input_data.seq > seq:
                # Reaplica input
                velocity = input_data.direction * SPEED
                move_and_slide()

    # Limpa inputs antigos
    input_history = input_history.filter(func(i): return i.seq > seq)

Complexo mas essential para jogos competitivos. Para jogos casuais, interpolation simples é suficiente.

Domine Todas as Áreas de Game Development

Multiplayer networking é apenas uma das muitas skills necessárias. Combinado com arte, design, áudio e marketing, você pode criar experiências completas e bem-sucedidas. Aprenda todas as disciplinas ou a colaborar efetivamente.

Candidate-se Agora

Segurança: Anti-Cheat Básico

Nunca confie no cliente. Sempre valide no servidor.

Server Authority

# MAU: Cliente processa gameplay crítico
@rpc("any_peer")
func take_damage(amount: int):
    health -= amount  # Cliente pode enviar amount = 0!

# BOM: Servidor valida
@rpc("any_peer")
func request_take_damage(attacker_id: int):
    if not multiplayer.is_server():
        return

    # Servidor verifica se ataque é válido
    if is_valid_attack(attacker_id, multiplayer.get_remote_sender_id()):
        var damage = calculate_damage(attacker_id)
        apply_damage(damage)
        rpc("sync_health", health)

@rpc("authority")
func sync_health(new_health: int):
    health = new_health

Validações Comuns

Movimento:

  • Cliente envia input, servidor simula
  • Servidor valida velocidade máxima
  • Detecta teleporting impossível

Combat:

  • Servidor verifica line of sight
  • Valida range de ataque
  • Verifica cooldowns no servidor

Resources/Inventory:

  • Servidor mantém authoritative state
  • Cliente apenas exibe
  • Transações validadas no servidor

Debugging Multiplayer

Multiplayer bugs são notoriamente difíceis de debug.

Ferramentas

Godot Remote Debugger:

  • Conecta a múltiplas instâncias simultaneamente
  • Monitora networking traffic
  • Debug > Deploy Remote Debug: Dois Clients

Print Statements Com Contexto:

func debug_print(message: String):
    var role = "SERVER" if multiplayer.is_server() else "CLIENT"
    var id = multiplayer.get_unique_id()
    print("[%s %d] %s" % [role, id, message])

Network Profiler: Enable em Project Settings > Debug > Network > Monitor

Simulando Latência

# Adiciona lag artificial para testar
const SIMULATED_LAG_MS = 100

func _send_with_lag(callable: Callable):
    await get_tree().create_timer(SIMULATED_LAG_MS / 1000.0).timeout
    callable.call()

Otimização de Bandwidth

Multiplayer consome bandwidth. Otimize agressivamente.

Técnicas

1. Envie Apenas Mudanças:

var last_sent_position: Vector2

func _physics_process(delta):
    if position.distance_to(last_sent_position) > 5.0:
        rpc("sync_position", position)
        last_sent_position = position

2. Use Unreliable Para Dados Frequentes:

@rpc("unreliable")  # OK perder pacote ocasional
func sync_position(pos: Vector2):
    position = pos

@rpc("reliable")  # PRECISA chegar
func player_died():
    health = 0

3. Comprima Dados:

# Ao invés de Vector2 (64 bits), use dois shorts (32 bits)
func compress_position(pos: Vector2) -> PackedInt32Array:
    return PackedInt32Array([int(pos.x), int(pos.y)])

4. Reduza Tick Rate:

var tick_rate = 20  # 20 updates/segundo, não 60
var time_since_last_tick = 0.0

func _physics_process(delta):
    time_since_last_tick += delta

    if time_since_last_tick >= 1.0 / tick_rate:
        sync_state()
        time_since_last_tick = 0.0

Conclusão: Multiplayer é Jornada

Multiplayer networking é uma das áreas mais complexas de game development. Não espere dominar imediatamente.

Recapitulando conceitos essenciais:

  1. Cliente-Servidor é arquitetura recomendada
  2. MultiplayerSynchronizer automatiza sincronização de propriedades
  3. RPCs permitem chamar funções remotamente
  4. Interpolation suaviza movimento apesar de latência
  5. Server authority previne cheating
  6. Otimize bandwidth enviando apenas o necessário
  7. Test com latência real - localhost não revela problemas

Seu roadmap de aprendizado:

Semana 1: Implemente chat multiplayer básico Semana 2: Crie movimento de player sincronizado Semana 3: Adicione interpolation e prediction Semana 4: Implemente gameplay (combat, objectives)

Recursos adicionais:

Multiplayer é iterativo. Seu primeiro jogo terá lag, bugs e design questionável. Tudo bem. Cada projeto ensina lições cruciais para o próximo.

Os jogos multiplayer que você admira foram construídos por teams que falharam centenas de vezes antes de acertar. Persistência e iteração são chaves.

Então crie seu projeto multiplayer básico hoje. Conecte dois clients. Faça eles verem um ao outro. Comemore cada pequena vitória.

A jornada para criar experiências multiplayer incríveis começa com "Hello, World!" em rede. Envie esse primeiro pacote agora.