
๐ ๏ธ 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
- ENet Documentation
- Raylib Documentation
- Game Networking
- Game Networking Patterns
- Game Programming Patterns
- Raylib ENet Example