How SDL2-Net Integration Enables Multiplayer in Lighthouse: A Deep Dive into the Network Architecture

Lighthouse uses SDL2-net to implement a lightweight, threaded TCP networking layer that is conditionally compiled via the USE_NETWORKING macro, allowing multiplayer features to be fully excluded from builds when not required.

The HarbourMasters/Lighthouse project—a reimplementation of the classic game engine—integrates SDL2-net to power its multiplayer capabilities. This article examines how the engine initializes SDL2-net, manages TCP sockets through a dedicated Network class, and handles packet framing with JSON messaging. All networking code resides in src/port/Network/ and is guarded by preprocessor directives to maintain binary flexibility.

Engine Initialization and Shutdown

The engine lifecycle for SDL2-net is managed in src/port/Engine.cpp. During startup, the engine calls SDLNet_Init() to allocate the networking subsystem. On shutdown, SDLNet_Quit() ensures proper resource cleanup.

This initialization happens only when USE_NETWORKING is defined at compile time. Without this flag, the entire networking stack is excluded from the build, producing smaller binaries for single-player-only deployments.

The Network Class: SDL2-Net Wrapper

The core abstraction layer lives in src/port/Network/Network.h and src/port/Network/Network.cpp. This class wraps SDL2-net's TCP socket API into a manageable interface with the following responsibilities:

  • Host resolution: Uses SDLNet_ResolveHost() to convert server addresses to network-ready structures
  • Socket creation: Opens TCP connections via SDLNet_TCP_Open()
  • Thread management: Spawns and controls a dedicated receive thread
  • State tracking: Maintains isEnabled and isConnected flags for lifecycle control

Connection Establishment

When Enable(host, port) is called, the Network class resolves the server address and opens a TCP socket. The method signature follows this pattern:

void Enable(const char* host, int port);

This spawns the receive thread that runs continuously while both isEnabled and isConnected remain true.

Threaded Receiving Without Blocking

The Network::ReceiveFromServer() method implements non-blocking I/O to prevent frame drops in the main game loop. The implementation uses SDL2-net's socket sets:

// Pseudo-structure of the receive loop
SDLNet_SocketSet set = SDLNet_AllocSocketSet(1);
SDLNet_TCP_AddSocket(set, tcpSocket);

while (isEnabled && isConnected) {
    if (SDLNet_CheckSockets(set, timeout) > 0) {
        // Data available - read without blocking main thread
        int received = SDLNet_TCP_Recv(tcpSocket, buffer, bufferSize);
        // Process into receivedData buffer...
    }
}

Key SDL2-net functions used:

  • SDLNet_AllocSocketSet() — Creates a set for polling multiple sockets
  • SDLNet_CheckSockets() — Non-blocking check for readable sockets
  • SDLNet_TCP_Recv() — Retrieves incoming byte data

Packet Framing for Message Boundaries

Raw TCP streams lack inherent message boundaries. Lighthouse solves this with null-terminated packet framing:

  • Outgoing packets: All messages terminate with '\0'
  • Incoming processing: Bytes accumulate in receivedData, split on the delimiter, and complete payloads forward to OnIncomingJson

This simple framing strategy guarantees that JSON objects arrive intact regardless of TCP segmentation or buffering behavior.

Virtual Callbacks for Game-Specific Logic

The Network class defines five virtual methods that subclasses override to implement gameplay logic:

Callback Purpose
OnIncomingData Raw byte array handling (rarely used directly)
OnIncomingJson Parsed JSON message dispatch
OnConnected Connection established handshake
OnDisconnected Cleanup and reconnection logic
ProcessOutgoingPackets Custom send-queue processing

Subclasses like game-specific network handlers implement protocol logic by overriding these methods. The src/port/Network/Anchor/ directory contains packet definitions—PlayerUpdate.cpp, FightUpdate.cpp, and others—that serialize game state into JSON for transmission.

Example: Handling Remote Player Movement

class GameNetwork : public Network {
public:
    void OnIncomingJson(nlohmann::json payload) override {
        if (payload["type"] == "player_move") {
            ApplyRemotePlayerMove(payload);
        }
    }
};

Sending Data to Remote Servers

The Network class provides two methods for outbound communication:

SendDataToRemote(const std::string& data)

  • Direct wrapper around SDLNet_TCP_Send()
  • Transmits raw string data including null terminator

SendJsonToRemote(const nlohmann::json& payload)

  • Serializes JSON via payload.dump()
  • Forwards to SendDataToRemote with framing applied
// Example: Transmitting player state update
nlohmann::json update;
update["type"] = "player_move";
update["playerId"] = 3;
update["position"] = { "x", 12.5, "y", 8.0 };

net->SendJsonToRemote(update);

Conditional Compilation with USE_NETWORKING

Every SDL2-net call is wrapped in #ifdef USE_NETWORKING blocks. This design:

  • Eliminates SDL2-net dependency for single-player builds
  • Reduces binary size when networking is unused
  • Simplizes porting to platforms without network support

The UI entry point in src/port/UI/LighthouseMenuNetwork.cpp provides the player-facing interface, but all underlying calls compile out cleanly when the flag is absent.

Summary

  • SDL2-net integration in Lighthouse centers on a Network class in src/port/Network/ that wraps TCP socket operations
  • Non-blocking receive threads use SDLNet_CheckSockets() to avoid main loop stalls
  • Null-terminated framing ensures reliable JSON message boundaries over TCP streams
  • Virtual callbacks decouple network transport from game-specific protocol handlers
  • The USE_NETWORKING macro enables complete compile-time exclusion of multiplayer code

Frequently Asked Questions

What SDL2-net functions does Lighthouse use for multiplayer?

Lighthouse uses SDLNet_Init(), SDLNet_Quit(), SDLNet_ResolveHost(), SDLNet_TCP_Open(), SDLNet_AllocSocketSet(), SDLNet_CheckSockets(), SDLNet_TCP_Recv(), and SDLNet_TCP_Send() according to the source in src/port/Network/Network.cpp.

How does Lighthouse prevent networking from blocking the game?

The engine uses a dedicated receive thread with SDLNet_CheckSockets() for non-blocking polling. This thread runs independently of the main game loop, checking for available data without halting rendering or input processing.

Can Lighthouse be built without multiplayer support?

Yes. All SDL2-net code is guarded by #ifdef USE_NETWORKING. Building without this flag excludes the entire networking subsystem, producing smaller binaries with no SDL2-net dependency.

Where is the multiplayer protocol defined in the source?

Game-specific message types are defined in src/port/Network/Anchor/, with files like PlayerUpdate.cpp and FightUpdate.cpp handling serialization. The base framing and transport logic resides in src/port/Network/Network.cpp.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →