Dans ce projet, nous allons jouer au jeu dino intégré au navigateur google Chrome, mais d'une manière particulière : nous allons déléguer à la carte microBit la tâche de contrôler le personnage en fonction de ce qu'elle va lire à l'écran !

Tout de suite une petite démontration du projet en action :

Mise en oeuvre matérielle

Matériel nécessaire

Pour réaliser ce projet, vous aurez besoin

  • d'une photorésistance (LDR) qui fera office de capteur de cactus sur l'écran. Ce sont des composants très faciles à trouver et très peu chers.
  • d'une résistance d'un cinquentaine d'ohms
  • d'un servo-moteur SG90. Là encore, il s'agit d'un modèle très répandu dans le domaine des actionneurs.
  • d'une carte microBit qu'il est inutile de présenter !
  • de quelques fils, d'une plaquette de prototypage (breadboard)

Bref, un projet ne nécessitant pas énormément de matériel.

Le jeu

Le jeu dino est intégré au navigateur google chrome. Pour le lancer, il suffit de taper l'URL spéciale : chrome://dino

Le montage

La photorésistance

La photorésistance (ou LDR comme Light Dependant Resistor) est comme son nom l'indique un composant électronique dont la résistance va varier en fonction de la lumière qu'elle reçoit. L'idée est de la scotcher tout contre l'écran à l'endroit où défileront les cactus. Le fond de l'écran étant blanc et les cactus noirs, cela va provoquer une différence de luminosité qui sera captée par la LDR. montage LDR Avec la résistance de 50 k-ohms nous feront un pont diviseur de tension permettant de convertir la variation de résistance de la LDR en une variation de tension qui sera détectable à l'aide des broches analogiques de notre carte microbit.

On ajoute ensuite le servo : Celui-ci sera connecté à la microbit par la broche 12 (vous pouvez utiliser la broche 1 si vous souhaitez utiliser uniquement des connexions croco). La microbit ne fournit pas assez de tension pour le servo. Celui-ci sera donc relié à une source 5V à part. Il est important de connecter les GND ensemble pour avoir un 0V commun. Voici le montage : montage

A l'aide de scotch, vous attacherez la LDR sur l'écran à l'endroit propice (juste au dessus de la ligne d'horizon) comme le montre la photo ci-desssus et vous scotcherez solidement le servo près de la barre espace du clavier en positionnant le bras de manière à ce qu'il actionne la touche. montage servo

Mise en oeuvre logicielle

Programme python

Voici le programme que j'ai utilisé. Il nécessitera quelques ajustements par rapport à votre configuration :

  • Les broches sur lesquelles seront branchées la LDR et le servo.
  • Les constantes au début pour parfaitement calibrer le saut par rapport à la taille de la fenêtre (voir section suivante).

⚠️ Attention ! ⚠️

Toutes les broches de la microbit ne sont pas utilisables pour la LDR
  • La LDR nécessite une broche pouvant être utilisée en entrée analogique (0, 1, 2, 3, 4, 10)
  • Le servo nécessite une broche pouvant générer un signal PWM ce qui est le cas de toutes les broches de la microbit.

Pour en savoir plus sur les possibilités des broches, je vous renvoie à mon guide microbit

import microbit as mb
import time

# Declaration des constantes

NBECH = 10        # Nb de mesures LDR
SEUIL = 340       # Seuil detection cactus
WAIT = 50         # Delai d'attente après detection cactus
WAITJ = 150       # Temps 'appui sur la touche par servo$ù
JUMPDELAY = 1020  # Delai entre detection d'un cactus et le saut
ACCEL = 8         # facteur d'acceleration du jeu
CALIB = False     # mode calibration pour mesurer JUMPDELAY

SERVO_JMP = 115   # Position du servo en mode appui touche
SERVO_NORM = 100  # Position du servo touche relachee

# Declaration des broches
PIN_LDR = mb.pin2
PIN_SERVO = mb.pin12

# Variables globales
jumpList = []     # pile des cactus a sauter
nbCalib = 0       # nb de mesures lors de la calibration
startTime = 0     # instant (en ms) du demarrage du jeu
startJump = 0     # instant du declanchement du saut

def start():
    """Lancement du jeu, initialisation variables"""
    global startTime
    startTime = time.ticks_ms()
    jumpList.clear()
    mb.display.clear()

def calib():
    """Calibration : mesure du temps entre detection cactus et saut"""
    global nbCalib, JUMPDELAY
    delay = heure - jumpList.pop(0)
    if JUMPDELAY == 0:
        JUMPDELAY = delay
    else:
        JUMPDELAY = (JUMPDELAY*nbCalib + delay)/(nbCalib+1)
    nbCalib += 1
    print(nbCalib, JUMPDELAY)
    mb.sleep(WAIT)

def jump():
    """Declanchement du saut"""
    global startJump
    startJump = time.ticks_ms()
    PIN_SERVO.write_analog(SERVO_JMP)

def unjump():
    """Fin du saut"""
    PIN_SERVO.write_analog(SERVO_NORM)

#
# Boucle principale
#

while True:
    valeur = 1024
    if mb.button_a.was_pressed():
        start()

    # Mesure LDR - Detection Cactus
    for _ in range(NBECH):
        val = PIN_LDR.read_analog()
        valeur = min(val, valeur)
    if valeur > SEUIL:
        # On empile les cactus qui restent a sauter
        jumpList.append(time.ticks_ms())
        mb.display.show(len(jumpList))
        mb.sleep(WAIT)

    heure = time.ticks_ms()

    # Mesure du temps pour la longueur du saut
    if startJump > 0 and heure - startJump > WAITJ:
        unjump()
        startJump = 0

    # Declanchement du saut par rapport au delai du 1er cactus
    if not CALIB and len(jumpList) > 0 and heure-jumpList[0] > JUMPDELAY - (heure - startTime) * ACCEL // 1000:
        jump()
        jumpList.pop(0)

    # Mode calibration
    if CALIB and len(jumpList) > 0 and mb.button_b.is_pressed():
        calib()

Explications du programme

Le programme est abondament commenté, je ne donnerai ici que quelques éléments d'ordre général :

Il s'écoule environ une seconde entre la détection d'un cactus et le déclanchement du saut. Or pendant cette seconde, d'autres cactus peuvent apparaître. Il est donc nécessaire une fois un cactus détecté de continuer à scruter la présence d'autre cactus et de mémoriser tous les cactus à sauter dans une file (FIFO) : j'utilise ici la liste jumpList. Lorsqu'un saut est réalisé, on enlève de la liste la valeur correspondante.

La file contient les instants de détection d'un cactus. L'algorithme de saut est simple : lorsque la différence entre l'instant présent et l'instant du premier cactus détecté est supérieure au délai paramétré dans la constante JUMPDELAY on déclanche le saut et on sort le cactus de la file. En réalité la formule est un peu plus complexe car on ajoute la notion d'accélération du jeu en enlevant à ce délai ACCEL millisecondes toutes les secondes.

La détection des cactus par la LDR est un peu délicate à cause de la fréquence de balayage de l'écran : si la lecture s'opère alors que l'écran est noir, cela fausse la mesure. Cela n'arrive pas très fréquemment mais il faut le prendre en compte. Pour cela, j'effectue 10 mesures (paramétrable dans NBECH) de la LDR et retiens la valeur minimum (une valeur élevée est retournée en cas d'écran sombre). Cela élimine donc les lectures abérrantes. Il faut trouver un compromis entre un nombre suffisant de lectures mais ne surtout pas bloquer la boucle principale trop longtemps ! Je développe ce point fondamental :

Un point critique est de garder la boucle principale la plus courte possible dans son exécution afin d'avoir une bonne réactivité pour la détection des cactus. Par conséquent, il ne faut pas utiliser de sleep dans la boucle principale. C'est un principe qui est vrai en général mais qui ici est critique. Cela explique pourquoi la fonction de saut est séparée en deux parties et que c'est le chronomètre interne qui déclanchera la fin du saut.

Il y a juste une exception à cette règle pour la détection du cactus ou j'utilise un time.sleep(WAIT) afin qu'un même cactus ne provoque pas l'empilement de plusieurs valeurs. On peut régler la constante WAIT selon ses besoins :

  • une valeur trop courte fera mémoriser plusieurs sauts pour un même cactus qui sera vu comme plusieurs
  • une valeur trop grande risquera de faire rater la détection de cactus lorsqu'il y en a plusieurs proches à se suivre

On peut assimiler cela au debouncing d'un bouton à l'aide d'un délai logiciel afin qu'un appui long ne provoque pas plusieurs fois l'action demandée.

Utilisation du programme

Calibration

Si vous souhaitez mettre en oeuvre ce projet, vous devrez ajuster à votre configuration les constantes déclarées en début de programme et dont je vais expliciter le rôle ici. Celles que vous aurez besoin d'ajuster sont :

  • SEUIL qui contient la valeur analogique lue sur la broche PIN_LDR et qui distingue le blanc du fond et le noir du cactus.
  • JUMPDELAY qui est le temps à attendre entre la détection d'un cactus et le déclanchement du saut correspondant.
  • SERVO_JMP et SERVO_NORM qui contiennent les valeurs PWM à envoyer pour positionner le servo en appui ou relâchement de touche.
  • PIN_LDR et PIN_SERVO qui sont les broches utilisées sur la microbit

Calibration du seuil

Ouvrez la console REPL afin de faire les lectures des valeurs de la LDR. Pour cela, tapez les lignes :

import microbit as mb
mb.pin2.read_analog()

répétez plusieurs fois la dernière ligne (en adaptant la broche) afin d'évaluer la valeur lorsque la LDR est sur le fond blanc.

Bougez à présent la fenêtre de chrome afin que la LDR soit sur un cactus. Vous en déduirez alors la bonne valeur à mettre dans cette variable SEUIL.

Calibration du délai de saut

J'ai fait une petite fonction de calibration qui s'active en positionnant CALIB = True. Flashez alors le programme et lancez-le depuis le REPL car je vais afficher des print...

Lancez le jeu du dino. Vous devrez sauter manuellement avec la touche espace. A chaque saut, vous appuirez simultanément sur ESPACE pour sauter et sur le bouton B de la microbit. Celle-ci affichera alors dans la console le temps moyen entre la détection d'un cactus et le déclanchement du saut. Cela vous donne une idée de la valeur à positionner dans la variable JUMPDELAY

Calibration du servo

Scotchez le servo près de la touche ESPACE comme indiqué sur la photo en début de tuto. Lancez la console REPL et tapez

import microbit as mb
mb.pin12.write_analog(100)

puis ajuster la valeur jusqu'à trouver la bonne valeur pour une position de touche ESPACE relâchée et une position enfoncée. Vous aurez alors les deux valeurs à positionner dans les constantes SERVO_NORM et SERVO_JMP

lancement du jeu

Tout dabord, vérifiez bien que vous avez remis la variable CALIB = False.

Une fois tout en place, lancez le jeu avec la touche ESPACE. Dans le même temps, appuyez sur le bouton A de la microBit. Cela permet de paramétrer l'instant de départ servant au calcul de l'accélération du jeu. Si tout se passe bien, votre dino sautera les obstacles.

Limitations

Il s'agit plus d'une POC (Proof Of Concept) que d'un projet abouti. En effet :

  • Certains oiseaux volent à mi-hauteur, nécessiant que le dino s'accroupisse. Cela n'est pas géré ici car cela demanderait une seconde LDR dédié à la détection de ces volatiles particuliers et un second servo pour actionner la touche CURSEUR BAS.
  • Au delà d'un certain score, le jeu passe en mode nuit ce qui change complètement la détection des cactus qui deviennent blanc alors que le fond est noir. Il faudrait ajouter la détection automatique de ce mode et gérer plus finement la détection du cactus afin de différentier jour/nuit.

Néanmoins, j'ai trouvé ce mini-projet extrèmement amusant à réaliser et il ne manquera pas de surprendre les personnes à qui vous le montrerez !