import io
with redirect(stdout=io.StringIO()):
import tdoc.pygame
setup_canvas()
import pygame
import math
import pathlib
from random import randint, choice
width, height = 600, 600
window = pygame.display.set_mode((width, height))
pygame.init()
async def refresh(color):
await animation_frame()
pygame.display.flip()
window.fill(color)
class Actor(pygame.sprite.Sprite):
def __init__(self, image_path, cx, cy):
super().__init__()
self.original_image = pygame.image.load(image_path).convert_alpha()
self.image = self.original_image.copy()
self.rect = self.image.get_rect(center=(cx, cy))
self.collision_margin = 0.05 # 5% de marge
@property
def collision_rect(self):
"""Crée le rect de collision à la volée basé sur le rect actuel"""
return self.rect.inflate(
-self.rect.width * self.collision_margin,
-self.rect.height * self.collision_margin
)
def draw(self):
global window
window.blit(self.image, self.rect)
def get_x(self):
return self.rect.x
def get_y(self):
return self.rect.y
def move(self, dx, dy):
self.rect.move_ip(dx, dy) # Plus simple que x += dx, y += dy
def set_position(self, x, y):
self.rect.x = x
self.rect.y = y
def collide(self, other_actor):
other_rect = getattr(other_actor, 'collision_rect', other_actor.rect)
return self.collision_rect.colliderect(other_rect)
def flip(self, horizontal=True, vertical=False):
self.image = pygame.transform.flip(self.image, horizontal, vertical)
self.rect = self.image.get_rect(center=self.rect.center)
def scale(self, factor):
new_size = (
int(self.original_image.get_width() * factor),
int(self.original_image.get_height() * factor)
)
self.image = pygame.transform.scale(self.original_image, new_size)
self.rect = self.image.get_rect(center=self.rect.center)
def kill(self):
self.image = pygame.Surface((0, 0))
self.rect = self.image.get_rect()
class Text(pygame.sprite.Sprite):
def __init__(self, text, cx, cy, *args):
super().__init__()
self.font = pygame.font.Font(None, 36)
self.image = self.font.render(text, *args)
self.rect = self.image.get_rect()
self.rect.centerx, self.rect.centery = cx, cy
def draw(self):
global window
window.blit(self.image, self.rect)
class Timer(pygame.time.Clock):
def __init__(self, time):
super().__init__()
self.time = time * 1000
self.started = False
def start(self):
self.started = True
self.start_time = animation_time()
self.end_time = self.start_time + self.time
def is_finished(self):
return animation_time() >= self.end_time
def __str__(self):
if not self.started:
return "Not started"
elif self.is_finished():
return "0.00s"
else:
return f"{(self.end_time - animation_time()) / 1000:.2f}s"
def get_pressed_keys():
"""
Retourne la liste des touches et boutons de souris ACTUELLEMENT enfoncés.
"""
pressed_list = []
keys = pygame.key.get_pressed()
# On utilise len(keys) pour déterminer combien de scancodes il faut vérifier.
# C'est la méthode la plus fiable depuis Pygame 2.
for scancode in range(len(keys)):
if keys[scancode]: # Si l'état de la touche à cet index est True (appuyé)
try:
# Utiliser try/except est plus sûr car certains scancodes peuvent ne pas
# avoir de nom de touche lisible, mais ils devraient être rares.
nom_touche = pygame.key.name(scancode).upper()
pressed_list.append(nom_touche)
except ValueError:
# Gérer les scancodes inconnus si nécessaire, mais on peut juste les ignorer.
pass
mouse_buttons = pygame.mouse.get_pressed()
if mouse_buttons[0]: # Gauche
pressed_list.append("MOUSE_1")
if mouse_buttons[1]: # Molette
pressed_list.append("MOUSE_2")
if mouse_buttons[2]: # Droit
pressed_list.append("MOUSE_3")
return pressed_list
try:
await main()
finally:
pygame.quit()
async def main():
pygame.init()
pineapples = []
basket = Actor("basket.png", 300, 550)
game_over = False
score = 0
game_over_text = Text(f"Game over. Score: {score}", 300, 300, True, (10, 10, 10), (255, 90, 20))
while True:
await refresh((111, 255, 255))
for p in pineapples:
p.draw()
basket.draw()
actions = get_pressed_keys()
for action in actions:
if action == "QUIT":
running = False
elif action == "D":
basket.move(3, 0)
elif action == "A":
basket.move(-3, 0)
if game_over == False:
if randint(0, 100) == 0:
pineapple = Actor("orange.png", randint(0, width - 1), -100)
pineapples.append(pineapple)
for pineapple in pineapples:
pineapple.move(0, 3)
if pineapple.collide(basket):
score += 1
pineapple.kill()
pineapples.remove(pineapple)
if pineapple.rect.y > height:
game_over = True
else:
game_over_text.draw()
Pygame - Introduction#
Qu'est-ce que Pygame ?#
Pygame est une librairie Python permettant de facilement de facilement créer de petits jeux en 2D. Avec celle-ci, vous pourrez facilement créer une fenêtre de jeu, ajouter des personnages, les faire se déplacer, gérer leurs collisions, afficher du texte, etc. Pour vous donner une idée de ce à quoi peut ressembler un jeu Pygame, vous pouvez exécuter le programme ci-dessous ci-dessous. Vous le reproduirez durant le 1er TP. Le but est d'attraper les oranges avec le panier en le déplaçant à l'aide des touches A et D.
#Exécutez cette cellule pour tester le jeu du TP1
Pour débuter avec Pygame, vous allez commencer par réaliser des exercices pratiques qui introduiront pas à pas les concepts qui vous permettront de réaliser un jeu complet.
Etape 1 : la boucle de jeu#
Comme décrit en cours, tout jeu vidéo n'est qu'une succession d'images fixes dépendant des entrées de l'utilisateur. Afin de pouvoir afficher des images en boucle, une boucle while infinie est nécessaire. De plus la fonction refresh permet de créer une nouvelle image avec un fond de la couleur RGB passée en paramètre (ici la couleur (255, 255, 0) correspond à du jaune).
async def main():
while True:
await refresh((255, 255, 0))
Exercice 1#
Modifiez la couleur de l'image ci-dessus. Si la notation RGB des couleurs n'est plus claire depuis la 1ère année, consultez ce site pour trouvez le code correspondant à la couleur de votre choix.
Etape 2 : les acteurs#
Le jeu créé à l'étape précédente est bien entendu complétement vide. Nous allons lui rajouter des acteurs. Dans un jeu Pygame, chaque élénebt du jeu est un acteur (en anglais et en Python Actor).
Le personnage principal : un acteur
Une pomme que l'on peut ramasser : un acteur
Un mur que l'on peut détruire : un acteur
...
Comme un acteur doit avoir un aspect visuel, il faut lui attribuer une image. En tant normal, il est possible de choisir n'importe quelle image. Toutefois, dans le cadre de ce site, seuls quelques images choisis au préalable sont utilisables. Pour cette étape, nous utiliserons un petit personnage du nom de Roger avec son image roger.png.
Pour ajouter Roger à un jeu Pygame en tant qu'acteur, faut non seulement préciser son image, mais également ses coordonnées de départ avec la notation suivante :
nouvelle_variable = Actor("image.png", coord_x, coord_y)
Par exemple, pour créer Roger au milieu du haut de l'écran, on pourrait écrire :
roger = Actor("roger.png", 300, 50)
Un acteur doit être créé avant la boucle de jeu. Cependant, il ne faut pas oublier après chaque refresh de redessiner chaque acteur sur l'écran grâce à la méthode draw. Ainsi, le code ci-dessous permet de créer Roger et de l'afficher à l'écran.
async def main():
roger = Actor("roger.png", 300, 50)
while True:
await refresh((197, 255, 143))
roger.draw()
Exercice 2#
Modifiez le code ci-dessus afin de déplacer Roger à divers endroits de l'écran.
Dans le coin en haut à gauche
Dans le coin en bas à droite
au milieu de l'écran
Pour cela, vous remarquerez que le système d'axe de Pygame (et de la grande majorité des applications informatiques) n'est pas exactement pareil que celui que vous utilisez couramment en mathématiques. Ici, l'axe vertical pointe vers le bas. Donc plus la coordonnée y est élevée, plus l'acteur sera bas.
Exercice 3#
Sur le site, une autre image
orange.pngest disponible. Créez un 2ème acteur avec cette image et placez l'orange à côté de Roger.Cette orange est un peu grande par rapport à Roger. Réduisez sa taille de moitié grâce à la méthode
scale. Par exemple, pour un acteurbloc, alorsbloc.scale(2)permettrait d'obtenir un bloc 2x plus grand.
Etape 3 : les entrées utilisateur#
Pour le moment, le jeu est complètement statique. Afin que les personnages puissent se déplacer, il faut, à chaque tour de boucle, contrôler quelles touches ont été pressées par le joueur. La liste des touches pressées peut être récupérée avec touches_pressées = get_pressed_keys(). Par exemple, si l'utilisateur a appuyé sur les touches S et P en même temps qu'un clic-gauche, alors la liste contiendra les valeurs ["S", "P", "MOUSE_0"].
Ainsi, avec la strucutre if ... in ... travaillée en cours, il est facile d'effectuer une action précise quand le joueur appuie sur une touche. Pour déplacer un acteur, il est possible d'utiliser la fonction move. Ses paremètres définissent le nombre de pixels sur l'axe horizontal et vertical dont l'acteur doit être déplacé.
Ainsi, le code ci-dessous permet de déplacer Roger vers la droite lorsqu'on appuie sur D.
async def main():
roger = Actor("roger.png", 300, 50)
while True:
await refresh((197, 255, 143))
touches_pressées = get_pressed_keys()
if "D" in touches_pressées:
roger.move(2, 0)
roger.draw()
Exercice 4#
Que devriez-vous modifier dans le programme ci-dessous pour que Roger se déplace plus rapidement ? Testez votre hypothèse.
Complétez le programme ci-dessus pour que Roger puisse également se déplacer vers la gauche, le haut, et le bas, grâce aux touches A, W et S. Pour cela, quelle est la différence entre utiliser des
if/elif/elif/elifouif/if/if/if?
Exercice 5#
Dans le programme ci-dessus, ajoutez à nouveau une orange à côté de Roger. Puis, ajoutez la ligne orange.move(0, 1) juste après le refresh. Que se passe-t-il ? Pourquoi ?
Etape 4 : les collisions#
L'interaction la plus commune entre deux acteurs est la collision.
Lorsqu'on le drapeau d'arrivée à la fin d'un niveau, il y a une collision entre le joueur et le drapeau.
Lorsqu'un ennemi se fait toucher par une flèche, il y a une collision entre la flèche et l'ennemi.
Lorsqu'un serpent mange une pomme, il y a une collision entre la pomme et le serpent.
...
Avec Pygame, il est possible de contrôler si deux acteurs sont en collision avec la méthode collide. Dans le programme ci-dessous, on contrôle à chaque tour de boucle de jeu si Roger est en collision avec une orange. Si tel est le cas, l'orange est détruite avec sa méthode kill et la vitesse de Roger est augmentée. Cela permet de simuler le fait que Roger ramasse l'orange qui lui octroie un bonus de rapidité.
async def main():
orange = Actor("orange.png", 300, 50)
orange.scale(0.75)
roger = Actor("roger.png", 57, 50)
vitesse = 3
while True:
await refresh((197, 255, 143))
touches_pressées = get_pressed_keys()
if "D" in touches_pressées:
roger.move(vitesse, 0)
if "A" in touches_pressées:
roger.move(-vitesse, 0)
if "S" in touches_pressées:
roger.move(0, vitesse)
if "W" in touches_pressées:
roger.move(0, -vitesse)
if roger.collide(orange):
orange.kill()
vitesse += 5
roger.draw()
orange.draw()
Exercice 6#
Utilisez l'image banana.png et strawberry.png afin de faire apparaître une banane et une fraise dans le jeu. Faites en sorte que Roger puisse ramasser chacun de ces fruits avec les effets suivants :
La fraise ralentit le joueur
La banane est fatale au joueur. S'il glisse dessus, alors Roger disparait et est killed.
Etape 5 : afficher du texte#
Avec Pygame, il est possible de facilement afficher du texte dans son jeu. Un texte est lui aussi un acteur, mais qui se définit différemment. Lisez attentivement l'exemple ci-dessous et testez-le.
async def main():
txt_welcome = Text("Bienvenue dans mon jeu !", 300, 50, True, (0,0,0), (255, 255, 255))
while True:
await refresh((197, 255, 143))
txt_welcome.draw()
Exercice 7#
Ajoutez un 2ème texte rouge avec un fond jaune en dessous du premier texte de bienvenue
Avec vos connaissances des entrées utilisateurs, faites en sorte que ce texte ne s'affiche maintenant plus que lorsqu'on appuie sur la barre espace (
SPACE).
Etape 6 : de l'aléatoire#
Afin que vos jeux ne soient pas trop prévisibles et restent intéressants, il faut leur ajouter une dose d'aléatoire :
Un ennemi qui apparaît à une position aléatoire
Des bonus qui apparaissent à un interval de temps aléatoire
Un choix d'objets aléatoire que l'on trouve dans un coffre
...
Pour cela, on peut utiliser la fonction randint (random integer) qui permet de sélectionner un nombre entier aléatoire entre deux bornes. Lisez et exécutez plusieurs fois le programme ci-dessous pour tester et comprendre le fonctionnement de randint.
async def main():
pos_x = randint(50, 550)
pos_y = randint(50, 550)
roger = Actor("roger.png", pos_x, pos_y)
banane = Actor("banana.png", 50, 50)
while True:
await refresh((197, 255, 143))
roger.draw()
banane.draw()
Exercice 8#
Modifiez le programme ci-dessus de sorte que la banane reste en haut de l'écran, mais que sa position horizontale soit aléatoire.
Exercice 9#
Dans le programme ci-dessous, à chaque fois que Roger entre en collision avec une fraise, celle-ci est déplacée à de nouvelles coordonnées aléatoires.
async def main():
roger = Actor("roger.png", 100, 100)
fraise = Actor("strawberry.png", 200, 100)
while True:
await refresh((197, 255, 143))
roger.draw()
fraise.draw()
touches_pressées = get_pressed_keys()
if "D" in touches_pressées:
roger.move(5, 0)
if "A" in touches_pressées:
roger.move(-5, 0)
if "S" in touches_pressées:
roger.move(0, 5)
if "W" in touches_pressées:
roger.move(0, -5)
if roger.collide(fraise):
pos_x = randint(50, 550)
pos_y = randint(50, 550)
fraise.set_position(pos_x, pos_y)
Avant la boucle
while, créez une nouvelle liste vide nomméeinventaire, et un nouveau texte (voir étape précédente) contenant le messageGagné !.A chaque fois que Roger entre en collision avec une fraise, ajoutez un élément
"fraise"à l'inventaire.Dès que la liste
inventairecontient au moins 5 éléments, le texteGagné !doit être affiché sur l'écran.
Etape 7 : des listes d'acteurs#
Pour le moment, les exemples ne contiennent qu'un seul acteur de chaque type (1 personnage, 1 orange, 1 banane, ...). Cependant, dans un vrai jeu, chaque type d'acteurs peut apparaître de nombreuses fois. Dans ce cas, les acteurs sont stockés dans une liste.
Une horde d'ennemis qui vous attaque : une liste d'acteurs
ennemisLes 10 pièces d'or à ramasser dans un niveau : une liste de 10 acteurs
pièces_d_or...
Lisez attentivement le programme ci-dessous avant de l'exécuter. Celui-ci permet de définir une liste de fruits à afficher à l'écran. Grâce à la boucle for ... in ..., la fonction draw est appelée pour chaque fruit de la liste.
async def main():
fruits = [Actor("strawberry.png", 200, 100), Actor("strawberry.png", 250, 200), Actor("banana.png", 400, 300), Actor("banana.png", 50, 300), Actor("strawberry.png", 50, 400), Actor("orange.png", 300, 500)]
while True:
await refresh((197, 255, 143))
for fruit in fruits:
fruit.draw()
Exercice 10#
Tous les fruits du programme ci-dessus sont trop gros. Grâce à une boucle
for ... in ...placée avant la boucle de jeu, réduisez de moitié la taille de chaque fruit avecscale.A nouveau grâce à la boucle
for ... in ..., mais cette fois dans la boucle de jeu, faites automatiquement se déplacer chaque fruit vers la droite.
Exercice 11#
Avec les listes et randint, il est possible de simuler l'apparition d'acteurs à une position aléatoire avec un interval de temps aléatoire. Vous devriez déjà avoir compris comment les positions aléatoires fonctionnent. En ce qui concerne l'interval de temps aléatoire, l'algorithme permettant de le simuler est le suivant :
Définir une liste d'acteurs
Dans la boucle de jeu
Choisir un nombre aléatoire entre 1 et 100
Si le nombre aléatoire est exactement 1
Ajouter un nouvel acteur à la liste
Dessiner la liste d'acteurs
Complétez le code ci-dessous de sorte qu'il corresponde à cet algorithme et fasse apparaitre des fraises à un interval aléatoire, et à une position aléatoire. Pour cela, il faudra ajouter un nouvel acteur avec l'image
strawberry.pngà la listefruitslorsque la condition portant sur le nombre aléatoire sera respectée.Que se passe-t-il si au lieu de tirer un nombre aléatoire entre 1 et 100, on le tire entre 1 et 20 ?
Ajoutez un nouveau
randintde sorte qu'à l'ajout d'un nouveau fruit, il y ait 1 chance sur 2 qu'il s'agisse d'une banane ou d'une fraise.
async def main():
fruits = []
while True:
await refresh((197, 255, 143))
#Compléter le code ici
for fruit in fruits:
fruit.draw()