R-Type 0.1
Loading...
Searching...
No Matches
Technical and comparative study

This page justifies each technical choice of the project. For every topic it lists the options we considered, compares them on the criteria that matter for a real time networked game, and states what we chose and why. Weak points of the current choices are listed too, with the planned answer.

1. Criteria

Criterion Why it matters here
Latency and performance The game simulates and draws at 60 frames per second on both sides.
Portability The server and the client must run on Linux and should run on Windows.
Decoupling Rendering, networking and gameplay must stay independent layers.
Robustness Any byte from the network may be hostile.
Simplicity A small team, six weeks, and code that new members can read.
Subject constraints C++, a build system generator, a package manager, UDP, no full game engine.

2. Programming language

Option Performance Portability Ecosystem for games Team knowledge
C++20 Native, no garbage collector Every platform Largest (Asio, Vulkan, SDL, SFML) High
Rust Native, memory safe Every platform Growing, fewer mature 2D libraries Low
C# JIT and garbage collector pauses Good with .NET Strong, but mostly tied to Unity Medium
C Native Every platform Large but low level High

Choice: C++20. It is required by the project, and it is also the best fit: deterministic memory management with no collector pause, direct access to Vulkan and to the operating system, and modern features that we use on purpose. Concepts (rtk::Game, rtk::Scene, rtk::ecs::concepts::IsRenderWindow) give interfaces checked at compile time without virtual calls, std::variant holds the active scene, and std::bit_cast converts sprites and big-endian values without undefined behavior.

3. Graphics, window, input and audio library

Option Scope Rendering path Sprite batching Portability Notes
rtk graphical Window, sprites, input, sound Vulkan Yes, hardware instancing Linux, Windows, macOS release builds Sprite layout identical to the engine one
SFML 2 or 3 Window, 2D, audio, network OpenGL Manual with vertex arrays Linux, Windows, macOS Very easy to use, one draw call per sprite by default
SDL3 Window, input, audio, 2D renderer Several backends Basic Every platform Low level, C API
Raylib Window, 2D and 3D, audio OpenGL Internal Every platform Broad scope, game oriented helpers

Choice: rtk graphical. It does only what the subject allows a library to do: open a window, draw sprites, read inputs and play sounds. It receives all the sprites of a frame in a single call and renders them with Vulkan hardware instancing and bindless textures, which keeps the cost per sprite low even with many bullets and tiles on screen. Its sprite structure has the same memory layout as rtk::ecs::concepts::SpriteData, so the client adapter converts a whole frame with std::bit_cast, without copying field by field.

Drawbacks. It requires a Vulkan driver, and it has a much smaller community than SFML or SDL. To limit the risk, the engine never includes it: the render and input systems only depend on the rtk::ecs::concepts::IsRenderWindow concept and the audio system on rtk::ecs::concepts::IsAudioManager. Switching to SFML or SDL would only require a new adapter class.

4. Engine architecture

4.1 Object model

Option Decoupling Data locality Adding a feature Drawbacks
Inheritance tree (Entity, Ship, Enemy) Low, behavior and data mixed Poor, objects scattered on the heap New subclass, often a diamond Rigid, "god" base classes
Component objects (Unity style GameObject with virtual components) Medium Poor, one allocation per component New component class Virtual call per component per frame
Entity Component System High, data and behavior separated Good, one dense array per component type New component and system, nothing else changes Logic spread across systems, needs a clear naming scheme

Choice: ECS, as recommended by the subject. It gives exactly the decoupling we need: the render system reads only component::Transform and rtk::ecs::concepts::SpriteData and knows nothing about speed, health or damage. The same components and systems run on the server and on the client, and the server simply does not register the rendering ones.

4.2 Communication between systems

Option Coupling Ordering Notes
Direct calls between systems Strong Explicit Systems need references to each other
Shared components only Weak Implicit, through stages Good for continuous data, awkward for one shot events
Mediator, as a typed event bus Weak Immediate Good for one shot events such as a collision or a spawn

Choice: components for continuous state, plus an event bus for events. busEvent is a mediator: publishers and subscribers only know the event type. It is how rtk::systems::ColliderSystem reports collisions without knowing the gameplay rules, and how rtk::systems::EnemySpawnerSystem tells the server code that an enemy must be announced to the clients.

4.3 Scene management

Option Allocation Checked at Notes
Abstract Scene class and std::unique_ptr One per switch Run time Classic, needs virtual calls
Stack of scenes One per push Run time Useful for overlays such as pause
std::variant of scene types None Compile time The set of scenes is fixed per program

Choice: std::variant. rtk::SceneManager checks at compile time that every scene satisfies rtk::Scene and can be built from the shared dependency object. Only the active scene is alive and switching does not allocate.

5. Algorithms and data structures

Problem Options considered Chosen Why
Component storage Map per entity, array of std::optional, archetype tables (EnTT, flecs), sparse set Sparse set (rtk ECS) O(1) insert, remove and lookup, dense arrays for iteration, simple to understand and debug. Archetype tables iterate faster on many components but cost more on every add and remove.
Entity destruction Immediate, deferred Deferred kill flushed after each stage Systems can kill entities while iterating over a view.
Broad phase collision Brute force O(n^2), quadtree, sweep and prune, uniform spatial hash grid Grid of 64 pixel cells, rebuilt every frame Sprites are 32 to 64 pixels and spread fairly evenly on a horizontal strip, which is the best case for a uniform grid. Rebuilding is linear, and the pair set guarantees each pair is reported once. A quadtree adapts to uneven density we do not have, and adds rebuild cost.
Narrow phase collision Circles, polygons (SAT), axis aligned boxes AABB overlap test Exact enough for 2D sprites, four comparisons per pair.
Collision filtering Pairwise rule table, layer and mask bits 32-bit layer and mask One AND per side, configured from JSON by name.
Blueprint identification on the wire Name string, central enum, 16-bit hash of the name FNV-1a folded to 16 bits Two bytes per entity, no shared table to keep in sync: both sides hash the same names. The current blueprint names were checked and do not collide.
Duplicate and loss detection Per packet ack, TCP, sequence plus 32-bit ack bitfield Sliding window in PacketTracker Constant time and memory, and one header acknowledges 33 packets, so acks survive packet loss.
Positions on the wire float (4 bytes), 16-bit fixed point 0.25 pixel steps over -8192 to 8191.75 Halves the size of each coordinate with no visible loss.
Rotations on the wire float radians, 16-bit fraction of a turn 65536 steps per turn 0.0055 degree resolution, wraps naturally.
Thread hand off Lock free queue, mutex protected std::queue One mutex per queue Only a few dozen messages per frame, so contention is negligible and the code stays obviously correct.
Event dispatch std::any list, virtual visitor, map from std::type_index to handlers busEvent Constant time lookup by type, any struct can be an event.

Custom algorithms. We wrote four algorithms ourselves instead of taking a library, each for a precise reason:

  • The spatial hash grid avoids pulling a full physics engine for what is only box overlap on a 2D strip.
  • The packet tracker is part of our protocol: its window size and its classification rules are protocol rules.
  • The position and rotation quantization is tied to the screen size and the precision the game needs.
  • The archetype hash lets data defined in JSON be identified on the wire without a hand maintained id table.

6. Networking

6.1 Transport

Option Latency under loss Ordering Message boundaries Notes
TCP High: one lost segment blocks every following byte (head of line blocking) Guaranteed None, it is a byte stream Retransmits stale positions that are useless by the time they arrive
UDP Low: each datagram stands alone None Kept, one datagram is one packet Loss, duplication and reordering must be handled by the application
UDP plus a TCP side channel Low for game data Mixed Mixed Two connections to manage and secure

Choice: UDP only. It is required by the subject for all game traffic, and it is the right tool: the world state is sent again 20 times per second, so a lost snapshot is simply replaced by the next one. Delivering a stale snapshot late, as TCP would, only adds lag. The few messages that matter more (creations) are repaired with QueryEntity. We do not need any message that would justify a TCP channel.

6.2 Network library

Option Portability Async model Dependencies Notes
Raw BSD sockets and Winsock Two code paths Manual (poll, select) None Must be wrapped in our own abstractions
Asio, standalone Linux, Windows, macOS Proactor, completion handlers on our thread Header only, no Boost Explicitly allowed by the subject
Boost.Asio Same as Asio Same All of Boost Heavier for the same API
ENet, GameNetworkingSockets Good Own event loop Library and its protocol Brings its own protocol, so we would not design ours

Choice: standalone Asio. One code base runs on Linux and Windows, the asynchronous handlers run on a thread we control, and it adds no other dependency. Libraries such as ENet would solve reliability for us, but the project asks us to design the protocol, and we want full control over validation of every byte.

6.3 Serialization

Option Size Endianness safe Padding safe Cost Attack surface
memcpy of native structs Small No No None Low
JSON or another text format Large Yes Yes Parsing Large parser
Protocol Buffers, FlatBuffers Small Yes Yes Code generation, extra dependency Medium
Fixed layout structs of rtype::protocol::BigEndian fields Small Yes Yes None Low

Choice: fixed layout big-endian structs. rtype::protocol::BigEndian stores a value as a byte array, so its alignment is 1 and a struct made only of such fields has no padding, whatever the field order. A static_assert on every payload size enforces it. The payload can then be copied byte for byte into the packet, on any host. Versioning is manual, which is acceptable for a protocol of eight messages.

Measured effect of quantization. A snapshot entry is 11 bytes. With a 32-bit float per coordinate and for the rotation it would be 17 bytes, so the encoding saves 35 percent. One Snapshot message carries up to 47 entities in a 548 byte datagram.

7. Build and dependencies

Option Integration with CMake Binary cache Notes
Conan Through generators Yes Python tool, separate profile system
vcpkg in manifest mode Native toolchain file Yes Dependencies listed in one JSON manifest
CMake CPM Native No, sources rebuilt Simple but slower clean builds

Choice: CMake and vcpkg. The manifest lists every third party library and vcpkg builds them inside the project tree, so nothing is installed on the system. The two rtk libraries are fetched as prebuilt release archives with CMake FetchContent, which avoids rebuilding them.

8. Storage

8.1 What the project stores

Data Where Written at run time Lifetime
Sprites and sounds PNG and WAV files in assets No Versioned in Git
Entity blueprints JSON files in assets/descriptionEntity No Versioned in Git
World state Server memory, in the ECS registry Yes One round
Connections Server memory Yes One session

There is no player account, progression or score to keep between runs yet.

8.2 Comparison

Technique Persistence Reliability Constraints Fit
Plain files (JSON, PNG, WAV) Permanent, read only Versioned and reviewed in Git, parse errors stop the program at startup Human readable, easy to diff, slower to parse than binary Assets and blueprints
Binary asset pack Permanent, read only Needs its own tooling to verify Fast loading, not diffable Large games, not needed at our size
In memory only None Lost on crash or restart Fastest, no I/O Live world state
SQLite Permanent, single file ACID transactions, survives crashes No server, one writer at a time Scores and settings, planned
Client and server database (PostgreSQL) Permanent Replication, backups A service to run and secure Accounts across many servers, beyond our scope

Choice. Assets and blueprints are plain files under version control: designers edit them without recompiling, every change is reviewed, and a broken file makes the program fail at startup instead of in the middle of a game. The live world stays in memory: a round lasts a few minutes and has no value once it ends, so persisting it would only add I/O on the game loop. If a scoreboard is added, SQLite is the planned choice because it gives crash safe transactions in a single file without running a database server.

9. Security

9.1 Main vulnerabilities by technology

Technology Typical vulnerability Mitigation in the project
C++ Buffer overflow, out of bounds read, use after free All network reads go through PacketReader, which checks the remaining size before every read. Payloads are copied into fixed size structs, never cast in place. Owned memory uses containers and smart pointers.
Custom binary protocol Lying length fields, truncated messages, huge counts that trigger large allocations Declared sizes are compared to the received bytes, packets with trailing bytes are dropped, fixed payloads must match their exact size, and entityCount is capped at 47 before any allocation.
UDP Source address spoofing, replay, flooding Connected packets must carry the connection id bound to their source address. Duplicate and old packets are dropped. Old inputs are rejected by input sequence. Invalid datagrams get no answer, so the server cannot be used to amplify traffic. Datagrams larger than 548 bytes are dropped by the transport.
Asio asynchronous I/O Handler running after the object it uses was destroyed Send buffers are kept alive by shared pointers captured in the handler, and the network thread is joined before the server object is destroyed.
Multithreading Data races Only the network thread touches the socket and the connection table. Data crosses threads through mutex protected queues, and flags are atomic.
JSON blueprints Malformed file, unexpected types Files ship with the game and are read only. Parse errors throw at startup, and missing fields fall back to defaults.
Third party dependencies Compromised or changed upstream release Dependencies come from vcpkg and from release archives, never copied into the repository.

9.2 Data integrity

  • Corruption in transit is caught by the UDP checksum, and then by the structural checks above: a corrupted datagram almost always fails a length or range check and is dropped.
  • The server is the only source of truth. Clients send intentions, so a modified client cannot move another ship, create entities or declare a kill.
  • Inputs are applied in order thanks to the input sequence, and a replayed or delayed input is ignored.

9.3 Known weaknesses

Weakness Risk Planned answer
No limit on the number of connections, and no inactivity timeout A flood of Hello from many addresses grows the connection table forever Cap the connection count, expire silent connections after a few seconds, and answer Rejected when full.
Sequential connection ids An attacker able to spoof the address of a player can guess its id Random 32-bit connection ids, then a nonce exchanged in the handshake.
No encryption or authentication Traffic can be read and forged on the path Acceptable for a LAN game. A keyed message authentication code per packet is the next step for Internet play.
rtk libraries fetched from the latest release An upstream change can alter the build without any change in our repository Pin release versions and verify archive hashes in CMake.

9.4 Long term security monitoring

Measure Status What it catches
Pull request review on every change In place Logic errors and unsafe code before merge
Conventional commit check in CI In place Keeps history readable for audits
NetworkMetrics counters (bytes, packets, duplicates, out of order) In place Abnormal traffic patterns
Server log of connections, disconnections and framing errors In place Suspicious clients, visible in the server console
AddressSanitizer and UndefinedBehaviorSanitizer build in CI Planned Memory errors during tests
Fuzzing of MessageParser, rtype::protocol::readSnapshot() and HandshakeHeader with libFuzzer Planned Parser crashes on unexpected input
clang-tidy with the bugprone and cert checks in CI Planned Common unsafe patterns
Pinned dependency versions with automatic update alerts Planned Known vulnerabilities in third party code
Per address counters of dropped datagrams, with a warning threshold Planned Floods and scanning attempts

10. Summary

Topic Choice
Language C++20
Rendering, input, audio rtk graphical behind engine concepts
Architecture ECS, event bus as mediator, std::variant scenes
Component storage Sparse sets
Collisions Uniform spatial hash grid and AABB
Transport UDP only
Network library Standalone Asio
Serialization Fixed layout big-endian structs, quantized positions
Reliability Sequence and ack bitfield, full snapshots, query based repair
Build CMake and vcpkg
Storage Versioned plain files for assets, memory for live state