Skip to content

Graphics rendering

Rendering is driven by Renderer3D (gui/src/renderer/Renderer3D.cpp), assisted by IsoCamera (camera), AnimatedSprite (animations) and PlayerMotion (movement interpolation). Everything happens in a single, single-threaded loop.

The main loop

Every frame, main runs the same sequence (gui/src/core/main.cpp:67):

flowchart LR
    A[net.poll] --> B[parse lines]
    B --> C[gs.update dt<br/>decrement timers]
    C --> D[motionTracker.update<br/>interpolation]
    D --> E[renderer.update<br/>camera input]
    E --> F[renderer.draw<br/>3D scene + HUD]
    F --> A
  1. net.poll() — reads the non-blocking socket; if the server closes the connection, we leave the loop (gui/src/core/main.cpp:68).
  2. Parsing — as long as complete lines remain, they are passed to the MessageParser, which mutates the GameState (gui/src/core/main.cpp:70).
  3. gs.update(dt) — decrements all animation timers (broadcast, eject, eat, drop, fork, spawn, incantation, death) and expires dead players (gui/src/core/GameState.hpp:48).
  4. motionTracker.update(dt) — advances each player's position and orientation interpolation (gui/src/core/main.cpp:75).
  5. renderer.update(dt, gs) — reads the keyboard for the camera (gui/src/renderer/Renderer3D.cpp:105).
  6. renderer.draw(gs) — draws the 3D scene then the HUD; otherwise Waiting for server data... until the map is received (gui/src/core/main.cpp:81).

The background is dark gray {30, 30, 35} (gui/src/core/main.cpp:79); an FPS counter is shown top-right, and the Winner: <team> banner appears at the end of the game.

The 3D scene

Camera (IsoCamera)

Despite its name, the camera is a perspective Camera3D (fovy 45°, CAMERA_PERSPECTIVE, gui/src/renderer/IsoCamera.cpp:12) that orbits the center of the map. Its position is computed from a radius, a height and an angle (gui/src/renderer/IsoCamera.cpp:33):

_camera.position = {
    target.x + std::cos(_angle) * _radius,
    _height,
    target.z + std::sin(_angle) * _radius,
};

On the first frame, fitCameraToMap fits radius and height to the map size (gui/src/renderer/Renderer3D.cpp:80). Keyboard controls (gui/src/renderer/IsoCamera.cpp:20):

Key Action
Q / E Orbit (angle ±)
W / S Camera height
A / D Radius (zoom in/out)

Radius and height are clamped (kMinRadius, kMinHeight). A hint is shown bottom-left: W/S height | A/D radius | Q/E orbit (gui/src/renderer/Renderer3D.cpp:327).

README key table

The GUI README lists keys that are partly inconsistent with the code. The reliable reference is the on-screen hint above, which matches the code.

Tiles

Each tile is a flat cube ({1, 0.2, 1}) drawn with a checkerboard gradient: the shade depends on (x + y) % 4 (gui/src/renderer/Renderer3D.cpp:111). A tile holding resources is slightly brightened. The cube is outlined with white wireframe and two corner lines.

Resources and stones are small colored spheres, one per type present on the tile (shown only if the quantity is > 0), laid out at fixed offsets (gui/src/renderer/Renderer3D.cpp:155). The color table follows the order of the ResourceType enum (gui/src/renderer/Renderer3D.cpp:140):

Index Resource Color
0 Food orange
1 Linemate silver
2 Deraumere purple
3 Sibur green
4 Mendiane blue
5 Phiras red
6 Thystame teal

Players (animated billboards)

Each player is a billboard sprite (DrawBillboardRec, gui/src/renderer/Renderer3D.cpp:234), always facing the camera. Its position and angle come from the PlayerMotion interpolation (renderX/renderY/renderAngle).

Left/right facing by mirroring. Rather than four sprites per direction, the code flips the texture horizontally (negative source width) when sin(angle) < 0 (gui/src/renderer/Renderer3D.cpp:224), which is enough to convey looking left or right.

Animation priority chain. The animation played is chosen by a cascade of else if (gui/src/renderer/Renderer3D.cpp:176), from highest to lowest priority:

spawn → dying (fall_crouch) → incantation end → incantation (loop)
      → broadcast (scream) → eject (push) → eat → drop (sit_anim)
      → fork (give birth) → walk (if moving)
      → iddle (< 3 s idle) → iddle_sit (after 3 s)

Each animation has its own frames-per-second (gui/src/renderer/Renderer3D.hpp:57). The player level is not drawn on the 3D sprite; it would only appear on the 2D info card — which is frozen (see below).

Team recoloring (GLSL shader)

The sprites are painted green in the assets; a shader recolors them per team at draw time. If the shader loaded (_recolorShader.id > 0), the renderer activates BeginShaderMode and passes the team color through the teamColor uniform (gui/src/renderer/Renderer3D.cpp:229).

The shader (gui/assets/shaders/recolor.frag, GLSL 330) works by hue:

  1. converts the pixel RGB → HSL;
  2. detects "green" pixels: saturation > 0.2 and hue within 0.15 of green (120° = 0.333), the distance computed accounting for the 0/1 wrap-around (gui/assets/shaders/recolor.frag:73);
  3. for those pixels only, replaces the hue with that of the team color, then converts back to RGB. Non-green pixels are left untouched.

Team colors come from a fixed 8-entry palette, assigned in encounter order (colorForTeam, gui/src/renderer/Renderer3D.cpp:87); beyond 8 teams, the palette repeats.

If the shader does not load

Without a GLSL 330 driver, LoadShader fails and _recolorShader.id stays 0: the sprites remain green (no recoloring). See Installing Raylib.

Eggs

Eggs are animated billboards (21 frames, kEggFps = 4, gui/src/renderer/Renderer3D.cpp:245), at reduced height (tileSize × 0.8). An egg whose showDelay is still positive is hidden (display deferred until the parent's fork animation, gui/src/renderer/Renderer3D.cpp:307).

Depth-sorting of transparent sprites

Because the billboards are transparent, their draw order matters. The renderer (gui/src/renderer/Renderer3D.cpp:279):

  1. flushes the opaque geometry batch (rlDrawRenderBatchActive) then disables depth writes (rlDisableDepthMask) and enables alpha blending;
  2. sorts players back-to-front by squared distance to the camera;
  3. draws them in that order, then flushes again before restoring depth writes.

Without this handling, nearby sprites would wrongly occlude distant ones.

Movement interpolation (PlayerMotion)

The server sends only integer positions; rendering, however, is smooth thanks to PlayerMotion (gui/src/player/PlayerMotion.cpp). Every frame, the current position advances toward the target at kMoveSpeed = 4 tiles/s, and the angle toward its target at kTurnSpeed = 12 rad/s (gui/src/player/PlayerMotion.hpp:35).

The key point is the shortest path on the torus. The Zappy map is toroidal: a player can "exit" one edge and reappear on the opposite one. On a retarget, the code computes the smallest signed displacement accounting for the wrap-around (shortestDelta, gui/src/player/PlayerMotion.cpp:33), so the sprite crosses the edge instead of walking back across the whole map. The same logic applies to the angle (wrap-around over 2π).

Asset layout

loadAssets (gui/src/renderer/Renderer3D.cpp:38) loads exactly 13 animation folders plus the shader, the font and the player card. Each AnimatedSprite::load(folder, maxFrames) reads 1.png, 2.png… up to maxFrames, skips missing files, and loops the index modulo the frame count (gui/src/renderer/AnimatedSprite.cpp:8).

Folder Frames Use
walk 30 walking
iddle 120 idle (< 3 s)
iddle_sit 120 sitting (> 3 s)
scream? 73 broadcast
push 46 eject
fall_crouch 35 death
eat 125 picking up food
sit_anim 28 drop
give birth 194 fork (laying egg)
spawn 16 spawn
egg 21 egg
incantation_start 87 incantation (loop)
incantation_end 145 incantation end

Added to that: shaders/recolor.frag (teamColor uniform), font.ttf (LoadFontEx size 96, HUD) and the playerCard/ folder (gui/src/renderer/Renderer3D.cpp:54). Many other folders present on disk (e.g. run, jump, Walk green cat…) are not loaded.

Incomplete areas

Frozen player info card (PlayerCard)

The 2D PlayerCard panel is drawn but not functional. The _focusedPlayerId field is always -1 and is never set (gui/src/renderer/Renderer3D.hpp:53), so the "selected player" branch never runs. The active draw always passes an empty PlayerData with the hardcoded name "Salut" (gui/src/renderer/Renderer3D.cpp:325, gui/src/renderer/PlayerCard.cpp:20). See Known limitations.

sst and mct commands not handled

The GUI has no handler for sst (server-side frequency change). The mct command is explicitly a no-op on the GUI side (the server expands it into a series of per-tile bct). Details in GUI ↔ Server protocol and Known limitations.

Broadcasts and incantations: no visual overlay

A broadcast only triggers the scream animation, with no text bubble; an incantation plays incantation_start/incantation_end on the sprite, with no tile highlight.