alt

๐Ÿ› ๏ธ Tools & Stack Link to heading

  • Language: C (server), C (client)
  • Network: WebSockets

๐Ÿ’ก Intro Link to heading

Now, we are going to explore more about our game client and how to communicate events to th game server.

Our idea with this series is not focus too much on the client side of things so to make easy on our side we are going to use a lib (raylib) to handle almost everything related to game development (Input, Graphics, OS Windows and Storage)

๐Ÿ’ก Client Game Design Link to heading

We are creating a simple top-down shooter, using Nuclear Thrones as our main inspiration. The game is a 2D top-down shooter where players can move around, shoot, and interact with the environment.

Some of the game mechanics we are going to implement are:

  • Simple 2D graphics
  • Simple tiled map (e.g., 32x32 tiles)
  • Player movement (WASD)
  • Player shooting (mouse click)
  • Player health system (hit points)
  • Game state synchronization (position, health, etc.)
  • Game win/lose conditions (e.g., player death, game over)
  • Game lobby (optional)
  • Game chat (optional)
  • Game Enemies (optional)

Our idea is not to copy all game play mecanics our visuals, since a project like that could take a long time and mental power, is only the camera and the dynamic aspect of the game that we are going to replicate.

Here is the simple example of our client and some explanations.

#include "raylib.h"
int main()
{
    InitWindow(800, 600, "TCP Client Game - Raylib");
    SetTargetFPS(60);

    int playerId = 1; // Replace with actual player ID
    int socket = 0;   // Replace with actual socket initialization

    // Connect to server (not implemented in this example)

    char playerIdChar[32];
    itoa(playerId, playerIdChar, 10); // Convert player ID to stringname

    GameState gameState;
    InitGameState(&gameState);

    while (!WindowShouldClose())
    {

        // Draw aime line to mouse position from the center of the player
        Vector2 mousePos = GetMousePosition();
        Vector2 centerPlayerPos = {gameState.player.entity.position.x + 10, gameState.player.entity.position.y + 10};

        Vector2 aimDirection = {mousePos.x - centerPlayerPos.x, mousePos.y - centerPlayerPos.y};
        float length = sqrtf(aimDirection.x * aimDirection.x + aimDirection.y * aimDirection.y);
        Vector2 aimEndPos = {centerPlayerPos.x + aimDirection.x, centerPlayerPos.y + aimDirection.y};

        gameState.player.entity.direction = aimDirection / length; // Normalize the direction vector

        handleInput(&gameState);

        BeginDrawing();
        ClearBackground(PURPLE);

        DrawText("Use WASD to move", 10, 40, 20, DARKGRAY);
        DrawAndUpdatePlayer(&gameState);

        // DrawLineV(playerPos, aimEndPos, RED);
        DrawLineEx(centerPlayerPos, aimEndPos, 2, RED);
        DrawRectangleV(aimEndPos, Vector2{10, 10}, RED);

        DrawAndUpdateBullet(&gameState);

        // Show Entities count on screen
        char buffer[32];
        sprintf(buffer, "Entities: %d", gameState.entitiesCount);
        DrawText(buffer, 10, 10, 20, DARKGRAY);
        // Show Player ID on screen
        sprintf(buffer, "Player ID: %d", gameState.player.id);
        DrawText(buffer, 10, 70, 20, DARKGRAY);

        EndDrawing();
    }
    CloseWindow();
    return 0;
}

This will give us a simple window with our player moving around and a line pointing to the mouse position. We are going to use this to test our game server and how we can send and receive data from it.

Source code available on GitHub

We are going to elaborate on the client in future posts for now it’s good enough to show how we can move our player around.

๐Ÿ“ฆ Packet Structure and Serialization in Game Networking Link to heading

In a networked game, every time your client sends input (like player movement) to the server, or the server sends game state back to clients, that data must be packed into network packets.

๐Ÿ”ง What Is a Packet? A packet is a formatted unit of data carried over a network connection. It typically contains:

A header: metadata (e.g., type of message, sequence number, ID)

A payload: actual game data (e.g., position, velocity, health)

Example (simplified TCP packet from client):

| Type (1 byte) | PlayerID (4 bytes) | VX (4 bytes) | VY (4 bytes) |

Thatโ€™s 13 bytes per message โ€” seems small, right? But that adds up fast.

โš™๏ธ Serialization Link to heading

Serialization is the process of converting your data into a byte format that can be transmitted over the network. In C, this typically means writing values into a char[] buffer using memcpy() or pointer arithmetic:

char buffer[13];
buffer[0] = 1; // Message type
memcpy(&buffer[1], &playerId, sizeof(int));
memcpy(&buffer[5], &vx, sizeof(float));
memcpy(&buffer[9], &vy, sizeof(float));
send(sock, buffer, sizeof(buffer), 0);

On the receiving end, you deserialize the same way โ€” extract each value from the buffer using known offsets.

๐Ÿงจ The Danger of Large Packets Link to heading

Many new developers just serialize entire game states or player objects into JSON and fire it over. But hereโ€™s why big packets = bad idea:

๐Ÿšง 1. MTU (Maximum Transmission Unit) Link to heading

The standard MTU for Ethernet is 1500 bytes. If your packet exceeds that, it will be fragmented into multiple packets.

Fragmentation increases latency.

Fragment loss = whole message loss.

UDP fragments are particularly dangerous (no retransmit).

๐Ÿ“‰ 2. Bandwidth & Latency Link to heading

If youโ€™re sending 1 KB per player every 50ms, with 10 players:

1000 bytes * 20 ticks/sec * 10 players = 200 KB/sec outbound

That adds up โ€” and slows the whole experience down, especially on mobile or poor networks.

โœ‚๏ธ Strategies to Keep Packets Small Link to heading

โœ… Only send deltas: changes since the last frame

โœ… Use compression: like snapshot delta encoding

โœ… Use binary over JSON: itโ€™s faster and way smaller

โœ… Limit float precision: 2 bytes instead of 4 (quantization)

โœ… Message types: structure packets like:

// Example binary layout
struct Packet {
    uint8_t type; // 1 = movement, 2 = shoot, etc.
    float x, y;
};

๐Ÿ” Replication: What, When, and How to Sync Link to heading

In real-time multiplayer games, replication refers to how you sync game state across the network โ€” from the server to the clients.

But just because the server knows everything doesnโ€™t mean it should send everything.

Sending every entity’s full state every tick is wasteful and will destroy your bandwidth, especially as the number of players grows. Instead, you want to be intentional about what data you replicate, to whom, and how often.

๐Ÿ“ฆ What to Replicate (and What Not To) Link to heading

Think about the data your game tracks:

Player positions and velocities

Animations

Health, inventory, score

Projectiles, enemies, triggers

Not all of this needs to be synced constantly.

โœ… High Priority (replicate every tick or near-realtime)

Your position and velocity (for interpolation & collision)

Combat events (shots fired, damage)

Critical world state (e.g., doors opening, capture point status)

๐Ÿ•“ Low Priority (sync less often or on change)

Player cosmetic data (skin, name, title)

Scores / stats

Chat messages

Audio / effects

๐ŸŽฏ Selective Replication: Donโ€™t Broadcast Everything Link to heading

Instead of broadcasting every entity to every client, ask:

Who needs to know about this data?

Use Area of Interest (AOI) logic or visibility filtering:

| Client A | Can see NPC in zone X โ†’ โœ… Replicate | | Client B | Is in another zone โ†’ โŒ Donโ€™t replicate it |

Smart servers track what each client can see, and only send deltas for entities in that clientโ€™s area.

๐Ÿง  Example: Smart Packet Selection

Instead of:

// For 20 players, 60 NPCs
send(player_data_all);
send(npc_data_all);

Do this:

for each client:
    build packet {
        include local player state
        include nearby entities (within 30 meters)
        exclude data marked as unchanged
    }
    send(client_packet);

This means each player gets a personalized snapshot of only what matters to them โ€” massively reducing bandwidth.

๐Ÿ“‰ Frequency Control (Send Rates) Link to heading

You donโ€™t have to replicate everything at 60Hz.

Data Type Update Rate

Data Type Update Rate
Player Position 20โ€“60 Hz
Player Animation 10โ€“20 Hz
Player Health 2โ€“5 Hz
Player Score 2โ€“5 Hz
Player Name onChange
Player Chat onChange
Player Projectiles 10โ€“20 Hz
Player Effects 2โ€“5 Hz

Use cooldowns, dirty flags, or tick counters to decide when to send each type of data.

๐Ÿง  TL;DR Link to heading

๐Ÿ’ก Donโ€™t treat all data equally โ€” some needs high priority, some doesnโ€™t

๐ŸŽฏ Only send what the client can see and needs

๐Ÿงน Filter, compress, and batch updates

โฑ๏ธ Control send frequency per data type

๐Ÿ“Œ Quick Tips Link to heading

Keep update packets under 512 bytes when possible Bundle multiple events into a single tick update (e.g., movement + action) Consider TCP for reliable events (e.g., login), and UDP for realtime state

๐Ÿ’ป Data Structure Link to heading

Now that we have a basic understanding of how to send and receive data over the network, we need to define our data structure. We are going to use a simple struct to represent our player and a list to store all players in the game.

typedef struct Player
{
    int id;
    Vector2 position;
    Vector2 velocity;
    Color color;
    char name[32];
    bool isAlive;
} Player;

typedef struct GameState
{
    Player players[MAX_PLAYERS];
    int playerCount;
} GameState;

For now we are replicating the entire GameState struct, but in the future we are going to be more specific about the data we are going to send and receive from the server.

๐Ÿ“ก Receiving data and sync to all clients Link to heading

We are going to continue using our socket example from the last post. We are going to add a new function to handle the data received from the server and sync it to all clients.

void handleServerMessages(char *buffer, int size)
{
    GameState gameState;
    memcpy(&gameState, buffer, sizeof(GameState));

    for (int i = 0; i < gameState.playerCount; i++)
    {
        Player player = gameState.players[i];
        // Update player position and velocity
        players[player.id].position = player.position;
        players[player.id].velocity = player.velocity;
        players[player.id].isAlive = player.isAlive;
    }
}

We are going to call this function in our main loop after we receive the data from the server.

while (!WindowShouldClose())
{
    // Handle input and update player position locally
    handleInput(&playerPos, &velocity);

    // Send data to server
    sendMessageToServer(direction, velocity);

    // Receive data from server
    char buffer[sizeof(GameState)];
    int size = recv(sock, buffer, sizeof(buffer), 0);
    if (size > 0)
    {
        handleServerMessages(buffer, size);
    }
    // Render the game
    renderGame(playerPos);
}

This will allow us to receive data from the server and update the game state in real time.

void handleInput(GameState *gameState)
{
    // Add friction to velocity
    gameState->player.entity.velocity.x *= 0.9f;
    gameState->player.entity.velocity.y *= 0.9f;

    // Update velocity based on input
    if (IsKeyDown(KEY_W))
        gameState->player.entity.velocity.y -= 1;
    if (IsKeyDown(KEY_S))
        gameState->player.entity.velocity.y += 1;
    if (IsKeyDown(KEY_A))
        gameState->player.entity.velocity.x -= 1;
    if (IsKeyDown(KEY_D))
        gameState->player.entity.velocity.x += 1;

    gameState->player.entity.position.x += gameState->player.entity.velocity.x * 0.5f;
    gameState->player.entity.position.y += gameState->player.entity.velocity.y * 0.5f;

    // Clamp player position to screen bounds
    if (gameState->player.entity.position.x < 0)
        gameState->player.entity.position.x = 0;
    if (gameState->player.entity.position.x > GetScreenWidth() - 20)
        gameState->player.entity.position.x = GetScreenWidth() - 20;
    if (gameState->player.entity.position.y < 0)
        gameState->player.entity.position.y = 0;
    if (gameState->player.entity.position.y > GetScreenHeight() - 20)
        gameState->player.entity.position.y = GetScreenHeight() - 20;

    // Check for shooting
    if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
    {
        // Shoot a bullet
        shootBulletLocally(gameState, gameState->player.entity.direction); // Replace 1 with actual player ID
    }
}

โœ… What We Built Link to heading

In this post, we:

[โœ”๏ธ] Defined what our game is going to be

[โœ”๏ธ] Implemented a basic game client using raylib

[โœ”๏ธ] Designed a simple packet structure for sending and receiving data

[โœ”๏ธ] Implemented a basic game state synchronization system

[โœ”๏ธ] Explained the importance of packet size and serialization

[โœ”๏ธ] Discussed replication strategies for real-time multiplayer games

๐Ÿ”œ Whatโ€™s Next Link to heading

In the next post, weโ€™ll level up by adding:

๐Ÿ“ก Focus on the server side and create a framework for communicating with the client

๐ŸŽฎ Change our client to accept messages from the server

๐Ÿ“š References Link to heading

๐Ÿค Stay Connected Link to heading