Faire des jeux avec Pygame

Contributeurs Wikibooks

Un énorme tutoriel qui couvre la façon de créer un jeu complet.

Commentez

- Titre : Faire des jeux avec Pygame

- Auteur : Contributeurs Wikibooks

- Parution : 9 octobre 2013

- Licence : Le contenu de cet article est rédigé par Contributeurs Wikibooks et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- I. Introduction

- I-A. Une remarque sur les styles d'écriture

- II. Révision : les fondamentaux de Pygame

- II-A. Le jeu Pygame de base

- II-B. Objets Pygame de base

- II-C. Blitting

- II-D. La boucle d'évènements

- II-E. Tada !

- III. Coup d'envoi

- III-A. Les premières lignes et le chargement de modules

- III-B. Fonctions de gestion des ressources

- IV. Classes d'objets de jeu

- IV-A. Une simple classe balle

- IV-A-1. Diversion 1 : Sprites

- IV-A-2. Diversion 2 : Physique des vecteurs

- V. Objets contrôlés par l'utilisateur

- V-A. Une simple classe Bat

- V-A-1. Diversion 3 : Évènements Pygame

- VI. Assembler le tout

- VI-A. Faire rebondir la balle sur les bords de l'écran

- VI-B. Faire rebondir la balle sur la raquette

- VI-C. Le produit final

- VII. Remerciements

I - Introduction

En premier lieu, je supposerai que vous avez lu le tutoriel Chimp - Ligne par ligne, lequel introduit les bases de Python et de Pygame. Dans le cas contraire, prenez-en connaissance avant de lire la suite, car je ne répèterai pas les bases fournies par cet autre tutoriel (en tout cas, pas dans les détails). Ce tutoriel est destiné à ceux qui savent réaliser un petit jeu ridiculement simple, et qui aimeraient réaliser un petit jeu relativement simple comme Pong. Il vous fournira une introduction à quelques concepts, comme l'architecture d'un jeu, quelques notions de mathématiques pour le fonctionnement physique de la balle, ainsi que sur la manière de garder votre jeu facile à maintenir et à améliorer.

Tout le code de ce tutoriel est utilisé dans Tom's Pong, un jeu que j'ai écrit. À la fin de ce tutoriel, vous devriez non seulement renforcer votre compréhension de Pygame, mais vous devriez aussi comprendre comment Tom's Pong fonctionne et comment concevoir votre propre version.

Maintenant, faisons une brève revue des bases sur l'utilisation de Pygame. Une méthode répandue d'organiser le code d'un jeu est de le diviser en six parties distinctes :

- le chargement des modules qui sont utilisés dans le jeu : Tous les modules standards, excepté les importations locales des espaces de nommage de Pygame et le module Pygame lui-même ;

- les classes de manipulation des ressources : La définition des classes gérant la plupart des ressources de base, que sont le chargement des images et des sons, ainsi que les procédures de connexion/déconnexion au réseau, le chargement des sauvegardes de jeu, et toutes les autres ressources que vous pouvez utiliser dans votre jeu ;

- les classes des objets du jeu : Cette partie devra contenir les définitions de classes pour les objets de votre jeu. Dans l'exemple de Pong, ce sera un objet pour la raquette du joueur (que vous pouvez initialiser plusieurs fois, une pour chaque joueur dans le jeu), et une pour la balle (laquelle peut aussi avoir de multiples instances). Si vous souhaitez avoir un menu sympa pour votre jeu, c'est aussi une bonne idée de faire une classe pour le menu ;

- toutes les autres fonctions du jeu : Dans cette partie seront contenues toutes les autres fonctions nécessaires à la bonne marche du jeu, comme celles qui définissent le tableau de scores, la gestion du menu, etc. Tout le code que vous pourriez mettre dans la logique de jeu principal, mais qui rendrait sa logique difficilement lisible et peu cohérente, devrait être contenu dans cette partie. Par exemple, tracer le tableau de scores ne relève pas du jeu en lui-même, cela devrait être fait par une fonction particulière située dans cette partie ;

- l'initialisation du jeu : Cela inclut les objets Pygame eux-mêmes, l'arrière-plan, les objets du jeu (initialisation des instances de classe) et les autres petits morceaux de code que vous pourriez vouloir ajouter ;

- la boucle principale : C'est dans cette boucle que vous placerez la gestion des entrées (c'est-à-dire l'acquisition des évènements utilisateurs que sont les actions sur le clavier/les boutons de souris), le code de mise à jour des objets du jeu, et finalement la mise à jour de l'écran.

Tous les jeux que vous ferez auront certaines, voire la totalité de ces sections, et probablement d'autres de votre propre cru. Dans le cadre de ce tutoriel, je parlerai de la façon dont Tom's Pong est agencé et de la façon d'appliquer cette organisation à chaque projet de jeu que vous pourriez avoir. Je supposerai également que vous voudriez garder tout le code dans un seul fichier, mais si vous faites un jeu plutôt conséquent en taille de code, c'est souvent une bonne idée de séparer le jeu en plusieurs modules. Mettre les classes des objets du jeu dans un fichier objects.py, par exemple, peut vous aider à séparer la logique du jeu de ses objets. Si vous avez énormément de code pour la manipulation des ressources, il peut être également pratique de le mettre dans un module ressources.py. Vous pourrez alors écrire from objects, resources import * pour importer toutes les classes et les fonctions.

I-A - Une remarque sur les styles d'écriture

La première chose à laquelle il faut penser, lors de l'approche d'un projet de programmation, est de décider d'un style d'écriture, et de le conserver. Python en lui-même facilite cela, à cause de son interprétation stricte des espaces et de l'indentation, mais cela ne vous empêche pas de choisir la largeur de votre indentation, de quelle manière vous placerez les importations, comment vous allez commenter le code, etc. Vous verrez comment je fais tout cela dans mes exemples de code. Quel que soit le style que vous adopterez, conservez-le tout au long de votre code. Essayez également de documenter toutes vos classes et de commenter tous les morceaux de code qui peuvent sembler obscurs. Par ailleurs, il ne sert à rien de commenter ce qui est évident. J'ai vu beaucoup de personnes faire la chose suivante :

player1.score += scoreup        # Ajoute scoreup au score de joueur 1

Ce n'est pas très grave, mais un peu inutile. Un mauvais code est mal agencé, possède des changements aléatoires dans le style d'écriture et n'a qu'une maigre documentation. Ce mauvais code ne sera pas seulement ennuyeux pour les autres personnes, mais il sera également difficile à maintenir pour vous.

II - Révision : les fondamentaux de Pygame

II-A - Le jeu Pygame de base

Pour la révision (ça ne peut pas faire de mal), et pour s'assurer que vous êtes familier avec la structure d'un programme Pygame standard, je vais brièvement parcourir un programme Pygame simple, qui n'affichera rien de plus qu'une fenêtre avec un peu de texte à l'intérieur et qui devrait finalement ressembler à ça (naturellement, la décoration de la fenêtre pourra être différente sur votre système) :

Dans cet exemple, le code complet ressemble à ça :

#!/usr/bin/python
# coding: utf-8

import pygame
from pygame.locals import *

def main():
# Initialisation de la fenêtre d'affichage
pygame.init()
screen = pygame.display.set_mode((300, 50))
pygame.display.set_caption('Programme Pygame de base')

# Remplissage de l'arrière-plan
background = pygame.Surface(screen.get_size())
background = background.convert()
background.fill((250, 250, 250))

# Affichage d'un texte
font = pygame.font.Font(None, 36)
text = font.render("Salut tout le monde", 1, (10, 10, 10))
textpos = text.get_rect()
textpos.centerx = background.get_rect().centerx
textpos.centery = background.get_rect().centery
background.blit(text, textpos)

# Afficher le tout dans la fenêtre
screen.blit(background, (0, 0))
pygame.display.flip()

# Boucle d'évènements
while 1:
for event in pygame.event.get():
if event.type == QUIT:
return

screen.blit(background, (0, 0))
pygame.display.flip()

if __name__ == '__main__': main()

II-B - Objets Pygame de base

Comme vous pouvez le constater, le code se divise en trois catégories principales : la fenêtre d'affichage (screen), l'arrière-plan (background) et le texte (text). Chacun de ces objets a pu être créé grâce à l'appel en premier lieu de la méthode pygame.init(), que nous avons modifiée ensuite pour qu'elle convienne à nos besoins. La fenêtre d'affichage est un cas un peu spécial, car elle modifie l'affichage à travers les appels pygame, plutôt que d'appeler les méthodes appartenant aux objets de l'écran. Mais pour tous les autres objets Pygame, nous créons d'abord l'objet comme une copie d'un objet Pygame, en lui affectant certains attributs, et développons les objets de notre jeu à partir de celui-ci.

Pour l'arrière-plan, nous créons d'abord un objet Surface et lui donnons la taille de la fenêtre. Nous utilisons ensuite la méthode convert() pour convertir la Surface en un unique espace colorimétrique. C'est particulièrement recommandé lorsque nous manipulons plusieurs images et surfaces, toutes dans un espace colorimétrique différent, sinon cela ralentirait de beaucoup le rendu. En convertissant toutes les surfaces, nous pouvons radicalement accélérer les temps de rendu. Enfin, nous remplissons la surface d'arrière-plan en blanc (255, 255, 255). Ces valeurs sont en RGB et nous pouvons les retrouver à partir de n'importe quel bon programme de dessin.

En ce qui concerne le texte, nous avons besoin de plus d'un objet. D'abord nous créons un objet font, qui définira quelle police nous utiliserons, ainsi que sa taille. Ensuite nous créons un objet text, en utilisant la méthode de rendu de notre objet font et en lui fournissant trois arguments : le texte à faire le rendu, qui sera ou non anticrènelé (1 = oui, 0 = non), ainsi que la couleur du texte (toujours dans un format RGB). Ensuite nous créons un troisième objet texte qui fournira le rectangle du texte. La manière la plus simple à comprendre est de s'imaginer en train de dessiner un rectangle qui englobera tout le texte. Vous pourrez alors utiliser ce rectangle afin d'obtenir ou de définir la position du texte sur la fenêtre d'affichage. Ainsi dans cet exemple nous avons le rectangle, et si nous définissons ses attributs centerx et centery pour correspondre aux centerx et centery de l'arrière-plan, alors le texte aura le même centre que l'arrière-plan. Dans cet exemple, le texte sera centré sur les axes x et y de la fenêtre d'affichage.

II-C - Blitting

Maintenant que nous avons créé les objets de notre jeu, nous avons besoin d'en faire le rendu. Si nous ne le faisons pas et que nous exécutons le programme, nous ne verrons qu'une fenêtre blanche, et nos objets resteront invisibles. Le terme employé pour faire un rendu des objets est le blitting (que nous franciserons par blit), qui correspond à la copie de pixels d'un objet source vers un objet de destination. Ainsi pour faire un rendu de l'objet background, vous le copiez sur l'objet screen. Dans cet exemple, pour faire les choses simples, nous copions le texte sur l'arrière-plan (donc l'arrière-plan possède une copie du texte sur lui), et ensuite nous copions l'arrière-plan sur l'écran.

Le blit est une des opérations les plus lentes dans un jeu, vous devez donc faire attention à ne pas trop en faire sur l'écran pour chaque image. Par exemple, si vous avez une image d'arrière-plan et une balle se déplaçant à travers l'écran, alors vous pouvez copier l'arrière-plan en entier et ensuite la balle, tout ceci à chaque image, ce qui recouvrira la position précédente de la balle et fera un rendu de la nouvelle balle, mais ce sera plutôt lent. Une meilleure solution consiste à copier une partie de l'arrière-plan sur la zone occupée par la balle à l'image précédente, qui peut être trouvée grâce au rect de la balle précédente, et ensuite copier la nouvelle balle, ce qui aura pour effet de copier seulement deux petites zones.

II-D - La boucle d'évènements

Une fois que vous avez défini le jeu, vous avez besoin de le mettre dans une boucle qui s'exécutera en continu jusqu'à ce que l'utilisateur signale qu'il veuille quitter. Vous démarrerez donc une boucle ouverte et à chaque itération de la boucle, qui sera chaque image du jeu, vous actualiserez le jeu. La première chose à contrôler pour chaque évènement est de savoir si l'utilisateur a enfoncé une touche du clavier, cliqué un bouton de la souris, déplacé le joystick, redimensionné la fenêtre, ou tenté de la fermer. Dans ce cas, nous voudrons simplement examiner si l'utilisateur a essayé de fermer la fenêtre, auquel cas le jeu engendrera un return, ce qui terminera la boucle while. Alors nous aurons simplement besoin de recopier l'arrière-plan, et faire un flip (actualisation de l'affichage) de l'écran pour que chaque chose soit redessinée. D'accord, étant donné que rien ne se passe ou se déplace dans cet exemple, nous n'avons aucunement besoin de recopier l'arrière-plan à chaque itération, mais je le mets parce que si certaines choses se déplacent à travers l'écran, vous aurez besoin de faire tous vos blits ici.

II-E - Tada !

Et voilà, votre jeu Pygame le plus basique. Tous les jeux prendront une forme similaire, mais avec beaucoup plus de code concernant les fonctions de jeu elles-mêmes, que vous concevrez vous-même sans les copier depuis un tutoriel ou un guide. C'est le but principal de ce tutoriel, nous allons maintenant rentrer dans le vif du sujet de la conception de jeux vidéos.

III - Coup d'envoi

Les premières sections du code sont relativement simples et, une fois écrites, peuvent souvent être réutilisées dans d'autres jeux que vous programmerez. Elles s'occuperont de toutes les tâches fastidieuses et génériques comme : charger des modules, charger des images, ouvrir des connexions réseau, jouer de la musique, etc. Elles incluront également de simples mais efficaces gestionnaires d'erreurs, et les quelques personnalisations que vous souhaiterez effectuer par-dessus les fonctions fournies par des modules comme sys et pygame.

III-A - Les premières lignes et le chargement de modules

Tout d'abord, vous avez besoin de démarrer votre jeu et de charger vos modules. C'est toujours une bonne idée de définir certaines choses directement en haut du fichier source principal, comme : le nom du fichier, ce qu'il contient, sa licence, ainsi que n'importe quelle autre information que vous jugerez utile de faire lire à ceux qui la regardent. Ensuite vous pouvez charger des modules, agrémentés d'une gestion d'erreur qui fera en sorte que Python ne vous affichera pas ces horribles traceback que les non-programmeurs ne comprennent pas. Le code est très simple, je ne m'étendrai pas dessus :

#!/usr/bin/env python
# coding: utf-8
#
# Tom's Pong
# A simple pong game with realistic physics and AI
# http://tom.acrewoods.net/projects/pong
#
# Released under the GNU General Public License

VERSION = "0.4"

try:
import sys
import random
import math
import os
import getopt
import pygame
from socket import *
from pygame.locals import *
except ImportError, err:
print "Impossible de charger le module. %s" % (err)
sys.exit(2)

III-B - Fonctions de gestion des ressources

Dans l'exemple Chimp - Ligne par ligne, le premier code à être écrit correspond au chargement des images et des sons. Étant donné que c'est totalement indépendant de la logique de jeu et/ou des objets de jeu, elles seront écrites en premier et dans des fonctions séparées, ce qui impliquera que le code qui s'ensuivra pourra les utiliser. Je mets généralement tout mon code de cette nature au départ, dans leur propre fonction, sans classe. Cela correspondra aux fonctions de gestion des ressources. Vous pouvez bien sûr créer des classes pour celle-ci, c'est à vous de développer votre propre style et vos meilleures pratiques.

C'est toujours une bonne idée d'écrire vos propres fonctions de gestion de ressources, car bien que Pygame possède des méthodes pour l'ouverture des images et des sons (ainsi que d'autres modules qui possèdent eux aussi leurs propres méthodes pour l'ouverture d'autres ressources), ces méthodes peuvent prendre plus d'une ligne et peuvent requérir de consistantes modifications faites par vous-mêmes. Bien souvent elles ne fournissent pas de gestion d'erreur satisfaisante. Écrire des fonctions de gestion de ressources vous donne un code sophistiqué, réutilisable, et vous offre plus de contrôle sur vos ressources. Prenez cet exemple d'une fonction de chargement d'image :

def load_png(name):
"""Charge une image et retourne un objet image"""
fullname = os.path.join('data', name)
try:
image = pygame.image.load(fullname)
if image.get_alpha() is None:
image = image.convert()
else:
image = image.convert_alpha()
except pygame.error, message:
print "Impossible de charger l'image : ", fullname
raise SystemExit, message
return image, image.get_rect()

Ici nous avons créé une fonction de chargement d'image plus sophistiquée que celle fournie par Pygame : image.load(). À noter que la première ligne de la fonction débute par un docstring (chaine de caractère de documentation) qui décrit ce que fait la fonction et quel objet elle retourne. La fonction suppose que toutes vos images soient dans un répertoire appelé data, et donc utilisera le nom de fichier et créera le chemin complet (par exemple data/ball.png), en utilisant le module os pour s'assurer de la compatibilité entre différentes plateformes (Linux, OS X, Windows...). Ensuite elle essaye de charger l'image, et de convertir les régions alpha (ce qui vous permettra d'utiliser la transparence), et le cas échéant retourne une erreur lisible par un être humain si elle rencontre un problème. Finalement elle retourne un objet image, ainsi que son rect.

Vous pouvez créer des fonctions similaires pour le chargement de n'importe quelle autre ressource, tel que le chargement des sons. Vous pouvez aussi créer des classes de gestion de ressources pour vous donner plus de flexibilité avec des ressources plus complexes. Par exemple, vous pouvez créer une classe Music, avec une fonction __init__() qui charge le son (peut-être en empruntant la fonction load_sound()), une méthode pour mettre en pause la musique, une méthode pour la redémarrer. Une autre classe de gestion de ressources utile peut être créée pour les connexions réseau. Des fonctions pour ouvrir des sockets, passer des données avec une sécurité appropriée et munies d'un contrôle d'erreur, fermer des sockets, finger des adresses, ainsi que d'autres tâches concernant le réseau, pourront rendre l'écriture d'un jeu avec des capacités réseau moins pénibles.

Souvenez-vous que la tâche première de ces fonctions/classes est de s'assurer qu'avec le temps, l'écriture des classes d'objets et de la boucle principale, il n'y ait presque plus rien à faire. L'héritage de classes peut rendre ces classes de base utiles. Ne vous emballez pas, des fonctions qui ne seront utilisées que par une classe devront être écrites dans cette classe, et non pas dans une fonction globale.

IV - Classes d'objets de jeu

Une fois les modules chargés et les fonctions de gestion de ressources écrites, vous aimeriez écrire certains objets du jeu. La manière de le faire est très simple, bien qu'elle semble complexe au début. Vous devez écrire une classe pour chaque type d'objet du jeu et ensuite, vous pourrez créer une instance de ces classes pour chaque objet. Vous pourrez utiliser les méthodes de ces classes pour manipuler les objets, les déplacer et leur donner des capacités d'interaction. Votre jeu ressemblera donc à ceci (pseudo-code) :

#!/usr/bin/python
# coding: utf-8

[Charger vos modules ici]

[Fonctions de gestion des ressources ici]

class Ball:
[fonctions de la balle (méthodes) ici]
[par exemple, une fonction qui calcule une nouvelle position]
[et une fonction pour vérifier si elle touche les bords]

def main:
[initier l'environnement du jeu ici]

[créer un nouvel objet, instance de la classe Ball]
ball = Ball()

while 1:
[Vérifier les entrées utilisateur]

[appel de la méthode update() de la balle]
ball.update()

Ce n'est bien sûr qu'un exemple très simple et vous aurez besoin d'y insérer tout le code nécessaire en lieu et place des commentaires entre crochets. Mais vous devez connaitre l'idée de base. Vous créez une classe, dans laquelle vous insérez toutes les fonctions d'une balle, en y incluant __init__(), qui créera tous les attributs d'une balle, et update(), qui déplacera la balle dans sa nouvelle position, avant de la copier à l'écran à cette position.

Vous avez la possibilité de créer d'autres classes pour tous les autres objets de jeu. Vous pourrez ensuite créer des instances pour chacun et ainsi les gérer facilement à partir de la fonction main et/ou de la boucle du programme principal. Contrastez ceci avec le fait d'initialiser la balle dans la fonction main et vous obtiendrez quantité de fonctions sans classes pour manipuler cet objet balle. Vous comprendrez heureusement pourquoi l'utilisation des classes est un avantage : cela vous permet de mettre tout le code de chaque objet à un seul endroit. Ceci rend l'utilisation des objets plus simple et l'ajout de nouveaux objets et leur manipulation plus flexible. Au lieu d'ajouter plus de code pour chaque nouvel objet balle, vous pouvez simplement créer de nouvelles instances pour chaque nouvel objet balle. Magique !

IV-A - Une simple classe balle

Voici une classe simple incluant les fonctions nécessaires pour la création d'un objet balle qui se déplacera sur l'écran si la fonction update() est appelée :

class Ball(pygame.sprite.Sprite):
"""Une balle qui se déplace sur lécran
Retourne: objet ball
Fonctions: update, calcNewPos
Attributs: area, vector"""

def __init__(self, vector):
pygame.sprite.Sprite.__init__(self)
self.image, self.rect = load_png('ball.png')
screen = pygame.display.get_surface()
self.area = screen.get_rect()
self.vector = vector

def update(self):
newPos = self.calcNewPos(self.rect,self.vector)
self.rect = newPos

def calcNewPos(self,rect,vector):
(angle,z) = vector
(dx,dy) = (z*math.cos(angle),z*math.sin(angle))
return rect.move(dx,dy)

Ici nous avons la classe Ball, avec une méthode __init__(), qui paramètre la balle, une méthode update() qui change le rectangle de la balle pour une nouvelle position, une méthode calcNewPos() pour calculer la nouvelle position de la balle basée sur sa position courante et le vecteur par lequel elle se déplace. J'expliquerai la gestion de la physique dans un moment. La seule autre chose à noter est le docstring, qui est un peu plus long cette fois et explique les bases de la classe. Ces chaines de caractères sont utiles non seulement pour vous-même et les autres programmeurs qui lisent votre code, mais aussi pour les outils qui analysent votre code et le documentent. Elles ne feront pas la différence dans de petits programmes, mais dans les gros, elles sont inestimables, c'est donc une bonne habitude à prendre.

IV-A-1 - Diversion 1 : Sprites

L'autre raison à la création d'une classe pour chaque objet est les sprites. Chaque image dont vous ferez le rendu dans votre jeu sera un objet sprite et pour commencer : la classe de chaque objet devra hériter de la classe Sprite. L'héritage de classe est une fonctionnalité géniale de Python. À partir de maintenant, la classe Ball possède toutes les méthodes de la classe Sprite, n'importe quelle instance d'objet de la classe Sprite sera enregistrée comme étant un sprite par Pygame. Tant que le texte et l'arrière-plan ne se déplacent pas, ça reste correct de copier l'objet sur l'arrière-plan. Pygame manipule les objets sprite d'une manière différente que vous verrez lorsque nous examinerons le code du programme en entier.

En résumé, vous créez un objet Ball et un objet Sprite pour cette balle, ensuite vous appelez la méthode update()sur l'objet Sprite, ce qui actualisera le sprite. Les sprites vous fournissent une manière sophistiquée de déterminer si deux objets sont en collision. Normalement vous pouvez simplement contrôler dans la boucle principale si leur rectangle se chevauchent, mais cela implique beaucoup de code qui sera inutile puisque la classe Sprite vous fournit spécialement les deux méthodes spritecollide() et groupcollide().

IV-A-2 - Diversion 2 : Physique des vecteurs

Les autres choses à connaitre à propos de ce code, en dehors de la structure de la classe Ball, ce sont les physiques de vecteurs utilisées pour calculer le mouvement de la balle. Dans n'importe quel jeu impliquant un mouvement angulaire, vous devez être à l'aise en trigonométrie, je ferai juste une petite introduction sur ce que vous devez savoir pour comprendre la méthode calcNewPos().

Pour commencer, vous avez remarqué que la balle possède un attribut vector, qui est construit à partir d'un angle et de z. L'angle est mesuré en radians et vous donne la direction dans laquelle se dirige la balle. z correspond à la vitesse à laquelle la balle se déplace. Ainsi en utilisant ce vecteur, nous pouvons déterminer la direction et la vitesse de la balle, et donc de combien elle doit se déplacer sur les axes X et Y.

Le diagramme ci-dessus illustre les bases mathématiques derrière les vecteurs. Dans la partie gauche du diagramme, vous pouvez voir le mouvement projeté de la balle, représenté par la ligne bleue. La longueur de cette ligne (z) représente sa vitesse et l'angle est la direction vers laquelle elle se déplace. L'angle 0 pour le mouvement de la balle sera toujours pris dans le sens positif de l'axe des X (vers la droite) et sera mesuré dans le sens des aiguilles d'une montre comme vu sur le diagramme.

À partir de l'angle et de la vitesse de la balle, nous pouvons maintenant définir de combien s'est déplacée la balle le long des axes X et Y. Nous en avons besoin car Pygame n'inclut pas les calculs de vecteurs et nous pouvons seulement déplacer la balle en bougeant son rectangle le long des deux axes. Nous avons donc besoin de faire correspondre l'angle et la vitesse en mouvement sur les axes X (dx) et Y (dy). C'est une simple question de géométrie et cela peut être obtenu grâce aux formules du diagramme.

Si vous avez étudié la trigonométrie élémentaire auparavant, rien ne sera nouveau pour vous. Mais au cas où vous l'auriez oubliée, voici quelques formules indispensables qui vous aideront à visualiser les angles (je trouve plus facile de visualiser les angles en degrés plutôt qu'en radians).

V - Objets contrôlés par l'utilisateur

Pour l'instant, vous avez créé une fenêtre Pygame et effectué un rendu d'une balle qui se déplace sur l'écran. La prochaine étape est de créer quelques raquettes qui seront contrôlées par l'utilisateur. C'est potentiellement plus simple que la balle, car ça ne requiert aucune physique. Toutefois ceci ne se vérifie plus lorsque votre objet possède des déplacements plus complexes que haut et bas, par exemple dans un jeu de plateforme comme Mario, auquel cas vous aurez besoin de mettre en jeu de la physique. Les objets contrôlables sont simples à mettre en œuvre, remerciez Pygame pour son système de file d'évènements, comme vous pourrez le voir.

V-A - Une simple classe Bat

Le principe derrière la classe Bat est similaire à la classe Ball. Vous avez besoin d'une méthode __init__() pour initialiser la raquette (vous pourrez donc créer des instances d'objet pour chaque raquette), d'une méthode update() pour appliquer les changements sur la raquette avant de la copier à l'écran et diverses autres méthodes qui définiront ce que fait cette classe. Voici un échantillon du code :

class Bat(pygame.sprite.Sprite):
"""Raquette de 'tennis' déplaçable qui peut frapper la balle
Retourne: objet bat
Méthode: reinit, update, moveup, movedown
Attributs: which, speed"""

def __init__(self, side):
pygame.sprite.Sprite.__init__(self)
self.image, self.rect = load_png('bat.png')
screen = pygame.display.get_surface()
self.area = screen.get_rect()
self.side = side
self.speed = 10
self.state = "still"
self.reinit()

def reinit(self):
self.state = "still"
self.movepos = [0,0]
if self.side == "left":
self.rect.midleft = self.area.midleft
elif self.side == "right":
self.rect.midright = self.area.midright

def update(self):
newpos = self.rect.move(self.movepos)
if self.area.contains(newpos):
self.rect = newpos
pygame.event.pump()

def moveup(self):
self.movepos[1] = self.movepos[1] - (self.speed)
self.state = "moveup"

def movedown(self):
self.movepos[1] = self.movepos[1] + (self.speed)
self.state = "movedown"

Comme vous pouvez le voir, cette classe est très similaire à la classe Ball dans sa structure. Mais les différences se situent dans ce que fait chaque fonction. Tout d'abord, il y a une méthode reinit() qui est utilisée lorsqu'un round est terminé : la raquette retourne dans sa position de départ et chacun de ses attributs à ses valeurs d'origine. Ensuite, la manière dont la raquette bouge est un peu plus complexe que la balle, étant donné que ses mouvements sont simples (haut/bas), mais dépendent de ce que désire l'utilisateur, tandis que la balle conservera son mouvement à chaque image. Pour mettre en évidence la façon dont la raquette bouge, il est pratique d'examiner ce petit diagramme pour voir la séquence des évènements :

Le joueur enfonce la touche

self.state = "moving"
self.moveup()

Le joueur relâche la touche

self.state = "still"
self.movepos = [0,0]

C'est ce qui se passe ici, si la personne qui contrôle la raquette enfonce la touche qui fait se déplacer la raquette vers le haut. À chaque itération de la boucle principale du jeu (à chaque image), si la touche est maintenue enfoncée, alors l'attribut state de cet objet raquette sera paramétré à « moving » et la méthode moveup() sera appelée, causant la réduction de la position Y de la raquette d'une valeur correspondant à l'attribut speed (dans cet exemple 10). En d'autres mots, tant que la touche reste enfoncée, la raquette se déplacera à l'écran de 10 pixels par image. L'attribut state n'est pas utilisé ici, mais c'est très utile de le connaitre si vous désirez appliquer des effets à la balle, ou si vous utilisez une sortie pour le débogage.

V-A-1 - Diversion 3 : Évènements Pygame

Alors, comment allons-nous savoir quand le joueur est en train d'enfoncer la touche, ou la relâche ? Avec le système de file d'évènements de Pygame, pardi ! C'est un système vraiment simple à utiliser et à comprendre, ça ne devrait pas être long. :) Vous avez déjà observé la file d'évènements en action dans le programme Pygame de base, où elle était utilisée pour vérifier si l'utilisateur voulait quitter l'application. Le code pour déplacer la raquette est aussi simple que ça :

for event in pygame.event.get():
if event.type == QUIT:
return
elif event.type == KEYDOWN:
if event.key == K_UP:
player.moveup()
if event.key == K_DOWN:
player.movedown()
elif event.type == KEYUP:
if event.key == K_UP or event.key == K_DOWN:
player.movepos = [0,0]
player.state = "still"

Ici nous supposons que vous avez déjà créé une instance de Bat et appelé l'objet player. Vous pouvez observer la couche familière de la structure for, qui produit une itération à chaque évènement trouvé dans la file d'évènements de Pygame, eux-même retrouvés grâce à la fonction event.get(). L'utilisateur enfonce une touche, appuie sur le bouton de la souris, ou bouge le joystick, toutes ces actions seront placées dans la file d'évènements de Pygame et conservées jusqu'à leur utilisation. Donc à chaque itération de la boucle de jeu principale, vous irez faire un tour dans ces évènements pour vérifier s'il y en a quelques-uns que vous pouvez utiliser. La fonction event.pump() qui était dans la méthode Bat.update() est appelée à chaque itération pour pomper les vieux évènements et garder la file à jour.

D'abord nous vérifions si l'utilisateur veut quitter le programme, si oui on quitte le programme. Ensuite nous vérifions si une touche est enfoncée, si oui, nous vérifions si elle correspond à une des touches affectées au déplacement de la raquette. Dans ce cas, nous appelons la méthode de déplacement appropriée et définissons l'état du joueur. À travers les états « moveup » et « movedown », modifiés par les méthodes moveup() et movedown(), nous produisons un code plus soigné et nous ne cassons pas l'encapsulation, ce qui signifie que vous assignez des attributs aux objets eux-mêmes, sans se référer au nom de l'instance de cet objet. Remarquez que nous avons trois états : « still », « moveup » et « mouvedown ». Encore une fois, ils deviennent utiles si vous voulez déboguer ou calculer un effet sur la balle. Nous vérifions aussi si une touche est « partie » (si elle n'est plus enfoncée) et si c'est la bonne touche, nous stoppons le déplacement de la raquette.

VI - Assembler le tout

Bien, vous avez appris toutes les bases nécessaires pour écrire un petit jeu. Vous devriez avoir compris comment créer un objet Pygame, comment Pygame affiche les objets, comment manipuler les évènements et comment vous pouvez utiliser la physique pour introduire des animations dans votre jeu. Maintenant je vais vous montrer comment vous pouvez prendre tous ces morceaux de code et les assembler dans un jeu qui fonctionne. Ce que nous avons besoin tout d'abord, c'est de faire rebondir la balle sur les bords de l'écran et que la raquette aussi soit capable de faire rebondir la balle. En d'autres termes, ce ne sera pas un jeu très compliqué. Pour ce faire, nous utiliserons les méthodes de collision de Pygame.

VI-A - Faire rebondir la balle sur les bords de l'écran

Le principe de base de ce type de rebond est simple à comprendre. Vous prenez les coordonnées des quatre coins de la balle et vous vérifiez s'ils correspondent avec les coordonnées X et Y des bords de l'écran. Donc si les coins haut gauche et haut droit ont leur coordonnée Y à 0, vous savez que la balle est actuellement contre le bord haut de l'écran. Nous ferons tout cela dans la fonction update(), après avoir défini la nouvelle position de la balle.

if not self.area.contains(newPos):
tl = not self.area.collidepoint(newPos.topleft)
tr = not self.area.collidepoint(newPos.topright)
bl = not self.area.collidepoint(newPos.bottomleft)
br = not self.area.collidepoint(newPos.bottomright)
if tr and tl or (br and bl):
angle = -angle
if tl and bl:
self.offcourt(player=2)
if tr and br:
self.offcourt(player=1)

self.vector = (angle,z)

Ici nous contrôlons que la variable area contienne la nouvelle position de la balle. Elle devrait toujours être vraie, nous n'aurons donc pas besoin de la clause else, bien que dans d'autres circonstances vous devriez considérer ce cas de figure. Nous contrôlons alors si les coordonnées des quatre coins entrent en collision avec les bords de l'écran et créons des objets pour chaque résultat. Si c'est vérifié, les objets auront leur valeur à 1, ou true. Sinon, la valeur sera None, ou false. Nous verrons alors si elle touche le dessus ou le dessous et si c'est le cas nous changerons la direction de la balle. En pratique, grâce à l'utilisation des radians, nous pourrons le faire facilement, juste en inversant sa valeur (positif/négatif). Nous contrôlons aussi que la balle ne traverse pas les bords, ou alors nous appellerons la fonction offcourt(). Ceci dans mon jeu, replace la balle, ajoute un point au score du joueur spécifié lors de l'appel de la fonction et affiche le nouveau score.

Enfin, nous réaffectons le vecteur basé sur le nouvel angle. Et voilà, la balle rebondira gaiement sur les murs et sortira du court avec un peu de chance.

VI-B - Faire rebondir la balle sur la raquette

Faire en sorte que la balle rebondisse sur les raquettes est similaire aux rebonds sur les bords de l'écran. Nous utilisons encore la méthode des collisions mais, cette fois, nous vérifions que les rectangles de la balle entrent en collision avec ceux des raquettes. Dans ce code, nous y ajoutons des suppléments pour éviter certains problèmes. Vous devriez trouver tout un tas de codes supplémentaires à ajouter pour éviter certains problèmes ou bogues, il est plus simple de les trouver lors de l'utilisation.

else:
# Réduire les rectangles pour ne pas frapper la balle derrière les raquettes
player1.rect.inflate(-3, -3)
player2.rect.inflate(-3, -3)

# Est-ce que la raquette et la balle entrent en collision ?
# Notez que je mets dans une règle à part qui définit self.hit à 1 quand ils entrent en collision
# et à 0 à la prochaine itération. C'est pour stopper un comportement étrange de la balle
# lorsqu'il trouve une collision *à l'intérieur* de la raquette, la balle s'inverse, et est
# toujours à l'intérieur de la raquette et donc rebondit à l'intérieur.
# De cette façon, la balle peut toujours s'échapper et rebondir correctement
if self.rect.colliderect(player1.rect) == 1 and not self.hit:
angle = math.pi - angle
self.hit = not self.hit
elif self.rect.colliderect(player2.rect) == 1 and not self.hit:
angle = math.pi - angle
self.hit = not self.hit
elif self.hit:
self.hit = not self.hit
self.vector = (angle,z)

Nous débutons cette section à partir de la condition else à cause de la partie précédente du code qui vérifie si la balle frappe les bords. Cela prend tout son sens si elle ne frappe pas les bords, elle pourrait frapper une raquette, donc nous poursuivons la condition précédente. Le premier problème est de réduire le rectangle des joueurs de trois pixels dans les deux dimensions pour empêcher la raquette de frapper la balle lorsqu'elle est derrière elle. Imaginez que vous venez juste de bouger la raquette et que la balle se trouvait derrière elle, les rectangles se chevauchent et la balle sera considérée comme « frappée », c'est pour prévenir ce petit bogue.

Ensuite nous vérifions si les rectangles entrent en collision, avec une correction de bogue de plus. Remarquez que j'ai commenté toute cette partie de code, c'est toujours préférable d'expliquer une partie du code qui parait obscur, que ce soit pour les autres qui regardent votre code, ou pour vous-même lorsque vous y reviendrez plus tard. Sans la correction du bogue, la balle pourra heurter un coin de la raquette, changer de direction et l'image d'après, trouver qu'elle est toujours à l'intérieur de la raquette. Alors le programme pensera que la balle est frappée une nouvelle fois et rechangera de direction. Cela peut survenir plusieurs fois, ce qui rendrait le comportement de la balle complètement irréaliste. Donc nous avons une variable, self.hit qui sera définie à true quand elle sera frappée et à false une image plus tard. Quand nous vérifions si les rectangles sont entrés en collision, nous contrôlons si self.hit est à true ou false pour stopper les rebonds internes.

L'imposant code ici est facile à comprendre. Tous les rectangles possèdent une fonction colliderect(), dans laquelle vous envoyez le rectangle d'un autre objet, qui retournera 1 (true) si les rectangles se chevauchent et 0 (false) dans le cas contraire. En cas de réponse positive, nous pouvons changer la direction en soustrayant l'angle actuel par (encore un petit truc que vous pouvez faire avec les radians, qui ajustera l'angle de 90 degrés et renverra la bonne direction. Vous pourrez trouver qu'à ce stade la compréhension approfondie des radians soit une exigence !). Pour terminer le contrôle du bogue, nous repassons self.hit à false à partir de l'image qui suit la frappe de la balle.

Nous réaffectons alors le vecteur. Vous aimeriez bien sûr enlever la même ligne dans la précédente partie de code, mais vous ne pourrez le faire qu'après le test conditionnel if-else. Et ce sera tout. Le code assemblé permettra maintenant à la balle de frapper les côtés et les raquettes.

VI-C - Le produit final

Le produit final, avec toutes les parties de code assemblées grâce à du code glu ressemblera à ceci : télécharger le code final de Tom's Pong.

Comme vous avez pu voir le produit fini, je vais vous rediriger vers Tom's Pong, jeu sur lequel est basé ce tutoriel. Téléchargez-le, étudiez le code source et vous verrez une implémentation de Pong utilisant tout le code que vous avez vu dans ce tutoriel, ainsi que d'autres ajouts faits dans des versions précédentes, comme des propriétés physiques pour les effets et ainsi que d'autres résolutions de bogues.

VII - Remerciements

Vous pouvez trouver Tom's Pong sur le site : http://www.pygame.org/docs/tut/tom/MakeGames.html (GPL v2)

Cette traduction est aussi disponible sur Wikibooks. La liste des contributeurs est disponible ici.