Aller au contenu

Rendu graphique

Le rendu est piloté par Renderer3D (gui/src/renderer/Renderer3D.cpp), assisté de IsoCamera (caméra), AnimatedSprite (animations) et PlayerMotion (interpolation des déplacements). Tout se déroule dans une boucle unique, mono-thread.

La boucle principale

Chaque frame, main exécute la même séquence (gui/src/core/main.cpp:67) :

flowchart LR
    A[net.poll] --> B[parse lignes]
    B --> C[gs.update dt<br/>décrémente timers]
    C --> D[motionTracker.update<br/>interpolation]
    D --> E[renderer.update<br/>entrées caméra]
    E --> F[renderer.draw<br/>scène 3D + HUD]
    F --> A
  1. net.poll() — lit la socket non bloquante ; si le serveur ferme la connexion, on quitte la boucle (gui/src/core/main.cpp:68).
  2. Parsing — tant qu'il reste des lignes complètes, on les passe au MessageParser, qui mute le GameState (gui/src/core/main.cpp:70).
  3. gs.update(dt) — décrémente tous les timers d'animation (broadcast, eject, eat, drop, fork, spawn, incantation, mort) et expire les joueurs morts (gui/src/core/GameState.hpp:48).
  4. motionTracker.update(dt) — fait avancer l'interpolation de position et d'orientation de chaque joueur (gui/src/core/main.cpp:75).
  5. renderer.update(dt, gs) — lit le clavier pour la caméra (gui/src/renderer/Renderer3D.cpp:105).
  6. renderer.draw(gs) — dessine la scène 3D puis le HUD ; sinon Waiting for server data... tant que la carte n'est pas reçue (gui/src/core/main.cpp:81).

Le fond est gris foncé {30, 30, 35} (gui/src/core/main.cpp:79) ; un compteur de FPS est affiché en haut à droite, et la bannière Winner: <team> apparaît en fin de partie.

La scène 3D

Caméra (IsoCamera)

Malgré son nom, la caméra est une Camera3D en perspective (fovy 45°, CAMERA_PERSPECTIVE, gui/src/renderer/IsoCamera.cpp:12) qui orbite autour du centre de la carte. Sa position est calculée à partir d'un rayon, d'une hauteur et d'un angle (gui/src/renderer/IsoCamera.cpp:33) :

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

Au premier affichage, fitCameraToMap ajuste rayon et hauteur à la taille de la carte (gui/src/renderer/Renderer3D.cpp:80). Les contrôles clavier (gui/src/renderer/IsoCamera.cpp:20) :

Touche Action
Q / E Orbite (angle ±)
W / S Hauteur de la caméra
A / D Rayon (zoom avant/arrière)

Le rayon et la hauteur sont bornés (kMinRadius, kMinHeight). Une aide est affichée en bas à gauche : W/S height | A/D radius | Q/E orbit (gui/src/renderer/Renderer3D.cpp:327).

Table de touches du README

Le README du GUI liste des touches partiellement incohérentes avec le code. La référence fiable est l'aide à l'écran ci-dessus, qui correspond au code.

Tuiles

Chaque tuile est un cube plat ({1, 0.2, 1}) dessiné avec un dégradé en damier : la teinte dépend de (x + y) % 4 (gui/src/renderer/Renderer3D.cpp:111). Une tuile contenant des ressources est légèrement éclaircie. Le cube est cerclé de fil de fer blanc et de deux lignes d'angle.

Les ressources et pierres sont des petites sphères colorées, une par type présent sur la tuile (affichée seulement si la quantité est > 0), disposées à des décalages fixes (gui/src/renderer/Renderer3D.cpp:155). La table de couleurs suit l'ordre de l'énumération ResourceType (gui/src/renderer/Renderer3D.cpp:140) :

Index Ressource Couleur
0 Food (nourriture) orange
1 Linemate argent
2 Deraumere violet
3 Sibur vert
4 Mendiane bleu
5 Phiras rouge
6 Thystame turquoise

Joueurs (billboards animés)

Chaque joueur est un sprite billboard (DrawBillboardRec, gui/src/renderer/Renderer3D.cpp:234), toujours orienté face à la caméra. Sa position et son angle proviennent de l'interpolation PlayerMotion (renderX/renderY/renderAngle).

Orientation gauche/droite par symétrie. Plutôt que quatre sprites par direction, le code retourne horizontalement la texture (largeur source négative) quand sin(angle) < 0 (gui/src/renderer/Renderer3D.cpp:224), ce qui suffit à donner l'impression de regarder à gauche ou à droite.

Chaîne de priorité d'animation. L'animation jouée est choisie par une cascade de else if (gui/src/renderer/Renderer3D.cpp:176), de la plus prioritaire à la moins prioritaire :

spawn → dying (fall_crouch) → fin d'incantation → incantation (boucle)
      → broadcast (scream) → eject (push) → eat → drop (sit_anim)
      → fork (give birth) → walk (si en mouvement)
      → iddle (< 3 s d'inactivité) → iddle_sit (après 3 s)

Chaque animation a son propre nombre d'images par seconde (gui/src/renderer/Renderer3D.hpp:57). Le niveau du joueur n'est pas dessiné sur le sprite 3D ; il n'apparaîtrait que sur la carte d'information 2D — qui est figée (voir plus bas).

Recoloration d'équipe (shader GLSL)

Les sprites sont peints en vert dans les assets ; un shader les recolore selon l'équipe au moment du dessin. Si le shader s'est chargé (_recolorShader.id > 0), le renderer active BeginShaderMode et transmet la couleur de l'équipe via l'uniforme teamColor (gui/src/renderer/Renderer3D.cpp:229).

Le shader (gui/assets/shaders/recolor.frag, GLSL 330) procède par teinte :

  1. convertit le pixel RGB → HSL ;
  2. détecte les pixels « verts » : saturation > 0.2 et teinte à moins de 0.15 du vert (120° = 0.333), distance calculée en tenant compte du bouclage 0/1 (gui/assets/shaders/recolor.frag:73) ;
  3. pour ces pixels seulement, remplace la teinte par celle de la couleur d'équipe, puis reconvertit en RGB. Les pixels non verts restent intacts.

Les couleurs d'équipe viennent d'une palette fixe de 8 entrées, attribuée dans l'ordre d'apparition (colorForTeam, gui/src/renderer/Renderer3D.cpp:87) ; au-delà de 8 équipes, la palette se répète.

Si le shader ne se charge pas

Sans pilote GLSL 330, LoadShader échoue et _recolorShader.id reste à 0 : les sprites restent verts (pas de recoloration). Voir Installation de Raylib.

Œufs

Les œufs sont des billboards animés (21 images, kEggFps = 4, gui/src/renderer/Renderer3D.cpp:245), à hauteur réduite (tileSize × 0.8). Un œuf dont le showDelay est encore positif est masqué (affichage différé jusqu'à l'animation de ponte du parent, gui/src/renderer/Renderer3D.cpp:307).

Tri en profondeur des sprites transparents

Comme les billboards sont transparents, leur ordre de dessin compte. Le renderer (gui/src/renderer/Renderer3D.cpp:279) :

  1. vide le lot de géométrie opaque (rlDrawRenderBatchActive) puis désactive l'écriture de profondeur (rlDisableDepthMask) et active le blending alpha ;
  2. trie les joueurs de l'arrière vers l'avant par distance au carré à la caméra ;
  3. les dessine dans cet ordre, puis vide à nouveau le lot avant de restaurer l'écriture de profondeur.

Sans cette gestion, des sprites proches masqueraient à tort des sprites éloignés.

Interpolation des déplacements (PlayerMotion)

Le serveur n'envoie que des positions entières ; le rendu, lui, est fluide grâce à PlayerMotion (gui/src/player/PlayerMotion.cpp). À chaque frame, la position courante avance vers la cible à kMoveSpeed = 4 tuiles/s, et l'angle vers sa cible à kTurnSpeed = 12 rad/s (gui/src/player/PlayerMotion.hpp:35).

Le point clé est le chemin le plus court sur le tore. La carte de Zappy est toroïdale : un joueur peut « sortir » d'un bord et réapparaître à l'opposé. Lors d'un retarget, le code calcule le plus petit déplacement signé en tenant compte du bouclage (shortestDelta, gui/src/player/PlayerMotion.cpp:33), de sorte que le sprite traverse le bord plutôt que de revenir en arrière à travers toute la carte. La même logique s'applique à l'angle (bouclage sur 2π).

Disposition des assets

loadAssets (gui/src/renderer/Renderer3D.cpp:38) charge précisément 13 dossiers d'animation plus le shader, la police et la carte joueur. Chaque AnimatedSprite::load(dossier, maxFrames) lit 1.png, 2.png… jusqu'à maxFrames, ignore les fichiers manquants, et boucle l'index modulo le nombre d'images (gui/src/renderer/AnimatedSprite.cpp:8).

Dossier Images Usage
walk 30 marche
iddle 120 repos (< 3 s)
iddle_sit 120 assis (> 3 s)
scream? 73 broadcast
push 46 éjection
fall_crouch 35 mort
eat 125 ramassage de nourriture
sit_anim 28 dépôt
give birth 194 fork (ponte)
spawn 16 apparition
egg 21 œuf
incantation_start 87 incantation (boucle)
incantation_end 145 fin d'incantation

À cela s'ajoutent : shaders/recolor.frag (uniforme teamColor), font.ttf (LoadFontEx taille 96, HUD) et le dossier playerCard/ (gui/src/renderer/Renderer3D.cpp:54). Beaucoup d'autres dossiers présents sur le disque (ex. run, jump, Walk green cat…) ne sont pas chargés.

Points incomplets

Carte d'information joueur (PlayerCard) figée

Le panneau 2D PlayerCard est dessiné mais non fonctionnel. Le champ _focusedPlayerId vaut toujours -1 et n'est jamais positionné (gui/src/renderer/Renderer3D.hpp:53), donc la branche « joueur sélectionné » ne s'exécute jamais. Le dessin actif passe systématiquement une PlayerData vide avec le nom codé en dur « Salut » (gui/src/renderer/Renderer3D.cpp:325, gui/src/renderer/PlayerCard.cpp:20). Voir Limitations connues.

Commandes sst et mct non traitées

Le GUI n'a aucun handler pour sst (changement de fréquence côté serveur). La commande mct est explicitement un no-op côté GUI (le serveur la développe en une série de bct par tuile). Détails dans Protocole GUI ↔ Serveur et Limitations connues.

Broadcasts et incantations : pas de surcouche visuelle

Un broadcast ne déclenche que l'animation scream, sans bulle de texte ; une incantation joue incantation_start/incantation_end sur le sprite, sans surlignage de tuile.