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ément : Programme 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
speed=1
accel=0.2
score=0
position=2
scuds = []
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.
def position_move(p):
global position
display.set_pixel(position,4,0)
position += p
position = max(min(4,position),0)
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.
def fire():
scuds.append((position,3,-1))
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.
def refresh():
# On redessine l'affichage
display.clear()
display.set_pixel(position,4,9)
for s in scuds:
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.
def perdu():
global speed
speed += accel # Le jeu s'accelere !
# envoi radio "HIT"
radio.send("HIT")
# animation
display.clear()
anim = [
Image("99999:90009:90009:90009:99999"),
Image("00000:06660:06060:06660:00000"),
Image("00000:00000:00300:00000:00000"),
Image("00000:06660:06060:06660:00000"),
Image("99999:90009:90009:90009:99999")]
display.show(anim, delay=100, loop=False, wait=True)
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.
def gagne():
global speed,score
speed += accel # Le jeu s'accelere !
score += 1
# animation
display.clear()
display.scroll(score)
sleep(500)
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.
def process_button():
# Gesion des boutons
ba = button_a.was_pressed()
bb = button_b.was_pressed()
if ba and bb :
fire()
else:
if ba:
position_move(-1)
if bb:
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)
def process_radio():
global lastLoop
# gestion des messages radio
incoming = radio.receive()
if incoming:
if incoming == "HIT":
gagne()
elif len(incoming)==1:
# arrivee d'un missile
lastLoop=0
scuds.append((int(incoming),-1,1))
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'affichageelif s1[1]==4:
on a été touché, on élimine le scud et on a perduelse:
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...
def process_fire():
setpos=set()
removeScuds=set()
for i,s in enumerate(scuds):
display.set_pixel(s[0],max(0,s[1]),0)
s1 = (s[0],s[1]+s[2],s[2]) # nouvelle position
s2 = (s1[0],s1[1],-s1[2]) # missile en collision eventuelle
if s2 in setpos:
# collision :autodestruction des 2 scuds
removeScuds.add(s1)
removeScuds.add(s2)
removeScuds.add(s)
removeScuds.add((s[0],s[1],-s[2]))
else :
if 0<=s1[1]<4:
setpos.add(s1)
# deplacement du scud
display.set_pixel(s1[0],s1[1],SCUDFRIENDCOLOR if s1[2]<0 else SCUDENMYCOLOR)
scuds[i]=s1
elif s1[1]==4:
removeScuds.add(s)
if s1[0] == position :
perdu()
else :
removeScuds.add(s)
# envoi radio
radio.send(str(s[0]))
# Nettoyage de la liste des scuds
for sr in removeScuds:
display.set_pixel(sr[0],max(0,sr[1]),0)
try:
scuds.remove(sr)
except:
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 :
delay = 1000000/speed
...
loop = ticks_us()
if(loop-lastLoop < delay):
# attente 200ms pour la detection tir
sleep(min((loop-lastLoop)//1000,200))
else :
lastLoop = loop
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.