Développer un moteur 3D en raycasting à partir de zéro est un exercice fascinant : il combine algorithmique, mathématiques, génération procédurale et rendu graphique.
Ce projet illustre parfaitement comment des concepts simples, bien maîtrisés, peuvent produire une expérience immersive et dynamique.
Génération procédurale : un labyrinthe créé par DFS
La carte 24×24 est générée via un Depth-First Search (DFS), garantissant un labyrinthe unique et entièrement parcourable.
Points clés :
- Grille initiale remplie de murs.
- Exploration en profondeur avec backtracking.
- Sélection aléatoire des voisins pour créer des chemins variés.
- Cassage des murs intermédiaires pour former des couloirs cohérents.
Un algorithme simple, efficace, et idéal pour comprendre les bases de la génération procédurale.
Déplacements fluides et gestion des collisions
Le joueur est positionné au centre de la première cellule ouverte.
Les déplacements reposent sur un vecteur directionnel calculé à partir de l’angle de vue, avec une fonction dédiée pour éviter toute traversée de mur.
Contrôles :
- Z / S : avancer / reculer
- Q / D : pivoter à gauche / droite
La fonction tenteDeplacement() vérifie la cellule cible avant de valider le mouvement, assurant une navigation réaliste.
Raycasting : un rendu 3D inspiré des moteurs historiques
Le moteur utilise 150 rayons couvrant un champ de vision de 60°.
Chaque rayon calcule :
- direction,
- distance à l’impact,
- hauteur du mur,
- correction de perspective,
- ombrage selon la distance et le côté touché.
Le rendu inclut :
- un dégradé de ciel, un sol lissé, des murs ombrés, et une profondeur visuelle convaincante.
Mini-carte dynamique : comprendre l’espace en un coup d’œil
La mini-carte 24×24 affiche :
- les murs en bleu,
- les couloirs en gris foncé,
- la position du joueur,
- son orientation via un vecteur de direction.
Un outil essentiel pour visualiser la structure du labyrinthe généré.
Un projet complet et formateur
Ce moteur 3D réunit :
- génération procédurale,
- mathématiques appliquées,
- optimisation en temps réel,
- rendu graphique,
- gestion d’interactions utilisateur.
Une excellente base pour comprendre les fondations des moteurs 3D modernes, tout en restant accessible et pédagogique.
Programme :
@ 0. CONSTANTES
constante TAILLE_CARTE vaut 24 // Carte 24x24
constante LARGEUR_ECRAN vaut 600
constante HAUTEUR_ECRAN vaut 400
constante NB_RAYONS vaut 150
constante EPAISSEUR_RAYON vaut 4
@ 1. CARTE DU MONDE (GÉNÉRATION ALÉATOIRE AVEC DFS)
carte est un tableau
// 1.1 Remplir toute la carte de murs (1)
pour i de 0 à (TAILLE_CARTE * TAILLE_CARTE) - 1
carte ajoute 1
fin pour
// 1.2 Initialisation de l'algorithme DFS
pile est un tableau
startX est un nombre
startX vaut 1
startY est un nombre
startY vaut 1
// Marquer la position de départ comme chemin (0)
idxDepart est un nombre
idxDepart vaut (startY * TAILLE_CARTE) + startX
carte[idxDepart] vaut 0
pile ajoute idxDepart
// 1.3 Boucle principale du parcours en profondeur
tant que longueur(pile) > 0
courantIdx est un nombre
courantIdx vaut dernier(pile)
cx est un nombre
cx vaut courantIdx mod TAILLE_CARTE
cy est un nombre
cy vaut arrondi_inferieur(courantIdx / TAILLE_CARTE)
voisins est un tableau
// Vérifier le voisin du HAUT (y - 2)
si cy - 2 > 0 alors
idxVaut est un nombre
idxVaut vaut ((cy - 2) * TAILLE_CARTE) + cx
si carte[idxVaut] = 1 alors
voisins ajoute 0
fin si
fin si
// Vérifier le voisin du BAS (y + 2)
si cy + 2 < TAILLE_CARTE - 1 alors
idxVaut est un nombre
idxVaut vaut ((cy + 2) * TAILLE_CARTE) + cx
si carte[idxVaut] = 1 alors
voisins ajoute 1
fin si
fin si
// Vérifier le voisin de GAUCHE (x - 2)
si cx - 2 > 0 alors
idxVaut est un nombre
idxVaut vaut (cy * TAILLE_CARTE) + (cx - 2)
si carte[idxVaut] = 1 alors
voisins ajoute 2
fin si
fin si
// Vérifier le voisin de DROITE (x + 2)
si cx + 2 < TAILLE_CARTE - 1 alors
idxVaut est un nombre
idxVaut vaut (cy * TAILLE_CARTE) + (cx + 2)
si carte[idxVaut] = 1 alors
voisins ajoute 3
fin si
fin si
// Si on a des voisins non visités
si longueur(voisins) > 0 alors
rIdx est un nombre
rIdx vaut hasard(0, longueur(voisins) - 1)
dir est un nombre
dir vaut voisins[rIdx]
nx est un nombre
nx vaut cx
ny est un nombre
ny vaut cy
mx est un nombre // Position du mur à casser
mx vaut cx
my est un nombre
my vaut cy
si dir = 0 alors
ny vaut cy - 2
my vaut cy - 1
sinon si dir = 1 alors
ny vaut cy + 2
my vaut cy + 1
sinon si dir = 2 alors
nx vaut cx - 2
mx vaut cx - 1
sinon si dir = 3 alors
nx vaut cx + 2
mx vaut cx + 1
fin si
// Casser le mur entre les deux cases
idxMur est un nombre
idxMur vaut (my * TAILLE_CARTE) + mx
carte[idxMur] vaut 0
// Marquer la nouvelle case comme chemin
idxNouveau est un nombre
idxNouveau vaut (ny * TAILLE_CARTE) + nx
carte[idxNouveau] vaut 0
// Empiler la nouvelle position
pile ajoute idxNouveau
sinon
// Cul-de-sac : on dépile (backtracking)
pile supprime longueur(pile) - 1
fin si
fin tant que
@ 2. POSITION ET DIRECTION DU JOUEUR
px, py est un nombre
px vaut 1,5 // Départ centré dans la première case générée par le DFS
py vaut 1,5
angleDeg est un nombre
angleDeg vaut 0
enCours est un booléen
enCours vaut vrai
action est un nombre
maToile est une toile
dimension(maToile, LARGEUR_ECRAN, HAUTEUR_ECRAN)
tant que enCours
effacer(maToile)
@ --- CIEL : Dégradé lissé ---
pour bandeC de 0 à 39
tC est un nombre
tC vaut bandeC / 39
rC est un nombre
rC vaut arrondi(5 + tC * 20)
gC est un nombre
gC vaut arrondi(15 + tC * 50)
bC est un nombre
bC vaut arrondi(35 + tC * 80)
rectangle(maToile, 0, bandeC * 5, LARGEUR_ECRAN, 6, rgb(rC, gC, bC))
fin pour
@ --- SOL : Dégradé lissé ---
pour bandeS de 0 à 39
tS est un nombre
tS vaut bandeS / 39
rS est un nombre
rS vaut arrondi(10 + tS * 40)
gS est un nombre
gS vaut arrondi(15 + tS * 40)
bS est un nombre
bS vaut arrondi(20 + tS * 45)
rectangle(maToile, 0, 200 + bandeS * 5, LARGEUR_ECRAN, 6, rgb(rS, gS, bS))
fin pour
@ 3. BOUCLE de RENDU 3D (RAYCASTING)
pour slice de 0 à NB_RAYONS - 1
offsetAngle est un nombre
offsetAngle vaut - 30 + (slice * (60 / (NB_RAYONS - 1)))
rayAngle est un nombre
rayAngle vaut angleDeg + offsetAngle
radRay est un nombre
radRay vaut radians(rayAngle)
rDirX est un nombre
rDirX vaut cosinus(radRay)
rDirY est un nombre
rDirY vaut sinus(radRay)
si absolue(rDirX) < 0.0001 alors
rDirX vaut 0.0001
fin si
si absolue(rDirY) < 0.0001 alors
rDirY vaut 0.0001
fin si
mX est un nombre
mX vaut arrondi_inferieur(px)
mY est un nombre
mY vaut arrondi_inferieur(py)
deltaX est un nombre
deltaX vaut absolue(1 / rDirX)
deltaY est un nombre
deltaY vaut absolue(1 / rDirY)
sX, sY, sideX, sideY est un nombre
si rDirX < 0 alors
sX vaut - 1
sideX vaut (px - mX) * deltaX
sinon
sX vaut 1
sideX vaut (mX + 1 - px) * deltaX
fin si
si rDirY < 0 alors
sY vaut - 1
sideY vaut (py - mY) * deltaY
sinon
sY vaut 1
sideY vaut (mY + 1 - py) * deltaY
fin si
toucher est un booléen
toucher vaut faux
cote est un nombre
cote vaut 0
tant que non toucher
si sideX < sideY alors
sideX vaut sideX + deltaX
mX vaut mX + sX
cote vaut 0
sinon
sideY vaut sideY + deltaY
mY vaut mY + sY
cote vaut 1
fin si
si (mX < 0) ou (mX > TAILLE_CARTE - 1) ou (mY < 0) ou (mY > TAILLE_CARTE - 1) alors
toucher vaut vrai
sinon
idxC est un nombre
idxC vaut (mY * TAILLE_CARTE) + mX
si carte[idxC] > 0 alors
toucher vaut vrai
fin si
fin si
fin tant que
distBrute est un nombre
si cote = 0 alors
distBrute vaut sideX - deltaX
sinon
distBrute vaut sideY - deltaY
fin si
distCorr est un nombre
distCorr vaut distBrute * cosinus(radians(offsetAngle))
si distCorr < 0.1 alors
distCorr vaut 0.1
fin si
hautMur est un nombre
hautMur vaut 300 / distCorr
hautMur vaut limiter(hautMur, 5, 450)
topY est un nombre
topY vaut 200 - (hautMur / 2)
posX est un nombre
posX vaut slice * EPAISSEUR_RAYON
intensite est un nombre
intensite vaut 1 - (distCorr / 18)
intensite vaut limiter(intensite, 0.05, 1.0)
si cote = 1 alors
intensite vaut intensite * 0.6
fin si
rBase, vBase, bBase est un nombre
si (mX + mY) mod 2 = 0 alors
rBase vaut 200
vBase vaut 80
bBase vaut 40
sinon
rBase vaut 70
vBase vaut 110
bBase vaut 150
fin si
rMur est un nombre
rMur vaut arrondi(rBase * intensite)
vMur est un nombre
vMur vaut arrondi(vBase * intensite)
bMur est un nombre
bMur vaut arrondi(bBase * intensite)
couleurMur est un texte
couleurMur vaut rgb(rMur, vMur, bMur)
rectangle(maToile, posX, topY, EPAISSEUR_RAYON, hautMur, couleurMur)
lisere est un texte
lisere vaut rgb(arrondi(rMur * 0.3), arrondi(vMur * 0.3), arrondi(bMur * 0.3))
rectangle(maToile, posX, topY, EPAISSEUR_RAYON, 3, lisere)
rectangle(maToile, posX, topY + hautMur - 3, EPAISSEUR_RAYON, 3, lisere)
fin pour
@ 4. MINI-CARTE OPTIMISÉE (RADAR 24x24)
rectangle(maToile, 8, 8, 126, 126, rgba(0, 0, 0, 0.75))
pour my de 0 à TAILLE_CARTE - 1
pour mx de 0 à TAILLE_CARTE - 1
idxM est un nombre
idxM vaut (my * TAILLE_CARTE) + mx
si carte[idxM] = 1 alors
rectangle(maToile, 11 + (mx * 5), 11 + (my * 5), 4, 4, #3B82F6)
sinon
rectangle(maToile, 11 + (mx * 5), 11 + (my * 5), 4, 4, #1B2437)
fin si
fin pour
fin pour
radVue est un nombre
radVue vaut radians(angleDeg)
joueurX est un nombre
joueurX vaut 11 + (px * 5)
joueurY est un nombre
joueurY vaut 11 + (py * 5)
ligne(maToile, joueurX, joueurY, joueurX + cosinus(radVue) * 8, joueurY + sinus(radVue) * 8, #FBBF24, 2)
cercle(maToile, joueurX, joueurY, 2, #EF4444)
rectangle(maToile, 142, 12, 290, 24, rgba(0, 0, 0, 0.6))
label(maToile, 150, 29, "Z / S : Déplacer | Q / D : Orienter", #FFFFFF, 13)
affiche maToile
@ 5. GESTION DES COMMANDES
appuyer ["x", "z", "s", "q", "d"] dans action
radMove est un nombre
radMove vaut radians(angleDeg)
dirX est un nombre
dirX vaut cosinus(radMove)
dirY est un nombre
dirY vaut sinus(radMove)
si action = 2 alors
appelle tenteDeplacement(1)
sinon si action = 3 alors
appelle tenteDeplacement(-1)
sinon si action = 4 alors
angleDeg soustrait 90
sinon si action = 5 alors
angleDeg ajoute 90
fin si
fin tant que
@ 6. FONCTION DE DEPLACEMENT
fonction tenteDeplacement(sens)
nextX est un nombre
nextX vaut px + (dirX * 0.8 * sens)
nextY est un nombre
nextY vaut py + (dirY * 0.8 * sens)
cX est un nombre
cX vaut arrondi_inferieur(nextX)
cY est un nombre
cY vaut arrondi_inferieur(nextY)
si (cX < 0) ou (cX > TAILLE_CARTE - 1) ou (cY < 0) ou (cY > TAILLE_CARTE - 1) alors
retourne faux
fin si
idxCheck est un nombre
idxCheck vaut (cY * TAILLE_CARTE) + cX
si carte[idxCheck] = 0 alors
px vaut cX + 0.5
py vaut cY + 0.5
fin si
retourne vrai
fin fonction





