We'll build a small but complete foundation: a server-authoritative, real-time game loop where two or more clients synchronize player position. That foundation scales to almost anything — a shooter, an RPG, a card game. The patterns are the same.
01 —
The Networking Landscape in GameMaker
GameMaker exposes low-level BSD-style socket networking through its built-in functions. You create sockets, connect them, send buffers, and listen for async events. It's not glamorous, but it's powerful and predictable.
The key functions you'll live in:
| Function | Role |
|---|---|
network_create_server() | Creates a listening server socket |
network_create_socket() | Creates a client socket |
network_connect() | Connect a socket to a server |
network_send_packet() | Send raw buffer data |
network_send_udp() | Fire-and-forget UDP packet |
network_destroy() | Clean up a socket |
All incoming data arrives through Async – Networking events. You never poll for data in a step event — GameMaker fires the async event for you. This is the most important mental shift when coming from other environments.
// Tip
GameMaker's networking works on both desktop and HTML5 exports, but UDP is unavailable in the browser. Plan your architecture around TCP if you're targeting web.
02 —
TCP vs UDP: Choosing Your Protocol
This is the most consequential early decision. Each protocol has a distinct personality, and the right one depends on what you're building.
| Property | TCP | UDP |
|---|---|---|
| Delivery guarantee | Yes — guaranteed in-order | No — fire and forget |
| Latency | Higher (ACK handshaking) | Lower (no handshake) |
| Packet loss handling | Automatic retransmission | Your problem |
| Best for | Chat, events, login, cards | Position, physics, input |
| GameMaker constant | network_socket_tcp | network_socket_udp |
In practice, most real-time action games use UDP for world state (positions, velocities, health) and TCP for critical events (hits confirmed, pickups, chat). If you're building a turn-based or card game, TCP alone is perfectly fine.
Creating protocol constants
Define your message type IDs in a script early on. Consistency here prevents a class of bugs that are nightmarish to diagnose:
// scr_net_types.gml
#macro MSG_PLAYER_MOVE 1
#macro MSG_PLAYER_SPAWN 2
#macro MSG_PLAYER_DESTROY 3
#macro MSG_CHAT 4
#macro MSG_PING 5
#macro MSG_PONG 6
03 —
Setting Up a Server
Your server can be a dedicated GameMaker runner launched headlessly, or a listen server running inside one player's client. For simple prototypes, a listen server is fine. For anything you care about, use a dedicated server — it eliminates host-advantage and makes cheat prevention tractable.
// Architecture Overview
In the server's Create event:
// obj_server — Create Event
server_socket = network_create_server(network_socket_tcp, 7777, 16);
if (server_socket < 0) {
show_debug_message("ERROR: Could not create server.");
game_end();
}
// Map: socket_id -> player data struct
clients = ds_map_create();
next_player_id = 1;
show_debug_message("Server listening on port 7777");
The third argument to network_create_server() is the maximum number of simultaneous connections. You can raise it freely — the real limit is your game logic and bandwidth.
Handling connections in the Async event
// obj_server — Async Networking Event
var _type = async_load[? "type"];
switch (_type) {
case network_type_connect:
var _sock = async_load[? "socket"];
var _data = {
socket: _sock,
player_id: next_player_id++,
x: room_width / 2,
y: room_height / 2
};
ds_map_add(clients, _sock, _data);
show_debug_message("Player connected: " + string(_data.player_id));
scr_send_spawn(_sock, _data);
break;
case network_type_disconnect:
var _sock = async_load[? "socket"];
ds_map_delete(clients, _sock);
show_debug_message("Player disconnected.");
break;
case network_type_data:
scr_handle_incoming(
async_load[? "socket"],
async_load[? "buffer"]
);
break;
}
04 —
Handling Clients & Messages
All network communication travels through buffers. A buffer is a typed byte array. You write values in, send it across the wire, and read the same values out on the other side. The first byte of every packet should be your message type ID — this is your dispatcher.
Sending a message
// scr_send_spawn(socket, player_data)
function scr_send_spawn(_sock, _pd) {
var _buf = buffer_create(16, buffer_fixed, 1);
buffer_write(_buf, buffer_u8, MSG_PLAYER_SPAWN);
buffer_write(_buf, buffer_u8, _pd.player_id);
buffer_write(_buf, buffer_f32, _pd.x);
buffer_write(_buf, buffer_f32, _pd.y);
network_send_packet(_sock, _buf, buffer_tell(_buf));
buffer_delete(_buf);
}
Reading a message
// scr_handle_incoming(socket, buffer)
function scr_handle_incoming(_sock, _buf) {
buffer_seek(_buf, buffer_seek_start, 0);
var _msg_type = buffer_read(_buf, buffer_u8);
switch (_msg_type) {
case MSG_PLAYER_MOVE:
var _pid = buffer_read(_buf, buffer_u8);
var _x = buffer_read(_buf, buffer_f32);
var _y = buffer_read(_buf, buffer_f32);
scr_server_apply_move(_sock, _pid, _x, _y);
break;
case MSG_PING:
scr_send_pong(_sock);
break;
}
}
// Warning
Never trust coordinates sent by a client directly. Always validate them on the server against the player's previous position and movement speed before broadcasting. A cheater will send x = 0, y = 0 in a heartbeat.
Broadcasting state to all clients
// scr_broadcast_move(player_id, x, y)
function scr_broadcast_move(_pid, _x, _y) {
var _buf = buffer_create(10, buffer_fixed, 1);
buffer_write(_buf, buffer_u8, MSG_PLAYER_MOVE);
buffer_write(_buf, buffer_u8, _pid);
buffer_write(_buf, buffer_f32, _x);
buffer_write(_buf, buffer_f32, _y);
var _key = ds_map_find_first(clients);
while (_key != undefined) {
var _client = clients[? _key];
network_send_packet(_client.socket, _buf, buffer_tell(_buf));
_key = ds_map_find_next(clients, _key);
}
buffer_delete(_buf);
}
05 —
State Synchronization
The central challenge of multiplayer: every machine needs to agree on what's happening. The server is the source of truth. Clients render what the server says happened — but with a twist that prevents the game from feeling like a slideshow.
Tick-based updates
Rather than sending state every frame (60 packets/second per player — don't), tick at a fixed rate. 20–30 ticks/second is standard for most action games. Use an alarm for this:
// obj_server — Create Event (addition)
tick_rate = 20; // ticks per second
alarm[0] = game_get_speed(gamespeed_fps) / tick_rate;
// obj_server — Alarm[0] Event
scr_server_tick();
alarm[0] = game_get_speed(gamespeed_fps) / tick_rate;
function scr_server_tick() {
// Collect authoritative positions
var _key = ds_map_find_first(clients);
while (_key != undefined) {
var _p = clients[? _key];
scr_broadcast_move(_p.player_id, _p.x, _p.y);
_key = ds_map_find_next(clients, _key);
}
}
Client-side interpolation
When a position update arrives, don't snap the sprite to the new position — interpolate toward it. This hides the gaps between server ticks and makes movement look silky even at 20 ticks/second:
// obj_remote_player — Step Event
x = lerp(x, target_x, 0.25);
y = lerp(y, target_y, 0.25);
The 0.25 interpolation factor is a starting point. Lower values (0.1–0.15) give smoother, floatier movement ideal for slow games. Higher values (0.3–0.5) feel snappier but can stutter on bad connections.
06 —
Lag Compensation Techniques
Interpolation hides visual jank. But the player controlling their own character needs something stronger: client-side prediction. This means the client moves the player locally the moment input happens, rather than waiting for the server to confirm it. The server still validates, but the player never feels the round-trip delay.
Input prediction
// obj_local_player — Step Event
// 1. Apply input locally IMMEDIATELY
var _dx = (keyboard_check(vk_right) - keyboard_check(vk_left)) * spd;
var _dy = (keyboard_check(vk_down) - keyboard_check(vk_up)) * spd;
x += _dx;
y += _dy;
// 2. Send input to server every tick
if (net_send_timer-- <= 0) {
scr_send_move(client_socket, x, y);
net_send_timer = game_get_speed(gamespeed_fps) / 20;
}
// 3. When server state arrives, reconcile
// (small errors are lerped away; large errors snap)
Reconciliation on server correction
// On receiving authoritative position from server
var _dist = point_distance(x, y, server_x, server_y);
if (_dist > 64) {
// Large desync — snap to server position
x = server_x;
y = server_y;
} else if (_dist > 4) {
// Small drift — smoothly correct
x = lerp(x, server_x, 0.3);
y = lerp(y, server_y, 0.3);
}
// If _dist <= 4, trust the client — no correction needed
// Note
These thresholds (64px, 4px) depend heavily on your game's scale and speed. Tune them during playtesting with simulated latency — GameMaker's network_set_config() supports artificial packet delay for exactly this purpose.
Ping measurement
Tracking round-trip time helps you adjust interpolation strength and display a connection indicator to players:
// Simplified ping cycle on the client
if (ping_timer-- <= 0) {
ping_send_time = current_time;
scr_send_ping(socket);
ping_timer = game_get_speed(gamespeed_fps); // once per second
}
// On receiving MSG_PONG:
ping_ms = current_time - ping_send_time;