Skip to main content

Overview

Wizard Duel uses ENet, a reliable UDP networking library, for multiplayer functionality. The implementation supports peer-to-peer connections with one player acting as host (server) and another as client.

Network architecture

Connection model

Host (Server):
  • Listens on port 7777
  • Accepts one client connection
  • Sends/receives position updates
Client:
  • Connects to server IP (default: 127.0.0.1)
  • Sends local position to server
  • Receives opponent position from server

Global network state

std::string connectionState = "DISCONNECTED";
ENetHost* clientHost = nullptr;
ENetPeer* serverPeer = nullptr; // for client connection to server
ENetHost* serverHost = nullptr; // for host server
ENetPeer* clientPeer = nullptr; // for host connection to client

const char* SERVER_IP = "127.0.0.1";
const int SERVER_PORT = 7777;
The game uses separate host/peer pointers for client and server roles, allowing either player to act as host.

ENet initialization

ENet must be initialized before any network operations:
if (enet_initialize() != 0) {
    fprintf(stderr, "An error occurred while initializing ENet.\n");
    return EXIT_FAILURE;
} else {
    std::cout << "enet initialized" << std::endl;
}
atexit(enet_deinitialize);
The atexit call ensures proper cleanup when the program terminates.

Setting up a host (server)

The SetupHost() function creates a server that listens for connections:
bool SetupHost() {
    ENetAddress address;
    address.host = ENET_HOST_ANY;
    address.port = SERVER_PORT;
    
    // Create the server host
    serverHost = enet_host_create(
        &address,
        1,  // max clients
        2,  // max channels
        0,  // incoming bandwidth (0 = unlimited)
        0   // outgoing bandwidth (0 = unlimited)
    );
    
    if (serverHost == NULL) {
        printf("failed to create server host");
        return false;
    }
    
    printf("Server hosting on port %d\n", SERVER_PORT);
    return true;
}

Parameters explained

The game is designed for 1v1 battles, so only one client connection is allowed.
Provides two communication channels for separating different types of game data (though currently only one is used).
No bandwidth limiting is applied, allowing maximum throughput for local network play.

Setting up a client

The SetupClient() function connects to an existing host:
bool SetupClient() {
    // Create a client host
    clientHost = enet_host_create(
        NULL,  // no address (client doesn't bind to port)
        1,     // max outgoing connections
        2,     // max channels
        0,     // incoming bandwidth
        0      // outgoing bandwidth
    );
    
    if (clientHost == NULL) {
        printf("failed to create client host\n");
        return false;
    }

    ENetAddress address;
    enet_address_set_host(&address, SERVER_IP);
    address.port = SERVER_PORT;

    serverPeer = enet_host_connect(
        clientHost,
        &address,
        2,  // channels
        0   // user data
    );
    
    if (serverPeer == NULL) {
        printf("failed to connect to server");
        return false;
    }
    
    printf("Connecting to %s:%d...\n", SERVER_IP, SERVER_PORT);
    return true;
}
Clients pass NULL as the address to enet_host_create, meaning they don’t bind to a specific port.

Position packet structure

Player positions are synchronized using a simple struct:
struct PositionPacket {
    float x;
    float y;
};
This minimal packet structure keeps network traffic low, sending only 8 bytes per position update.

Event handling

The game processes network events using enet_host_service:

Connection events (HOST state)

ENetEvent event;
while (enet_host_service(serverHost, &event, 0) > 0) {
    if (event.type == ENET_EVENT_TYPE_CONNECT) {
        printf("Client connected!\n");
        clientPeer = event.peer;
        connectionState = "CONNECTED";
        
        // Initialize players and transition to MULTIPLAYER
        all_players.clear();
        all_players.push_back(player);
        all_players[0].is_local = true;
        
        Character opponent;
        opponent.pos.x = x_distr(gen);
        opponent.pos.y = y_distr(gen);
        opponent.is_local = false;
        all_players.push_back(opponent);
        
        state = "MULTIPLAYER";
    }
}

Connection events (JOIN state)

ENetEvent event;
while (enet_host_service(clientHost, &event, 0) > 0) {
    if (event.type == ENET_EVENT_TYPE_CONNECT) {
        printf("Connected to server!\n");
        connectionState = "CONNECTED";
        
        // Initialize players and transition to MULTIPLAYER
        all_players.clear();
        all_players.push_back(player);
        all_players[0].is_local = true;
        
        Character opponent;
        opponent.pos.x = x_distr(gen);
        opponent.pos.y = y_distr(gen);
        opponent.is_local = false;
        all_players.push_back(opponent);
        
        state = "MULTIPLAYER";
    }
}
The timeout parameter in enet_host_service is set to 0, making it non-blocking. This allows the game to continue rendering while checking for network events.

Position synchronization

In the MULTIPLAYER state, the game continuously syncs positions:

Receiving position updates

ENetEvent event;
if (clientHost != nullptr) {
    while (enet_host_service(clientHost, &event, 0) > 0) {
        if (event.type == ENET_EVENT_TYPE_RECEIVE) {
            PositionPacket* posPacket = (PositionPacket*)event.packet->data;
            all_players[1].pos.x = posPacket->x;
            all_players[1].pos.y = posPacket->y;
            enet_packet_destroy(event.packet);
        }
    }
} else if (serverHost != nullptr) {
    while (enet_host_service(serverHost, &event, 0) > 0) {
        if (event.type == ENET_EVENT_TYPE_RECEIVE) {
            PositionPacket* posPacket = (PositionPacket*)event.packet->data;
            all_players[1].pos.x = posPacket->x;
            all_players[1].pos.y = posPacket->y;
            enet_packet_destroy(event.packet);
        }
    }
}

Sending position updates

Position updates are sent only when the player moves:
Vector2 old_pos = all_players[0].pos;
if (all_players[0].is_local) {
    if (IsKeyDown(KEY_D)) all_players[0].pos.x += 0.5f;
    else if (IsKeyDown(KEY_A)) all_players[0].pos.x -= 0.5f;
    else if (IsKeyDown(KEY_S)) all_players[0].pos.y += 0.5f;
    else if (IsKeyDown(KEY_W)) all_players[0].pos.y -= 0.5f;
    
    if (checkCollision(all_players[0].pos, tree_pos)) {
        all_players[0].pos = old_pos;
    }
}

// Send update if position changed
if (all_players[0].pos.x != old_pos.x || all_players[0].pos.y != old_pos.y) {
    PositionPacket posPacket;
    posPacket.x = all_players[0].pos.x;
    posPacket.y = all_players[0].pos.y;

    ENetPacket* packet = enet_packet_create(
        &posPacket,
        sizeof(PositionPacket),
        ENET_PACKET_FLAG_RELIABLE
    );

    if (clientHost != nullptr && serverPeer != nullptr) {
        enet_peer_send(serverPeer, 0, packet);
    } else if (serverHost != nullptr && clientPeer != nullptr) {
        enet_peer_send(clientPeer, 0, packet);
    }
}
Using ENET_PACKET_FLAG_RELIABLE ensures position updates arrive in order and are resent if lost, preventing desync issues.

Packet creation and sending

Creating packets

ENetPacket* packet = enet_packet_create(
    &posPacket,                    // data pointer
    sizeof(PositionPacket),        // data size
    ENET_PACKET_FLAG_RELIABLE     // flags
);

Sending packets

From client to server:
if (clientHost != nullptr && serverPeer != nullptr) {
    enet_peer_send(serverPeer, 0, packet);
}
From server to client:
if (serverHost != nullptr && clientPeer != nullptr) {
    enet_peer_send(clientPeer, 0, packet);
}
The channel parameter (0) specifies which of the 2 available channels to use.

Latency monitoring

The game displays real-time ping information:
DrawText(TextFormat("Ping: %d ms", 
    clientPeer ? clientPeer->roundTripTime : 
    (serverPeer ? serverPeer->roundTripTime : 0)), 
    30, 600, 20, BLACK);
ENet automatically tracks round-trip time (RTT) for each peer connection.

Network cleanup

Proper cleanup is essential to free network resources:
// At program exit
if (serverHost != nullptr) {
    enet_host_destroy(serverHost);
}
if (clientHost != nullptr) {
    enet_host_destroy(clientHost);
}
This releases all associated connections and memory before the program terminates.

Event types

ENet provides several event types (though only these are currently used):
Fired when a connection is established. Used to transition from HOST/JOIN states to MULTIPLAYER state.
if (event.type == ENET_EVENT_TYPE_CONNECT) {
    printf("Client connected!\n");
    clientPeer = event.peer;
}
Fired when a packet is received. Contains packet data that must be manually destroyed after processing.
if (event.type == ENET_EVENT_TYPE_RECEIVE) {
    PositionPacket* posPacket = (PositionPacket*)event.packet->data;
    all_players[1].pos.x = posPacket->x;
    all_players[1].pos.y = posPacket->y;
    enet_packet_destroy(event.packet);
}
Would be fired when a peer disconnects. Currently not handled, which could lead to undefined behavior if a player disconnects mid-game.

Limitations and future improvements

No spell synchronization: Currently only position data is synced. Spells are not transmitted over the network, meaning players can’t damage each other in multiplayer. No disconnect handling: The game doesn’t handle ENET_EVENT_TYPE_DISCONNECT events. No health/mana sync: Health and mana values are not synchronized between players. No lobby system: Players must manually coordinate who hosts and who joins. Local network only: Default configuration uses 127.0.0.1 (localhost).
To play over LAN, change SERVER_IP to the host’s local IP address (e.g., “192.168.1.100”).

Build docs developers (and LLMs) love