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.5 f ;
else if ( IsKeyDown (KEY_A)) all_players [ 0 ]. pos . x -= 0.5 f ;
else if ( IsKeyDown (KEY_S)) all_players [ 0 ]. pos . y += 0.5 f ;
else if ( IsKeyDown (KEY_W)) all_players [ 0 ]. pos . y -= 0.5 f ;
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 );
}
ENET_EVENT_TYPE_DISCONNECT (not implemented)
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”).