La structure d'un jeu Pygame
Introduction
Le chapitre précédent nous a permis de prendre un premier exemple succinct basé sur Pygame. Celui-ci nous a permis d’introduire concrètement la notion de boucle de jeu, que nous allons approfondir dans le présent chapitre.
Ici, nous étudierons en premier lieu la structure habituelle d’un jeu vidéo Pygame. Ainsi, nous verrons comment procéder à l’initialisation du jeu, à la définition de l’écran et donc de l’espace dédié au jeu. Nous verrons également comment procéder à la mise en place d’une boucle de jeu et comment gérer le temps. Nous expliquerons enfin comment le processus de rafraîchissement est géré dans Pygame. Nous en profiterons pour expliquer la gestion des couleurs en code RGB (Red-Green-Blue) ainsi que le système de coordonnées que l’on utilise. On entend par système de coordonnées le système qui nous permet d’indentifier l’emplacement précis d’un objet au sein de la fenêtre de jeu.
Le but est donc ici d’acquérir toutes les bases nécessaires pour appréhender correctement le chapitre Le dessin et le graphisme dans tous leurs états avec Pygame.
Initialisation
Comme n’importe quel module ou bibliothèque que l’on utilise dans un programme Python, il faut importer Pygame :
import pygame
En tout début de programme, il s’agit d’initialiser Pygame en écrivant la ligne suivante :
pygame.init()
Aide en ligne de commande
Bien sûr, à chaque utilisation d’une fonction Pygame, on peut essayer d’en savoir plus sur la syntaxe, les paramètres attendus, en consultant l’aide en ligne.
Par exemple, la documentation relative à pygame.init() est disponible à cette adresse : https://www.pygame.org/docs/ref/pygame.html#pygame.init
Mais on peut également obtenir des informations directement dans le terminal Python, en utilisant la commande help. Par exemple, si l’on tape ceci :
help("pygame.init")
On obtient cette courte description de la fonction et de son utilisation.
pygame.init = init(...)
init() -> (numpass, numfail)
initialize all imported pygame modules
Affichage de la fenêtre
On pense immédiatement à la gestion du temps lorsque l’on réfléchit à la conception d’un jeu vidéo. Pourtant, la gestion de l’espace est tout aussi importante.
En effet, il faut réfléchir au périmètre physique du jeu et donc très rapidement définir la taille de la fenêtre de jeu. Ainsi, on définit grâce à la fonction set_mode une fenêtre de 600 pixels de largeur sur 400 pixels de hauteur. On se propose également d’inscrire un titre dans la fenêtre grâce à la fonction set_caption.
FENETRE = pygame.display.set_mode((600, 400))
pygame.display.set_caption("Ma fenêtre")
À noter que l’écriture ((600, 400)) signifie que l’on passe un tuple en paramètre. Nous reviendrons dans ce qui suit sur cette notion de tuple.
Que peut faire de plus set_mode ? Il suffit de consulter l’aide pour le savoir. Tapez donc cette instruction en ligne de commande :
help("pygame.display.set_mode")
Et vous obtenez les informations suivantes :
pygame.display.set_mode = set_mode(...)
set_mode(size=(0, 0), flags=0, depth=0, display=0) -> Surface
Initialize a window or screen for display
On voit qu’outre le tuple size que l’on utilise déjà, on a d’autres paramètres, tel flags, qui s’avère particulièrement utile.
1. Le tuple size de set_mode
Un tuple en langage Python peut être vu comme une liste (voir chapitre Notions avancées en langage Python), mais avec la spécificité que cette collection n’est pas modifiable.
Prenons un exemple de création d’un tuple pour illustrer la syntaxe à utiliser.
tuple = ('A', 'B', 'C', 'D')...
Rappels concernant la boucle de jeu
Une boucle de jeu (game loop) est une boucle infinie qui s’interrompra grâce à l’atteinte de certains critères. La notion est en quelque sorte associée à une sorte d’horloge interne du jeu. Ainsi, à chaque itération de la boucle de jeu, on peut déplacer un personnage, ou tenir compte que tel objet a atteint tel autre, ou que la ligne d’arrivée a été franchie et donc que le jeu est terminé. C’est donc l’occasion, à chaque itération, d’actualiser toutes les données relatives à l’état courant de la partie. Schématiquement, à chaque itération, on effectue les tâches suivantes :
1. Vérifier que les conditions d’arrêt ne sont pas atteintes, auquel cas, interrompre la boucle.
2. Mettre à jour les ressources nécessaires pour l’itération courante.
3. Obtenir les entrées soit issues du système, soit issues de l’interaction avec le joueur.
4. Mettre à jour l’ensemble des entités qui caractérisent le jeu.
5. Rafraîchir l’écran.
Les surfaces Pygame
1. Définition d’une surface
En Pygame, la notion de surface est fondamentale, car la manipulation de cet élément de géométrie est un aspect important et conséquent du développement du jeu vidéo. Une surface correspond à une ligne affichée sur l’écran ou à un polygone affiché sur l’écran ; ce polygone peut être rempli de couleur ou non. Il n’y a guère de limites quant aux dimensions d’une surface Pygame, ni tellement de limites quant au nombre de surfaces qui peuvent être manipulées dans le jeu. On comprend ainsi qu’une part importante de la gestion graphique consistera donc à créer et à manipuler les surfaces Pygame.
En Pygame, la surface correspond donc à l’affichage d’un polygone coloré ou d’une ligne brisée ou d’une image (comme dans l’exemple Fusée et planètes du chapitre Concepts du jeu vidéo et premiers pas à propos de Pygame), ou encore à l’affichage de texte en surimpression. Ces quelques exemples sont autant d’implémentations de surfaces Pygame.
Une surface se crée de différentes manières, selon le type de surfaces que l’on désire afficher :
-
image.load() quand il s’agit d’une image.
-
font.render() quand il s’agit d’afficher du texte.
-
pygame.Surface() pour une surface qui n’a rien de spécial à la création.
-
pygame.display.set_mode() pour la fenêtre de jeu, qui est également une surface (un peu particulière).
La page de documentation en ligne de l’objet Surface, située à l’adresse https://www.pygame.org/docs/ref/surface.html, propose un grand nombre de fonctions qui seront pour certaines détaillées...
Gestion des couleurs
La gestion des couleurs est une problématique relative aux surfaces dont on cherche parfois à colorer l’intérieur, mais est propre au développement Pygame en général et par extension au développement Python.
Si nous reprenons ce fragment de code :
bleu = (0, 0, 255)
bleu_surface.fill(bleu)
Nous voyons qu’une couleur est définie par un triplet de trois grandeurs numériques. Chacune de ces grandeurs est un entier compris entre 0 et 255, ce qui fait 256 valeurs possibles.
Nous avons au passage 2563 (256 exposant 3) possibilités, soit donc 16 777 216 variantes de couleurs possibles. C’est beaucoup plus que ce que peut distinguer l’œil humain.
Ce système de codification se nomme codage RGB (Red Green Blue, rouge - vert - bleu), chaque composante du triplet étant en effet une déclinaison respective du rouge, du vert et du bleu.
Ainsi :
(0, 0, 0) correspond au noir.
(255, 255, 255) correspond au blanc.
(255, 0, 0) correspond au rouge primaire.
(0, 255, 0) correspond au vert primaire.
(0, 0, 255) correspond au bleu primaire.
De nombreuses applications en ligne permettent de choisir le triplet correspondant à la couleur désirée. Par exemple, celle-ci : https://www.rapidtables.com/web/color/RGB_Color.html.
Une documentation du module Color de Pygame est disponible dans le chapitre Les principaux modules Pygame.
À noter qu’au moment de l’écriture de ce livre, le développement Pygame sur macOS présente parfois le dysfonctionnement suivant : un affichage inopérant des couleurs dans certaines situations. Le lien suivant aborde le sujet et suggère diverses pistes de contournement : https://github.com/pygame/pygame/issues/555
Système de coordonnées
Nous considérons un repère orthonormé dont l’origine (0,0) est le point supérieur gauche de la fenêtre de jeu. L’axe horizontal x est l’axe orienté de gauche à droite ; l’axe vertical y est, lui, orienté du haut vers le bas.
Modifions un peu le code précédent de manière à faire apparaître une nouvelle surface rectangulaire, rouge cette fois, qui permet de préciser les différentes coordonnées mises en jeu.
import pygame
bleu = (0, 0, 255)
rouge = (255, 0, 0)
pygame.init()
pygame.display.set_caption(u'Surface')
fenetre = pygame.display.set_mode((400, 400))
bleu_surface = pygame.Surface((400, 400))
bleu_surface.fill(bleu)
rouge_surface = pygame.Surface((120, 240))
rouge_surface.fill(rouge)
fenetre.blit(bleu_surface, (0, 0))
fenetre.blit(rouge_surface, (50, 100))
pygame.display.flip()
while True:
event = pygame.event.wait()
if event.type == pygame.QUIT:
break
pygame.quit()
Nous ne changeons rien à l’affichage de la fenêtre, dont le fond est toujours bleu. Par contre, on ajoute un rectangle rouge au sein de cette fenêtre.
Le positionnement des points supérieurs gauches des rectangles se fait grâce à la fonction blit. Le rafraîchissement de la fenêtre et donc l’affichage actualisé des surfaces se réalisent grâce à la fonction flip.
Ci-après, la copie d’écran de la fenêtre de jeu.
Fenêtre de jeu
Si nous cherchons à représenter les diverses coordonnées mises en jeu, nous obtenons le schéma suivant qui reprend les diverses grandeurs...
Gestion du temps et des événements
1. Gestion du temps dans Pygame
Le rapport au temps est ce que l’on appelle ici la gestion du temps. Cela revêt une grande importance dans le développement de jeux vidéo et donc évidemment en Pygame.
Une documentation du module time de Pygame est disponible dans le chapitre Les principaux modules Pygame.
Ce module offre plusieurs fonctions qui permettent de chronométrer la session en cours (depuis le init) ou de faire une pause dans l’exécution, par exemple. On utilise pour cela les deux fonctions suivantes :
-
pygame.time.get_ticks
-
pygame.time.wait ou pygame.time.delay
Le module pygame.time inclut également un objet Clock qui permet d’aller plus loin dans la gestion du temps. La documentation officielle en ligne de cet objet Clock est ici : https://www.pygame.org/docs/ref/time.html#pygame.time.Clock
On remarque au passage que, en termes de typographie, le nommage des fonctions en Pygame commence par une minuscule. Le nommage des objets (comme Surface ou Clock) commence lui par une majuscule. Un bon moyen de savoir en un coup d’œil à quoi on a affaire.
La fonction tick de Clock (pygame.time.Clock.tick) permet de mettre à jour l’horloge relative au jeu en cours. Appelée à chaque mise à jour de l’affichage du jeu, tick permet d’indiquer le nombre maximal d’images affichées par seconde et ainsi de limiter et de contrôler la vitesse d’exécution du jeu.
Ainsi, la ligne suivante insérée dans une boucle de jeu garantit de ne jamais aller plus « vite » que cinquante images par seconde.
Clock.tick(50)
Quelle que soit la plateforme sur laquelle vous programmez, la vitesse d’affichage sera forcément supérieure à trente images par seconde, ce qui constitue un minimum....
Les codes globaux des deux exemples
1. Premier exemple
Le code du premier exemple (FIL.py), qui correspond à de l’affichage utilisant le système de coordonnées, est le suivant.
import pygame
bleu = (0, 0, 255)
rouge = (255, 0, 0)
pygame.init()
pygame.display.set_caption(u'Surface')
fenetre = pygame.display.set_mode((400, 400))
bleu_surface = pygame.Surface((400, 400))
bleu_surface.fill(bleu)
rouge_surface = pygame.Surface((120, 240))
rouge_surface.fill(rouge)
fenetre.blit(bleu_surface, (0, 0))
fenetre.blit(rouge_surface, (50, 100))
pygame.display.flip()
while True:
event = pygame.event.wait()
if event.type == pygame.QUIT:
break
pygame.quit()
2. Deuxième exemple
Ci-dessous, le code global du « carré qui rebondit » :
import sys
import pygame
rouge = 255, 0, 0
bleu = 0, 0, 255
*
pygame.init()
fenetre = pygame.display.set_mode((400, 400))
pygame.display.set_caption("Le carré qui rebondit")
clock = pygame.time.Clock()
XX = 300
DEPLACEMENT = 3
while 1:
clock.tick(50)
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
fenetre.fill(bleu)
XX += DEPLACEMENT
if XX >= 320:
XX = 320
DEPLACEMENT = -3
elif XX <= 0:
XX = 0
DEPLACEMENT = 3
pygame.draw.rect(fenetre, rouge...