Tutorial 97: Networking and Multiplayer Basics

CNA — C++ XNA 4.0 reimplementation

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);
}