Bringing the game online: Godot's high-level multiplayer API with the ENet transport, RPC and state synchronization through MultiplayerSynchronizer, the authoritative server versus peer-to-peer patterns, and the basics of lag compensation and network security so the multiplayer game isn't easy to cheat.

In episode 12, you polished the game with animation, particles, and shaders — the game now feels alive. Episode 13 raises the stakes: making your game playable together. We build multiplayer with Godot's high-level API, which hides the complexity of TCP/UDP sockets, so you spend more time writing game logic than network protocols.
This episode's roadmap: get to know the high-level multiplayer API and the ENet transport, build a simple lobby with host and join, use RPC to call functions across machines, synchronize state with MultiplayerSynchronizer, then finish with the authoritative server versus peer-to-peer patterns and the basics of lag compensation and network security.
Godot has a high-level multiplayer API: a set of nodes and methods that handle connections, serialization, and routing automatically. You don't need to write byte buffers yourself — just define what data to send, and Godot takes care of it.
Behind the scenes, its default transport is ENet, a UDP-based networking library with a reliability layer. ENet gives fast, UDP-like delivery while also ensuring important messages arrive like TCP. In Godot 4, the class used is ENetMultiplayerPeer. (In Godot 3, this class was named NetworkedMultiplayerENet — if you read older tutorials, that's the old name.)
There are three core components: MultiplayerAPI (the routing brain), MultiplayerPeer (the transport, filled by ENet), and helper nodes like MultiplayerSynchronizer and MultiplayerSpawner for automatic synchronization.
The most basic pattern is one player becoming the host (server and client at once) and other players joining their address. Create an autoload node named Net so the whole game can access the connection:
func jadi_host(port: int = 9999) -> void:
var peer := ENetMultiplayerPeer.new()
peer.create_server(port, 4)
multiplayer.multiplayer_peer = peer
func on_peer_connected(id: int) -> void:
print("Pemain masuk dengan ID ", id)func gabung(ip: String, port: int = 9999) -> void:
var peer := ENetMultiplayerPeer.new()
peer.create_client(ip, port)
multiplayer.multiplayer_peer = peerEach client is identified by a peer ID: the host is always 1, and clients are given a unique ID after joining. This ID is used to route messages to a specific player. Connect the peer_connected and peer_disconnected signals from multiplayer to catch players entering and leaving.
Info
Connection signals (peer_connected) are only triggered on the host. A client knows its connection succeeded via MultiplayerAPI.connection_succeeded. Get in the habit of checking is_server() before adding a player to the game list.
RPC (Remote Procedure Call) is the way to call a function on another machine as if it were local. In Godot 4, a function is marked with the @rpc annotation, then called via rpc() or rpc_id():
@rpc("any_peer", "call_local")
func muat_level(level: int) -> void:
get_tree().change_scene_to_file("res://levels/level_%d.tscn" % level)
func minta_ganti_level(level: int) -> void:
rpc("muat_level", level)@rpc accepts several modes: any_peer (anyone may call) versus authority (only the authority may), call_local (the caller executes too), and reliable (use the reliable transport) versus unreliable. For positions updated every frame, use unreliable; for critical data like damage, use reliable.
Spreading rpc calls for every position change would flood the network. For state that changes continuously, Godot 4 provides MultiplayerSynchronizer: a node that registers another node's properties, then automatically sends their latest values to other peers several times per second.
Place a MultiplayerSynchronizer as a child of the player node, register the target node in the root_path property, then register the properties you want synchronized:
$MultiplayerSynchronizer.root_path = get_path()
$MultiplayerSynchronizer.update_interval = 0.05
$MultiplayerSynchronizer.replication_configFor objects created at runtime — bullets, spawned enemies — pair it with MultiplayerSpawner: every instance added to the scene tree on the creating machine is automatically recreated on other machines. With Spawner + Synchronizer, most action games can be synchronized without writing manual network code.
The most important architecture question: who determines the game's truth? There are two big patterns:
In Godot, the authoritative server pattern means physics and damage code only run on the host, while clients send actions and receive results. A small example: the client sends a "jump" command, the host executes it:
@rpc("any_peer")
func minta_lompat() -> void:
if multiplayer.is_server():
karakter.melompat()Even though peer-to-peer is easier for prototypes, build your game with the authoritative server pattern from the start — moving it later will be far more painful.
On real networks, packets arrive with latency — not instantly. MultiplayerSynchronizer helps with interpolation, but for action games you need a few lag compensation techniques:
Security follows: because all data passes through the network, you can't trust clients. The basic rules:
unreliable and a reasonable update_interval.Warning
Godot's high-level API isn't anti-cheat. It simplifies communication, but all authority still depends on your design. The simplest rule that saves many games: "the server is the only source of truth, clients are only senders of intent."
Episode 13 took you from zero to functional multiplayer: ENetMultiplayerPeer as the ENet-based transport, the host and join patterns with peer IDs, @rpc for calling functions across machines, MultiplayerSynchronizer and MultiplayerSpawner for automatic synchronization, a comparison of the authoritative server versus peer-to-peer patterns, and the basics of lag compensation and network security rules.
The key takeaways:
ENetMultiplayerPeer; in Godot 3 it's named NetworkedMultiplayerENet.@rpc for rare events and MultiplayerSynchronizer for continuously changing state.is_server() and has peer ID 1; clients get a unique ID after joining.In episode 14, after the game can be played together, you face a new question: why does the game feel heavy on your machine? We dissect performance & profiling — optimizing draw calls and batching, physics, the built-in profiler, memory usage and FPS, up to optimization at export time. A fast game isn't luck; it's the result of measurement.