====== Snake sur Pygame ======
===== Présentation =====
{{ :nsi:tds:pygame:snake.png?direct&400 |}}
On se propose de réaliser un jeu de serpent sur Pygame. Le jeu du serpent a un principe simple : on contrôle un serpent sur une grille, on peut le faire aller en haut, en bas, à gauche et à droite.
* C'est la tête du serpent que l'on contrôle. Les anneaux du serpents passent toujours par les mêmes cases que les anneaux précédents.
* Si la tête du serpent percute un de ses anneaux, c'est perdu.
* Des récompenses apparaissent aléatoirement. Quand le serpent passe dessus, il grandit.
* Le serpent devient donc de plus en plus long et il est de plus en plus difficile de le déplacer sans qu'il se croque lui même.
**Pour les premières :** Pygame utilise la programmation orientée objet. On parle de classes et des objets. On peut s'en sortir sans comprendre tous les détails.
L'idée générale est la suivante : En Python, on dispose d'entités de formes diverses. On a des entiers, des tableaux... On a le droit de manipuler ces entités, elles ont des propriété. Par exemple on peut faire ''len'' sur un tableau et une chaîne mais pas sur un entier.
Une classe permet de définir une nouvelle entité et ses propriétés. La classe est donc une sorte de plan de construction et un manuel d'utilisation. Les entités crées selon se modèle sont appelées des **objets**. Quand on crée un objet, on dit qu'on l'**instancie**.
Dit autrement, par exemple, on peut parler du concept de pomme. Qu'est ce que c'est qu'une pomme ? à quoi on la reconnaît ? qu'est ce qu'on peut faire avec ? Toutes ces considérations abstraites, c'est la classe. Quand on a une vraie pomme, c'est une instance de pomme.
===== Les modules =====
Nous allons créer 2 modules et un principal. Un premier module définira le fonctionnement du serpent. Un second module définira les récompenses.
==== Les récompenses ====
C'est extrêmement simple et cela va permettre de présenter des choses.
# fichier apple.py
import pygame
class Apple(pygame.sprite.Sprite):
COLOR = (255,100,0)
SIZE = 20
def __init__(self, x, y):
super().__init__()
self.image = pygame.Surface((self.SIZE, self.SIZE))
self.image.fill(self.COLOR)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
Dans les versions classiques du Snake, le serpent mange des pommes, c'est pourquoi j'ai appelé la récompense ''Apple''.
Dans ce module bien sûr, on import ''pygame''.
== signature de la classe ==
class Apple(pygame.sprite.Sprite):
signifie que l'on crée un modèle ''Apple''. Ce modèle est censé être un objet graphique, un **sprite**. Gérer le graphique n'est jamais simple c'est pourquoi on utilise une bibliothèque comme Pygame.
''(pygame.sprite.Sprite)'' signifie que notre modèle reprend tout un tas de définitions déjà faites par Pygame. Notre ''Apple'' sera donc un sprite.
== constantes de classe ==
COLOR = (255,100,0)
SIZE = 20
Ces constantes nous permettent de définir l'apparence de la pomme. Pour nous, ce sera un simple carré de couleur. La couleur est définie en RGB, ici c'est du rouge. ''SIZE'' nous donne la taille en pixels. La pomme sera donc un carré rouge de 20 pixels de côté.
Comme ces constantes sont définies à l'intérieur de la classe, on peut les atteindre en écrivant ''Apple.COLOR'' par exemple, ou bien ''self.COLOR'' quand on parle depuis l'intérieur de la classe.
== initialisation ==
def __init__(self, x, y):
Cette fonction spéciale (en Python, les ''%%__%%'' indiquent toujours quelque chose de particulier) sert à la création de l'objet.
Quand on va créer un objet ''Apple'', Python va réserver l'espace nécessaire en mémoire puis va automatiquement appeler la fonction ''%%__init__%%'' pour l'initialiser.
Dans toutes les fonctions de la classe (on dit des méthodes), ''self'' représente la pomme elle même.
Dans ce cas, on veut créer la pomme quelque à la position x et y.
== super ==
super().__init__()
Comme notre ''Apple'' est un sprite Pygame, Pygame a déjà défini sa propre fonction d'initialisation. Cette ligne permet d'appeler l'initialisation Pygame de sorte que notre ''Apple'' ait bien toutes les caractéristiques nécessaires pour que Pygame en fasse un sprite valable.
Nous pourrons poursuivre avec nos initialisations.
== Le graphique ==
self.image = pygame.Surface((self.SIZE, self.SIZE))
self.image.fill(self.COLOR)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
Les sprites Pygame ont toujours un attribut ''image'' et un attribut ''rect''.
* ''image'' définit ce qui est affiché. Ici on crée une surface carrée que l'on remplit en couleur.
* ''rect'' représente le cadre où sera affiché cette image. Si nous voulions pouvoir déplacer le sprite, il faudrait jouer sur ''rect''.
==== Le main avec les récompenses ====
# fichier main.py
import pygame
from random import randrange
pygame.init() # initialisation de pygame
from apple import Apple
clock = pygame.time.Clock() # Pour contrôler la vitesse d'affichage
# création de la zone de dessin
WIDTH = 600
HEIGHT = 500
screen = pygame.display.set_mode((WIDTH, HEIGHT))
# c'est plus simple quand on met les sprites dans des groupes
apples = pygame.sprite.Group() # groupe contenant les pommes
all_sprites = pygame.sprite.Group() # groupe contenant tous les sprites
# ajout de 15 pommes
for i in range(15):
x = randrange(100, 500)
y = randrange(100, 400)
apple = Apple(x, y) # création de la pomme en x, y
apples.add(apple) # ajout de la pomme crée dans le groupe des pommes
all_sprites.add(apple) # ajout de la pomme au groupe de tous les sprites
# lancement d'une boucle assurant le rafraichissement du jeu
running = True
BLACK = (0,0,0) # couleur noire
while running:
clock.tick(60) # limite a 60 fps (frames per second)
# remplit le fond en noir pour créer la prochaine image
screen.fill(BLACK)
# La gestion des éléments du jeu aura lieu ici : déplacement du personnage
# apparition des bonbons, etc.
for event in pygame.event.get():
# parcours les événements qui se sont produit
if event.type == pygame.QUIT:
# c'est un événement de type fermeture de fenêtre
running = False # permet la fin de la boucle
# dessine les sprites
all_sprites.draw(screen)
# met à jour l'affichage
pygame.display.flip()
# boucle finie
pygame.quit() # lance la fermeture de la fenêtre
Le code est commenté, vous devriez comprendre.
Avec le module ''apple.py'', ce programme est déjà fonctionnel. Mais il n'affiche que des pommes fixes et pas de serpent...
==== La tête du serpent ====
On va se contenter pour l'instant d'un anneau de serpent que l'on pourra déplacer avec les flèches du clavier.
# fichier snake.py
import pygame
class Head(pygame.sprite.Sprite):
COLOR = (0, 255, 0)
SIZE = 20
VELOCITY = 2 # vitesse du serpent
def __init__(self, x, y):
super().__init__()
self.image = pygame.Surface((self.SIZE, self.SIZE))
self.image.fill(self.COLOR)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
self.vx = 0
self.vy = 0
def move_up(self):
# amorce un mouvement vers le haut
# attention, sur l'écran, aller vers le haut, c'est diminuer les y !
if self.vy > 0:
# interdit d'aller dans le sens opposé -> sort de la fonction sans rien faire
return
self.vy = - self.VELOCITY
self.vx = 0
def move_down(self):
# amorce un mouvement vers le haut
# attention, sur l'écran, aller vers le bas, c'est augmenter les y !
if self.vy < 0:
# interdit d'aller dans le sens opposé -> sort de la fonction sans rien faire
return
self.vy = self.VELOCITY
self.vx = 0
def move_left(self):
if self.vx > 0:
# interdit d'aller dans le sens opposé -> sort de la fonction sans rien faire
return
self.vx = - self.VELOCITY
self.vy = 0
def move_right(self):
if self.vx < 0:
# interdit d'aller dans le sens opposé -> sort de la fonction sans rien faire
return
self.vx = self.VELOCITY
self.vy = 0
def update_position(self):
# modifie les coordonnees selon la vitesse en cours
self.rect.x += self.vx
self.rect.y += self.vy
==== Le main avec la tête de serpent ====
==== Le main avec les récompenses ====
# fichier main.py
import pygame
from random import randrange
pygame.init() # initialisation de pygame
from apple import Apple
from snake import Head
clock = pygame.time.Clock() # Pour contrôler la vitesse d'affichage
# création de la zone de dessin
WIDTH = 600
HEIGHT = 500
screen = pygame.display.set_mode((WIDTH, HEIGHT))
# c'est plus simple quand on met les sprites dans des groupes
apples = pygame.sprite.Group() # groupe contenant les pommes
all_prites = pygame.sprite.Group() # groupe contenant tous les sprites
# ajout de 15 pommes
for i in range(15):
x = randrange(100, 500)
y = randrange(100, 400)
apple = Apple(x, y) # création de la pomme en x, y
apples.add(apple) # ajout de la pomme crée dans le groupe des pommes
all_sprites.add(apple) # ajout de la pomme au groupe de tous les sprites
# création de la tête
head = Head(300,200)
all_sprites.add(head)
# lancement d'une boucle assurant le rafraichissement du jeu
running = True
BLACK = (0,0,0) # couleur noire
while running:
clock.tick(60) # limite a 60 fps (frames per second)
# remplit le fond en noir pour créer la prochaine image
screen.fill(BLACK)
# La gestion des éléments du jeu aura lieu ici : déplacement du personnage
# apparition des bonbons, etc.
for event in pygame.event.get():
# parcours les événements qui se sont produit
if event.type == pygame.QUIT:
# c'est un événement de type fermeture de fenêtre
running = False # permet la fin de la boucle
# récupère la liste des états des touches
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] and not keys[pygame.K_RIGHT]:
# la touche flèche gauche est appuyée mais pas la flèche droite
head.move_left()
elif keys[pygame.K_RIGHT] and not keys[pygame.K_LEFT]:
# la touche flèche droite est appuyée
head.move_right()
if keys[pygame.K_UP] and not keys[pygame.K_DOWN]:
# la touche flèche haut est appuyée mais pas la flèche bas
head.move_up()
elif keys[pygame.K_DOWN] and not keys[pygame.K_UP]:
# la touche flèche bas est appuyée mais pas la flèche haut
head.move_down()
# met à jour la tête
head.update_position()
# la tête touche-t-elle une pomme ?
# l'argument True indique que les pommes touchées sont détruites
hits = pygame.sprite.spritecollide(head, apples, True)
n = len(hits)
# pour l'instant on ne fait rien de n, on se contente de l'afficher en console
if n > 0:
print("mangé !")
# dessine les pommes
all_sprites.draw(screen)
# met à jour l'affichage
pygame.display.flip()
# boucle finie
pygame.quit() # lance la fermeture de la fenêtre
===== Il reste à faire... =====
* ne faire apparaître les pommes qu'alignés sur une grille,
* adapter le mouvement pour obéir à une logique de grille,
* prévoir les anneaux du serpent et les faire se déplacer convenablement,
* augmenter la taille du serpent à mesure qu'il mange,
* faire perdre quand le serpent touche un bord ou quand il se croque lui-même,
* afficher un score
== Aligner sur une grille ==
Les pommes on un taille ''Apple.SIZE = 20''. On voudrait qu'elle ne puissent avoir que des coordonnées en 0, 20, 40, 60, ... de sorte qu'elles ne tombent pas sur n'importe quelles coordonnées mais sur une grille.
C'est rapide à faire : si ''x'' est un entier quelconque et qu'on veut le ramener à un multiple de ''Apple.SIZE'', il suffit de faire le calcul ''x -= x % Apple.SIZE'' (ou ''self.SIZE'' si on fait ce calcul depuis l'intérieur de la classe).
De la même façon, on désire que le serpent ne puisse tourner que quand il est aligné sur une grille. C'est simple également : il suffit n'autoriser le changement de sens que quand les coordonnées sont multiples de ''SIZE''.
self.rect.x % self.SIZE == 0 and self.rect.y % self.SIZE == 0
plusieurs approche sont possibles :
* ne permettre le changement que quand la tête est alignée. Autrement dit une pression sur un bouton est ignorée si elle n'est pas faite au moment de l'alignement,
* enregistrer le dernier bouton appuyé mais n'en tenir compte qu'au prochain alignement.
== Créer un anneau ==
On peut créer la classe ''Anneau'' dans le module ''snake.py''. L'anneau sera très proche de ''Head''. Voici ce que l'on pourrait faire :
* définir ''class Anneau(Head):'' de sorte que l'anneau hérite de toutes les fonctions de ''Head''.
* modifier la fonction ''__init__'' : quand on crée un anneau, on le superpose avec le dernier anneau du serpent. Cette anneau sera l'anneau précédent. L'anneau créé devra suivre l'anneau précédent d'une certaine façon et devra donc toujours se rappeler son anneau précédent. Un anneau est donc comme une tête qui a un anneau précédent...\\
def __init__(self, precedent):
super().__init__(precedent.rect.x, precedent.rect.y)
self.precedent = precedent
* enfin modifier la fonction ''update_position'' de façon à, si l'anneau est aligné et si le précédent est à au moins une distance ''SIZE'' d'ajuster la vitesse.\\
def update_position(self):
# code disant : si aligné
# si précédent assez loin
# ajuster la vitesse
super().update_position() # pour le reste, faire comme avec Head