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
isEnabledandisConnectedflags 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 socketsSDLNet_CheckSockets()— Non-blocking check for readable socketsSDLNet_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 toOnIncomingJson
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
SendDataToRemotewith 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
Networkclass insrc/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_NETWORKINGmacro 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →