Jeu vidéo / UI & outillage

SoulTide

Le code et l'outillage d'un jeu UE5 fait en un mois

Unreal Engine 5C++UMG / WBPUIPowerShellCI/CDÉquipe

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 bateau de Charon amarré près d'un ponton, de nuit

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
Des îles de l'archipel vues d'en haut

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 HUD
  • WBP_MapMenu : la carte plein écran, ouverte avec IA_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 :

La mini-map ronde avec le bateau au centre et les bancs de poissons
  • WBP_MapIcon-Boat : la position du joueur, orientée selon le cap
  • WBP_MapIcon-Fish : les bancs de poissons repérés
  • WBP_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ées
  • Audio + Volum : voix optionnelle
  • TypingSpeed, TypingSound, TypingSoundVolum : le rendu du typing par phrase
  • Sprite : avatar du locuteur
La DataTable des dialogues et l'éditeur d'une row

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 actif
  • NotifyDialogClosed : signale la fin du dialogue

IDialogWidgetInterface, côté widget :

  • FI_SetTexte, FI_SetSprite : pousse le contenu
  • FI_TextBoxAnimation_IN/OUT : anime la box
  • FI_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.

Une réplique en cours de typing, avec la mini-map et l'avatar de l'âme

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.

Le graphe Blueprint de chargement asynchrone d'un widget

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 gh via winget
  • configure core.hooksPath sur .githooks
Le dossier VSSetup du dépôt

Au quotidien, des .bat dans scripts/ s’en occupent :

  • build.bat : ferme l’éditeur s’il est ouvert, compile, puis le rouvre
  • live_build.bat : envoie Ctrl+Alt+F11 à l’éditeur ouvert pour lancer Live Coding, sinon compile normalement
  • package.bat : package final
  • clean.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.

La release latest sur GitHub avec SoulTide.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

Video Game / UI & Tooling

SoulTide

The code and tooling of a UE5 game made in one month

Unreal Engine 5C++UMG / WBPUIPowerShellCI/CDÉquipe

The game

SoulTide is an Unreal Engine 5.5 game made by four people in one month. You play Charon, the ferryman of the Styx, dressed up as a plague doctor.

Charon's boat moored near a pier, at night

The goal is to gather the lost souls drifting around the archipelago and take each one back to its own island. Eels block some of the passages: to calm them down, you feed them fish you caught yourself.

World and art direction

The world is split into seven archipelagos, one per deadly sin.

  • Pride and Sloth: forested island with a lighthouse on the coast
  • Greed: ruins, with a tower still standing
  • Wrath: cold iceberg zone
  • Envy: autumn area with leaves drifting in the wind
  • Gluttony: small sandy island with skeletons of creatures rising out of the ground
  • Lust: rainy zone with weeping willows and lily pads
Islands of the archipelago seen from above

Souls are hooded silhouettes of which you only see the glowing eyes. Their color and shape show which sin they are tied to.

The art direction is stylized, in the spirit of Animal Crossing or A Short Hike: flat colors, soft gradients, little fine detail.

My part in the project

The rest of the team mostly did the art and the game design. I took the code, the tooling and the rigs: the mini-map, the dialogue system, the UI Manager, the build scripts and the automated release, all detailed below. The humanoid rigs came out in a rush two days before the deadline. The repo also shows my commits on fishing (fish inventory, fish school spawning), the camera and the main menu.

Mini-map

The mini-map is built in Widget Blueprint, in two views:

  • WBP_MapComponent: the in-game HUD widget
  • WBP_MapMenu: the full-screen map, opened with IA_OppenMap

It uses the T_FinaleMap texture, projected through the MI_MapProjection material, and a circular mask M_MapCircleMask crops it into a circle.

Three icons are updated dynamically:

The round mini-map with the boat at the center and fish schools
  • WBP_MapIcon-Boat: the player’s position, rotated to match the heading
  • WBP_MapIcon-Fish: spotted fish schools
  • WBP_MapIcon-Objectif: the current soul’s destination

For the projection, I rendered the map orthographically in Blender, noted the scale ratio and prepared a PSD for the artist. At runtime, Get Geometry gives the mini-map size in pixels and a rule of three converts world coordinates to a position on the texture. The map was centered at [0,0,0].

When a target leaves the visible circle, the icon is clamped to the edge: we compute the intersection of the player-target line with the circle, and the icon sticks there without spilling outside the mask.

Dialogue system (C++ + DataTable)

The system handles the exchanges between Charon and the souls he ferries. It lives in Source/SoulTide/Dialog/ and is built around UDialogManager, a UCLASS(Blueprintable) deriving from UObject.

All the narrative content is in a UDataTable editable from Unreal. No text is hardcoded, so designers change lines without rebuilding.

A row is typed FDialogPhrase : FTableRowBase and carries:

  • NextPhrase (FName): the next row (NAME_None ends the dialogue)
  • Fr_Contents / En_Contents (TArray<FString>): localized variants
  • Audio + Volum: optional voice line
  • TypingSpeed, TypingSound, TypingSoundVolum: typing feedback per line
  • Sprite: speaker avatar
The dialogue DataTable and the row editor

The sprite and the sounds are TSoftObjectPtr<...> loaded with LoadSynchronous() when the row is shown, not at game startup.

A Language field on the manager ("Fr" or "En") picks the right TArray at each draw. When a row has several variants, GetRandomOrSingleText picks one at random, so a soul doesn’t always repeat the same line. StartDialog(TArray<FName>) works the same way: it takes several candidate RowNames and picks one.

Interface and animations

The manager knows no widget class. It talks to the HUD and the widget through two interfaces.

IDialogHUDInterface, on the HUD:

  • GetCurrentDialogWidget: fetches the active widget
  • NotifyDialogClosed: signals the dialogue is done

IDialogWidgetInterface, on the widget:

  • FI_SetTexte, FI_SetSprite: push the content
  • FI_TextBoxAnimation_IN/OUT: animate the box
  • FI_SpriteSlideAnimation_IN/OUT: animate the avatar

Any Blueprint widget can implement them: designers change the visuals without touching the C++.

To sync animations and typing, ExpectAnimations(N) sets a counter. The Blueprint animations call back into the manager via NotifyTextBoxINComplete or NotifySpriteINComplete (and their OUT variants), which decrement it. At zero, the typing starts (IN) or the dialogue closes (OUT), so no text appears before the box has finished its entry animation.

A line being typed, with the mini-map and the soul's avatar

Pass() shows the full line if typing is still running, otherwise it moves on to NextPhrase. A 0.3 s cooldown through FTimerManager stops a double-click from skipping two lines. If the sprite changes on the next line (Next->Sprite != CurrentSprite), the Sprite-IN animation is replayed before typing restarts; otherwise it chains directly.

UI Manager

The UI Manager lives in the HUD and exposes three functions: SwitchUI, SwitchPopUp and StartDialog.

The Blueprint graph that loads a widget asynchronously

Widget classes are never hard-referenced. Each one is loaded asynchronously on first open and unloaded on destruction, so closed UIs cost no memory at startup, and mini-games or modals can be added later without saturating it.

Build tooling for the team

I wrote a PowerShell setup, VSSetup/Setup.ps1, so the team can compile the project without assembling Unreal’s tools by hand. On a fresh machine, it prepares everything needed to build UE 5.5:

  • detects Visual Studio 2022 through vswhere
  • downloads the VS bootstrapper if it’s missing
  • installs the 13 required components (Native Game, Unreal Ide, Windows SDKs, MSVC 14.38, etc.)
  • repairs the VS registry if needed
  • installs gh through winget
  • sets core.hooksPath to .githooks
The VSSetup folder in the repo

Day to day, .bat files in scripts/ handle the rest:

  • build.bat: closes the editor if it is open, compiles, then reopens it
  • live_build.bat: sends Ctrl+Alt+F11 to the open editor to trigger Live Coding, otherwise compiles normally
  • package.bat: final package
  • clean.bat: cleans the binaries

package.ps1 runs a Shipping BuildCookRun (cook, pak) and compresses the result into SoulTide.zip.

CI/CD via Git hook

A PowerShell pre-push hook fires on the release branch. It checks that the zip from package.ps1 exists, asks for a version bump (Major or Minor) and creates the tag, then recreates the latest release on GitHub with the zip.

The latest release on GitHub with SoulTide.zip

I push with git push origin release and the release goes out, downloadable by the team and by players.

What it changes

Before, everyone set up Visual Studio and its components by hand on their own machine, and a release was published by hand. Now Setup.ps1 prepares a blank machine, build.bat compiles, and one git push origin release is enough to ship the release with its zip.

Links

Team