Voici donc les grandes étapes de son développement, depuis le choix du style graphique jusqu'au moteur de dialogues et aux menus.
ÉTAPE 1 — TROUVER LE TYPE DE JEU
Quand je commence un concours Casio, la première question que je me pose est généralement :
Est-ce que je pars sur de la 2D classique ? Un platformer ? De l'isométrique ? De la 3D ? Une vue de dessus ?
Ce choix est beaucoup plus important qu'il n'en a l'air. Il détermine la manière de dessiner les décors, de gérer les collisions, de déplacer le joueur et même de construire les outils. Autant dire qu'une fois le développement bien lancé, changer complètement de direction devient presque impossible.
Pour trouver des idées, je passe donc beaucoup trop de temps sur itch.io, Pinterest et différents sites de pixel art, et je fais une petite liste de références :
Cette fois, je suis tombé sur cette image :

Je ne sais pas exactement pourquoi, mais j'ai immédiatement flashé sur ce rendu. Ce n'était ni vraiment de la vue de profil, ni vraiment de l'isométrique : plutôt une sorte de 2.5D semi-isométrique, avec des plateformes vues de côté mais suffisamment de profondeur pour donner l'impression d'un petit monde en volume.
Je me suis donc dit : « Parfait, je vais faire ça ! »
Puis j'ai cherché un tilesheet adapté.
Et là… rien.
Je trouvais des tilesheets de RPG vus du dessus, des jeux de plateforme de profil et de l'isométrique classique, mais absolument rien qui corresponde précisément à ce mélange. J'ai donc eu l'idée brillante de fabriquer mon propre tilesheet (c'est ironique, je précise au cas où).
Je regrette.
Comme je ne suis pas particulièrement doué en pixel art, j'ai utilisé ce que je maîtrise beaucoup mieux : Photoshop. J'ai composé le décor à partir de nombreuses textures et de petits éléments 2D ou isométriques, provenant notamment de tilesheets de RPG Maker, d'assets trouvés sur itch.io et de différentes références visuelles.
Pour créer cette « fausse 3D », je partais par exemple d'une texture de mur complètement plate. Avec l'outil de transformation de perspective de Photoshop, je créais ensuite une seconde face décalée de 16 pixels. Ce simple décalage donne de la profondeur au bloc.
Le résultat n'est donc pas un vrai décor 3D. C'est une composition de petites images 2D soigneusement dessinées pour donner l'illusion du volume.
Cela m'a pris un temps fou.
ÉTAPE 2 — IMAGINER LE SCÉNARIO
Je pense que ce n'est plus un secret pour personne : je suis un très grand fan de Zelda.
Avec un concours sur le thème de l'eau, il ne m'a donc pas fallu longtemps pour penser au fameux Temple de l'Eau de Zelda: Ocarina of Time, celui qui a traumatisé une génération entière de joueurs.
À côté de ça, j'aime aussi beaucoup l'arc d'Alabasta dans One Piece, avec sa grande cité désertique menacée par le manque d'eau. J'ai donc naturellement décidé de fusionner les deux idées : une ville-oasis construite au-dessus d'un temple ancien, dont les mécanismes contrôlent toute l'irrigation de la cité.
À l'origine, le scénario devait être beaucoup plus ambitieux. Il était question de complot, de secrets liés à la fondation de la ville, de plusieurs révélations et même de faire de mon propre avatar le boss final.
Oui, rien que ça.
Puis le temps restant avant la fin du concours m'a rappelé à la réalité.
La version actuelle est donc beaucoup plus simple : le système d'irrigation fonctionne mal, le roi envoie le joueur dans le temple, et il faut comprendre dans quel ordre actionner les mécanismes pour rétablir l'eau.
En bref, je suis passé d'une grande conspiration politique à un problème de plomberie.
OOF, le downgrade.
Mais ce scénario plus direct possède aussi un avantage : il laisse immédiatement la place à l'exploration et aux énigmes. Les personnages peuvent apporter progressivement le contexte, tandis que le joueur comprend la situation en avançant dans le temple.
ÉTAPE 3 — LE MOTEUR
Bon… pour la énième fois, je vais vous parler de mon
Particule Engine.
Pour ceux qui viennent de débarquer, cela fait environ six ans que je développe mon propre moteur de jeu en C++, très largement inspiré du fonctionnement de Unity.
L'objectif n'est évidemment pas de reproduire tout Unity sur une calculatrice. L'idée est plutôt de conserver son organisation générale : au lieu d'écrire un énorme programme monolithique où tout est mélangé, le jeu est construit à partir de petits objets et de comportements réutilisables.
Le moteur repose principalement sur quatre éléments :
SceneManager,
Scene,
GameObject et
Component.
Le fonctionnement général ressemble à ceci :
SceneManager
└── Scene « Hydralis »
├── GameObject « Player »
│ ├── Transform
│ ├── BoxCollider
│ ├── Rigidbody
│ ├── PlayerController
│ ├── DepthProjector
│ └── Shadow
├── GameObject « Alice »
│ ├── SpriteRenderer
│ ├── Shadow
│ └── Interact
└── GameObject « Door »
├── SpriteRenderer
└── Door
Le SceneManager
Le SceneManager est le chef d'orchestre. Il connaît les différentes scènes du jeu, charge celle qui doit devenir active, détruit l'ancienne lors d'un changement et exécute les grandes phases d'une frame :
•
FixedUpdate pour la physique ;
•
Update pour la logique classique ;
• mise à jour des
coroutines ;
•
LateUpdate pour les comportements qui doivent intervenir après le reste ;
• rendu des composants selon leur ordre d'affichage.
Il contient également le gestionnaire d'interface et centralise les ordres de rendu. Cela permet par exemple de dessiner certains éléments du décor avant le joueur et d'autres après lui.
La Scene
Une Scene contient tous les GameObject d'une zone du jeu. Elle est responsable de leur création, de leur destruction et de l'appel de leurs composants.
Une scène peut être dans plusieurs états : active, mise à jour uniquement, dessinée uniquement ou complètement désactivée. Cette distinction est très pratique. Pendant un dialogue ou le menu principal, je peux par exemple continuer à afficher le monde en arrière-plan tout en gelant les déplacements et la physique.
Le GameObject
Un GameObject représente une entité logique du monde : le joueur, une porte, un PNJ, une caméra ou même un contrôleur invisible.
À lui seul, il ne fait presque rien. Il possède surtout un nom, un Transform, un état actif, éventuellement un tag ou un layer, puis une liste de Component.
Le Transform stocke sa position, sa rotation, son échelle et sa relation parent-enfant. Le visuel du Player peut ainsi être un enfant de l'objet physique du joueur, et suivre automatiquement tous ses déplacements.
Le Component
Les Component contiennent le comportement réel.
Un Rigidbody gère la physique. Un PlayerController lit les touches. Un SpriteRenderer dessine une image. Un Interact détecte une interaction. Une Door téléporte le joueur. Une Shadow affiche une ombre. Tous ces éléments peuvent être ajoutés ou retirés indépendamment.
Chaque composant possède un cycle de vie inspiré de Unity : Awake, Start, OnEnable, OnDisable, FixedUpdate, Update, LateUpdate, OnRender et OnDestroy.
J'ai également ajouté des coroutines C++20. Elles permettent d'écrire une séquence qui s'étale sur plusieurs frames sans fabriquer une énorme machine à états manuelle. Les dialogues et les transitions de portes les utilisent beaucoup : une fonction peut attendre la frame suivante, fermer un rideau, déplacer le joueur, puis rouvrir l'écran de manière très lisible.
L'intérêt de toute cette architecture est la composition. Au lieu de créer une classe géante « JoueurQuiBougeQuiSauteQuiParleQuiSeTéléporte », j'assemble plusieurs petits composants spécialisés.
Évidemment, développer tout cela prend plus de temps que d'écrire directement le jeu.
ÉTAPE 4 — CONSTRUIRE LA MAP
La carte utilise ce qu'on appelle une
Tilemap. Le principe est de découper le tilesheet en nombreuses petites tuiles, puis de stocker pour chaque cellule uniquement l'indice de l'image à afficher.
Au lieu d'enregistrer une immense image contenant toute la ville, le jeu mémorise donc essentiellement ceci :
Cellule (0, 0) → tuile 83
Cellule (1, 0) → tuile 84
Cellule (2, 0) → vide
etc.
Pour créer la ville et le temple sans devenir fou, j'ai utilisé l'éditeur de Tilemap de Unity. J'ai ensuite développé mes propres outils pour adapter l'éditeur à la projection particulière d'Hydralis et exporter les données dans un format compris par le moteur.
Le pipeline ressemble à ceci :
Unity
→ export de la Tilemap en JSON
→ conversion par un outil Python/fxconv
→ intégration des données directement dans le binaire Casio
→ création du composant Tilemap au chargement de la scène
Les fichiers ne sont donc pas relus depuis le stockage de la calculatrice pendant le jeu : les données converties sont déjà compilées dans l'add-in.
La map n'est pas composée d'une seule couche. Plusieurs Tilemaps sont superposées afin de séparer le sol, les murs, les détails et les éléments de premier plan.
Le moteur possède un ordre de rendu individuel par couche. Les couches basses sont dessinées avant les personnages, puis les objets du monde sont affichés, et enfin les couches hautes peuvent repasser devant eux. C'est ce qui permet au joueur de marcher derrière une barrière ou devant un mur sans que l'illusion de profondeur s'effondre (sauf à certains endroits où j'ai merdé

).
Le moteur ne parcourt pas non plus toute la carte à chaque frame. Il calcule la zone visible depuis la caméra et envoie uniquement les tuiles nécessaires à un shader Azur spécialisé. Sur une carte beaucoup plus grande que l'écran de la calculatrice, ce culling est indispensable.
Pour le temple, le niveau d'eau ne déforme pas dynamiquement une gigantesque carte. Deux états ont été préparés dans Unity : une base « eau basse » et une couche supplémentaire « eau haute ». Lorsque l'eau monte, la base reste affichée, la couche haute vient se superposer et le collider bas est remplacé par le collider haut. C'est beaucoup plus simple, plus fiable et surtout beaucoup moins coûteux sur calculatrice.
ÉTAPE 5 — PHYSIQUE ET COLLISIONS
Voici probablement la partie la plus indispensable à réussir… et la plus pénible à développer.
La première solution évidente aurait été d'utiliser un véritable tableau 3D. Chaque coordonnée X, Y et Z aurait indiqué si un bloc était vide ou solide.
Techniquement, c'est simple et très efficace à consulter.
Le problème, c'est la mémoire.
Une ville assez large, avec plusieurs hauteurs et plusieurs profondeurs, produit très vite des dizaines ou centaines de milliers de cellules et dont la plupart sont vides en plus. Sur PC ce n'est pas dramatique. Sur une Graph 90+E, chaque octet compte.
J'ai donc choisi une autre méthode : représenter les collisions à partir de leur projection 2D.
Le collider est dessiné dans Unity comme une carte vue avec la même projection que le décor. Chaque entrée tient dans un octet et contient plusieurs informations compactées :
• le type de surface : vide, interdite, plate ou pente ;
• un indicateur permettant de distinguer sol et plafond ;
• la profondeur logique de la cellule.
Grâce à cette profondeur, une grille 2D peut décrire plusieurs plans du monde sans réserver un énorme volume 3D entièrement vide.
Lorsqu'un objet cherche le sol sous ses pieds, le moteur lance un rayon vertical. À partir de sa position X/Z, le TilemapCollider retrouve les cellules projetées qui correspondent à cette profondeur, calcule la hauteur de la surface et renvoie le résultat le plus proche.
Pour une surface plate, le calcul est trivial. Pour une pente gauche ou droite, la hauteur dépend de la position horizontale dans la cellule. Pour une pente qui part vers l'arrière, elle dépend de la profondeur. Les zones marquées « Forbidden » ont priorité sur un sol normal afin d'éviter qu'une surface praticable située derrière un mur soit choisie par erreur.
Plusieurs colliders peuvent être actifs dans la même scène. Le système fusionne leurs raycasts en tenant compte de la position de chaque GameObject. C'est notamment ce qui permet d'avoir la ville à l'origine et le temple décalé de plusieurs milliers de pixels dans le même monde.
Au-dessus de ce système se trouve mon
Rigidbody.
Il conserve une vitesse sur les trois axes, applique la gravité pendant FixedUpdate, déplace temporairement l'objet puis vérifie le terrain :
• un rayon vers le bas recherche le sol ;
• un rayon vers le haut empêche de traverser un plafond ;
• une marche trop haute annule le déplacement horizontal ;
• les pentes ajustent la hauteur du joueur ;
• la dernière position valide sert de sécurité lorsqu'un mouvement mènerait dans le vide.
Le joueur possède aussi un
BoxCollider. Le
Rigidbody en déduit notamment la position exacte de ses pieds et de sa tête.
Ce n'est pas un moteur physique généraliste capable de simuler une voiture ou une pile de caisses. Il est volontairement spécialisé pour les déplacements d'Hydralis, ce qui le rend beaucoup plus léger.
Enfin, les coordonnées et les calculs utilisent principalement du point fixe Q16.16 plutôt que des flottants : 16 bits pour la partie entière et 16 bits pour la partie décimale.
ÉTAPE 6 — LE JOUEUR
Cette étape se divise en deux grandes parties : le
visuel et les
contrôles.
Le visuel : une fausse 3D dans une fausse 3D
Pour le joueur, j'ai encore fraudé.
Ce n'est ni un vrai modèle 3D rendu en temps réel, ni une simple sprite sheet contenant huit directions toutes prêtes.
Le personnage est construit dans Unity comme un petit assemblage de segments : tête, corps, bras, jambes, accessoires… Chaque morceau est associé à un nœud d'une hiérarchie et peut avoir sa propre position, rotation et échelle.
Un outil de capture tourne autour du personnage et génère automatiquement plusieurs vues de chaque segment dans un atlas. Il exporte également :
• la hiérarchie des nœuds ;
• les poses locales ;
• les différentes directions disponibles ;
• les animations ;
• les contraintes d'ordre entre les morceaux du personnage.
Un convertisseur Python transforme ensuite tout cela en données directement utilisables par le moteur.
Au runtime, le BillboardCharacterRenderer reconstruit la pose du personnage à partir de cette hiérarchie. Pour chaque segment, il choisit l'image correspondant le mieux à l'orientation de la caméra, puis l'affiche comme un billboard : une image 2D placée dans le monde de manière à simuler un volume.
Le BillboardAnimator applique les poses exportées pour jouer Idle, Walk, Run et Jump. Les segments sont ensuite triés afin qu'un bras puisse passer devant ou derrière le corps selon l'angle de vue.
Le résultat donne l'impression d'un personnage 3D, alors que la calculatrice ne dessine au final que plusieurs sprites 2D.
C'est donc une fausse 3D composée de faux morceaux 3D dans un décor lui-même en fausse 3D.
(J'avais aussi l'intention de rajouter des ennemis genre gobelin et squelette mais par manque de temps je n'ai pas pu)
Les contrôles
Le PlayerController lit les flèches directionnelles et transforme l'entrée en vitesse sur les axes X et Z du monde. ALPHA permet de courir et SHIFT déclenche un saut uniquement lors du nouvel appui, afin d'éviter de resauter automatiquement en maintenant la touche.
La projection oblique pose un petit problème : certaines diagonales paraissent plus rapides à l'écran, car les déplacements X et Z s'additionnent visuellement. Le contrôleur applique donc une correction spécifique à ces directions pour conserver une vitesse perçue cohérente.
Le visuel est orienté sur huit directions selon le déplacement. Les animations ne dépendent pas uniquement des touches appuyées : le contrôleur vérifie aussi si le Rigidbody s'est réellement déplacé pendant le dernier pas physique. Ainsi, pousser contre un mur ne joue pas éternellement l'animation de marche.
Enfin, une caméra suit le joueur avec un léger lissage. Elle utilise la même projection de profondeur que les sprites afin de conserver le joueur correctement centré pendant les changements de hauteur.
ÉTAPE 7 — LES ENTITÉS DU MONDE
Une fois le terrain, les collisions et le joueur fonctionnels, il faut encore remplir le monde.
Dans le moteur, une « entité » n'est pas un type spécial. C'est simplement un GameObject construit avec le bon ensemble de composants.
Un PNJ contient par exemple :
• un objet racine positionné dans le monde ;
• un SpriteRenderer sur un enfant visuel ;
• un DepthProjector pour calculer sa position à l'écran ;
• un BoxCollider ;
• une ombre ;
• un composant Interact qui affiche « Parler » et lance son dialogue.
Une porte combine un sprite ouvert ou fermé, un état verrouillé, une zone de déclenchement, le nom de sa destination et une transition en rideau. Lorsqu'elle est utilisée, le rideau se ferme, le joueur est déplacé vers la porte cible, la caméra est resynchronisée, puis le rideau se rouvre.
Les interrupteurs possèdent le même principe d'interaction, mais exécutent une action de gameplay. Les mécanismes du temple utilisent même des triggers invisibles : le bouton est déjà dessiné dans la Tilemap, donc l'objet C++ n'a besoin d'aucun sprite. Il vérifie simplement que le joueur se trouve exactement sur la bonne cellule avant d'inverser l'état de l'eau.
Pour éviter de recopier cette construction partout, j'ai créé des fonctions de prefab en C++. Elles reçoivent une configuration et assemblent automatiquement tous les GameObject et Component nécessaires.
À cause du temps très limité, les entités de la version actuelle sont ensuite placées directement en dur dans Scenes.cpp. L'outil Unity me sert à récupérer leurs cellules et à prévisualiser le placement, mais le jeu ne charge aucun fichier d'entités au runtime.
Ce n'est pas la solution la plus élégante pour un énorme RPG mais pour un concours, c'est simple, rapide et fiable.
La progression de l'histoire suit la même philosophie. Un simple entier indique l'étape actuelle. Lorsqu'on reparle à un PNJ, son action consulte cette valeur et choisit le dialogue correspondant. Parler au roi peut par exemple passer l'histoire de l'état 0 à l'état 1 et déverrouiller la porte du temple.
Pas besoin d'un système de quêtes intergalactique pour gérer une aventure volontairement linéaire.
ÉTAPE 8 — LE SYSTÈME DE DIALOGUE
Le système de dialogue est construit autour d'une structure très simple. Chaque page contient :
• le nom du personnage ;
• son portrait ;
• trois lignes de texte.
Une conversation est donc simplement un tableau de plusieurs pages.
Lorsque le dialogue commence, une coroutine prend temporairement le contrôle de l'interface. La scène passe en mode « Draw » : le décor continue d'être affiché, mais la physique et les contrôleurs sont gelés, les Update de façon générale. Le Canvas de dialogue apparaît alors par-dessus le jeu.
À chaque page, le système remplit la boîte de nom, les trois lignes et le portrait. Si le nom est vide, la boîte correspondante est automatiquement masquée — c'est ce que j'utilise pour le narrateur. Si aucun portrait n'est fourni, sa zone disparaît également.
Le joueur avance avec EXE ou SHIFT. Le système détecte uniquement le nouvel appui, et non une touche maintenue, afin de ne pas faire défiler toute la conversation en une frame.
Une fois la dernière page terminée, le Canvas précédent est restauré et la scène reprend exactement l'état qu'elle avait avant le dialogue.
Les conversations liées à l'histoire sont choisies dynamiquement selon l'entier de progression. Cela permet aux PNJ de réagir différemment après chaque événement sans dupliquer toute la logique des PNJ.
Certaines conversations exécutent aussi une action à leur fin : modifier l'état du scénario, remettre une autorisation, ouvrir une porte ou lancer une transition.
ÉTAPE 9 — L'INTERFACE ET LES MENUS
Pour terminer, j'ai développé mon propre petit moteur d'interface.
Il reprend là encore une organisation proche de Unity :
CanvasManager
└── Canvas
└── Panel
├── SpriteUI
├── TextUI
└── ButtonUI
Le CanvasManager conserve toutes les interfaces chargées et choisit celle qui est actuellement visible. Un Canvas peut contenir plusieurs panneaux, par exemple le menu principal, les options, les commandes ou l'overclocking.
Chaque Panel contient des objets simples :
• SpriteUI pour les cadres, portraits et images ;
• TextUI pour les textes ;
• ButtonUI pour les éléments sélectionnables.
Les boutons possèdent des voisins haut, bas, gauche et droite. La navigation ne dépend donc pas de leur position approximative à l'écran : elle est explicitement définie dans l'asset. Quand le joueur appuie sur une flèche, le Canvas sélectionne le bouton relié dans cette direction. EXE invoque ensuite son callback C++.
Les interfaces sont préparées dans un outil d'édition, exportées sous forme d'assets puis converties et intégrées au binaire, comme les Tilemaps et les billboards.
Le menu principal utilise ce système pour Start, Settings, Exit, le panneau des commandes et même le choix du profil d'overclocking. Lorsqu'il est affiché, la scène reste dessinée mais ne se met plus à jour. Le monde sert donc directement de décor derrière le menu.
Le même moteur est réutilisé pour les dialogues, le petit message « Interagir » et les transitions. Chaque système peut mémoriser le Canvas précédemment actif, afficher temporairement le sien, puis restaurer proprement l'interface précédente.