Networking in GameMaker:
Building a Real-Time
Multiplayer Game

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 guaranteeYes — guaranteed in-orderNo — fire and forget
LatencyHigher (ACK handshaking)Lower (no handshake)
Packet loss handlingAutomatic retransmissionYour problem
Best forChat, events, login, cardsPosition, physics, input
GameMaker constantnetwork_socket_tcpnetwork_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

Client A
Dedicated Server
Client B
Server holds authoritative game state · Clients send inputs · Server broadcasts results

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;