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

1
import pgzrun # import du module pgzrun
2
3
WIDTH=800# taille de la fenêtre
4
HEIGHT=600
5
6
tank = Actor('tank_blue')# création du tank
7
tank.y = 575 # position du tank
8
tank.x = 400
9
tank.angle = 90 # angle de 90 donc orienté vers le haut
10
background = Actor('grass') # le fond d'écran
11
12
def draw(): # la fonction qui dessine (et s'exécute 60 fois par seconde)
13
    screen.fill((0,0,0)) # on efface l'écran
14
    background.draw()# on dessine le fond
15
    tank.draw() # on dessine le tank
16
17
pgzrun.go() # Must be last line

MéthodeMouvement 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.

1
def mouvement_tank():
2
	if keyboardleft:
3
		tank.x = tank.x -2
4
		tank.angle = 180
5
	elif keyboardright:
6
		tank.x = tank.x + 2
7
		tank.angle = 0
8
		

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éthodeLes 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.

1
# création des murs
2
walls = []
3
for x in range(16):
4
    for y in range(1,11):
5
        if randint(0, 100) < 50: # une chance sur 2
6
            wall = Actor('wall') # création du mur
7
            wall.x = x * 50 + 25 # coordonnées 
8
            wall.y = y * 50 + 25
9
            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 :

1
def draw():
2
    screen.fill((0,0,0))
3
    background.draw()
4
    tank.draw()
5
    for wall in walls:
6
        wall.draw()

MéthodeCollisions : 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.

1
def update():
2
    original_x = tank.x
3
    original_y = tank.y
4
    mouvement_tank()
5
    if tank.collidelist(walls) != -1:
6
        tank.x = original_x
7
        tank.y = original_y

MéthodeLe 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...

1
bullets= [] # création de la liste des obus
2
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

1
def tir_tank():
2
    global attente # on spécifie que la variable attente est la variable globale déjà déclarée
3
    if attente == 0: # si sa valeur vaut 0
4
        if keyboard.space: # si on appuie sur la touche espace
5
            bullet = Actor('bulletblue2') # on crée l'obus
6
            bullet.angle = tank.angle # on précise ses caractéristique (angle, coordonnées)
7
            bullet.x = tank.x
8
            bullet.y = tank.y
9
            bullets.append(bullet) # on l'ajoute à la liste des obus
10
            attente = 100 # on change la valeur de attente
11
    else:
12
        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é

1
def mouvement_bullets():
2
    for bullet in bullets: # pour chaque obus dans la liste
3
        # On déplace les obus en fonction de la valeur de l'angle
4
        if bullet.angle == 0: # vers la droite
5
            bullet.x = bullet.x + 5
6
        elif bullet.angle == 90: # vers le haut
7
            bullet.y = bullet.y - 5
8
        elif bullet.angle == 180: # vers la gauche
9
            bullet.x = bullet.x - 5
10
        elif bullet.angle == 270: # vers le bas
11
            bullet.y = bullet.y + 5
12
        # suppression des obus hors de la fenêtre
13
        if bullet.x < 0 or bullet.x > 800 or bullet.y < 0 or bullet.y > 600:
14
            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()

1
def update():
2
    original_x = tank.x
3
    original_y = tank.y
4
    mouvement_tank()
5
    if tank.collidelist(walls) != -1:
6
        tank.x = original_x
7
        tank.y = original_y
8
    tir_tank()
9
    mouvement_bullets()
10
11
def draw():
12
    screen.fill((0,0,0))
13
    background.draw()
14
    tank.draw()
15
    for wall in walls:
16
        wall.draw()
17
    for bullet in bullets:
18
        bullet.draw()
19

MéthodeLes 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...

1
# suppression du mur touché et de l'obus
2
    for bullet in bullets: # pour chaque obus
3
        wall_index = bullet.collidelist(walls)# on récupère son indice dans la liste
4
        if wall_index != -1:# s'il n'est pas égal à -1 (collision détectée)
5
            del walls[wall_index]# on le supprime de la liste
6
            bullets.remove(bullet)# on supprime l'obus
7

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éthodeVoici ce qu'il faut faire si on ne rajoute qu'un seul ennemi.

Création de l'ennemi et affichage.

1
import pgzrun
2
from random import randint # on aura besoin de randint
1
enemy = Actor('tank_red')
2
enemy.y = 25
3
enemy.x = 400
4
enemy.angle = 270
5
enemy.move_count = 0 # on met à zéro un compteur de mouvement (spécificité des modules)
1
def draw():
2
    screen.fill((0,0,0))
3
    background.draw()
4
    tank.draw()
5
    for wall in walls:
6
        wall.draw()
7
    for bullet in bullets:
8
        bullet.draw()
9
    enemy.draw() # affichage de l'ennemi
10

Mouvement autonome de l'ennemi.

La fonction mouvement_enemy() est à ajouter et à faire mettre à jour (elle est commentée)

1
def mouvement_ennemy():
2
    choice = randint(0, 2) # choix d'un entier entre 0,1 et 2
3
    if enemy.move_count > 0: # si le compteur est positif on le décrémente (il peut se déplacer)
4
        enemy.move_count = enemy.move_count - 1
5
        # on mémorise la position de l'ennemi
6
        original_x = enemy.x
7
        original_y = enemy.y
8
        # gestion des mouvement
9
        if enemy.angle == 0:# vers la droite
10
            enemy.x = enemy.x + 2
11
        elif enemy.angle == 90:# vers le bas
12
            enemy.y = enemy.y - 2
13
        elif enemy.angle == 180:# vers la gauche
14
            enemy.x = enemy.x - 2
15
        elif enemy.angle == 270:# vers le bas
16
            enemy.y = enemy.y + 2
17
        # collision avec les murs
18
        if enemy.collidelist(walls) != -1:
19
            enemy.x = original_x
20
            enemy.y = original_y
21
            enemy.move_count = 0# remise à 0 du compteur
22
        # sortie de la fenêtre interdite
23
        if enemy.x < 0 or enemy.x > 800 or enemy.y < 0 or enemy.y > 600:
24
            enemy.x = original_x
25
            enemy.y = original_y
26
            enemy.move_count = 0# remise à 0 du compteur
27
28
    elif choice == 0: # si le 0 est sorti
29
        enemy.move_count = 20 # il pourra se déplacer
30
    elif choice == 1:# si le 1 est sorti chagement de direction
31
        enemy.angle = randint(0, 3) * 90
32
    else: # si c'est le 2 il tire un obus 
33
        print('shoot')# pour l'instant on fait afficher shoot sdans la console

Tirs d'obus de l'ennemi.

1
enemy_bullets = [] # on crée une liste pour contenir les tirs de l'ennemi
2

À la place de print(''shoot '') mettre :

1
bullet = Actor('bulletred2')# création de l'obus
2
        bullet.angle = enemy.angle
3
        bullet.x = enemy.x
4
        bullet.y = enemy.y
5
        enemy_bullets.append(bullet)

Une fonction mouvement_enemy_bullets() à mettre à jour dans update()

1
def mouvement_enemy_bullets():
2
    for bullet in enemy_bullets: # poir chaque obus mouvement en fonction de la direction
3
        if bullet.angle == 0:
4
            bullet.x = bullet.x + 5
5
        elif bullet.angle == 90:
6
            bullet.y = bullet.y - 5
7
        elif bullet.angle == 180:
8
            bullet.x = bullet.x - 5
9
        elif bullet.angle == 270:
10
            bullet.y = bullet.y + 5
11
    # collisions
12
    for bullet in enemy_bullets:
13
        # pour les murs
14
        wall_index = bullet.collidelist(walls)
15
        if wall_index != -1:
16
            del walls[wall_index]
17
            enemy_bullets.remove(bullet)
18
        # sortie de la fenêtre
19
        if bullet.x < 0 or bullet.x > 800 or bullet.y < 0 or bullet.y > 600:
20
            enemy_bullets.remove(bullet)

Gagné/Perdu

Si l'un des tanks touche l'autre il a gagné.

1
game_over = False
2
message = ''# pour le message à afficher

une fonction jeu() à mettre à jour dans update()

1
def jeu():
2
    global game_over,message  #variable globales modifiées dans jeu
3
    for bullet in bullets: # collision 
4
        if bullet.colliderect(enemy):
5
            game_over = True
6
            message = "YOU WIN"
7
    for bullet in enemy_bullets:
8
        if bullet.colliderect(tank):
9
            game_over = True
10
            message = "YOU LOOSE"

Modification de la fonction draw() :

1
def draw():
2
    global message
3
    if game_over == False:
4
        screen.fill((0,0,0))
5
        background.draw()
6
        tank.draw()
7
        for wall in walls:
8
            wall.draw()
9
        for bullet in bullets:
10
            bullet.draw()
11
        enemy.draw()
12
        for bullet in enemy_bullets:
13
            bullet.draw()
14
        mouvement_enemy_bullets()
15
    else:
16
        screen.fill((0,0,0))
17
        screen.draw.text(message, (260,250), color=(255,255,255), fontsize=100)

Ce qui est fait pour un ennemi peut l'être pour plusieurs...