alt

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

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

Errata Link to heading

  • In the second part we mention wesocket++, since I noticed that most people are more use to Enet, we are going to use Enet for our project, Enet is a cool lib on top of UDP that give us the best of both world (UDP, TCP). In the future, we going to migrate this to use Valve Network lib anywhay.

  • I used memcpy in the client to copy the data from the server to the client, but this is not a good practice. We should use a serialization library like Protobuf or FlatBuffers to serialize the data before sending it over the network. This will make the code more readable and easier to maintain.

  • Our Packet structure is not optimal. We should use a more efficient way to store the data, like using a union or a struct with a variable length array. This will make the code more efficient and easier to read.

๐Ÿ’ก Intro Link to heading

In this post, we will explore the server side of our game and how to communicate with the client. We will use Enet for the server and for the client. We will also look at how to send and receive data over the network, and how to handle multiple clients.

โœจ ENet Server Overview Link to heading

ENet is a network communication library built on top of UDP that gives you fine control over how packets are delivered. You can mark packets as unreliable, reliable, ordered, or unordered. In a fast-paced game, we want to minimize latency while still being able to guarantee delivery of important messages.

Our server is a minimal example that:

Accepts client connections

Receives inputs from clients

Updates authoritative player state

Broadcasts updated state back to each client

๐Ÿ”— Setting Up the Server Link to heading


ENetAddress address;
ENetHost* server;
address.host = ENET_HOST_ANY;
address.port = 1234;
server = enet_host_cr

eate(&address, 32, 2, 0, 0);

This snippet binds the server to all interfaces on port 1234. It allows up to 32 clients and supports 2 channels. One channel could be used for game state updates, and another for chat or events.

โœจ Handling Client Connections Link to heading

When a client connects, we assign them an initial position and store their state in a hash map:

case ENET_EVENT_TYPE_CONNECT:
    players[event.peer] = PlayerState{Vector2{0, 0}, 0};

When they disconnect, we clean up their state:

case ENET_EVENT_TYPE_DISCONNECT:
    players.erase(event.peer);

๐Ÿš€ Receiving Input and Updating State Link to heading

The server receives inputs, applies them, and tracks the last input sequence:

case ENET_EVENT_TYPE_RECEIVE:
    Input input;
    DeserializeInput(input, event.packet);
    ApplyInput(players[event.peer], input);
    players[event.peer].lastInputSeq = input.seq;

This is the core of server-side authority. The server always decides what the true game state is.

๐Ÿ“ก Broadcasting Game State Link to heading

Every frame, the server loops over all connected peers and sends their latest state:

for (auto& [peer, state] : players) {
    state.timestamp = GetTimeMs();
    ENetPacket* packet;
    SerializeState(packet, state);
    enet_peer_send(peer, 0, packet);
}

This allows each client to reconcile or interpolate what they see locally with what the server says is true.

๐Ÿ•น๏ธ Integrating ENet into the Client Link to heading

The client uses ENet to connect to the server, send inputs, and receive the game state:

ENetHost* client = enet_host_create(nullptr, 1, 2, 0, 0);
ENetAddress address;
ENetPeer* peer;
enet_address_set_host(&address, "localhost");
address.port = 1234;
peer = enet_host_connect(client, &address, 2, 0);

Every frame, the client constructs an input packet and sends it:

Input input;
input.seq = inputSeq++;
input.moveLeft = (rand() % 2);
input.moveRight = !input.moveLeft;

ApplyInput(localPlayer, input);
ENetPacket* packet;
SerializeInput(packet, input);
enet_peer_send(peer, 0, packet);

It also handles state updates from the server:

ENetEvent event;
while (enet_host_service(client, &event, 10) > 0) {
    if (event.type == ENET_EVENT_TYPE_RECEIVE) {
        PlayerState state;
        DeserializeState(state, event.packet);
        HandleServerState(state);
    }
}

This lets the client simulate input instantly while staying in sync with the server.

๐ŸŽฎ Defining and Syncing the Input Structure Link to heading

To ensure consistency between client and server, the Input structure must be identical on both sides. Here’s an updated version that supports full directional movement:

struct Input {
    uint32_t seq;   // Input sequence number
    int8_t moveX;   // -1 = left, 1 = right, 0 = none
    int8_t moveY;   // -1 = down, 1 = up, 0 = none
    uint8_t action; // 0 = none, 1 = fire, 2 = jump, etc.
};

And hereโ€™s how we serialize it:

void SerializeInput(ENetPacket*& packet, const Input& input) {
    uint8_t data[7];
    // Copy the sequence number
    memcpy(data, &input.seq, sizeof(input.seq));
    // 0 = none, 1 = left, 2 = right, etc.
    data[4] = static_cast<int8_t>(input.moveX + 128);
    data[5] = static_cast<int8_t>(input.moveY + 128);
    // 0 = none, 1 = fire, 2 = jump, etc.
    data[6] = static_cast<uint8_t>(input.action);
    // Create the packet with the data
    packet = enet_packet_create(data, sizeof(data), ENET_PACKET_FLAG_UNSEQUENCED);
}

void DeserializeInput(Input& input, const ENetPacket* packet) {
    memcpy(&input.seq, packet->data, sizeof(input.seq));
    input.moveX = static_cast<int8_t>(packet->data[4] - 128);
    input.moveY = static_cast<int8_t>(packet->data[5] - 128);
    input.action = static_cast<uint8_t>(packet->data[6]);
}

The total size of the Input structure is 7 bytes, which is efficient for network transmission. The sequence number is 4 bytes, and the moveX and moveY fields are 1 byte each. The action field is also 1 byte. This gives us a compact representation of player inputs.

๐Ÿ› ๏ธ Building the Server Link to heading

To build the server, you need to link against the ENet library. Hereโ€™s a simple CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(GameServer)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(ENet REQUIRED)
include_directories(${ENet_INCLUDE_DIRS})
add_executable(GameServer main.cpp)
target_link_libraries(GameServer ${ENet_LIBRARIES})

๐Ÿ› ๏ธ Building the Client Link to heading

To build the client, you can use the same CMakeLists.txt as above, but change the project name to GameClient and link against the ENet library.

_minimum_required(VERSION 3.10)
project(GameClient)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(ENet REQUIRED)
include_directories(${ENet_INCLUDE_DIRS})
add_executable(GameClient main.cpp)
target_link_libraries(GameClient ${ENet_LIBRARIES})

Building and Running Link to heading

mkdir build
cd build
cmake ..
make
./GameServer & ./GameClient

โœ… Summary Link to heading

With just a few hundred lines of C++, we have a working authoritative server that:

Accepts multiple connections

Applies player inputs

Keeps the game state synchronized

โœ… What We Built Link to heading

In this post, we:

[โœ”๏ธ] Implemented a basic ENet server that accepts client connections

[โœ”๏ธ] Handled client disconnections and state cleanup

[โœ”๏ธ] Implemented a simple input structure for player actions

[โœ”๏ธ] Serialized and deserialized input data for network transmission

[โœ”๏ธ] Broadcasted game state updates to all connected clients

OBS: If you take a look at the repo, You will noticed that we have some changes there, since I’m building it to linux, I had to make some changes on the original code.

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

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

๐Ÿ“ก Network debugging tools to visualize packets

๐Ÿ“ก Interpolation techniques to smooth out player movements

๐Ÿ“š References Link to heading

๐Ÿค Stay Connected Link to heading