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
net.poll()— reads the non-blocking socket; if the server closes the connection, we leave the loop (gui/src/core/main.cpp:68).- Parsing — as long as complete lines remain, they are passed to the
MessageParser, which mutates theGameState(gui/src/core/main.cpp:70). 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).motionTracker.update(dt)— advances each player's position and orientation interpolation (gui/src/core/main.cpp:75).renderer.update(dt, gs)— reads the keyboard for the camera (gui/src/renderer/Renderer3D.cpp:105).renderer.draw(gs)— draws the 3D scene then the HUD; otherwiseWaiting 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:
- converts the pixel RGB → HSL;
- detects "green" pixels: saturation
> 0.2and hue within0.15of green (120° = 0.333), the distance computed accounting for the 0/1 wrap-around (gui/assets/shaders/recolor.frag:73); - 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):
- flushes the opaque geometry batch (
rlDrawRenderBatchActive) then disables depth writes (rlDisableDepthMask) and enables alpha blending; - sorts players back-to-front by squared distance to the camera;
- 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.