En février 2026, je me suis disputé sur Reddit. Quelqu’un affirmait que les derniers modèles de codage écrivaient du meilleur code que n’importe quel développeur humain, et citait les toutes dernières versions d’OpenAI et d’Anthropic comme preuve. Je n’étais pas d’accord. Les modèles sont très bons pour les pages d’accueil, les back-ends CRUD et le genre de démo qui existe en mille copies sur GitHub. J’avais des doutes sur ce qui se passe quand la tâche exige du raisonnement spatial, une arithmétique en virgule flottante soignée et un avis sur ce qui est agréable à jouer.
J’ai donc choisi un projet qui n’existe quasiment pas sur internet et je l’ai construit moi-même — un jeu de course en pseudo-3D dans le style d’OutRun, tournant sur un ESP32-S3 avec un écran ILI9341. En chemin, j’ai demandé aux modèles de construire la même chose. Cet article explique comment le jeu fonctionne, plus en détail que le résumé plus court que j’ai publié sur Medium, et se termine par ce que les modèles ont raté. Le code source complet est sur github.com/davidmonterocrespo24/esp32s3-arcade-3d.

Ce que fait le jeu
L’ensemble représente environ 2 500 lignes de C++ dans le framework Arduino, dessinant via la bibliothèque TFT_eSPI. Il comprend :
- Une route faite de segments avec virages, collines et creux, générée aléatoirement à chaque démarrage.
- Une voiture joueur 3D texturée chargée depuis un maillage OBJ — 428 sommets, 312 triangles, une texture de 128 par 128.
- Des bâtiments des deux côtés de la route, un long tunnel avec des lumières au plafond, et des arbres, buissons, rochers et lampadaires dans les interstices.
- Six voitures de trafic avec leur propre vitesse et leur propre voie.
- Un cycle jour, coucher de soleil et nuit avec un brouillard exponentiel vers l’horizon.
- Une physique avec rampes d’accélération, friction, gravité des collines et dérive latérale dans les virages, plus les collisions avec le trafic, les murs et les objets en bord de route.
- Un HUD avec un compteur de vitesse rond, un compteur de tours et les temps du tour en cours et du meilleur tour.
Le matériel est délibérément ordinaire :
| Composant | Détails |
|---|---|
| SoC | ESP32-S3, bicœur à 240 MHz, avec PSRAM |
| Écran | TFT ILI9341, 320 par 240 pixels, RGB565 16 bits, en SPI |
| Broches de l’écran | SCK 12, MOSI 11, MISO 13, CS 10, DC 9, RST 8 |
| Rétroéclairage | GPIO 39 |
| Boutons | GPIO 17 (gauche) et GPIO 16 (droite), avec pull-ups internes |

Pourquoi il n’y a pas de Z-buffer
La première réponse que ChatGPT et Claude m’ont donnée était que le projet n’était pas réalisable sur ce matériel, parce qu’un moteur de rendu 3D a besoin d’un tampon de profondeur et que le microcontrôleur n’a pas la place pour en accueillir un. Le calcul vaut la peine d’être fait, car il montre où se situe la vraie contrainte.
Une image de 320 par 240 en RGB565 fait 153 600 octets. Un tampon de profondeur 16 bits de même taille en fait 153 600 de plus. L’ESP32-S3 dispose de 512 Ko de SRAM interne, dont une bonne partie est occupée par le cœur Arduino, le pilote d’écran et le tas, donc deux tampons plein écran ne tiennent pas confortablement en mémoire interne. Avec la PSRAM, ils tiennent. L’argument de la mémoire est faible.
L’argument plus solide est le coût par pixel. Un tampon de profondeur implique une lecture, une comparaison et une écriture conditionnelle pour chaque pixel de chaque triangle, en logiciel, sur un cœur qui doit aussi remplir 76 800 pixels par image et les envoyer en SPI. À 240 MHz, ce budget est mince.
Les jeux de course d’arcade des années 1980 n’avaient ni la mémoire ni le taux de remplissage, et ils ont résolu le problème par l’ordonnancement plutôt que par le test de profondeur — dessiner les objets lointains en premier et laisser les objets proches les recouvrir. C’est l’algorithme du peintre, et il fonctionne parce qu’une route est une scène très bien comportée. Les segments sont déjà triés par distance, et tout le reste (arbres, bâtiments, trafic) est rattaché à un segment. Le seul objet qui nécessite une vraie 3D est la voiture du joueur, et elle se trouve à une distance fixe devant la caméra, donc elle peut être triée à part.
La route — segments et projection
La route est un tableau de 200 segments, chacun long de 200 unités monde. Un segment stocke sa courbure, son élévation, un sprite optionnel en bord de route, un drapeau de tunnel et la hauteur et la couleur d’un bâtiment de chaque côté :
struct Segment {
float curve; // Segment curvature
float y; // Height (elevation)
int8_t spriteType; // Sprite type (-1 = none)
float spriteOffset; // Lateral sprite offset
bool tunnel; // true = inside tunnel
int buildL, buildR; // Left/Right building height (0 = no building)
uint16_t colorL, colorR; // Building facade color
};
L’échelle du monde découle de la largeur de la route — ROAD_W vaut
2000 unités pour la moitié de la route, ce que je considère comme environ
10,5 m, donc une unité fait environ 5,25 mm. Toutes les autres constantes
de config.h sont exprimées dans ces unités.
La technique centrale suit les
articles sur le jeu de course en JavaScript
de Jake Gordon, qui constituent l’explication la plus claire des routes
en pseudo-3D que je connaisse. La caméra se trouve CAM_HEIGHT unités
au-dessus de la route. Le champ de vision définit une profondeur de
caméra :
float fovRad = FOV_DEG * PI / 180.0;
cameraDepth = 1.0 / tanf(fovRad / 2.0);
playerZdist = CAM_HEIGHT * cameraDepth;
Projeter un bord de segment revient alors à une division et deux
multiplications. camZ1 est la distance entre la caméra et le bord
proche du segment n, et sc1 est l’échelle à cette distance :
float sc1 = cameraDepth / camZ1;
float cyp1 = p1Y - camY; // height relative to the camera
float cxp1 = playerX * ROAD_W - curveX; // lateral offset incl. the curve
int16_t sy1 = SCR_CY - (int)(sc1 * cyp1 * SCR_CY);
int16_t sx1 = SCR_CX + (int)(sc1 * (-cxp1) * SCR_CX);
int16_t sw1 = (int)(sc1 * ROAD_W * SCR_CX);
Les virages sont l’astuce classique — la curve d’un segment n’est pas
un angle, c’est un changement d’offset horizontal par segment. À mesure
que la boucle s’éloigne de la caméra, elle accumule curveDX += seg.curve
et curveX += curveDX, si bien que l’offset croît quadratiquement avec
la distance et que la route se courbe en douceur. Les collines sont plus
simples encore — chaque segment a son propre y, et la projection utilise
la différence de hauteur par rapport à la caméra, donc une route montante
grimpe à l’écran et un creux disparaît sous l’horizon.
Le détail intéressant est que drawRoad() effectue deux passes sur les
segments, dans des directions opposées.
- De l’avant vers l’arrière, en projetant. La première boucle part
du segment le plus proche et s’éloigne, projette chaque bord dans
rCache[n], et enregistre dansrClip[n]la ligne d’écran la plus basse encore visible à cette distance. À mesure que la boucle avance,maxyne fait que monter — un segment derrière une crête de colline se projette sous la crête et est éliminé. C’est ainsi que les collines occultent ce qui se trouve derrière elles sans aucune comparaison de profondeur. - De l’arrière vers l’avant, en dessinant. La seconde boucle part de
l’extrémité lointaine et se rapproche de la caméra, et dessine pour
chaque segment l’herbe, les bandes rugueuses, la route et les lignes
de voie sous forme de bandes horizontales entre
rCache[n]etrCache[n-1], découpées parrClip. Près de la caméra, un segment peut faire plusieurs dizaines de lignes de haut, il est donc subdivisé en jusqu’à trois bandes avec la largeur interpolée entre les deux bords, ce qui évite que les virages ressemblent à des trapèzes empilés.
Les bâtiments et le tunnel partagent la même boucle
Une première version dessinait la route dans une boucle et les bâtiments dans une autre. Cela semblait correct sur terrain plat et s’effondrait sur les collines — un bâtiment qui aurait dû être caché derrière une crête était peint par-dessus, parce que la seconde boucle n’avait aucune idée de ce que la première avait déjà dessiné.
La solution consistait à dessiner tout ce qui appartient à un segment dans la même boucle arrière-vers-avant. Pour chaque segment, dans l’ordre — les murs et le plafond du tunnel si le segment est à l’intérieur du tunnel, les bâtiments s’il est à l’extérieur, puis la surface de la route. Un bâtiment est une paire de quads (face avant et face latérale) construits à partir des bords projetés de la route du segment et de celui qui le précède :
void drawQuad(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, uint16_t c) {
spr.fillTriangle(x1, y1, x2, y2, x3, y3, c);
spr.fillTriangle(x1, y1, x3, y3, x4, y4, c);
}
Le tunnel repose sur la même idée avec les quads tournés vers l’intérieur — un quad de plafond à une hauteur fixe au-dessus de la route, deux quads de murs, une lumière jaune tous les quatre segments et un portail gris dessiné sur le premier segment. Comme tout est émis dans l’ordre du peintre, une entrée de tunnel à mi-hauteur d’une colline rend correctement sans cas particuliers.
Les sprites de bord de route et les voitures de circulation vont dans une troisième et courte boucle après que la
route est complète. Ils sont dessinés comme des formes plates mises à l’échelle par l’échelle de projection du segment
et découpés selon rClip, de sorte qu’un arbre derrière une colline est coupé
à la crête exactement comme la route l’est.

La voiture du joueur est vraiment en 3D
Je ne voulais pas d’un sprite pour le joueur. La voiture est un maillage OBJ converti hors ligne en un en-tête C par un petit script Python, donc rien n’est analysé au runtime :
python assets/obj_to_header.py assets/Car2.obj > car2_mesh.h
python assets/png_to_rgb565.py assets/car2.png > car2_texture.h
L’en-tête contient 428 sommets avec position et coordonnées de texture, et
312 triangles sous forme de liste d’indices. La texture de 128 par 128 représente 32 Ko de RGB565
en flash, lue avec pgm_read_word() pour qu’elle ne touche jamais la RAM.
À chaque image, le maillage passe par un petit pipeline fixe dans
render_player.cpp :
- Transformation. Chaque sommet est tourné autour de l’axe vertical d’un angle dérivé de la position latérale de la voiture (la voiture tourne visiblement dans la courbe), puis incliné selon la pente de la route, moyennée sur les six segments suivants et lissée dans le temps pour que la voiture s’incline sur les collines sans tremblement.
- Projection.
x = centerX + rx * fov / z, avec une distance de caméra fixe et une focale de 130 pixels. Les sommets derrière la caméra sont signalés et leurs triangles ignorés. - Tri. La profondeur moyenne de chaque triangle est calculée et les 312 triangles sont triés du plus lointain au plus proche. Un tri par insertion suffit à cette taille et est quasiment gratuit quand l’ordre change à peine entre les images.
- Élimination et éclairage. Les triangles orientés vers l’arrière sont écartés en utilisant le signe du produit vectoriel 2D. La normale de face est calculée dans l’espace objet et multipliée par une lumière fixe venant du dessus et légèrement de face, donnant une luminosité entre 0,35 et 1,0.
- Rastérisation. Chaque triangle est dessiné par un rastériseur à balayage de lignes avec un mapping de texture affine.
Le rastériseur est la partie sur laquelle j’ai passé le plus de temps. Il trie les trois sommets par Y à l’écran, parcourt les lignes entre le haut et le bas, et pour chaque ligne interpole les X gauche et droit et les coordonnées de texture le long des deux arêtes actives. Puis il avance le long de la ligne, échantillonne la texture, applique l’éclairage et écrit un pixel :
for (int x = x0; x <= x1; x++) {
float t = (x - xL) * dx;
float u = uL + t * (uR - uL);
float v = vL + t * (vR - vL);
int tx = (int)(u * (CAR2_TEX_W - 1));
int ty = (int)((1.0f - v) * (CAR2_TEX_H - 1)); // OBJ V runs bottom-up
tx = max(0, min(tx, CAR2_TEX_W - 1));
ty = max(0, min(ty, CAR2_TEX_H - 1));
uint16_t texel = pgm_read_word(&car2_texture[ty * CAR2_TEX_W + tx]);
if (light < 0.99f) {
uint8_t r = ((texel >> 11) & 0x1F) * light;
uint8_t g = ((texel >> 5) & 0x3F) * light;
uint8_t b = ( texel & 0x1F) * light;
texel = (r << 11) | (g << 5) | b;
}
spr.drawPixel(x, y, texel);
}
Le mapping affine interpole u et v linéairement dans l’espace écran, ce qui
n’est pas correct du point de vue de la perspective. Sur un grand mur, cela produit l’oscillation qui
a rendu célèbres les jeux PlayStation. Sur une voiture qui occupe environ un cinquième de
l’écran, toujours à la même distance, l’erreur est inférieure à un pixel et
la division par pixel qu’un mapping correct nécessiterait ne vaut pas la peine d’être
payée. Le moteur de rendu refuse aussi toute ligne de balayage plus large que 160 pixels et
tout triangle avec un sommet à plus de 20 pixels hors écran. Ces deux garde-fous
existent à cause de bugs réels — un seul sommet mal trié ou mal découpé se transforme
en une traînée sur toute l’image, et il est bien moins coûteux d’abandonner le
triangle que de le découper correctement.
Sous la voiture se trouve une ellipse sombre dessinée comme un empilement de lignes horizontales. Elle se décale avec la position latérale de la voiture et c’est l’élément le moins coûteux du projet avec le plus grand effet sur l’ancrage visuel de la voiture.

Ciel, parallaxe et heure de la journée
Le ciel n’est pas dessiné à chaque image. Au démarrage, initBackground() construit un
sprite de 640 par 120 en PSRAM (deux fois la largeur de l’écran, la moitié supérieure de
l’écran en hauteur) avec un dégradé vertical, un soleil, et deux couches d’une
ligne d’horizon procédurale avec des fenêtres éclairées. À chaque image, la boucle principale le fait défiler
d’une quantité proportionnelle à la courbe et à la vitesse actuelles, et le blitte
deux fois pour que le bouclage soit transparent :
int bgX = (int)skyOffset % (SCR_W * 2);
bgSpr.pushToSprite(&spr, -bgX, 0);
bgSpr.pushToSprite(&spr, (SCR_W * 2) - bgX, 0);
C’est l’effet qu’utilise Horizon Chase — l’horizon glisse latéralement quand on tourne, ce qui vend la courbe bien plus que la seule géométrie de la route.
La palette vit dans colors.cpp sous forme de trois ensembles de couleurs RGB565 pour le jour,
le coucher du soleil et la nuit — ciel, herbe claire et sombre, route claire et sombre, bandes
de bordure, marquages de voie et brouillard. Le jeu passe à l’ensemble suivant tous les
180 000 unités de monde parcourues, ce qui à vitesse maximale représente environ quatorze
secondes. Le brouillard est exponentiel en distance et mélangé par segment :
float expFog(float d, float density) {
return 1.0f - clampF(1.0f / expf(d * d * density), 0, 1);
}
lerpCol() mélange deux valeurs RGB565 canal par canal, ce qui suffit
pour faire fondre la route et l’herbe dans la couleur du brouillard sans jamais convertir en
24 bits.
Physique — ce qui donne la sensation d’une voiture
Les graphismes attirent l’attention sur un jeu de course. La physique décide si quelqu’un y joue plus d’une minute. Tout le réglage vit dans config.h :
#define SPEED_MULTIPLIER 65.0f // maxSpeed = SEG_LEN * SPEED_MULTIPLIER (~246 km/h)
#define ACCEL_TARGET 0.9f // targetAccel = maxSpeed * ACCEL_TARGET
#define ACCEL_RAMP 180.0f // How fast acceleration ramps up (u/s^2)
#define FRICTION 0.996f // Friction per frame (braking ~3.3s from max)
#define GRAVITY_FACTOR 1600.0f // Effect of slopes on acceleration
#define CENTRIFUGAL 0.18f // Centrifugal force in curves
#define CURVE_FORCE 3.0f // Lateral force multiplier in curves
#define LATERAL_FRICTION 0.90f // Lateral velocity damping per frame
La mise à jour dans physics.cpp fait quatre choses dans l’ordre. La pente est lue à partir de la différence de hauteur entre le segment courant et le précédent, puis transformée en accélération, de sorte que les montées font perdre de la vitesse et les descentes en ajoutent. La friction est appliquée comme une décroissance multiplicative. L’accélération, qui augmente progressivement au lieu d’atteindre directement sa cible, est intégrée dans la vitesse. Puis le virage pousse la voiture latéralement :
float curveForce = segments[pSeg].curve * centrifugal * spPct;
velocityX += curveForce * dt * CURVE_FORCE; // lateral velocity builds up
velocityX *= LATERAL_FRICTION; // and decays every frame
playerX -= velocityX * dt;
C’est la vitesse latérale qui produit le dérapage. Une voiture qui entre dans un virage ne glisse pas immédiatement ; la force s’accumule, le glissement se construit, et quand le virage se termine, l’amortissement ramène la voiture. L’angle du dérapage est ensuite calculé à partir du rapport entre la vitesse latérale et la vitesse longitudinale, et utilisé uniquement pour l’affichage.
Les collisions sont gérées par la distance le long de la piste plus le chevauchement sur l’axe latéral. Percuter par l’arrière une voiture de circulation plus lente vous fait tomber à soixante-dix pour cent de sa vitesse ; toucher un mur de tunnel, un arbre ou sortir complètement de la route réduit davantage la vitesse, et n’importe lequel de ces cas au-dessus d’un seuil déclenche l’état de collision de deux secondes.
La version du dépôt tourne comme une démo — un pilote automatique lit le virage à venir et contre-braque dedans, et l’accélérateur est toujours à fond. Les deux boutons sont déclarés et configurés en entrée avec pull-up dans setup(), et l’émulateur PC mappe les touches fléchées dessus, donc la direction manuelle se résume à quelques lignes dans handleInput().
Aucune de ces constantes ne vient d’une formule. J’ai esquissé la perspective sur papier, construit les courbes de vitesse dans un tableur, puis changé un nombre à la fois jusqu’à ce que la voiture soit agréable à conduire. Cette boucle n’a fonctionné que parce que chaque itération prenait quelques secondes, ce qui m’amène à l’émulateur.
Développer sur PC avec Raylib
Flasher un ESP32-S3 prend assez longtemps pour que régler une constante au feeling soit pénible. Le même code source compile donc sous Windows avec
Raylib, les appels à Arduino et TFT_eSPI étant remplacés par une fine couche d’adaptation dans le dossier emulator/ :
car_game_wrapper.cppfait simplement#include "../car_game.ino", de sorte que le sketch devient une unité de traduction C++ ordinaire, sans aucune modification.Arduino.hetArduino.cppfournissentmillis(),delay(),random(), unSerialqui écrit sur stdout, et undigitalRead()qui renvoie l’état des touches fléchées pour les deux broches de bouton.TFT_eSPI.cppimplémenteTFT_eSpriteau-dessus d’une texture de rendu Raylib.fillRect,fillTriangle,drawPixelet le reste deviennent des appels de dessin Raylib ;pushSprite()copie la texture vers la fenêtre.
Un exemple du genre de chose que la couche d’adaptation doit gérer correctement :
void TFT_eSprite::fillTriangle(int32_t x0, int32_t y0, int32_t x1, int32_t y1,
int32_t x2, int32_t y2, uint16_t color) {
BeginTextureMode(sd->texture);
// Draw twice so both winding orders show; Raylib culls one of them
DrawTriangle({x0, y0}, {x1, y1}, {x2, y2}, r565(color));
DrawTriangle({x0, y0}, {x2, y2}, {x1, y1}, r565(color));
EndTextureMode();
}
TFT_eSPI ne se soucie pas de l’ordre d’enroulement, Raylib si, et le code de la route n’en garantit jamais un. Dessiner chaque triangle deux fois ne coûte rien sur un GPU et a rendu l’émulateur fidèle au pixel près par rapport à la carte.
Une fois cela en place, la boucle consistait à modifier, make, exécuter, en quelques secondes. Les captures d’écran de cet article proviennent de l’émulateur ; l’animation en haut est la carte.
Mémoire et budget de frame sur la carte
Où vont les octets :
| Tampon | Taille | Emplacement |
|---|---|---|
| Sprite de frame, 320 par 240 en 16 bits | 153 600 o | PSRAM |
| Sprite ciel et skyline, 640 par 120 en 16 bits | 153 600 o | PSRAM |
| Texture de voiture, 128 par 128 en 16 bits | 32 768 o | Flash |
| Maillage de voiture, 428 sommets et 312 triangles | environ 10 Ko | Flash |
| Piste, 200 segments | environ 6 Ko | SRAM interne |
| Caches de projection et tableaux par frame | quelques Ko | SRAM interne |
Les deux sprites sont créés avec spr.setAttribute(PSRAM_ENABLE, true)
avant createSprite(). Cette seule ligne fait la différence entre un jeu qui tourne et un qui échoue à allouer. Les 8 Mo de PSRAM du module sont en grande partie inutilisés ; ce qui compte, c’est que les deux tampons pleine largeur n’entrent pas en concurrence avec le cœur Arduino pour la mémoire interne.
La frame est entièrement composée dans le sprite et envoyée au panneau une fois par boucle avec spr.pushSprite(0, 0). C’est ce qui élimine le tearing, et c’est aussi le plafond du taux de rafraîchissement — 153 600 octets sur SPI à 40 MHz prennent à eux seuls environ 31 ms, avant tout dessin. Sur la carte, le jeu se stabilise autour de 30 images par seconde, ce qui correspond exactement à ce que prédit ce calcul. Augmenter l’horloge SPI dans User_Setup.h de TFT_eSPI est le premier endroit à regarder si vous en voulez plus.

Là où les modèles d’IA ont échoué
En parallèle de la version écrite à la main, j’ai demandé aux modèles de codage disponibles en février 2026 de construire le même jeu à partir d’une description. Voici ce qui s’est passé, aussi équitablement que je peux le formuler :
- ChatGPT a produit du code qui ne compilait pas, et une fois corrigé à la main, ses calculs de perspective étaient très loin du compte. Son conseil constant était d’ajouter un Z-buffer.
- Claude a produit la meilleure structure de fichiers et le code le plus lisible, puis a cassé le pipeline de rendu. Il n’a pas conservé l’ordre arrière-vers-avant entre la route, le tunnel et les bâtiments, et il a introduit des bugs subtils dans les calculs en virgule flottante.
- Gemini 3 Pro s’est approché le plus des mathématiques de projection, puis n’avait aucun avis sur le reste. Les couleurs semblaient fausses, la physique paraissait flottante, et lorsque sa propre sortie présentait des artefacts de rendu, il ne parvenait pas à les trouver.
Je ne pense pas que tout cela soit surprenant. Les trois choses dont le projet avait le plus besoin sont les trois choses dont un modèle de langage dispose le moins :
- L’ordonnancement dans l’espace. Savoir ce qui est devant quoi, sur une colline, à l’intérieur d’un tunnel, est un fait spatial, pas textuel.
- Le soin numérique. Le fichier OBJ est en Y vers le haut et l’écran est en Y vers le bas, l’axe V de la texture va du bas vers le haut, et le décalage de courbe est accumulé en virgule flottante sur quarante segments à chaque frame. Chacun de ces points est à un signe ou à un arrondi près d’une route qui dérive ou d’une voiture dessinée à l’envers.
- Le goût. Savoir si 0,18 ou 0,25 est la bonne constante centrifuge ne se déduit pas. Il faut le ressentir au volant.
J’ai bien utilisé l’IA sur ce projet, et elle a été utile exactement pour ce en quoi elle excelle — échafauder la structure des modules, écrire les convertisseurs OBJ et PNG, et refactoriser le code de dessin répétitif une fois la conception figée. Chaque constante, chaque projection et chaque décision d’ordonnancement ont été faites à la main. Le modèle était un outil, pas l’architecte. C’était vrai en février 2026 et cela pourrait ne pas le rester éternellement ; si vous parvenez à faire produire à un modèle une version jouable de ceci à partir de la seule description, je serais sincèrement curieux de la voir.
Construisez-le vous-même
Le dépôt est github.com/davidmonterocrespo24/esp32s3-arcade-3d.
Sur la carte :
- Câblez un ILI9341 à l’ESP32-S3 sur les broches indiquées dans le tableau matériel ci-dessus, ainsi que les deux boutons-poussoirs entre GPIO 17, GPIO 16 et la masse.
- Installez la bibliothèque TFT_eSPI et configurez les mêmes broches et l’horloge SPI dans
son
User_Setup.h. - Ouvrez
car_game.inodans l’IDE Arduino, sélectionnez ESP32S3 Dev Module, activez la PSRAM dans les options de la carte, et téléversez.
Sur un PC (Windows, MinGW et Raylib installés) :
cd emulator/
make
./car_game_emu.exe
Le même câblage est décrit dans le diagram.json du dépôt, et aussi bien
l’ESP32-S3 DevKit que l’ILI9341 existent dans le catalogue de composants de Velxio si vous
souhaitez disposer le circuit sans fer à souder. Je n’ai pas encore essayé
d’exécuter le jeu complet dans la carte émulée ; la configuration des broches de TFT_eSPI et
le sprite en PSRAM en font un test plus intéressant qu’un sketch blink, et
c’est sur ma liste.
Si vous le construisez, modifiez le générateur de piste, ou parvenez à faire écrire le jeu par un modèle, ouvrez une issue sur le dépôt. Je les lis toutes.