Jeu de tir : scudFighter - Niveau intermédiaire

scudFighter est un jeu de tir dans lequel on doit détruire son adversaire en lui lançant des missiles. Le jeu se joue sur 2 cartes micro:bit qui communiquent entre-elles via le module radio.

La manipulation est très simple :

  • le bouton A permet de se déplacer vers la droite

  • le bouton B permet de se déplacer vers la gauche

  • un appui simultané sur A + B permet de lancer un missile

ComplémentProgramme complet

Vous trouverez ci-dessous le détail de fonctionnement du programme avec des explications sur les extraits de code les plus intéressants. Vous trouverez le programme complet en suivant ce lien.

Les variables globales utilisées

1
speed=1
2
accel=0.2
3
score=0
4
position=2
5
scuds = []
6
lastLoop = ticks_us()

speed gère la vitesse du jeu. Le jeu fait speed mouvements par secondes. A chaque coup gagnant ou perdant, speed augmente de la valeur accel. Le jeu va donc globalement de plus en plus vite.

position est l'abscisse du joueur (entre 0 et 4).

scuds est une liste de missiles. Chaque missile est un tuple de la forme (x,y,d) avec

  • x : abscisse du missile

  • y : ordonnée du missile

  • d : direction du missile.

    • 1 = le missile descend

    • -1, le missile monte

lastLoop : gestion du temps. Afin d'avoir une bonne réactivité du jeu et un rythme régulier, on utilise pas (ou presque...) la commande sleep(), mais ticks_us() qui donne un repère temporel en microsecondes. ticks_us()-lastLoop permet donc de savoir le temps écoulé depuis la dernière action.

Fonctions simples

position_move

Cette fonction permet le déplacement du joueur. Elle agit sur la variable globale position.

1
def position_move(p):
2
    global position
3
    display.set_pixel(position,4,0)
4
    position += p
5
    position = max(min(4,position),0)
6
    display.set_pixel(position,4,9)

fire

Cette fonction permet de tirer un missile. Il suffit pour cela d'ajouter une entrée à la liste scuds. Le mouvement est géré ailleurs, dans la fonction process_fire qui est la plus compliquée du projet.

1
def fire():
2
    scuds.append((position,3,-1))
3
    display.set_pixel(position,3,SCUDFRIENDCOLOR)

refresh

Lors de l'effacement de l'écran (affichage du score, animation explosion), il est nécessaire de réafficher le plateau de jeu : position joueur et missiles. C'est ce que fait la fonction refresh.

1
def refresh():
2
    # On redessine l'affichage
3
    display.clear()
4
    display.set_pixel(position,4,9)
5
    for s in scuds:
6
        display.set_pixel(s[0],max(0,s[1]),SCUDFRIENDCOLOR if s[2]<0 else SCUDENMYCOLOR)

perdu

Lorsque l'on est touché par un missile, on accélère le jeu, on envoie l'information à l'autre carte par radio afin qu'elle actualise son score et on joue l'animation d'explosion. Il faut ensuite redessiner la scène de jeu.

1
def perdu():
2
    global speed
3
    speed += accel # Le jeu s'accelere !
4
    
5
    # envoi radio "HIT"
6
    radio.send("HIT")
7
    
8
    # animation
9
    display.clear()
10
    anim = [
11
    Image("99999:90009:90009:90009:99999"),
12
    Image("00000:06660:06060:06660:00000"),
13
    Image("00000:00000:00300:00000:00000"),
14
    Image("00000:06660:06060:06660:00000"),
15
    Image("99999:90009:90009:90009:99999")]
16
    display.show(anim, delay=100, loop=False, wait=True)
17
    refresh()

gagne

Lorsque l'on gagne, on accélère le jeu, on actualise le score et on l'affiche. Après un petit temps d'attente, on reprend le cours du jeu. 

1
def gagne():
2
    global speed,score
3
    speed += accel # Le jeu s'accelere !
4
    score += 1
5
    # animation
6
    display.clear()
7
    display.scroll(score)
8
    sleep(500)
9
    refresh()

Traitement des événements

Le traitement des événements se fait dans trois fonctions nommées process_*. Ces fonctions sont au coeur du projet.

process_button

On gère ici l'appui sur les boutons. L'appel à la méthode was_pressed() vide la mémoire tampon qui mémorise si un bouton a été préssé. On on doit tester les boutons deux fois : pour l'appui simple et pour la détection de l'appui simultané A+B. On va donc mémoriser l'état des boutons dans des variables.

Le reste est simple puisqu'on invoque le tir ou le déplacement selon les boutons actionnés.

1
def process_button():
2
    # Gesion des boutons
3
    ba = button_a.was_pressed()
4
    bb = button_b.was_pressed()
5
    if ba and bb :
6
        fire()
7
    else:
8
        if ba:
9
            position_move(-1)
10
        if bb:
11
            position_move(1)

process_radio

Il s'agit ici d'écouter les messages radio qui parviennent à la carte. Ils sont de 2 type :

  • HIT : signifie que l'adversaire a été touché. On a alors gagné.

  • 0->4 : Arrivée d'un missile. On l'ajoute à la liste scuds avec la bonne direction (1)

1
def process_radio():
2
    global lastLoop
3
    # gestion des messages radio
4
    incoming = radio.receive()
5
    if incoming:
6
        if incoming == "HIT":
7
            gagne()
8
        elif len(incoming)==1:
9
            # arrivee d'un missile
10
            lastLoop=0
11
            scuds.append((int(incoming),-1,1))
12
            display.set_pixel(int(incoming),0,SCUDENMYCOLOR)

process_fire

C'est la fonction la plus complexe du projet. Elle doit faire avancer chaque missile, détecter les éventuelles collisions de ces derniers ainsi que si on a été touché.

On utilise deux variables de type set (ensembles) :

  • setpos : les positions à venir de tous les missiles afin de détecter les colllisions de ces derniers : en effet, si lors du mouvement d'un missile, on se retrouve dans une position qui existe déjà, on détruit les missiles.

  • removeScuds : l'ensemble des missiles qu'il faudra éliminer car ils se seront percutés ou qu'ils auront quitté le plateau de jeu. Une boucle en fin de fonction se chargera de ce nettoyage.

On calcule la position suivante du missile en tenant compte de sa direction par cette ligne.

s1 = (s[0],s[1]+s[2],s[2])

La détection de collision utilise setpos : if s2 in setpos:

S'il n'y a pas de collision :

  • if 0<=s1[1]<4: on procède au déplacement du missile en actualisant la liste scuds et l'affichage

  • elif s1[1]==4: on a été touché, on élimine le scud et on a perdu

  • else: le missile quitte l'écran, on communique sa position à l'autre carte par radio.

Enfin on nettoie les scuds à supprimer en parcourant le set removeScuds. On supprime les missiles de la liste dans un try...except afin d'éviter les erreurs car on a été un peu laxistes dans les vérifications...

1
def process_fire():
2
    setpos=set()
3
    removeScuds=set()
4
    for i,s in enumerate(scuds):
5
        display.set_pixel(s[0],max(0,s[1]),0)
6
        s1 = (s[0],s[1]+s[2],s[2]) # nouvelle position
7
        s2 = (s1[0],s1[1],-s1[2])  # missile en collision eventuelle
8
        if s2 in setpos:
9
            # collision  :autodestruction des 2 scuds
10
            removeScuds.add(s1)
11
            removeScuds.add(s2)
12
            removeScuds.add(s)
13
            removeScuds.add((s[0],s[1],-s[2]))
14
        else :
15
            if 0<=s1[1]<4:
16
                setpos.add(s1)
17
                # deplacement du scud
18
                display.set_pixel(s1[0],s1[1],SCUDFRIENDCOLOR if s1[2]<0 else SCUDENMYCOLOR)
19
                scuds[i]=s1
20
            elif s1[1]==4:
21
                removeScuds.add(s)
22
                if s1[0] == position :
23
                    perdu()
24
            else :
25
                removeScuds.add(s)
26
                # envoi radio
27
                radio.send(str(s[0]))
28
29
    # Nettoyage de la liste des scuds
30
    for sr in removeScuds:
31
        display.set_pixel(sr[0],max(0,sr[1]),0)
32
        try:
33
            scuds.remove(sr)
34
        except:
35
            pass

La boucle principale

Pour finir, la boucle principale. Le code est très court car le gros du traitement se fait dans les fonctions que l'on a décrit précédemment. Il y a néanmoins la gestion du timing qui prend place dans cette partie :

1
delay = 1000000/speed
2
...
3
loop = ticks_us()
4
if(loop-lastLoop < delay):
5
   # attente 200ms pour la detection tir
6
   sleep(min((loop-lastLoop)//1000,200))
7
else :
8
   lastLoop = loop
9
   process_fire()

L'idée ici est d'attendre que le delay soit écoulé pour faire avancer les missiles.

Si le délai n'est pas encore écoulé, on fait une micro-sieste de 200ms afin d'imposer une vitesse maximale au jeu et de rendre l'appui A+B plus facile. Il faut en effet s'assurer lors d'un appui double que le bouton A et B soient testés dans une même boucle. Ce petit délai permet cela.