Labyrinthe
Introduction
Bienvenue à ce dernier travail pratique de l'atelier, où vous allez créer un jeu de labyrinthe. Dans ce jeu, vous allez contrôler une petite bille qui se déplace à travers un labyrinthe en utilisant un accéléromètre. L'objectif du jeu est de guider la bille du point de départ vers un trou dans le labyrinthe, en évitant les murs.
Vous pouvez personnaliser votre labyrinthe en modifiant les dimensions, la sensibilité de l'accéléromètre, et même le message de victoire affiché à l'écran une fois que vous avez terminé le labyrinthe.
Amusez-vous à créer votre propre labyrinthe et à relever le défi de guider la bille jusqu'au trou pour gagner la partie. C'est une excellente occasion d'explorer la programmation, la logique, et l'interaction avec des capteurs, tout en créant un jeu amusant.
Les Besoins
- Un ordinateur
- Une connexion à Internet
- Une Oxocard Science
- Un câble USB-USB type C
Quelques Explications
Quelques notions sont expliqués dans ce chapitre pour comprendre le principe du fonctionnement. Tout le monde ne sait pas ce qu'est un accéléromètre ou comment certains principes physiques fonctionne. Ce chapitre va sert à vous informez sur ces choses afin d'enrichir votre culture générale et de comprendre un peu mieux le fonctionnement du programme.
La physique de la bille
Imagine que tu as une petite boule que tu fais rouler sur une pente. Si la pente est inclinée, la boule va naturellement commencer à rouler et, à mesure qu'elle descend la pente, elle va devenir de plus en plus rapide.
Dans la vraie physique, c'est ce qu'on appelle l'accélération. Cela signifie que la boule change sa vitesse au fur et à mesure qu'elle bouge. C'est comme si quelqu'un appuyait sur l'accélérateur d'une voiture pour la faire avancer de plus en plus vite.
Maintenant, en ce qui concerne notre jeu de labyrinthe, il est vrai que reproduire cette physique précise avec une boule qui accélère sur une pente peut être un peu compliqué en programmation. Cela demanderait plus de calculs et de logique pour gérer ces changements de vitesse. Pour simplifier le code et le rendre plus accessible, nous avons décidé de garder la vitesse de la bille constante.
Cela signifie que, dans notre jeu, la bille roule à la même vitesse tout le temps, qu'elle soit en haut de la pente ou en bas. Cela facilite la programmation du jeu même s'il ne reproduit pas exactement la physique du monde réel. C'est un compromis pour rendre le jeu plus accessible.
Le principe des zones de collision
Pour déterminer s'il y a une collision entre un rectangle et un rond sur un écran avec des coordonnées x et y, nous regardons si le périmètre du rond (la distance autour de sa bordure) se chevauche avec le périmètre du rectangle, en prenant en compte les coordonnées x et y.
Le périmètre du rectangle est simplement la somme des longueurs de ses quatre côtés, et le périmètre du rond est la distance autour de sa bordure. Si le périmètre du rond chevauche ou traverse le périmètre du rectangle, en tenant compte des coordonnées x et y sur l'écran, alors il y a une collision.
En d'autres termes, si le rond touche ou traverse le rectangle sur l'écran, en prenant en compte les coordonnées x et y, nous disons qu'il y a une collision entre les deux formes. C'est comme quand deux objets se touchent dans la vie réelle, sauf que nous utilisons des mathématiques pour le vérifier sur un écran d'ordinateur en utilisant des coordonnées x et y.
L'accéléromètre
Un accéléromètre est un petit composant électronique qui peut mesurer l'accélération d'un objet. L'accélération, c'est le changement de vitesse d'un objet, que ce soit pour aller plus vite, ralentir ou changer de direction. L'accéléromètre est un peu comme un détecteur de mouvement.
Dans le contexte de notre jeu de labyrinthe, l'accéléromètre est utilisé pour détecter comment vous inclinez ou bougez l'appareil sur lequel vous jouez. Par exemple, si vous inclinez l'appareil vers la gauche, l'accéléromètre le détecte et fait bouger la bille dans la même direction. Si vous inclinez vers le haut, la bille ira vers le haut, et ainsi de suite.
C'est comme si l'accéléromètre était un petit détective qui vous aide à contrôler la bille en fonction de comment vous tenez ou inclinez votre appareil. C'est une technologie qui permet de créer des jeux interactifs
Le code
Le code est réalisé en deux phases afin d'y aller pas à pas. Dans une première phase vous allez faire le code pour bouger la bille sans qu'il n'y ait d'obstacle. Dans une deuxième phase vous ajouterez des murs sur l'écran.
Phase 1 : Bouger la bille sans les murs
La ligne background(0, 0, 0)
détermine la couleur de fond de l'écran. Dans ce cas, les valeurs 0, 0, 0 correspondent à la couleur noire. Cela signifie que l'écran de jeu sera tout d'abord noir.
Ensuite, il y a plusieurs lignes qui définissent des couleurs à utiliser dans le jeu. Par exemple, const RED = 235
indique que la couleur rouge sera utilisée avec une intensité de 235 sur une échelle de 0 à 255. De même, const GREEN = 170
indique que la couleur verte sera utilisée avec une intensité de 170, et "const BLUE = 22" indique que la couleur bleue sera utilisée avec une intensité de 22.
La ligne const toggleGrid = false
signifie qu'il y a un paramètre appelé "toggleGrid" qui est défini comme "false". Cela peut être utilisé pour activer ou désactiver une grille dans le jeu, mais dans ce cas, elle est désactivée (false).
Enfin, const sensibilite = 0.3
signifie qu'il y a un paramètre appelé "sensibilité" qui est défini à 0,3. Ce paramètre peut être utilisé pour ajuster à quel point le jeu réagit aux mouvements, où 0,1 serait plus sensible et 0,9 serait moins sensible. Dans ce cas, il est réglé à une sensibilité moyenne de 0,3.
background(0, 0, 0)
const RED = 235 # 0 .. 255
const GREEN = 170 # 0 .. 255
const BLUE = 22 # 0 .. 255
const toggleGrid = false
const sensibilite = 0.3 # 0.1 .. 0.9
Une classe peut être comparée à un modèle ou à un plan pour créer des objets spécifiques. Pensez à une classe comme à une recette pour cuisiner un plat délicieux. La recette vous dit quels ingrédients utiliser et comment les mélanger pour obtenir un résultat final.
Dans la programmation, une classe est un ensemble de règles qui définissent les caractéristiques (appelées attributs) et les actions (appelées méthodes) d'un objet. Par exemple, si nous parlons d'une classe "Animal", elle pourrait avoir des attributs comme "nom" et "âge", ainsi que des méthodes comme "manger" et "dormir".
Dans ce cas là, la section intitulée "Hole class" (classe Trou) est une partie du code qui définit comment créer et dessiner un trou dans le jeu.
###############################
# Hole class #
###############################
class Hole:
positionX:int
positionY:int
width:int
height:int
def draw():
fill 255, 0, 0
stroke 255, 0, 0
drawRectangle(positionX, positionY, width, height)
Hole
a plusieurs attributs, dont positionX
, positionY
, width
(largeur), et height
(hauteur). Ces attributs définissent la position et la taille du trou sur l'écran.
positionX:int
positionY:int
width:int
height:int
draw()
est une méthode qui permet de dessiner le trou. Elle utilise les attributs positionX
, positionY
, width
, et height
pour déterminer où le trou doit être dessiné et quelle forme il doit avoir. Dans ce cas, le trou est dessiné en utilisant une couleur rouge (avec fill 255, 0, 0
) et une bordure rouge (avec stroke 255, 0, 0
). Le trou est ensuite dessiné en utilisant les valeurs de positionX
, positionY
, width
, et height
.
def draw():
fill 255, 0, 0
stroke 255, 0, 0
drawRectangle(positionX, positionY, width, height)
###############################
# Ball class #
###############################
class Ball:
radius:int
positionX:int
positionY:int
newPositionX:int
newPositionY:int
def draw():
fill 0, 0, 0
stroke 0, 0, 0
drawCircle(positionX, positionY, radius)
fill 250, 250, 250
stroke 250, 250, 250
drawCircle(newPositionX, newPositionY, radius)
positionX = newPositionX
positionY = newPositionY
Ball
a plusieurs attributs, dont radius
(rayon), positionX
(position en X), positionY
(position en Y), newPositionX
(nouvelle position en X) et newPositionY
(nouvelle position en Y). Ces attributs définissent la taille et la position de la balle sur l'écran.
radius:int
positionX:int
positionY:int
newPositionX:int
newPositionY:int
draw()
est une méthode qui permet de dessiner la balle. Elle utilise les attributs positionX
et positionY
pour déterminer la position actuelle de la balle et les attributs newPositionX
et newPositionY
pour déterminer sa nouvelle position. La balle est dessinée en utilisant une couleur noire (avec fill 0, 0, 0
) et une bordure noire (avec stroke 0, 0, 0
). Deux cercles sont dessinés : l'un représente la position actuelle de la balle, et l'autre représente sa nouvelle position. La balle est ensuite mise à jour pour se déplacer vers sa nouvelle position.
def draw():
fill 0, 0, 0
stroke 0, 0, 0
drawCircle(positionX, positionY, radius)
fill 250, 250, 250
stroke 250, 250, 250
drawCircle(newPositionX, newPositionY, radius)
positionX = newPositionX
positionY = newPositionY
-
ball
: C'est une variable qui stocke une instance de la classeBall
(balle) que nous avons définie précédemment. Cela signifie que "ball" représente la balle du jeu. -
hole
: C'est une variable qui stocke une instance de la classeHole
(trou). Elle représente le trou dans lequel la balle doit arriver pour gagner le jeu. -
vectorAccel
: C'est une variable qui stocke des informations sur l'accélération du système. Elle est utilisée pour déplacer la balle en fonction de l'orientation du dispositif. -
walls
: C'est un tableau de 24 objets de la classeWall
(mur). Chaque élément de ce tableau représente un mur dans le labyrinthe du jeu. Ce tableau sera utile dans la phase 2. -
nbrMurs
: C'est une variable qui est initialisée à 0 et qui est utilisée pour suivre la position actuelle du mur dans le tableauwalls
. Là aussi, cette variable sera utile dans la phase 2. -
victoryScreen
: C'est une variable booléenne (vrai ou faux) qui est initialisée àfalse
(faux). Elle indique si le joueur a gagné la partie en atteignant le trou. -
str
: C'est une variable qui stocke une chaîne de texte. Elle est utilisée pour afficher un message de victoire à l'écran lorsque le joueur gagne.
###############################
# Global Variables #
###############################
ball:Ball
hole:Hole
vectorAccel:vector
walls:Wall[24]
index = 0
victoryScreen = false
str:byte[120]
circleRect
(cercleRect) est utilisée pour vérifier s'il y a une collision entre un cercle et un rectangle dans le jeu avec des mathématiques.
def circleRect(cx, cy, radius, rx, ry, rw, rh) -> bool:
testX = cx
testY = cy
if (cx < rx):
testX = rx; # test left edge
else if (cx > rx+rw):
testX = rx+rw; # right edge
if (cy < ry)
testY = ry; # top edge
else if (cy > ry+rh)
testY = ry+rh; # bottom edge
# get distance from closest edges
distX = cx-testX
distY = cy-testY
distance = sqrt( (distX*distX) + (distY*distY) );
# if the distance is less than the radius, collision!
if (distance <= radius) :
return true
return false
Voici comment cela fonctionne :
Les paramètres, les entrées, de la fonction sont les suivants :
- cx
et cy
représentent les coordonnées du centre du cercle.
- radius
est le rayon du cercle.
- rx
et ry
sont les coordonnées du coin supérieur gauche du rectangle.
- rw
et rh
sont la largeur et la hauteur du rectangle.
def circleRect(cx, cy, radius, rx, ry, rw, rh)
testX
et testY
, qui sont initialement définies aux coordonnées du centre du cercle (cx, cy).
testX = cx
testY = cy
testX
et testY
sont mis à jour pour qu'ils se trouvent sur le bord le plus proche du rectangle.
if (cx < rx):
testX = rx # test left edge
else if (cx > rx+rw):
testX = rx+rw; # right edge
if (cy < ry)
testY = ry # top edge
else if (cy > ry+rh)
testY = ry+rh # bottom edge
testX
, testY
). Cela se fait en utilisant le théorème de Pythagore pour calculer la distance en ligne droite.
# get distance from closest edges
distX = cx-testX
distY = cy-testY
distance = sqrt( (distX*distX) + (distY*distY) )
true
(vrai). Sinon, elle renvoie false
(faux) pour indiquer qu'il n'y a pas de collision.
# if the distance is less than the radius, collision!
if (distance <= radius) :
return true
return false
arrivedAtHole
(arrivéAuTrou), et elle est utilisée pour vérifier si notre balle a atteint le trou (ouvert dans le mur) du labyrinthe. Voici comment cela fonctionne :
Les paramètres de la fonction sont les suivants :
- cx
et cy
représentent les coordonnées du centre de la balle.
- radius
est le rayon de la balle.
- rx
et ry
sont les coordonnées du coin supérieur gauche du trou (ouverture dans le mur).
- rw
et rh
sont la largeur et la hauteur du trou.
La fonction arrivedAtHole
fait quelque chose de très simple : elle utilise une autre fonction appelée circleRect
pour vérifier si la balle entre en collision avec le trou.
Si circleRect
renvoie true
(vrai), cela signifie que la balle touche le trou, ce qui signifie que la balle est arrivée au trou. Dans ce cas, arrivedAtHole
renvoie également true
.
Si circleRect
renvoie false
(faux), cela signifie que la balle n'a pas touché le trou, ce qui signifie que la balle n'est pas encore arrivée au trou. Dans ce cas, arrivedAtHole
renvoie false
.
def arrivedAtHole(cx, cy, radius, rx, ry, rw, rh) -> bool:
return circleRect(cx, cy, radius, rx, ry, rw, rh)
init
(initialisation) est utilisée pour préparer toutes les parties de notre petit jeu de labyrinthe avant que le joueur ne commence à jouer.
def init()
###############################
# PERIMETER #
###############################
createWall(10,240,0,0)
createWall(220,10,10,230)
createWall(220,10,10,0)
createWall(10,240,230,0)
###############################
# Ball #
###############################
ball.positionX = 40
ball.positionY = 40
ball.radius = 5
ball.newPositionX = ball.positionX
ball.newPositionY = ball.positionY
###############################
# Hole #
###############################
hole.positionX = 210
hole.positionY = 210
hole.width = 10
hole.height = 10
hole.draw()
init()
D'abord elle crée les limites du labyrinthe en ajoutant des murs tout autour. Ces murs empêchent la balle de sortir du labyrinthe. Les lignes comme createWall(10,240,0,0
signifient que nous créons un mur de 10 pixels de large qui s'étend sur toute la hauteur (240 pixels) de l'écran, en commençant à la position (0, 0). Nous faisons la même chose pour les trois autres côtés du labyrinthe, en créant ainsi une enceinte pour notre jeu.
###############################
# PERIMETER #
###############################
createWall(10,240,0,0)
createWall(220,10,10,230)
createWall(220,10,10,0)
createWall(10,240,230,0)
###############################
# Ball #
###############################
ball.positionX = 40
ball.positionY = 40
ball.radius = 5
ball.newPositionX = ball.positionX
ball.newPositionY = ball.positionY
hole.draw
dessine le trou.
###############################
# Hole #
###############################
hole.positionX = 210
hole.positionY = 210
hole.width = 10
hole.height = 10
hole.draw()
init
a été exécutée, tout est prêt pour que le joueur puisse commencer à jouer au labyrinthe mais sans les murs.
Maintenant la fonction vu et revu depuis le début de l'atelier : onDraw()
Pour rappel, la fonction onDraw
est une partie essentielle de notre petit jeu de labyrinthe. Elle est appelée périodiquement par le système embarqué pour dessiner sur l'écran et mettre à jour ce qui se passe dans le jeu.
def onDraw():
if victoryScreen == false:
###############################
# Print a white grid #
###############################
drawGrid()
###############################
###############################
# The mouvements #
###############################
vectorAccel = getAccelerationXY()
newPosX = ball.positionX
newPosY = ball.positionY
if vectorAccel.y < sensibilite * (-1):
#La board est penchée en avant
newPosY ## TO DO ##
elif vectorAccel.y > sensibilite:
#La board est penchée en arrière
newPosY ## TO DO ##
if vectorAccel.x < sensibilite * (-1):
#La board est penchée vers la gauche
newPosX ## TO DO ##
elif vectorAccel.x > sensibilite:
#La board est penchée vers la droite
newPosX ## TO DO ##
###############################
# The Collisions #
###############################
# Check for collision with walls before updating the position
collision_detected = false
for i in nbrMurs:
if circleRect(newPosX, newPosY, ball.radius,
walls[i].positionX, walls[i].positionY, walls[i].width, walls[i].height):
collision_detected = true
i = 30
if not collision_detected:
ball.newPositionX = newPosX
ball.newPositionY = newPosY
###############################
# Dessine la ball et du trou #
###############################
hole.draw()
ball.draw()
###############################
# Victory Condition #
###############################
victoryScreen = arrivedAtHole(ball.positionX, ball.positionY, ball.radius,
hole.positionX, hole.positionY, hole.width, hole.height)
if victoryScreen:
str = textInput("Text de victoire à afficher", "Écrire au clavier")
else:
###############################
# Victory Screen #
###############################
fill 0, 0, 0
drawRectangle(0, 0, 240, 240)
drawTextCentered(120,120, str)
update()
###############################
if victoryScreen == false:
###############################
# Print a white grid #
###############################
drawGrid()
## TO DO ##
indiquent que ces parties du code doivent être complétées pour gérer les déplacements de la balle en fonction de l'inclinaison de la carte.
###############################
# The mouvements #
###############################
vectorAccel = getAccelerationXY()
newPosX = ball.positionX
newPosY = ball.positionY
if vectorAccel.y < sensibilite * (-1):
#La board est penchée en avant
newPosY ## TO DO ##
elif vectorAccel.y > sensibilite:
#La board est penchée en arrière
newPosY ## TO DO ##
if vectorAccel.x < sensibilite * (-1):
#La board est penchée vers la gauche
newPosX ## TO DO ##
elif vectorAccel.x > sensibilite:
#La board est penchée vers la droite
newPosX ## TO DO ##
collision_detected
devient vraie, et le mouvement de la balle est bloqué.
Si aucune collision n'est détectée, la nouvelle position de la balle est mise à jour en fonction des mouvements précédemment calculés.
###############################
# The Collisions #
###############################
# Check for collision with walls before updating the position
collision_detected = false
for i in nbrMurs:
if circleRect(newPosX, newPosY, ball.radius,
walls[i].positionX, walls[i].positionY, walls[i].width, walls[i].height):
collision_detected = true
i = 30
if not collision_detected:
ball.newPositionX = newPosX
ball.newPositionY = newPosY
###############################
# Dessine la ball et du trou #
###############################
hole.draw()
ball.draw()
###############################
# Victory Condition #
###############################
victoryScreen = arrivedAtHole(ball.positionX, ball.positionY, ball.radius,
hole.positionX, hole.positionY, hole.width, hole.height)
if victoryScreen:
str = textInput("Text de victoire à afficher", "Écrire au clavier")
else:
###############################
# Victory Screen #
###############################
fill 0, 0, 0
drawRectangle(0, 0, 240, 240)
drawTextCentered(120,120, str)
update()
Phase 2 : Avec les murs
Après l'initialisation du jeu, nous définissons les murs situés à l'intérieur du labyrinthe. Ces murs sont placés à des endroits stratégiques pour rendre le jeu plus intéressant et stimulant.
La section "The inside walls" est marquée par "## TO DO ##", ce qui signifie que c'est à cet endroit que vous devez ajouter les détails spécifiques de ces murs. Vous devrez définir la largeur, la hauteur, la position en X (horizontale), et la position en Y (verticale) de chaque mur à l'intérieur du labyrinthe. Ces murs empêcheront la balle de se déplacer librement et créeront un défi pour le joueur.
Vous pouvez ajouter jusqu'à 24 murs, c'est bien assez dans le cas de ce jeu.
###############################
# The inside walls #
###############################
## TO DO ##
###############################
Cela garantira que les murs ont la même épaisseur et que le jeu est équilibré en termes de difficulté. N'oubliez pas de garder cette cohérence tout en plaçant les murs à l'intérieur du labyrinthe pour maintenir une expérience de jeu uniforme.
Les murs se font de la manière suivante :
Lorsque nous appelons createWall(10, 240, 0, 0)
, nous créons un mur dans notre jeu. Imaginez que ce mur est comme une barrière dans un labyrinthe. Voici ce que signifient les chiffres :
-
Le premier chiffre,
10
, représente la largeur du mur. Donc, ce mur aura une largeur de 10 pixels. C'est comme dire que le mur est large de 10 petits carrés côte à côte. -
Le deuxième chiffre,
240
, représente la hauteur du mur. Donc, ce mur aura une hauteur de 240 pixels. C'est comme dire que le mur est haut de 240 petits carrés empilés les uns sur les autres. -
Les deux derniers chiffres,
0, 0
, représentent la position du mur sur notre écran. Dans ce cas, 0, 0 signifie que le coin supérieur gauche du mur sera situé tout en haut et tout à gauche de notre écran.
En combinant ces informations, nous créons un mur de 10 pixels de large et 240 pixels de haut, et nous le plaçons tout en haut à gauche de notre écran. C'est comme si nous construisions la première partie de notre labyrinthe.
Le Final
Le résultat est un est jeu sur la carte réalisé par vous-même. La bille bouge dans tous les sens, elle ne passe pas dans les murs l'écran change lorsque vous arrivez à un certains point sur l'écran. Vous avez même un écran de victoire personnalisé.