Tutorial 97: Networking and Multiplayer Basics
CNA has no networking API
CNA is a rendering, input, and audio framework. Networking is outside its scope. This tutorial shows
how to add multiplayer to your CNA game using popular C++ networking libraries. All networking code
lives outside the CNA API — it integrates through your Game::Update() method.
TCP/UDP with BSD sockets or ASIO
For simple games, raw BSD sockets work. For production code, use Asio (standalone header-only C++ networking library, part of Boost or as standalone).
// Simple UDP sender (BSD sockets)
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
class UDPSender {
public:
UDPSender(const char* ip, uint16_t port) {
fd_ = socket(AF_INET, SOCK_DGRAM, 0);
addr_.sin_family = AF_INET;
addr_.sin_port = htons(port);
addr_.sin_addr.s_addr = inet_addr(ip);
}
~UDPSender() { if (fd_ >= 0) close(fd_); }
void Send(const void* data, int len) {
sendto(fd_, data, len, 0,
reinterpret_cast<sockaddr*>(&addr_), sizeof(addr_));
}
private:
int fd_;
sockaddr_in addr_{};
};
ENet library for UDP
ENet provides reliable UDP with packet sequencing on top of raw UDP. It is the networking layer used in many indie games.
# Add ENet to CMake:
find_package(ENet REQUIRED)
# Or: FetchContent_Declare(enet GIT_REPOSITORY https://github.com/lsalzman/enet.git)
target_link_libraries(MyGame PRIVATE enet)
#include <enet/enet.h>
#include <functional>
#include <cstdint>
#include <cstddef>
class NetworkManager {
public:
bool InitHost(uint16_t port) {
if (enet_initialize() != 0) return false;
ENetAddress address;
address.host = ENET_HOST_ANY;
address.port = port;
host_ = enet_host_create(&address, 32, 2, 0, 0);
return host_ != nullptr;
}
bool Connect(const char* ip, uint16_t port) {
if (enet_initialize() != 0) return false;
host_ = enet_host_create(nullptr, 1, 2, 0, 0);
ENetAddress address;
enet_address_set_host(&address, ip);
address.port = port;
peer_ = enet_host_connect(host_, &address, 2, 0);
return peer_ != nullptr;
}
void PollEvents(std::function<void(const uint8_t*, size_t)> onReceive) {
ENetEvent event;
while (enet_host_service(host_, &event, 0) > 0) {
if (event.type == ENET_EVENT_TYPE_RECEIVE) {
onReceive(event.packet->data, event.packet->dataLength);
enet_packet_destroy(event.packet);
}
}
}
void Send(const void* data, size_t len, bool reliable = true) {
if (!peer_) return;
ENetPacket* pkt = enet_packet_create(data, len,
reliable ? ENET_PACKET_FLAG_RELIABLE : 0);
enet_peer_send(peer_, 0, pkt);
}
~NetworkManager() {
if (host_) enet_host_destroy(host_);
enet_deinitialize();
}
private:
ENetHost* host_ = nullptr;
ENetPeer* peer_ = nullptr;
};
SDL3_net option
SDL3_net provides a thin, cross-platform socket abstraction that pairs naturally with CNA (since CNA already uses SDL3). It supports TCP streams and UDP datagrams.
find_package(SDL3_net REQUIRED)
target_link_libraries(MyGame PRIVATE SDL3_net::SDL3_net)
Lockstep vs client-server
Lockstep: all clients simulate the same game state using the same inputs, synchronized frame by frame. Simple to implement but sensitive to latency — one slow client stalls everyone. Best for turn-based or slow-paced games. Client-server: one machine is authoritative; clients send inputs and receive state updates. More complex but handles latency and cheating better. Best for action games.
Serialization of game state
Simple peer-to-peer position sync skeleton:
#include "Microsoft/Xna/Framework/Vector2.hpp"
#include <unordered_map>
#include <cstdint>
using namespace Microsoft::Xna::Framework;
// Compact player state packet
#pragma pack(push, 1)
struct PlayerState {
uint8_t playerId;
float x, y;
float velX, velY;
uint32_t sequenceNumber;
};
#pragma pack(pop)
void SendPlayerState(NetworkManager& net, uint8_t id,
const Vector2& pos, const Vector2& vel,
uint32_t seq) {
PlayerState pkt;
pkt.playerId = id;
pkt.x = pos.X;
pkt.y = pos.Y;
pkt.velX = vel.X;
pkt.velY = vel.Y;
pkt.sequenceNumber = seq;
net.Send(&pkt, sizeof(pkt), false); // unreliable UDP for position
}
void OnReceiveState(const uint8_t* data, size_t len,
std::unordered_map<uint8_t, Vector2>& remotePositions) {
if (len < sizeof(PlayerState)) return;
const PlayerState* pkt = reinterpret_cast<const PlayerState*>(data);
remotePositions[pkt->playerId] = Vector2(pkt->x, pkt->y);
}
Latency hiding
Techniques to hide network latency: Client-side prediction — apply your own input immediately, reconcile with server state when it arrives. Interpolation — render remote players at a slightly delayed time using the last two received positions. Dead reckoning — extrapolate remote player positions using last known velocity.
// Simple interpolation between two received positions
Vector2 InterpolateRemote(const Vector2& prev, const Vector2& next,
float alpha) {
return Vector2::Lerp(prev, next, alpha);
}