SoulTide
Code, build et Tools d'un jeu UE5 en 1 mois
Le jeu
SoulTide est un jeu Unreal Engine 5.5 qu’on a fait à quatre en un mois. On y incarne Charon, le passeur du Styx, déguisé en médecin de la peste.
Le but : récupérer les âmes égarées dans l’archipel et les ramener chacune sur leur île. Sur la route, des anguilles bloquent certains passages : il faut les calmer en leur donnant du poisson, qu’on pêche soi-même.
La boucle de jeu tient là-dedans : récupérer une âme, naviguer, pêcher, nourrir l’anguille, âme suivante.
Univers et direction artistique
L’univers se découpe en sept archipels, un par péché capital.
- Orgueil et Paresse : île forestière, phare sur la côte
- Avarice : ruines, tour encore debout
- Colère : zone glaciaire, presque iceberg
- Envie : décor automnal, feuilles qui volent
- Gourmandise : petite île de sable, squelettes de créatures qui sortent du sol
- Luxure : zone pluvieuse, saules pleureurs et nénuphars
Les âmes apparaissent comme des silhouettes encapuchonnées, on ne voit que leurs yeux qui brillent. La couleur et la forme indiquent le péché auquel elles sont liées.
La DA est stylisée, dans l’esprit Animal Crossing ou A Short Hike : aplats de couleur, légers gradients, peu de détails fins.
Ma part dans le projet
L’équipe s’occupait surtout de l’art et du game design. Moi j’ai pris toute la couche tech : code, outillage et rig. J’ai livré :
- la mini-map et la carte plein écran (Widget Blueprint)
- le système de dialogue, en C++ piloté par DataTable
- l’UI Manager dans le HUD (
SwitchUI,SwitchPopUp,StartDialog) avec chargement asynchrone des widgets - les rigs humanoïdes, sortis en urgence à J-2 du livrable
- le setup d’environnement et les scripts de build pour que les autres puissent compiler sans moi
- la CI/CD qui publie chaque release sur GitHub automatiquement
Mini-map
La mini-map est faite en Widget Blueprint, et se décline en deux vues :
WBP_MapComponent: le mini-affichage in-game (HUD)WBP_MapMenu: la carte plein écran, raccourciIA_OppenMap
Le rendu utilise la texture de carte T_FinaleMap, projetée via le matériau MI_MapProjection. Un masque circulaire M_MapCircleMask découpe le tout en rond.
Trois icônes sont gérées dynamiquement :
WBP_MapIcon-Boat: la position du joueur, orientée selon le capWBP_MapIcon-Fish: les bancs de poissons repérésWBP_MapIcon-Objectif: la destination de l’âme en cours
Pour la projection, j’ai rendu la map orthographiquement dans Blender, noté le rapport d’échelle, et préparé un PSD pour le graphiste. En runtime, Get Geometry me donne la taille de la minimap en pixels, et je fais un simple calcul linéaire (règle de trois) pour convertir les coordonnées monde en position sur la texture. La map était centrée en [0,0,0].
Quand une cible sort du cercle visible, l’icône est clampée sur le bord : on calcule l’intersection de la droite (joueur vers cible) avec le cercle de la mini-map, et l’icône s’y colle pour rester visible côté joueur, sans jamais déborder du masque.
Système de dialogue (C++ + DataTable)
Le système gère les échanges entre Charon et les âmes qu’il transporte. Il se trouve dans Source/SoulTide/Dialog/ et repose sur UDialogManager, une UCLASS(Blueprintable) dérivée de UObject.
Tout le contenu narratif est stocké dans une UDataTable éditable depuis Unreal. Aucun texte n’est écrit en dur, donc les designers ajoutent ou modifient des répliques sans rebuild.
Une row de la table est typée FDialogPhrase : FTableRowBase et contient :
NextPhrase(FName) : chaîne vers la row suivante (NAME_None= fin du dialogue)Fr_Contents/En_Contents(TArray<FString>) : variantes localiséesAudio+Volum: voix optionnelleTypingSpeed,TypingSound,TypingSoundVolum: feedback du typing par phraseSprite: avatar du locuteur
Les assets lourds (sprite, sons) passent par des TSoftObjectPtr<...> et sont chargés à la demande avec LoadSynchronous() au moment où la row s’affiche, pas au démarrage du jeu.
Pour le multilingue, un champ Language sur le manager ("Fr" ou "En") sélectionne la bonne TArray à chaque draw. GetRandomOrSingleText ajoute un peu d’aléatoire quand plusieurs variantes existent dans la même row, donc une âme ne resort pas toujours exactement la même phrase. Côté entrée, StartDialog(TArray<FName>) peut recevoir plusieurs RowNames candidats et en pioche un au hasard.
Interface et animations
Côté UI, le manager ne connaît aucune classe widget en dur. Il parle au HUD et au widget via deux interfaces :
IDialogHUDInterface côté HUD :
GetCurrentDialogWidget: récupère le widget actifNotifyDialogClosed: signale la fin du dialogue
IDialogWidgetInterface côté widget :
FI_SetTexte,FI_SetSprite: pousse le contenuFI_TextBoxAnimation_IN/OUT: anime la boxFI_SpriteSlideAnimation_IN/OUT: anime l’avatar
N’importe quel widget Blueprint peut les implémenter, et les designers retouchent le visuel sans toucher au C++.
Pour synchroniser animations et typing, j’ai utilisé un pattern ExpectAnimations(N) qui pose un compteur. Les animations Blueprint rappellent le manager via NotifyTextBoxINComplete ou NotifySpriteINComplete (et leurs variantes OUT), qui décrémentent. Quand on arrive à zéro, soit le typing démarre (en IN), soit le dialogue se ferme proprement (en OUT). Aucun texte ne s’affiche donc avant la fin de l’animation d’entrée de la box.
Côté pass et chaining, Pass() saute à l’affichage complet si le typing est en cours, sinon il avance à NextPhrase. Un cooldown de 0.3 s via FTimerManager empêche un double-clic de skipper deux répliques d’un coup. Au passage à la phrase suivante, si la sprite change (Next->Sprite != CurrentSprite), on rejoue l’animation Sprite-IN avant de relancer le typing ; sinon on enchaîne sans coupure visuelle.
UI Manager
L’UI Manager vit dans le HUD et expose trois fonctions : SwitchUI, SwitchPopUp et StartDialog.
Les classes widget ne sont jamais hard-référencées. Chacune est chargée en asynchrone à la première ouverture et déchargée à sa destruction :
- aucun coût mémoire au démarrage pour des UI fermées
- on peut ajouter des mini-jeux ou des modales plus tard sans saturer la mémoire
Outillage de build pour l'équipe
Pour que l’équipe puisse compiler le projet sans avoir à monter à la main toute la chaîne d’outils Unreal, j’ai écrit un setup PowerShell, VSSetup/Setup.ps1.
Sur un poste vierge, il prépare la machine pour builder UE 5.5 :
- détecte Visual Studio 2022 via
vswhere - télécharge le bootstrapper VS s’il manque
- installe les 13 composants requis (Native Game, Unreal Ide, Windows SDKs, MSVC 14.38, etc.)
- répare le registre VS si besoin
- installe
ghvia winget - configure
core.hooksPathsur.githooks
Au quotidien, des .bat packagés permettent de compiler et packager sans même ouvrir l’éditeur :
build.bat: compilelive_build.bat: compile avec Live Coding actifpackage.bat: package finalclean.bat: nettoie les binaires
package.ps1 lance la compilation en mode Shipping, récupère uniquement les fichiers nécessaires et sort un zip à distribuer.
CI/CD via githook
Un hook pre-push PowerShell se déclenche sur la branche release. Il demande un bump de version (Major ou Minor), crée le tag, et publie automatiquement le zip sur GitHub Releases.
Je pousse avec git push origin release et la release versionnée sort, accessible à toute l’équipe et aux joueurs.
Liens
Équipe
- Pauline LALOUE : environnement et tech art
- Giulia Haut Perucca : tech art, support dev, character artist
- Leslie Ozanam : écriture, animation