Ce projet illustre comment un moteur de jeu peut être construit sans assets pré-fabriqués, uniquement grâce à des calculs mathématiques, des règles procédurales et un rendu 2.5D.
Le résultat : une expérience d’exploration dynamique, cohérente et entièrement générée à la volée.
Une carte infinie, générée par formules
Le monde du jeu repose sur une logique simple mais puissante : aucune carte n’est stockée, tout est calculé à partir des coordonnées du joueur.
Grâce à quelques opérations modulaires, chaque case peut devenir :
- herbe (zone traversable)
- terre (frontière naturelle)
- eau (obstacle)
Et selon des règles de hash :
- arbre
- rocher
- orbe d’énergie
- coffre d’or
Cette approche permet un monde infini, cohérent et sans répétition visible.
Une sortie générée aléatoirement… mais toujours atteignable
Le jeu place une porte de sortie sur une case valide, en vérifiant qu’elle n’est ni eau, ni arbre, ni rocher.
Le joueur doit l’atteindre avec au moins 50 pièces d’or, ce qui crée un objectif clair et motivant.
« Trouve la porte aux coordonnées (X, Y) avec 50 or ! »
Gestion d’énergie et interactions
Chaque déplacement consomme de l’énergie.
Le joueur doit donc :
- éviter les obstacles,
- ramasser des orbes pour recharger,
- ouvrir des coffres pour accumuler de l’or,
- surveiller son HUD pour ne pas tomber à zéro.
L’énergie devient une ressource stratégique, transformant l’exploration en véritable gestion de survie.
Un rendu pseudo-3D basé sur la perspective
Le moteur utilise une projection simple mais efficace :
- 15 couches de profondeur,
- un élargissement ou rétrécissement des polygones selon la distance,
- un décor qui se dessine en temps réel autour du joueur.
Chaque élément (arbre, rocher, coffre, orbe, porte) possède sa propre forme 2.5D, calculée à partir de la distance et de la taille projetée.
Le résultat : une impression de relief et de profondeur, sans recourir à un vrai moteur 3D.
Un personnage animé par la logique
Le personnage n’est pas statique :
- ses jambes bougent selon la somme des coordonnées, créant une animation de marche minimaliste mais expressive.
- Son ombre, son torse, ses bras et sa tête sont dessinés à partir de formes simples, mais l’ensemble donne une vraie présence visuelle.
Un HUD complet pour guider le joueur
Le jeu affiche :
- la position du joueur,
- la position de la sortie,
- la direction optimale,
- l’énergie restante,
- l’or collecté,
- un message dynamique selon les interactions.
« CAP SORTIE : NORD (Z) et EST (D) »
Ce guidage transforme l’exploration en quête structurée, tout en laissant la liberté de se déplacer.
Une boucle de gameplay simple, mais efficace
Le joueur avance, explore, collecte, évite, recharge…
Jusqu’à atteindre la porte avec suffisamment d’or.
Lorsqu’il y parvient :
- « Porte déverrouillée ! Victoire ! »
Une boucle courte, mais parfaitement cohérente et gratifiante.
Un projet qui démontre la puissance du procédural
Ce moteur RPG minimaliste prouve qu’avec :
- des règles mathématiques,
- un rendu 2.5D intelligent,
- une gestion d’état claire,
- et une logique d’interaction bien pensée,
- on peut créer une expérience de jeu immersive, dynamique et entièrement générée en temps réel.
Un excellent exemple de ce que l’on peut accomplir en combinant créativité, algorithmique et design.
Programme :
@ Initialisation
toile1 est une toile
dimension(toile1, 550, 400)
jX est un nombre
jY est un nombre
jX vaut 0
jY vaut 0
// Génération de la porte sur une case libre (ni eau, ni arbre, ni rocher)
exitX est un nombre
exitY est un nombre
porteValide est un booléen
porteValide vaut faux
tant que porteValide = faux
exitX vaut hasard(40, 200)
exitY vaut hasard(40, 200)
eNX est un nombre
eNY est un nombre
eNX vaut exitX + 50000
eNY vaut exitY + 50000
eSol est un nombre
eSol vaut 0
si eNX mod 14 = 0 ou eNY mod 14 = 0 alors
eSol vaut 1
sinon si (eNX * 3 + eNY * 2) mod 31 < 2 alors
eSol vaut 2
fin si
eDecor est un nombre
eDecor vaut 0
si eSol = 0 alors
eHash est un nombre
eHash vaut (eNX * 17 + eNY * 37) mod 100
si eHash < 12 alors eDecor vaut 1
sinon si eHash < 20 alors eDecor vaut 2
fin si
fin si
// On valide seulement si ce n'est pas de l'eau, un arbre ou un rocher
si eSol <> 2 et eDecor <> 1 et eDecor <> 2 alors
porteValide vaut vrai
fin si
fin tant que
orRequis est un nombre
orRequis vaut 50
action est un nombre
derniereAction est un nombre
derniereAction vaut 1
@ Inventaire et Énergie
orJoueur est un nombre
orJoueur vaut 0
objetsDetruits est un dictionnaire
objetsDetruits vaut [:]
energie est un nombre
energie vaut 100
victoire est un booléen
victoire vaut faux
messageHUD est un texte
messageHUD vaut "Trouve la porte aux coordonnées (" + convertir_texte(exitX) + ", " + convertir_texte(exitY) + ") avec 50 or !"
@ Boucle principale
tant que vrai
effacer(toile1)
remplir(toile1, #0f172a)
// 1. Ciel et Soleil
rectangle(toile1, 0, 0, 550, 120, #0f172a)
cercle(toile1, 460, 40, 20, #f1c40f)
// 2. Rendu du terrain 3D
iY est un nombre
pour iY de 0 à 15
rY est un nombre
rY vaut 15 - iY
rX est un nombre
pour rX de - 8 à 8
gx est un nombre
gy est un nombre
gx vaut jX + rX
gy vaut jY + rY
nx est un nombre
ny est un nombre
nx vaut gx + 50000
ny vaut gy + 50000
dLoin est un nombre
dPres est un nombre
dLoin vaut rY + 0.5 + 2.0
dPres vaut rY - 0.5 + 2.0
sLoin est un nombre
sPres est un nombre
sLoin vaut 180 / dLoin
sPres vaut 180 / dPres
xHautG est un nombre
yHautG est un nombre
xHautD est un nombre
yHautD est un nombre
xBasD est un nombre
yBasD est un nombre
xBasG est un nombre
yBasG est un nombre
xHautG vaut 275 + (rX - 0.5) * sLoin * 1.3
yHautG vaut 120 + sLoin * 2.5
xHautD vaut 275 + (rX + 0.5) * sLoin * 1.3
yHautD vaut 120 + sLoin * 2.5
xBasD vaut 275 + (rX + 0.5) * sPres * 1.3
yBasD vaut 120 + sPres * 2.5
xBasG vaut 275 + (rX - 0.5) * sPres * 1.3
yBasG vaut 120 + sPres * 2.5
dCentre est un nombre
dCentre vaut rY + 2.0
sCentre est un nombre
sCentre vaut 180 / dCentre
cx est un nombre
cy est un nombre
cx vaut 275 + rX * sCentre * 1.3
cy vaut 120 + sCentre * 2.5
typeSol est un nombre
typeSol vaut 0
si nx mod 14 = 0 ou ny mod 14 = 0 alors
typeSol vaut 1
sinon si (nx * 3 + ny * 2) mod 31 < 2 alors
typeSol vaut 2
fin si
ptsSol est un tableau
ptsSol ajoute xHautG
ptsSol ajoute yHautG
ptsSol ajoute xHautD
ptsSol ajoute yHautD
ptsSol ajoute xBasD
ptsSol ajoute yBasD
ptsSol ajoute xBasG
ptsSol ajoute yBasG
si typeSol = 0 alors
polygone(toile1, ptsSol, #27ae60)
sinon si typeSol = 1 alors
polygone(toile1, ptsSol, #d35400)
sinon
polygone(toile1, ptsSol, #2980b9)
fin si
contour(toile1, #1a252f, 1)
// Génération des éléments sur la carte
typeDecor est un nombre
typeDecor vaut 0
si gx = exitX et gy = exitY alors
typeDecor vaut 5
sinon si typeSol = 0 alors
hash est un nombre
hash vaut (nx * 17 + ny * 37) mod 100
si hash < 12 alors typeDecor vaut 1 // Arbre
sinon si hash < 20 alors typeDecor vaut 2 // Rocher
sinon si hash < 26 alors typeDecor vaut 3 // Orbe d'Énergie
sinon si hash < 31 alors typeDecor vaut 4 // Coffre d me d'Or
fin si
fin si
cleCase est un texte
cleCase vaut convertir_texte(nx) + "," + convertir_texte(ny)
si contient(objetsDetruits, cleCase) et typeDecor <> 5 alors
typeDecor vaut 0
fin si
hTaille est un nombre
hTaille vaut sCentre * 0.85
si typeDecor = 1 alors
// Arbre (Obstacle)
ellipse(toile1, cx, cy, hTaille * 0.3, hTaille * 0.1, #0e171e)
rectangle(toile1, cx - hTaille * 0.08, cy - hTaille * 0.6, hTaille * 0.16, hTaille * 0.6, #5d4037)
cercle(toile1, cx, cy - hTaille * 0.85, hTaille * 0.35, #1e8449)
sinon si typeDecor = 2 alors
// Rocher (Obstacle)
ellipse(toile1, cx, cy, hTaille * 0.25, hTaille * 0.1, #0e171e)
cercle(toile1, cx, cy - hTaille * 0.15, hTaille * 0.2, #7f8c8d)
sinon si typeDecor = 3 alors
// Orbe d'Énergie
cercle(toile1, cx, cy - hTaille * 0.2, hTaille * 0.22, rgba(56, 189, 248, 0.4))
cercle(toile1, cx, cy - hTaille * 0.2, hTaille * 0.14, #00d2ff)
cercle(toile1, cx, cy - hTaille * 0.2, hTaille * 0.06, #ffffff)
sinon si typeDecor = 4 alors
// Coffre d'Or
rectangle(toile1, cx - hTaille * 0.2, cy - hTaille * 0.3, hTaille * 0.4, hTaille * 0.3, #d35400)
rectangle(toile1, cx - hTaille * 0.2, cy - hTaille * 0.3, hTaille * 0.4, hTaille * 0.08, #f1c40f)
cercle(toile1, cx, cy - hTaille * 0.15, hTaille * 0.05, #f1c40f)
sinon si typeDecor = 5 alors
// Porte de Sortie
rectangle(toile1, cx - hTaille * 0.25, cy - hTaille * 0.7, hTaille * 0.5, hTaille * 0.7, #f1c40f)
rectangle(toile1, cx - hTaille * 0.18, cy - hTaille * 0.63, hTaille * 0.36, hTaille * 0.6, #0f172a)
cercle(toile1, cx, cy - hTaille * 0.35, hTaille * 0.08, #f1c40f)
fin si
fin pour
fin pour
// 3. Personnage principal
pX est un nombre
pY est un nombre
pX vaut 275
pY vaut 340
etapeMarche est un nombre
etapeMarche vaut absolue(jX + jY) mod 4
rebond est un nombre
rebond vaut 0
si etapeMarche = 1 ou etapeMarche = 3 alors rebond vaut -2 fin si
ellipse(toile1, pX, pY + 16, 22, 6, rgba(0, 0, 0, 0.4))
jambeG_Y est un nombre
jambeD_Y est un nombre
jambeG_Y vaut pY + 4
jambeD_Y vaut pY + 4
si etapeMarche = 1 alors jambeG_Y vaut pY
sinon si etapeMarche = 3 alors jambeD_Y vaut pY
fin si
rectangle_arrondi(toile1, pX - 7, jambeG_Y, 6, 12, 2, #34495e)
rectangle_arrondi(toile1, pX - 8, jambeG_Y + 10, 8, 5, 2, #111111)
rectangle_arrondi(toile1, pX + 1, jambeD_Y, 6, 12, 2, #34495e)
rectangle_arrondi(toile1, pX + 0, jambeD_Y + 10, 8, 5, 2, #111111)
cY est un nombre
cY vaut pY - 12 + rebond
rectangle_arrondi(toile1, pX - 12, cY - 2, 6, 14, 3, #2980b9)
cercle(toile1, pX - 9, cY + 12, 3, #f1c27d)
rectangle_arrondi(toile1, pX + 6, cY - 2, 6, 14, 3, #2980b9)
cercle(toile1, pX + 9, cY + 12, 3, #f1c27d)
rectangle_arrondi(toile1, pX - 8, cY - 5, 16, 18, 4, #3498db)
si derniereAction = 2 alors
rectangle(toile1, pX - 8, cY + 9, 16, 4, #2c3e50)
rectangle(toile1, pX - 2, cY + 8, 4, 6, #f1c40f)
tY est un nombre
tY vaut cY - 12
cercle(toile1, pX, tY, 9, #f1c27d)
arc(toile1, pX, tY + 2, 9, 180, 360, #6e2c00)
cercle(toile1, pX - 3, tY - 1, 1, #000000)
cercle(toile1, pX + 3, tY - 1, 1, #000000)
sinon
rectangle_arrondi(toile1, pX - 6, cY - 2, 12, 14, 3, #d35400)
rectangle(toile1, pX - 6, cY + 3, 12, 2, #873600)
tY est un nombre
tY vaut cY - 12
cercle(toile1, pX, tY, 9, #f1c27d)
cercle(toile1, pX, tY - 2, 8, #6e2c00)
fin si
// 4. Interface HUD & Guidage
rectangle(toile1, 0, 0, 550, 90, rgba(15, 23, 42, 0.95))
contour(toile1, #334155, 1)
label(toile1, 10, 15, "Or: " + convertir_texte(orJoueur) + " / " + convertir_texte(orRequis), #f1c40f, 12)
rectangle(toile1, 140, 7, 120, 10, #334155)
si energie > 0 alors
colNRG est un texte
colNRG vaut #38bdf8
si energie < 30 alors colNRG vaut #ef4444 fin si
rectangle(toile1, 140, 7, (energie / 100) * 120, 10, colNRG)
fin si
label(toile1, 270, 15, "Énergie: " + convertir_texte(energie) + "%", #38bdf8, 10)
distX est un nombre
distY est un nombre
distX vaut exitX - jX
distY vaut exitY - jY
distTotale est un nombre
distTotale vaut absolue(distX) + absolue(distY)
txtPos est un texte
txtPos vaut "Position: (" + convertir_texte(jX) + ", " + convertir_texte(jY) + ") | Sortie: (" + convertir_texte(exitX) + ", " + convertir_texte(exitY) + ")"
label(toile1, 10, 36, txtPos, #94a3b8, 10)
dirConseil est un texte
dirConseil vaut "CAP SORTIE : "
si distY > 0 et distX > 0 alors dirConseil vaut dirConseil + "NORD (Z) et EST (D)"
sinon si distY > 0 et distX < 0 alors dirConseil vaut dirConseil + "NORD (Z) et OUEST (Q)"
sinon si distY < 0 et distX > 0 alors dirConseil vaut dirConseil + "SUD (S) et EST (D)"
sinon si distY < 0 et distX < 0 alors dirConseil vaut dirConseil + "SUD (S) et OUEST (Q)"
sinon si distY > 0 alors dirConseil vaut dirConseil + "NORD (Touche Z)"
sinon si distY < 0 alors dirConseil vaut dirConseil + "SUD (Touche S)"
sinon si distX > 0 alors dirConseil vaut dirConseil + "EST (Touche D)"
sinon si distX < 0 alors dirConseil vaut dirConseil + "OUEST (Touche Q)"
sinon dirConseil vaut "SUR LA PORTE !"
fin si
label(toile1, 10, 56, dirConseil + " (" + convertir_texte(distTotale) + " pas)", #f59e0b, 11)
label(toile1, 10, 78, ">> " + messageHUD, #22c55e, 11)
si victoire alors
rectangle(toile1, 75, 130, 400, 110, rgba(15, 23, 42, 0.95))
rectangle(toile1, 75, 130, 400, 4, #f1c40f)
label(toile1, 155, 175, "VICTOIRE !", #f1c40f, 20)
label(toile1, 105, 210, "Porte atteinte aux coordonnées (" + convertir_texte(exitX) + ", " + convertir_texte(exitY) + ") !", #ffffff, 12)
fin si
affiche toile1
// 5. Mouvements et Interactions
appuyer ["z", "s", "q", "d"] dans action
si action > 0 et victoire = faux alors
derniereAction vaut action
nouvX est un nombre
nouvY est un nombre
nouvX vaut jX
nouvY vaut jY
si action = 1 alors nouvY vaut jY + 1 fin si
si action = 2 alors nouvY vaut jY - 1 fin si
si action = 3 alors nouvX vaut jX - 1 fin si
si action = 4 alors nouvX vaut jX + 1 fin si
chkX est un nombre
chkY est un nombre
chkX vaut nouvX + 50000
chkY vaut nouvY + 50000
solFutur est un nombre
solFutur vaut 0
si chkX mod 14 = 0 ou chkY mod 14 = 0 alors
solFutur vaut 1
sinon si (chkX * 3 + chkY * 2) mod 31 < 2 alors
solFutur vaut 2
fin si
decorFutur est un nombre
decorFutur vaut 0
si nouvX = exitX et nouvY = exitY alors
decorFutur vaut 5
sinon si solFutur = 0 alors
hFutur est un nombre
hFutur vaut (chkX * 17 + chkY * 37) mod 100
si hFutur < 12 alors decorFutur vaut 1
sinon si hFutur < 20 alors decorFutur vaut 2
sinon si hFutur < 26 alors decorFutur vaut 3
sinon si hFutur < 31 alors decorFutur vaut 4
fin si
fin si
cleDest est un texte
cleDest vaut convertir_texte(chkX) + "," + convertir_texte(chkY)
si contient(objetsDetruits, cleDest) et decorFutur <> 5 alors
decorFutur vaut 0
fin si
si solFutur = 2 alors
messageHUD vaut "L'eau bloque le passage !"
sinon si decorFutur = 1 alors
messageHUD vaut "Un arbre bloque le passage !"
sinon si decorFutur = 2 alors
messageHUD vaut "Un rocher bloque le passage !"
sinon si decorFutur = 5 alors
si orJoueur >= orRequis alors
victoire vaut vrai
jX vaut nouvX
jY vaut nouvY
messageHUD vaut "Porte déverrouillée ! Victoire !"
sinon
messageHUD vaut "Il te faut " + convertir_texte(orRequis) + " pièces d'or ! (Tu en as " + convertir_texte(orJoueur) + ")"
fin si
sinon si decorFutur = 3 alors
objetsDetruits[cleDest] vaut vrai
energie vaut energie + 35
si energie > 100 alors energie vaut 100 fin si
jX vaut nouvX
jY vaut nouvY
messageHUD vaut "Orbe d'Énergie ramassé ! +35% Énergie !"
sinon si decorFutur = 4 alors
orJoueur vaut orJoueur + 10
objetsDetruits[cleDest] vaut vrai
jX vaut nouvX
jY vaut nouvY
messageHUD vaut "Coffre ouvert ! +10 Or récolté !"
sinon
jX vaut nouvX
jY vaut nouvY
energie vaut energie - 1
si energie <= 0 alors
energie vaut 0
messageHUD vaut "Énergie épuisée ! Trouve un orbe bleu !"
sinon
messageHUD vaut "En route..."
fin si
fin si
fin si
fin tant que