1. Introduction

Un des aspects les plus connus de la programmation et auquel on pense en premier à ce sujet est le développement de jeux. Depuis petite je joue à des jeux vidéo sur diverses consoles et sites de jeux en ligne et, probablement comme beaucoup de joueurs, ai été attirée par l'idée de coder mon propre jeu. Aujourd'hui c'est chose faite. J'ai effectivement choisi pour mon projet printanier dans le cadre du cours d'OC informatique de programmer un jeu de platformes simple en python. Celui-ci consiste basiqument à avoir un joueur qui se déplace vers la droite en sautant sur des plateformes générées aléatoirement. Le but du jeu est d'aller le plus loin possible sans tomber.

2. Matériel et méthodes

2.1 Matériel

La création de mon jeu n'a nécessité aucun hardware.

Mon jeu a été programmé en Python 3 avec la libraire libre Pygame [1] qui facilite le développement d'applications multimédias telles que des jeux.

Bien que nano sur le terminal aurait pu faire l'affaire, j'ai choisi d'utiliser l'éditeur de texte libre Atom [2] qui m'a permis de gérer le projet dans son ensemble en tant que dossier et non fichier par fichier. Le code de couleur associé au langage de programmation rend aussi le travail plus agréable.

2.2 Méthode

Pour la méthode, je me suis beaucoup aidée du travail de KidsCanCode, en particulier la playlist de sa chaîne youtube Pygame Tutorial #2: Platformer [3] [4].

Pour réaliser mon projet, j'ai d'abord commencé par créer un squelette du jeu et c'est à partir de celui-ci que j'ai développé mon jeu étape par étape. Je vais maintenant détailler ces étapes.

2.2.1 Modèle

La première chose à faire est de créer différents fichiers pour séparer en plusieurs parties notre code. Cela permet de rester organisé et de ne pas se perdre dans un seul fichier trop long. J'ai créer 3 fichiers:

  • main.py : C'est le corps de mon projet, il contient la boucle du jeu.
  • sprites.py : C'est la partie du programme où les différents éléments graphiques du jeu sont initialisés.
  • settings.py : Ici sont paramétrer différentes variables utilisées, cela permet de pouvoir changer radicalement l'aspect du jeu rapidement et facilement.
2.2.1.1 Quelques paramètres

Pour commencer, nous mettons quelques paramètres dont nous aurons rapidement besoin dans le fichier settings.py. Cela comprend les dimensions de la fenêtre de jeu (en pixels), la vitesse à laquelle le boucle du jeu s'effectuera (FPS = frame per second), le titre du jeu, la police utilisée et quelques couleurs:

LONGUEUR = 1200
HAUTEUR = 600
FPS = 30 # vitesse à laquelle la boucle du jeu s'effectue
TITRE = "Platformer by Elsa"
POLICE = 'arial'
BLANC = (255, 255, 255)
NOIR = (0, 0, 0)
ROUGE = (255, 0, 0)
FOND = (121, 248, 248)
ROSE = (253, 108, 158)

Les couleurs sont exprimées par un mélange de rouge, vert et bleu (RVB) avec des valeurs allant de 0 à 255.

2.2.1.2 Jeu

Dans le fichier main.py, nous allons créer le corps du jeu. Avant toutes choses, il nous faut importer pygame. Nous aurons aussi besoin de random et os, ainsi que du contenu des fichiers settings.py et sprites.py.

import pygame as pg, random, os
from settings import *
from sprites import *

Nous pouvons maintenant créer la classe Jeu où nous définirons les diverses méthodes (ou fonctions) qui définissent l'objet Jeu.

class Jeu:

La première méthode est l'initialisation du jeu:

def init(self):
            # initialise la fenêtre de jeu
            pg.init()
            pg.mixer.init()
            # création de la fenêtre
            self.screen = pg.display.set_mode
            pg.display.set_caption(TITRE) # nom affiché en haut de la page
            self.horloge = pg.time.Clock() # relève du temps
            self.running = True
            # trouve une police proche de celle choisie
            self.police = pg.font.match_font(POLICE)

Ensuite vient la fonction qui lance un nouveau jeu:

def new(self):
        # commence une nouvelle partie
        self.all_sprites = pg.sprite.Group()
self.run()

Ici nous avons déjà créer un groupe dans lequel nous pourrons regrouper tous nous éléments graphiques.

Puis, nous créons la boucle du jeu:

def run(self):
            # boucle du jeu
            self.playing = True
            while self.playing:
                # contrôle que la boucle s'effectue à la bonne vitesse
                self.horloge.tick(FPS) 
                self.events()
                self.update()
                self.affichage()

Tant que self.playing = True, la boucle effectuera les sections events, update et affichage qui seront définies plus bas. La commande self.horloge.tick(FPS) contrôle que la boucle fasse toujours le même temps; si elle s'est effectuée plus rapidement que prévu, le programme attendra avant de refaire une boucle, si la boucla a dépassé le temps prévu, le programme commencera à laguer.

Ensuite vient la fonction update qui effectue dans changement dans le jeu selon certaines conditions (nous détaillerons celles-ci plus bas):

def update(self):
            # boucle du jeu - update
            self.all_sprites.update()

Suit la fonction events qui suit les différents événements (par exemple un clique de la souris ou une touche du clavier enfoncée):

def events(self):
           # boucle du jeu - événements
            for event in pg.event.get():
                if event.type == pg.QUIT: # si la fenêtre est fermée
                    if self.playing:
                        self.playing = False
                    self.running = False

Ici l'événement où l'utilisateur clique sur la croix en haut à gauche de la fenêtre est détaillé. Il arrête le jeu et quitte le programme.

Finalement, nous affichons tous les éléments instantanément:

def affichage(self):
            # boucle du jeu - affichage
            self.screen.fill(FOND)
            self.all_sprites.draw(self.screen)
            # double buffering: après avoir tout dessiné, fait apparaître tout instantanément
            pg.display.flip()

En dehors de Jeu, nous créons une simple boucle qui est l'essence de notre programme:

j = Jeu()
while j.running:
    j.new()

pg.quit()

Nous avons désormais un patron utilisable pour différents types de jeux, nous pouvons nous attaquer aux choses plus concrètes et uniques afin de donner une véritable identité à notre jeu.

2.2.2 Platformer

Avant de pouvoir créer les différents éléments graphiques de notre jeu, nous devons préparer notre fichier sprites.py:

import pygame as pg, os, random
from settings import *

vec = pg.math.Vector2

game_folder = os.path.dirname(file)
img_folder = os.path.join(game_folder, "img")

Pour décrire les mouvements du joueur nous utiliserons des vecteurs (vec). Les 2 dernières lignes de commande permettent de décrire où se trouvent les différentes images que nous utiliserons.

Les éléments graphiques utilisés (plateeforme et personnage) sont le travail de Kenney [5].

2.2.2.1 Platformes

Nous créons dans sprites.py la classe Plateforme:

class Platforme(pg.sprite.Sprite):
    def init(self, x, y):

Les platformes sont définies par leur position sur les axes x et y.

        pg.sprite.Sprite.init(self)
        self.image = pg.image.load(os.path.join(img_folder, "grass.png")).convert()

L'image des platformes est téléchargée.

        self.image.set_colorkey(NOIR)

La couleur noire est rendue transparente, cela fait disparaître le rectangle noir autour des plateformes.

        self.image = pg.transform.scale(self.image, (random.randrange(60, 300), 40))

Les images sont redimensionnées. Pour avoir des plateformes de différentes longueurs, on utilise la fonction randrange qui choisit une valeur aléatoirement entre 60 et 300 pixels. La hauteur est fixée à 40 pixels.

        self.rect = self.image.get_rect()

L'image est rendue rectangulaire.

        self.rect.x = x
        self.rect.y = y

Dans settings.py, nous créons une liste de plateformes qui prennent chacunes 2 paramètres: leur longueur et leur hauteur. On entre manuellement 6 plateformes qui seront sur l'écran de base:

PLATFORMES_LISTE = [(0, HAUTEUR - 30),
                    (200,  500),
                    (LONGUEUR / 2, HAUTEUR * 3 / 4),
                    (400, 420),
                    (1000, 470),
                    (800, 390)]

Dans main.py, dans la fonction new de Jeu, nous créons un groupe plateformes:

self.platformes = pg.sprite.Group()

Nous ajoutons chaque élément de la liste de platformes dans les groupes all_sprites et plateformes:

for plat in PLATFORMES_LISTE:
                p = Platforme(*plat) # * tout les éléments de la liste
                self.all_sprites.add(p)
                self.platformes.add(p)

Toujours dans main.py, dans la fonction update du Jeu, nous faisons apparaître des plateformes aléatoirement de manière à en avoir toujours 10:

while len(self.platformes) < 10:
                p = Platforme(random.randrange(1200, 1850), random.randrange(HAUTEUR / 2, HAUTEUR - 30))   
                self.platformes.add(p)
                self.all_sprites.add(p)

Pour chaque nouvel élément graphique, nous l'ajoutons aux groupes all_sprites et plateformes.

2.2.2.2 Joueur

Nous devons en premier rajouter quelques propriétés du joueur dans settings.py:

JOUEUR_ACC = 0.8
JOUEUR_FRICTION = -0.1
JOUEUR_GRAVITE = 1
JOUEUR_SAUT = -40

Nous créons la classe Joueur avec ses propirétés: class Joueur(pg.sprite.Sprite):

    def init(self, jeu): # jeu donne comme une copie de Jeu
        pg.sprite.Sprite.init(self)
        self.jeu = jeu
        self.image = pg.image.load(os.path.join(img_folder, "player_idle.png")).convert()
        self.image.set_colorkey(NOIR) 
        self.rect = self.image.get_rect()
        self.rect.center = (LONGUEUR / 2, HAUTEUR / 2)
        self.pos = vec(40, HAUTEUR - 100)
        self.vel = vec(0, 0)
        self.acc = vec(0, 0

Sa fonction pour sauter:

def saute(self):
        # selement si sur une plateforme
        self.rect.x += 2 # regarde 1 pixel sous le Joueur
        collision = pg.sprite.spritecollide(self, self.jeu.platformes, False)
        self.rect.x -= 2 # puis remonte
        if collision:
            self.vel.y = JOUEUR_SAUT

Et sa méthode de mouvements:

def update(self):
        self.acc = vec(0, JOUEUR_GRAVITE) #gravité
        keys = pg.key.get_pressed()
        if keys[pg.K_LEFT]:
            self.acc.x = -JOUEUR_ACC
        if keys[pg.K_RIGHT]:
            self.acc.x = JOUEUR_ACC

        # frottement
        self.acc += self.vel * JOUEUR_FRICTION
        # équations du mouvement
        self.vel += self.acc
        self.pos += self.vel + (0.5 * self.acc)

        # la position est définie par le milieu du bas du rectangle du joueur
        self.rect.midbottom = self.pos

Maintenant, nous ajoutons le Joueur dans le groupe all_sprites dans fonction new de Jeu:

self.joueur = Joueur(self) 
            self.all_sprites.add(self.joueur)

Dans la section update de Jeu, nous gérons les collisions entre le Joueur et les Platformes:

# quand le joueur touche une platforme en tombant
            if self.joueur.vel.y > 0:
                collision = pg.sprite.spritecollide(self.joueur, self.platformes, False)
                if collision:
                    # seulement si le bas du joueur dépasse le haut de la platforme et ne dépasse pas sur les côtés
                    if self.joueur.pos.y < collision[0].rect.bottom \
                    and self.joueur.pos.x < collision[0].rect.right + 5 \
                    and self.joueur.pos.x > collision[0].rect.left - 5:
                        self.joueur.pos.y = collision[0].rect.top
                        self.joueur.vel.y = 0

Le Joueur perd s'il tombe:

# Game Over
            if self.joueur.rect.top > HAUTEUR:
                self.playing = False

Dans la section events de Jeu, nous définissons que le Joueur saute lorsque la barre espace est enfoncée:

if event.type == pg.KEYDOWN:
                    if event.key == pg.K_SPACE:
                        self.joueur.saute()

Finalement, dans la fonction affichage de Jeu, nous mettons le sprite de Joueur en avant plan:

self.screen.blit(self.joueur.image, self.joueur.rect)
2.2.2.3 Défilement de l'écran

Nous définissons dans la fonction update de Jeu que, lorsque le Joueur est aux 3/4 de la longueur de la fénètre, l'écran avec les platformes commencent à défiler vers la gauche à la vitesse du Joueur:

# quand le joueur est à 3/4 de l'écran -> l'écran se "déroulle"
            if self.joueur.rect.right >= 3 * LONGUEUR / 4:
                self.joueur.pos.x -= self.joueur.vel.x
                for plat in self.platformes:
                    plat.rect.x -= self.joueur.vel.x
                    if plat.rect.right <= 0:
                        plat.kill()

Quand une platforme disparìt de l'écran, elle est supprimée.

2.2.2.4 Score

Dans la fonction new de Jeu, nous initialisons le score:

self.score = 0

Dans la fonction update, lorsque nous supprimons une platforme, nous incrémentons le score de 10 points:

plat.kill()
self.score += 10
2.2.2.5 Start screen et game over screen

Nous allons faire apparître un écran start au début et un écran game over à la fin d'une partie, pour cela nous devons d'abord créer une fonction qui attend que l'on presse une touche pour évitier que ces écrans apparaissent une miliseconde puis disparaissent:

def wait_for_key(self):
            waiting = True
            while waiting:
                self.horloge.tick(FPS)
                for event in pg.event.get():
                    if event.type == pg.QUIT:
                        waiting = False
                        self.running = False
                    if event.type == pg.KEYUP:
                        waiting = False

Nous créons également une fonction qui permet d'afficher du text:

    def affichage_text(self, text, taille, couleur, x, y):
            police = pg.font.Font(self.police, taille)
            text_surface = police.render(text, True, couleur)
            text_rect = text_surface.get_rect()
            text_rect.midtop = (x, y)
            self.screen.blit(text_surface, text_rect)

Maintenant, nous créons la fonction start screen:

def show_start_screen(self):
            self.screen.fill(ROSE)
            self.affichage_text(TITRE, 48, NOIR, LONGUEUR / 2, HAUTEUR / 4)
            self.affichage_text("Utilises les flèches pour te déplacer et la barre espace pour sauter", \
                                              22, BLANC, LONGUEUR / 2, HAUTEUR / 2)
            self.affichage_text("Appuies sur une touche pour commencer ;)", \
                                             22, BLANC, LONGUEUR / 2, HAUTEUR * 3/4)
            pg.display.flip()
            self.wait_for_key()

Et la fonction game over screen:

def show_go_screen(self):
            if not self.running:
                return
            self.screen.fill(NOIR)
            self.affichage_text("GAME OVER !", 48, ROUGE, LONGUEUR / 2, HAUTEUR / 4)
            self.affichage_text("Score: " + str(self.score), 22, BLANC, LONGUEUR / 2, HAUTEUR / 2)
            self.affichage_text("Appuies sur une touche pour rejouer", \
                                             22, BLANC, LONGUEUR / 2, HAUTEUR * 3/4)
            pg.display.flip()
            self.wait_for_key()

Dans la simple boucle en dehors de Jeu, nous rajoutons les 2 écrans:

j = Jeu()
j.show_start_screen()
while j.running:
    j.new()
    j.show_go_screen()
pg.quit()
2.2.2.6 Musique


Dans la fonction new de Jeu, nous allons télécharger la musique, ici j'utilise "Jump Higher Run Faster" [6]:

pg.mixer.music.load(os.path.join("jump_higher_run_faster.ogg"))

Puis dans la fonction run nous contrôllons quand la joue:

def run(self):
            # boucle du jeu
            pg.mixer.music.play(loops=-1) # joue la musique indéfiniment
            self.playing = True
            while self.playing:
                self.horloge.tick(FPS) # contrôle que la boucle s'effectue à la bonne vitesse
                self.events()
                self.update()
                self.affichage()
            pg.mixer.music.fadeout(1000)

3. Résultats

Le jeu fonctionne, dans le sens le code rend ce qu'il est sensé rendre (il n'y a pas d'erreur). Lorsque le joueur est en collision avec plusieurs platformes, il ne sait pas sur quelle platforme se positionner et parfois tombe tout simplement. Souvent les platformes sont trop éloignées pour éviter de tomber.

4. Discussion

Le jeu est loin d'être parfait. Le plus grand inconvénient est la manière dont les platformes sont créées. Leur apparition aléatoire fait qu'elles sont souvent entassées, ce qui provoquent des bugs lorsque le joueur est en collision avec plusieurs platformes, aussi un jeu devrait toujours être "possible" alors que là le joueur perd quand, par manque de chance, les platformes sont apparues trop éloignées les unes des autres. Un moyen d'amélioration serait de supprimer automatiquement toute nouvelle platforme qui touche une platforme déjà existante et de rendre les collisions plus précises grâce à un masque de collision. J'ai tenté de faire apparaître des étoiles à collecter mais pour une raison que je n'est pas réussi à déterminer le code pour cela ne fonctionnait pas, c'est pourquoi cette partie est commentée dans le code source. Aussi la manière dont le score est incrémenté n'est pas vraiment représentative de comment le joueur se débrouille, mais avec l'amélioration de l'apparition des platformes ce serait moins grave, aussi, si on rajoute des étoiles, celles-ci pourraient être l'objet du score.

note: j'ai écrit partout dans mon code plateforme platforme, j'en suis désolée.

5. Conclusion

Malgré le fait que mon jeu ne soit pas merveilleux, je suis assez satisfaite de ce que j'ai accompli. Comme précisé plus haut, la majorité du code ne vient pas de moi mais de KidsCanCode. Je précise que je n'ai fait aucun copié-collé et ai tenté de faire le travail de refléxion moi-même au préalable. Le travail que j'ai fait est, plus qu'un travail de création, un travail de compréhension et d'apprentissage. J'héstime que ce projet est un succès car j'a i beaucoup appris et suis capable d'expliquer chacune des lignes de code que j'ai écrit.

Références

Notes

[1] https://www.pygame.org/wiki/about

[2] https://atom.io

[3] https://www.youtube.com/playlist?list=PLsk-HSGFjnaG-BwZkuAOcVwWldfCLu1pq

[4] https://github.com/kidscancode/pygame_tutorials

[5] Kenney.nl

[6] https://opengameart.org/users/bart