Projet 3 ( Battle City )
Préparation
Téléchargez ce dossier projet3.zip (qu'il faudra décompresser) et ouvrez projet3.py avec Thonny
le programme commenté est ci-dessous
import pgzrun # import du module pgzrun
WIDTH=800# taille de la fenêtre
HEIGHT=600
tank = Actor('tank_blue')# création du tank
tank.y = 575 # position du tank
tank.x = 400
tank.angle = 90 # angle de 90 donc orienté vers le haut
background = Actor('grass') # le fond d'écran
def draw(): # la fonction qui dessine (et s'exécute 60 fois par seconde)
screen.fill((0,0,0)) # on efface l'écran
background.draw()# on dessine le fond
tank.draw() # on dessine le tank
pgzrun.go() # Must be last line
Méthode : Mouvement du tank
Les mouvements du tank seront gérés par les flèches du clavier dans une fonction mouvement_tank()
que l'on rafraîchira dans la fonction update()
.
Par exemple pour les flèche gauche et droite:
Remarque : On utilise des elif
pour que si on appuie sur deux flèches à la fois le tank ne puisse pas se déplacer en diagonale.
def mouvement_tank():
if keyboardleft:
tank.x = tank.x -2
tank.angle = 180
elif keyboardright:
tank.x = tank.x + 2
tank.angle = 0
Créez cette fonction et complétez-là pour que le tank puisse aller dans toutes les directions ( les angles à utiliser sont : 180 , 90, 0 et 270)
Puis créez la fonction
update()
en y ajoutant mouvement_tank()
À faire : Faites en sorte que le tank ne puisse pas sortir de le fenêtre ( voir le projet2)
Méthode : Les murs
Dans le dossier images on trouve une image de mur dont les dimensions sont 50x50 pixels. '
Le code suivant crée une liste qui contient des murs et leurs coordonnées .
Ce code est à placer avant l'écriture des fonctions.
# création des murs
walls = []
for x in range(16):
for y in range(1,11):
if randint(0, 100) < 50: # une chance sur 2
wall = Actor('wall') # création du mur
wall.x = x * 50 + 25 # coordonnées
wall.y = y * 50 + 25
walls.append(wall)
Pourquoi 16 ?
la largeur de l'écran est de 800pixels et celle de l'image est 50 ; donc \(800/50 = 16\)
Pourquoi 10 ?
La hauteur de l'écran est de 600pixels et celle de l'image est de 50pixels ; donc \(600/50 = 12\)
Pour empêcher l'affichage des murs sur la première et la dernière ligne( ligne pour les tanks), on boucle sur y de 1 à 10 , ce qui exclut la première et le dernière ligne.
On affiche les murs :
def draw():
screen.fill((0,0,0))
background.draw()
tank.draw()
for wall in walls:
wall.draw()
Méthode : Collisions : murs - tank
Nous devons faire en sorte que le tank ne puisse pas traverser les murs.
Dans la fonction update(),
on mémorise la position du tank et s'il y a collision on remet le tank dans sa position précédente.
def update():
original_x = tank.x
original_y = tank.y
mouvement_tank()
if tank.collidelist(walls) != -1:
tank.x = original_x
tank.y = original_y
Méthode : Le tank tire des obus..
Le tank tire des obus pour détruire les murs.
On commence par créer une liste vide bullets
et on crée une variable
attente
qui nous permettra de gérer les tirs d'obus.
On les déclare en début de programme avant les fonctions...
bullets= [] # création de la liste des obus
attente = 0 # création d'une variable attente pour gérer les tirs
Création des tirs quand on utilise la touche espace :
On crée une fonction tir_tank(),
le code est donné ci-dessous et commenté.
l'utilisation de la, variable attente permet de ne pas avoir un effet « tir laser » . Il y a un délai avant de pouvoir tirer à nouveau
def tir_tank():
global attente # on spécifie que la variable attente est la variable globale déjà déclarée
if attente == 0: # si sa valeur vaut 0
if keyboard.space: # si on appuie sur la touche espace
bullet = Actor('bulletblue2') # on crée l'obus
bullet.angle = tank.angle # on précise ses caractéristique (angle, coordonnées)
bullet.x = tank.x
bullet.y = tank.y
bullets.append(bullet) # on l'ajoute à la liste des obus
attente = 100 # on change la valeur de attente
else:
attente = attente - 1 # si attente n'est pas à 0 on la décrémente de 1
Mouvement des obus.
On crée pour cela une fonction mouvement_bullets()
, qui va gérer les déplacement des obus dans la fenêtre et qui éliminera ces obus s'ils sortent de la fenêtre.
le code est ci-dessous et commenté
def mouvement_bullets():
for bullet in bullets: # pour chaque obus dans la liste
# On déplace les obus en fonction de la valeur de l'angle
if bullet.angle == 0: # vers la droite
bullet.x = bullet.x + 5
elif bullet.angle == 90: # vers le haut
bullet.y = bullet.y - 5
elif bullet.angle == 180: # vers la gauche
bullet.x = bullet.x - 5
elif bullet.angle == 270: # vers le bas
bullet.y = bullet.y + 5
# suppression des obus hors de la fenêtre
if bullet.x < 0 or bullet.x > 800 or bullet.y < 0 or bullet.y > 600:
bullets.remove(bullet)
Affichage et mise à jour.
Dans la fonction update()
, on appelle les fonctions tir_tank()
,
mouvement_bullets()
et on affiche les tirs dans la fonction draw()
def update():
original_x = tank.x
original_y = tank.y
mouvement_tank()
if tank.collidelist(walls) != -1:
tank.x = original_x
tank.y = original_y
tir_tank()
mouvement_bullets()
def draw():
screen.fill((0,0,0))
background.draw()
tank.draw()
for wall in walls:
wall.draw()
for bullet in bullets:
bullet.draw()
Méthode : Les obus détruisent les murs...
Le principe : On examine la collision des obus avec les murs, s'il y a collision, on récupère l'indice du mur touché dans la liste, on le supprime et on supprime l'obus concerné.
l'instruction : wall_index = bullet.collidelist(walls) récupère l'indice du mur qui est en collision avec l'obus dans la variable
wall_index
qui par défaut est égale à $-1$.
Voici le code à rajouter à la fonction mouvement_bullets()
, il est commenté.
Vous pouvez si vous le souhaitez créer une fonction spéciale pour cela, il faudra bien sûr la mettre à jour...
# suppression du mur touché et de l'obus
for bullet in bullets: # pour chaque obus
wall_index = bullet.collidelist(walls)# on récupère son indice dans la liste
if wall_index != -1:# s'il n'est pas égal à -1 (collision détectée)
del walls[wall_index]# on le supprime de la liste
bullets.remove(bullet)# on supprime l'obus
Le jeu
Le jeu
À ce stade, nous avons un tank qui peut se déplacer dans une fenêtre et qui détruit des murs avec des obus.
On peut maintenant envisager plusieurs scénarios pour un jeu.
On crée un second joueur piloté par un autre humain avec d'autres touches du clavier
On crée un joueur piloté par l'ordinateur
On d'autres joueurs pilotés par l'ordinateur
...
Nous allons utiliser le 3ème scénario :
Cahier des charges :
Créer une liste de 3 tanks ennemis rouges placés dans la bande supérieure de façon à ce qu'il ne se chevauche pas.
Faire en sorte que les tanks ennemis puissent se déplacer dans la fenêtre de façon autonome et gérant les collisions.
Faire en sorte que ces tanks tire des obus aléatoirement en détruisant des murs ou le joueur, mais ses frères d'armes...
Faire en sorte que le gagnant du jeu soit le survivant.
Affichage de scores (nombre de murs détruits par exemple)
mettre des sons , des explosions...
...
Votre travail seul ou en groupe est de réaliser ce jeu :
Méthode : Voici ce qu'il faut faire si on ne rajoute qu'un seul ennemi.
Création de l'ennemi et affichage.
import pgzrun
from random import randint # on aura besoin de randint
enemy = Actor('tank_red')
enemy.y = 25
enemy.x = 400
enemy.angle = 270
enemy.move_count = 0 # on met à zéro un compteur de mouvement (spécificité des modules)
def draw():
screen.fill((0,0,0))
background.draw()
tank.draw()
for wall in walls:
wall.draw()
for bullet in bullets:
bullet.draw()
enemy.draw() # affichage de l'ennemi
Mouvement autonome de l'ennemi.
La fonction mouvement_enemy()
est à ajouter et à faire mettre à jour (elle est commentée)
def mouvement_ennemy():
choice = randint(0, 2) # choix d'un entier entre 0,1 et 2
if enemy.move_count > 0: # si le compteur est positif on le décrémente (il peut se déplacer)
enemy.move_count = enemy.move_count - 1
# on mémorise la position de l'ennemi
original_x = enemy.x
original_y = enemy.y
# gestion des mouvement
if enemy.angle == 0:# vers la droite
enemy.x = enemy.x + 2
elif enemy.angle == 90:# vers le bas
enemy.y = enemy.y - 2
elif enemy.angle == 180:# vers la gauche
enemy.x = enemy.x - 2
elif enemy.angle == 270:# vers le bas
enemy.y = enemy.y + 2
# collision avec les murs
if enemy.collidelist(walls) != -1:
enemy.x = original_x
enemy.y = original_y
enemy.move_count = 0# remise à 0 du compteur
# sortie de la fenêtre interdite
if enemy.x < 0 or enemy.x > 800 or enemy.y < 0 or enemy.y > 600:
enemy.x = original_x
enemy.y = original_y
enemy.move_count = 0# remise à 0 du compteur
elif choice == 0: # si le 0 est sorti
enemy.move_count = 20 # il pourra se déplacer
elif choice == 1:# si le 1 est sorti chagement de direction
enemy.angle = randint(0, 3) * 90
else: # si c'est le 2 il tire un obus
print('shoot')# pour l'instant on fait afficher shoot sdans la console
Tirs d'obus de l'ennemi.
enemy_bullets = [] # on crée une liste pour contenir les tirs de l'ennemi
À la place de print(''shoot '') mettre :
bullet = Actor('bulletred2')# création de l'obus
bullet.angle = enemy.angle
bullet.x = enemy.x
bullet.y = enemy.y
enemy_bullets.append(bullet)
Une fonction mouvement_enemy_bullets()
à mettre à jour dans update()
def mouvement_enemy_bullets():
for bullet in enemy_bullets: # poir chaque obus mouvement en fonction de la direction
if bullet.angle == 0:
bullet.x = bullet.x + 5
elif bullet.angle == 90:
bullet.y = bullet.y - 5
elif bullet.angle == 180:
bullet.x = bullet.x - 5
elif bullet.angle == 270:
bullet.y = bullet.y + 5
# collisions
for bullet in enemy_bullets:
# pour les murs
wall_index = bullet.collidelist(walls)
if wall_index != -1:
del walls[wall_index]
enemy_bullets.remove(bullet)
# sortie de la fenêtre
if bullet.x < 0 or bullet.x > 800 or bullet.y < 0 or bullet.y > 600:
enemy_bullets.remove(bullet)
Gagné/Perdu
Si l'un des tanks touche l'autre il a gagné.
game_over = False
message = ''# pour le message à afficher
une fonction jeu()
à mettre à jour dans update()
def jeu():
global game_over,message #variable globales modifiées dans jeu
for bullet in bullets: # collision
if bullet.colliderect(enemy):
game_over = True
message = "YOU WIN"
for bullet in enemy_bullets:
if bullet.colliderect(tank):
game_over = True
message = "YOU LOOSE"
Modification de la fonction draw()
:
def draw():
global message
if game_over == False:
screen.fill((0,0,0))
background.draw()
tank.draw()
for wall in walls:
wall.draw()
for bullet in bullets:
bullet.draw()
enemy.draw()
for bullet in enemy_bullets:
bullet.draw()
mouvement_enemy_bullets()
else:
screen.fill((0,0,0))
screen.draw.text(message, (260,250), color=(255,255,255), fontsize=100)
Ce qui est fait pour un ennemi peut l'être pour plusieurs...