Devlog : La renaissance de Zelda Tears of the NES
Posté le 12/05/2025 01:37
Bien le bonjour les gens !
Aujourd’hui, et pendant tout le reste du mois, je vais partager avec vous le développement de la nouvelle version de Zelda: Tears of the NES.
Je me suis lancé le défi de reprendre mon ancien prototype et d’en faire un vrai jeu, propre, fini, jouable.
Je vais organiser ça en épisodes, ou chapitres, et je mettrai à jour ce topic au fur et à mesure de l’avancement du projet.
Chapitre 1 – Un passé lointain : Particule
Si tu traînes régulièrement sur ce site, tu sais peut-être déjà que ça fait 5 ans

que je bosse sur un moteur de jeu nommé Particule.
J’en parle peu, mais pourtant il a bien évolué. Pas mal de bugs ont été corrigés, et plusieurs optimisations ont été apportées.
Pourquoi je t’en parle ici ? Tout simplement parce que ce jeu va tourner sur Particule.
Qu’est-ce que ça apporte ? Comment ça fonctionne ?
Particule est divisé en trois parties :
-Particule API
-Particule Engine
-Particule Editor
▸ Particule API
La Particule API est une interface qui permet de compiler un même projet sur plusieurs plateformes (tant qu’elles sont prises en charge), sans modifier une seule ligne de code !
Voici un exemple de fichier de configuration :
Cliquez pour découvrir
Cliquez pour recouvrir
{
"common": {
"is_library": false,
"clean": false,
"debug": false,
"source_files": [
"Particule/Engine/src/Components/Camera.cpp",
"Particule/Engine/src/Core/Coroutine/CoroutineManager.cpp",
"Particule/Engine/src/Core/GameObject.cpp",
"Particule/Engine/src/Core/Transform.cpp",
"Particule/Engine/src/Scene/Scene.cpp",
"Particule/Engine/src/Scene/SceneManager.cpp",
"Sources/src/main.cpp",
"Sources/src/Sys/Game.cpp",
"Sources/src/Scenes/MainMenuScene.cpp"
],
"include_paths": ["Particule/Engine/include","Sources/include"],
"library_paths": [],
"defines": {},
"build_dir": "build",
"bin_dir": "bin",
"project_name": "ZeldaTOTN"
},
"CasioCg": {
"libraries": [],
"compile_flags": "-Wall -Wextra -Werror",
"link_flags": "",
"assets_files": {
"textures": [
{"path": "Assets/common/Images/UI/UI.png","reference_path":"assets/Images/UI/UI.png", "format": "p8", "alpha":true, "external":false},
{"path": "Assets/common/Images/UI/MainLogo.png","reference_path":"assets/Images/UI/MainLogo.png", "format": "p8", "alpha":true, "external":false}
],
"fonts": [
{"path": "Assets/common/Fonts/PressStart2P.ttf","reference_path":"assets/Fonts/PressStart2P.font",
"resolution": 8, "charset": " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~éèêëôöûüçàâäæœÉÈÊËÔÖÛÜÇÀÂÄÆŒ"},
{"path": "Assets/common/Fonts/The Wild Breath of Zelda.otf","reference_path":"assets/Fonts/The Wild Breath of Zelda.font",
"resolution": 32, "charset": " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz."}
],
"other": []
},
"inputs": {"UP":"KEY_UP", "DOWN":"KEY_DOWN", "LEFT":"KEY_LEFT", "RIGHT":"KEY_RIGHT", "ENTER":"KEY_EXE", "ENTER2":"KEY_SHIFT"},
"display_name": "",
"icon_uns": "Assets/cg/icon-uns.png",
"icon_sel": "Assets/cg/icon-sel.png",
"output_file": "ZeldaTOTN",
"memtrack": false
},
"Windows": {
"assets_files": {
"textures": [
{"path": "Assets/common/Images/UI/UI.png","reference_path":"assets/Images/UI/UI.png"},
{"path": "Assets/common/Images/UI/MainLogo.png","reference_path":"assets/Images/UI/MainLogo.png"}
],
"fonts": [
{"path": "Assets/common/Fonts/PressStart2P.ttf","reference_path":"assets/Fonts/PressStart2P.font",
"resolution": 8, "charset": " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~éèêëôöûüçàâäæœÉÈÊËÔÖÛÜÇÀÂÄÆŒ"},
{"path": "Assets/common/Fonts/The Wild Breath of Zelda.otf","reference_path":"assets/Fonts/The Wild Breath of Zelda.font",
"resolution": 32, "charset": " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz."}
],
"audio": [],
"other": []
},
"inputs": {"UP":"SDLK_UP", "DOWN":"SDLK_DOWN", "LEFT":"SDLK_LEFT", "RIGHT":"SDLK_RIGHT", "ENTER":"SDLK_RETURN", "ENTER2":"SDLK_KP_ENTER"},
"icon": "Assets/win/icon.ico",
"console": true,
"architecture": "x64",
"lib_api": "SDL2",
"compiler": "MSVC",
"output_file": "ZeldaTOTN"
},
"Linux": {
"assets_files": {
"textures": [
{"path": "Assets/common/Images/UI/UI.png","reference_path":"assets/Images/UI/UI.png"},
{"path": "Assets/common/Images/UI/MainLogo.png","reference_path":"assets/Images/UI/MainLogo.png"}
],
"fonts": [
{"path": "Assets/common/Fonts/PressStart2P.ttf","reference_path":"assets/Fonts/PressStart2P.font",
"resolution": 8, "charset": " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~éèêëôöûüçàâäæœÉÈÊËÔÖÛÜÇÀÂÄÆŒ"},
{"path": "Assets/common/Fonts/The Wild Breath of Zelda.otf","reference_path":"assets/Fonts/The Wild Breath of Zelda.font",
"resolution": 32, "charset": " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz."}
],
"audio": [],
"other": []
},
"inputs": {"UP":"SDLK_UP", "DOWN":"SDLK_DOWN", "LEFT":"SDLK_LEFT", "RIGHT":"SDLK_RIGHT", "ENTER":"SDLK_RETURN", "ENTER2":"SDLK_KP_ENTER"},
"lib_api": "SDL2",
"output_file": "ZeldaTOTN"
}
}
Je lui donne ça… et il fait le reste tout seul, automatiquement.
Deux grosses "nouveautés" à noter :
-la gestion des fonts ;
-la gestion des textures.
📌
Les fonts
Le système convertit une font .ttf ou .otf en un format spécial. Ce format permet de stocker uniquement un charset donné (super pratique pour économiser de l’espace), gère automatiquement les unicodes, et permet de dessiner avec différentes tailles.
Par exemple : si tu sauvegardes une font à 16 px, tu peux l’afficher à l’écran en 19 px sans souci.
Tu peux bien sûr changer la couleur; et le spacing entre les caractères reste fidèle à la font d’origine (et oui, tous les caractères n’ont pas la même largeur).
🖼️
Les textures
Tu sais sûrement que gint propose 3 types de compression d’image (P4, P8, RGB565).
Particule, lui, prend tout ça en charge. Peu importe le format, si tu fais image->GetPixel(x, y), tu obtiens un type Color optimisé, sans te prendre la tête.
Et si tu veux afficher un pixel, tu peux utiliser image->PutPixel(xTexture, yTexture, xScreen, yScreen) pour le dessiner directement à l’écran.
Évidemment, pour les affichages classiques, on a aussi Draw, DrawSub, DrawSubSize, DrawSubSizeColor, etc.
Tu peux afficher une image entière ou une portion, à la taille que tu veux, avec la couleur que tu veux, et même la retourner (flip horizontal/vertical) en donnant une taille négative.
👉 Pour l’instant, la rotation (en degré) n’est pas encore supportée.
▸ Particule Engine
Le Particule Engine, c’est une copie maison de Unity 3D.
Son fonctionnement est similaire : scènes, GameObjects, composants…
Je te laisse imaginer le fonctionnement à la Unity : une scène contient des objets, chaque objet peut avoir des composants qui lui ajoutent des comportements ou propriétés.
J’ai même intégré un système de Coroutine, très proche de IEnumerator, pour faire des fonctions asynchrones.
Par exemple, l’animation du logo au lancement du jeu est maintenant gérée via une coroutine.
▸ Particule Editor
Là on parle de l’éditeur.
Tu vois Unity ? Ben c’est pareil.
Sauf que ça fait 5 ans que j’essaie de faire cet éditeur à la noix…
J’ai tenté de le faire 4 fois en Python avec Tkinter : ça ramait, c’était instable. Une horreur.
Mais cette fois… c’est la bonne.
Pourquoi ? Parce que le Particule Editor est développé avec… Particule API.
La boucle est bouclée.
En résumé : Particule, c’est génial, il fait tout le sale boulot pour toi
Chapitre 2 – Le Commencement : Menu principal
Maintenant qu’on a l’API et le moteur, on peut passer à la suite : le menu principal.
Il a été entièrement refait, façon Unity 3D.
Avant, j’avais une seule ligne d’exécution. Maintenant, plusieurs processus se partagent les tâches, et il faut bien les gérer.
C’est vrai que pour l’interface utilisateur, c’est pas toujours pratique.
J’ai mis en place un système de sprites dans l’API.
L’idée ? Sur une seule texture, tu regroupes plusieurs icônes.
Par exemple : toutes les icônes de l’UI sur une image. Ensuite, tu les découpes en sprites, et rien qu’avec ça, tu peux générer tout le menu principal.
Chapitre 3 – l'Éditeur de Sprites
J’ai développé un éditeur de sprites bien pratique pour faciliter le travail sur les assets du jeu.
Il permet de charger une image (sprite sheet, tileset, etc.) et de découper visuellement les sprites directement dans le logiciel.
C’est super intuitif : tu traces les zones que tu veux, et chaque sprite est enregistré, le tout dans un fichier .json.
Tu peux ensuite :
renommer chaque sprite pour t’y retrouver facilement ;
modifier ou corriger une découpe si tu t’es trompé ;
et même changer la couleur de fond pour un meilleur contraste visuel, selon ton image.
C’est un petit outil maison, mais franchement, il me fait gagner un temps fou. Et surtout, il s’intègre parfaitement au reste de l’écosystème Particule.
Chapitre 5 – La Skybox
La skybox, c’est le fond visuel de ta scène.
Dans mon cas, j’ai opté pour un système simple, léger et efficace : un dégradé vertical.
Elle est composée de deux couleurs :
- une couleur pour le haut du ciel (top) ;
- une couleur pour le bas (bottom).
Chaque ligne de l’écran est ensuite coloriée par interpolation linéaire entre ces deux teintes.
Résultat : un joli dégradé qui donne de la profondeur et évite le vide noir souvent présent dans les rendus 3D sans skybox.
C’est rapide à calculer, visuellement agréable, et largement suffisant.
Chapitre 6 – La projection 3D
Entrons maintenant dans le vif du sujet : la projection 3D.
L’objectif est simple à formuler mais complexe à mettre en œuvre : prendre un modèle 3D et calculer où ses points (appelés sommets) doivent apparaître à l’écran en 2D.
Mais comment ça fonctionne ?
Chaque objet a :
- une position (où il est dans l’espace),
- une rotation (dans quelle direction il est orienté),
- une échelle (s’il est agrandi ou réduit).
On commence par appliquer tous ces paramètres à chaque sommet du modèle.
Translation par rapport à la caméra
Ensuite, on place l’objet par rapport à la caméra. En gros, on « recentre » la scène pour que ce que la caméra voit soit bien positionné.
Rotation de la caméra
Après avoir bougé les objets, il faut les orienter comme si on tournait la tête. La rotation de la caméra est appliquée à chaque sommet pour simuler cette vue.
Projection en 2D
Une fois les sommets bien placés et orientés, on les projette sur l’écran.
C’est comme regarder à travers une vitre : on trace une ligne entre l’œil (la caméra) et le point 3D, et on regarde où cette ligne touche la surface de l’écran.
Le calcul principal ici ressemble à :
x à l’écran = (x dans l’espace / profondeur) * facteur
C’est ce qui donne l’effet de perspective : plus un objet est loin, plus il paraît petit.
Chapitre 7 – Rendu 3D : dessiner les textures
Maintenant que les sommets sont bien positionnés à l’écran, il est temps de dessiner les faces du modèle 3D : on entre dans la phase de rendu.
Le principe des UV
Chaque face du modèle (triangle ou quadrilatère) est mappée à une texture, une image qu’on applique sur la surface.
Les coordonnées UV servent à dire quelle partie de l’image correspond à chaque coin de la face.
(U pour l’axe horizontal, V pour l’axe vertical)
C’est comme découper une image pour la coller précisément sur un objet en 3D.
Et comment je m’y prends ?
Selon la forme de la face, plusieurs stratégies sont utilisées :
Si la face est un quadrilatère :
C’est un rectangle ? → j’utilise une fonction optimisée pour les rectangles (plus rapide).
C’est un parallélogramme ? → j’ai une autre fonction dédiée, qui prend en compte l’inclinaison.
Autre cas ? → je divise la face en deux triangles pour un rendu plus universel.
Si la face est déjà un triangle :
→ je la dessine telle quelle, en utilisant un algorithme de rendu triangle + texture.
Ces choix sont faits pour optimiser les performances, car chaque méthode est adaptée à la géométrie qu’elle traite.
Et voilà le résultat à l’écran :
Voilà où j’en suis pour le moment.
Les prochains chapitres arrivent très bientôt !
Citer : Posté le 13/05/2025 16:10 | #
Hello !
Les chapitres 5, 6 et 7 sont ajoutés.
Albert Einstein
Citer : Posté le 13/05/2025 16:22 | # |
Fichier joint
Joli !
Pour la skybox tu devrais insérer des teintes intermédiaires en incrémentant les bits de r, g, b à des moments différents. Ça coûte pas cher et ça réduit sensiblement les bandes.
C'est l'algo de quads qu'on voit à l'écran ? J'ai un trou sur l'interpolation, ça ressemble à de l'affine dans certains coins ? Genre là y'a des lignes très louches. C'est un compromis vitesse ?
Citer : Posté le 13/05/2025 16:49 | #
Merci !
Pour la skybox tu devrais insérer des teintes intermédiaires en incrémentant les bits de r, g, b à des moments différents. Ça coûte pas cher et ça réduit sensiblement les bandes.
Ca le fait déjà, il calcule le step RGB et les incrémentes.
C'est l'algo de quads qu'on voit à l'écran ? J'ai un trou sur l'interpolation, ça ressemble à de l'affine dans certains coins ? Genre là y'a des lignes très louches. C'est un compromis vitesse ?
C'est exacte ! Je triche un peu
L'algo du quad est utilisé uniquement quand c'est un parallélogramme, donc ce n'est pas le cas sur cette image, on a deux 2 triangles.
Et pour des raisons de performance, c'est bien de l'affine qui est utilisée, d'où les déformations. J'ai dû faire un compromis entre rendu et performances.
Albert Einstein