SoulTide
Le code et l'outillage d'un jeu UE5 fait en un mois
Le jeu
SoulTide est un jeu Unreal Engine 5.5 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. Des anguilles bloquent certains passages : pour les calmer, il faut leur donner du poisson qu’on pêche soi-même.
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 sont des silhouettes encapuchonnées dont on ne voit que les yeux qui brillent. Leur couleur et leur forme indiquent le péché auquel elles sont liées.
La DA est stylisée, dans l’esprit d’Animal Crossing ou de A Short Hike : aplats de couleur, légers dégradés, peu de détails fins.
Ma part dans le projet
L’équipe faisait surtout l’art et le game design. J’ai pris le code, l’outillage et les rigs : la mini-map, le système de dialogue, l’UI Manager, les scripts de build et la release automatique, détaillés plus bas. Les rigs humanoïdes sont sortis en urgence à J-2 du rendu. Le dépôt montre aussi mes commits sur la pêche (inventaire de poissons, apparition des bancs), la caméra et le menu principal.
Mini-map
La mini-map est faite en Widget Blueprint, en deux vues :
WBP_MapComponent: le mini-affichage dans le HUDWBP_MapMenu: la carte plein écran, ouverte avecIA_OppenMap
Le rendu utilise la texture T_FinaleMap, projetée par le matériau MI_MapProjection, et un masque circulaire M_MapCircleMask la découpe en rond.
Trois icônes sont mises à jour 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 en orthographique dans Blender, noté le rapport d’échelle et préparé un PSD pour la graphiste. En runtime, Get Geometry donne la taille de la mini-map en pixels et une règle de trois convertit 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-cible avec le cercle, et l’icône s’y colle sans 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 dans une UDataTable éditable depuis Unreal. Aucun texte n’est écrit en dur, donc les designers modifient les répliques sans rebuild.
Une row est typée FDialogPhrase : FTableRowBase et contient :
NextPhrase(FName) : la row suivante (NAME_None= fin du dialogue)Fr_Contents/En_Contents(TArray<FString>) : variantes localiséesAudio+Volum: voix optionnelleTypingSpeed,TypingSound,TypingSoundVolum: le rendu du typing par phraseSprite: avatar du locuteur
Le sprite et les sons sont des TSoftObjectPtr<...> chargés avec LoadSynchronous() au moment où la row s’affiche, pas au démarrage du jeu.
Un champ Language sur le manager ("Fr" ou "En") choisit le bon TArray à chaque affichage. Quand une row a plusieurs variantes, GetRandomOrSingleText en tire une au hasard, donc une âme ne répète pas toujours la même phrase. StartDialog(TArray<FName>) fonctionne pareil : il reçoit plusieurs RowNames candidats et en pioche un.
Interface et animations
Le manager ne connaît aucune classe widget. Il parle au HUD et au widget par 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 : les designers retouchent le visuel sans toucher au C++.
Pour synchroniser animations et typing, ExpectAnimations(N) pose un compteur. Les animations Blueprint rappellent le manager via NotifyTextBoxINComplete ou NotifySpriteINComplete (et leurs variantes OUT), qui le décrémentent. À zéro, le typing démarre (en IN) ou le dialogue se ferme (en OUT), si bien qu’aucun texte ne s’affiche avant la fin de l’animation d’entrée de la box.
Pass() affiche la phrase en entier si le typing est en cours, sinon il avance à NextPhrase. Un cooldown de 0.3 s via FTimerManager empêche un double-clic de sauter deux répliques. Si le sprite change à la phrase suivante (Next->Sprite != CurrentSprite), l’animation Sprite-IN est rejouée avant de relancer le typing ; sinon l’enchaînement est direct.
UI Manager
L’UI Manager vit dans le HUD et expose trois fonctions : SwitchUI, SwitchPopUp et StartDialog.
Les classes widget ne sont jamais référencées en dur. Chacune est chargée en asynchrone à la première ouverture et déchargée à sa destruction, donc les UI fermées ne coûtent rien en mémoire au démarrage, et on pourra ajouter des mini-jeux ou des modales sans la saturer.
Outillage de build pour l'équipe
J’ai écrit un setup PowerShell, VSSetup/Setup.ps1, pour que l’équipe compile le projet sans monter à la main les outils d’Unreal. 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 dans scripts/ s’en occupent :
build.bat: ferme l’éditeur s’il est ouvert, compile, puis le rouvrelive_build.bat: envoie Ctrl+Alt+F11 à l’éditeur ouvert pour lancer Live Coding, sinon compile normalementpackage.bat: package finalclean.bat: nettoie les binaires
package.ps1 lance un BuildCookRun en Shipping (cook, pak) et compresse le résultat en SoulTide.zip.
CI/CD via githook
Un hook pre-push PowerShell se déclenche sur la branche release. Il vérifie que le zip de package.ps1 existe, demande un bump de version (Major ou Minor) et crée le tag, puis recrée la release latest sur GitHub avec le zip.
Je pousse avec git push origin release et la release sort, téléchargeable par l’équipe et par les joueurs.
Ce que ça change
Avant, chacun montait Visual Studio et ses composants à la main sur son poste, et une release se publiait à la main. Maintenant, Setup.ps1 prépare un poste vierge, build.bat compile, et un git push origin release suffit pour sortir la release avec son zip.
Liens
Équipe
- Pauline LALOUE : environnement et tech art
- Giulia Haut Perucca : tech art, support dev, character artist
- Leslie Ozanam : écriture, animation