
๐ ๏ธ 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
- ๐ซ marcius@criogenio.com
- ๐ผ LinkedIn
- ๐ป GitHub
- Thanks for reading โ see you in the next devlog! ๐น๏ธ