R-Type
R-Type is a remake of the classic R-Type game, built with a custom modern C++ game engine. It also features an advanced multiplayer mode, allowing players to complete levels together.
Full documentation is available at baptiste0928.github.io/epitech-rtype.
Building
The R-Type is written in C++ and uses CMake and Ninja as its build system.
It can be built on both Linux and Windows.
Dependencies
The project compiles the raylib graphical library during its build process, which may require additional dependencies depending on your platform. Refer to the raylib README for platform-specific instructions.
Building
We provide CMake presets for building for Linux and Windows using Ninja.
If you are using Linux, you can cross-compile for Windows by installing MSVC using msvc-wine. Make sure to add
cl
to yourPATH
.
# Linux (output: build/)
$ cmake --preset ninja
# Windows (output: build-win/)
$ cmake --preset ninja-win
This may take a while the first time you run it, as it will download all the
required dependencies. These are cached in .cpm_cache
for subsequent builds.
Then, build the project:
$ cmake --build build
The build will default to a Debug
build. Use the --config Release
option to
build in Release
mode (with optimizations enabled).
The resulting executable will be in build/r-type/Debug/r-type
(or
build-win/r-type/Debug/r-type.exe
on Windows).
Networking
The R-Type game features a multiplayer mode, backed by a custom advanced networking protocol. The following document explains the high-level architecture of the protocol. You can refer to the RFCs for detailed information about the protocol and the design decisions.

Design goals
While the protocol is designed to be as simple and modular as possible (to allow some parts to be reused in other games), it should also be designed around the constraints of a fast-paced co-op game like the R-Type.
The protocol is designed around the following goals:
- Good gameplay experience: the player should be able to enjoy the game
in the best possible way, even in bad network conditions.
- R-Type is a fast-paced game, where moving a pixel too far or a millisecond too far may be the difference between a win and a loss.
- The game is a co-op game, not a PvP game. Even if coordination with teammates should feel reactive when possible, we should prioritize the interaction with the game-controlled entities.
- We should avoid any rollbacks/jumps of any entity due to bad network conditions (resulting in a possible loss of synchronisation with the server).
- The player must have the lowest perceptible latency when playing online. For example, we should avoid any latency between the player input and the screen update.
- Graceful degradation: the game may be played in bad network conditions.
The protocol should be designed to provide the best experience possible in
these conditions.
- The protocol should use the least amount of bandwidth possible.
- We should handle high latency or packer loss/temporary disconnection gracefully, without resulting in a loss of synchronisation with the server.
- Dumb clients: the game will provide a global leaderboard, where players can compare their performances. The server must be the source of truth of the game state, and no client should be able to manipulate this state.
The protocol will be used for all communications between the client and the server, including real-time synchronization of the game state or more traditional communications (such as fetching the global leaderboard or in-game chat).
High-level design
The game networking is built around multiple components that are designed to be modular and reusable:
- Transport: the transport layer is protocol built on top of UDP, responsible for sending and receiving packets.
Previous work
The following articles were used as inspiration for the design of the protocol:
- Quake 3 Source Code Review: Network Model
- Peeking into VALORANT’s Netcode
- Reliability and Congestion Avoidance over UDP
Other open-source libraries/protocols were also used as reference:
Transport
The transport protocol is a custom UDP-based communication layer designed for real-time multiplayer games like R-Type. It facilitates reliable server-client communication with essential features for game networking.
Core Features:
- Unreliable messaging for time-critical game state updates
- Reliable messaging with acknowledgment system
- Robust client/server connection handling with timeout detection
- Future enhancements planned:
- Congestion control and latency monitoring
- Protection against replay attacks
- Packet encryption and signing
This protocol is internally referred to as WOKE
(World-state Online
Kommunication Engine).
Packets
The protocol transmits data through UDP datagrams, with a maximum packet size of 1472 bytes (including UDP and protocol headers) 1.
The fundamental unit of communication is a command. The protocol bundles these commands into packets. Application code interacts only with commands, while packet handling is managed internally by the protocol.
Connection Management
The protocol operates on a 30Hz fixed-rate loop for optimal real-time performance.
During each cycle:
- Both client and server transmit packets containing pending commands
- If no commands are queued, a
KeepAlive
packet maintains the connection - Reliable commands are retransmitted until acknowledged
- Connections are terminated after 32 packets remain unacknowledged
Acknowledgement System
The protocol implements a robust acknowledgment mechanism using two header fields:
ack
: Indicates the sequence number of the last received packetack_bitfield
: A 32-bit field tracking previously acknowledged packets
This dual-field system ensures reliable delivery even when acknowledgment packets are lost.
Reliable Command Delivery
For reliable commands, the sender tracks the initial sequence number and continues transmission until acknowledgment. This ensures guaranteed delivery of critical game data.
Reliable commands are guaranteed to be delivered once, but there is not guarantee of order preservation. This must be implemeted at the application level if required for some commands (e.g. by embedding a timestamp in the command payload).
Communication automatically terminates if the number of pending reliable commands exceeds 1024.
Sequence Number Implementation
The protocol uses 16-bit unsigned integers for sequence numbers, implementing
proper wraparound handling to maintain continuous operation (if sequence numbers
reach 65535
, the next sequence number will be 0
).
Splitted Commands
If a reliable command is larger than the maximum packet size, it is split into multiple parts sent in separate packets. The receiver reassembles the command based on the split index and count (see the command structure below).
Packet Structure
Header Format
Every packet includes a 12-byte header with the following structure:
struct PacketHeader {
uint8_t protocol_id; // Fixed value 0x57 ('W')
uint8_t packet_type; // Defines packet purpose
uint16_t client_id; // Client identifier
uint16_t sequence; // Packet sequence number
uint16_t ack; // Last acknowledged sequence
uint32_t ack_bitfield; // Acknowledgment history
}
enum PacketType {
ConnectionRequest = 0,
ConnectionClosed = 1,
Commands = 2,
KeepAlive = 3,
}
Connection Packets
- ConnectionRequest: Initial client connection request with optional validation payload
- ConnectionClosed: Immediate connection termination signal
- Commands: Container for multiple game commands
- KeepAlive: Connection maintenance packet
Command Structure:
struct Command {
uint16_t command_type; // 10 bits type + 6 bits flags
uint16_t data_size; // Payload length
uint8_t data[]; // Variable-length payload
}
The command_type
field is at least 16
bits but might contain additional data depending on the command flags.
- The first flag bit indicates whether the command is reliable
- The second flag bit indicates whether the command is a part of a larger splitted command
Note the splitted commands are only supported on reliable commands.
Reliable Commands Type
struct ReliableCommandType {
uint16_t command_type; // 10 bits type + 6 bits flags (0b10)
uint16_t reliable_id; // Unique identifier for reliable command
}
Split Commands Type
struct SplitCommandType {
uint16_t command_type; // 10 bits type + 6 bits flags (0b11)
uint16_t reliable_id; // Unique identifier for reliable command
uint16_t split_id; // Unique identifier for splitted command
uint16_t split_index; // Index of the command part
uint16_t split_count; // Total number of command parts
}
References
Built upon principles from:
Aligns with standard network MTU. Behavior on networks with smaller MTU is undefined.
Usage
The transport module interface is designed to be minimal and similar to existing networking libraries in C++.
The library interface is fully synchronous, but the transport runtime is executed in a separate asynchronous thread to allow for high-performance networking.
The library is uses spdlog
for logging.
Command types
Clients and servers designed to work together must share the same command types. These command types define the unique id of the command and whether it is reliable or not.
constexpr auto ReliableMsgType = woke::CommandType(1, woke::CommandTypeFlag::Reliable);
constexpr auto RegularMsgType = woke::CommandType(2);
Deserialization is left to the user to implement, but the library provides utility functions to work with the serialize.h
library.
struct MessageData {
uint32_t value;
bool flag = true;
template <typename Stream>
bool Serialize(Stream &stream)
{
serialize_bits(stream, value, 32);
serialize_bool(stream, flag);
serialize_align(stream);
return true;
}
};
Server
Server is created with a listen port, and can accept new connections with the accept()
methods.
woke::Server server(13042);
server.start(); // Starts the server thread
while (true) {
auto connection = server.accept();
if (connection.isClosed()) {
break;
}
std::thread(handleClient, std::move(connection)).detach();
}
Client
Client is created with a remote address and port, and can connect to the server with the connect()
method.
woke::Client client(argv[1], 13042);
auto connection = client.connect();
Connection
The connection object is used to send and receive messages.
MessageData data{42, true};
connection.send(woke::Command::serialize<MessageData>(ReliableMsgType, data));
for (const auto &command : connection.receive_all()) {
if (command.type() == ReliableMsgType) {
auto msg = command.deserialize<MessageData>();
}
Examples
See the client.cpp
and server.cpp
examples for more information.
RaySchism
RaySchism is a C++20 game engine built on top of raylib.
Dependencies :
- [
raylib
] (https://github.com/raysan5/raylib), cross platform rendering librairy - personnal [
raylib-cpp
] (https://github.com/JacquesHumule/raylib-cpp) fork cpp wrapper for raylib
How to build :
cmake -B . -S . -DCMAKE_BUILD_TYPE=Release
cmake --build .
How to use :
Engine :
#include <rayschism.hpp>
void main()
{
auto engine = rs::Engine(1000, 1000, "R-Type");
auto ship = engine.AddSprite({500, 500},
{0, 0, 32, 32},
{"image.png"},
{0, 0, 0});
while (!WindowShouldClose())
{
engine.UpdatePositions();
engine.Display();
}
}
Entities
Entities are the basic unit of reference described by its components.
Using entities :
Creating an empty entity :
Engine engine;
Entity entity = engine.CreateEntity();
The result represents the unique identifier of the entity. It let you group components together.
Destroying an entity :
Entity entity = engine.CreateEntity();
engine.DestroyEntity(entity);
This will destroy the entity and all its components.
View also :
Components
Components are the basic unit of data. They are used to store data but are also the set of prerequisites for a system to update them.
Using components :
Registering a component :
Engine engine;
engine.RegisterComponent<Position>();
Warning: Components must be registered before being used.
Unregistering a component :
engine.UnregisterComponent<Position>();
Adding a component to an entity :
engine.AddComponent(entity, Position{0, 0});
Getting a component from an entity :
auto &position = engine.GetComponent<Position>(entity);
Removing a component from an entity :
engine.RemoveComponent(entity);
Systems
Systems are the description of how the game should be updated.
How to use systems:
Registering a system:
You must register your system with your component dependencies.
auto engine = rs::Engine(1000, 1000, "R-Type");
engine.RegisterComponent<Position>();
engine.RegisterComponent<Velocity>();
engine.RegisterSystem<VelocityMovement, Position, Velocity>();
Warning: All the components that a system will use must be registered beforehand.
Updating a system:
All entities matching the system signature will be updated.
engine.UpdateSystem<VelocityMovement, Position, Velocity>();
Info: All entities matching the system signature will be updated.