7.3 JEUX D’ARCADE, FROGGER

 

 

INTRODUCTION

 

De nombreux jeux vidéo rencontrés sur les consoles de jeux ou sur Internet sont constitués d’images qui se déplacent sur une image d’arrière-fond. Il arrive même que l’arrière-fond soit en mouvement, particulièrement lorsque les personnages du jeu se déplacent vers les bords de la fenêtre de jeu. Cet effet procure au joueur la sensation que le monde du jeu est bien plus grand que l’aire de jeu visible. Bien que les images d’animations demandent beaucoup de ressources de calcul, les concepts sous-jacents n’en sont pas pour autant difficiles à comprendre et à assimiler. On peut dire pour l’instant que la boucle de jeu (game loop en anglais) recalcule le contenu de l’écran, copie l’image d’arrière-fond ainsi que les sprites des personnages dans une mémoire tampon invisible (buffer en anglais) de manière à pouvoir tout afficher en une seule fois dans la fenêtre. Dès que l’on dépasse les 25 images par seconde dans les animations (25 fps = Frame Per Second), les mouvements seront assez fluides tandis qu’en-dessous, les animations paraîtront saccadées.

Dans de nombreux jeux, les personnages interagissent entre eux par des collisions, ce qui fait de la détection et de la gestion de collisions un élément central d’un moteur de jeu. Une bibliothèque de jeux développée de manière minutieuse telle que JGameGrid met à disposition des programmeurs des mécanismes de détection de collisions contrôlables à l’aide de la programmation événementielle. Le programmeur se charge de définir quels objets peuvent rentrer en collision et le système détecte par lui-même lorsqu’il y a collision et lance la fonction de rappel que le programmeur lui aura fourni.

CONCEPTS DE PROGRAMMATION: Conception de jeux, sprite, acteur /personnage, collision, superviseur

 

 

SCÉNARIO DE JEU

 

Lors du développement d’un jeu vidéo, il est très important de commencer par mettre au point un scénario aussi détaillé que possible permettant de rédiger des spécifications fonctionnelles du programme. Très souvent, on a tendance à viser trop haut pour la première mouture du jeu au lieu de commencer simplement, en implémentant les fonctionnalités de base et en améliorant le jeu par versions successives. La grande idée est de développer de manière à ce que le programme soit facilement extensible par la suite, sans qu’il soit nécessaire de modifier des tas de lignes de code dans tous les coins pour supporter l’extension voulue. L’idéal est qu’il soit possible d’ajouter des nouvelles fonctionnalités en n’ayant pratiquement rien à retoucher au code déjà existant. Dans la pratique, même les meilleurs développeurs ont parfois bien de la peine à prévoir toutes les éventualités et à écrire un code parfaitement évolutif. Voilà pourquoi le développement de jeu est souvent fait de grands moments de joie et de satisfaction mais aussi de moments très frustrants. Cela ne fera qu’amplifier votre plaisir et votre satisfaction de pouvoir finalement présenter votre propre jeu à vos amis.

En attendant de devenir un programmeur chevronné, il est recommandé de développer des clones de jeux bien connus en y insérant votre petite touche personnelle à l’aide d’images de sprites personnalisées. Durant cette phase de formation, il n’est pas très important que ce développement de jeux aboutisse à des produits parfaits car il ne s’agit pas avant tout d’y jouer des heures mais surtout d’apprendre comment ils sont développés. Un jeu très connu s’appelle « Frogger » et possède le scénario suivant :

Une grenouille tente de traverser une route noire de trafic pour parvenir au marécage situé de l’autre côté. Si la tortue rentre en collision avec un véhicule, la grenouille perd une vie. Le but est d’utiliser les touches directionnelles du clavier pour amener la grenouille saine et sauve au marécage.

Il faut implémenter quatre voies de circulation : deux voies empruntées par des camions et des bus roulant en sens inverse et deux autres voies fréquentées par des voitures anciennes roulant en sens inverse (voir l’image ci-contre).
 

On commence par développer les mouvements de la grenouille et des véhicules. Ensuite, il faudra ajouter la gestion des collisions, le calcul des points ainsi que les conditions de fin de jeu (game over).

Dans la GameGrid, les véhicules sont modélisés comme des instances de la classe Car qui dérive elle-même de Actor. Les mouvements des véhicules sont programmés dans la méthode act().

On utilisera les images car0.gif, …, car19.gif présentes dans la distribution de TigerJython. Il est bien entendu possible d’utiliser des images personnalisées qui ont pour dimensions au maximum 70 pixels de hauteur et 200 pixels de largeur avec un fond transparent.

Dans les jeux d’arcade, on utilise habituellement un plateau de jeu dont les cases de la grille ont une largeur de un pixel, de sorte que la grille du jeu coïncide avec la grille de pixels de l’écran. On fixe la taille de la fenêtre à 800 x 600 pixels et on affiche en image de fond la route contenue dans le fichier lane.gif dont les dimensions sont de 801 x 601 pixels. Il nous faut encore générer 20 véhicules avec la fonction initCars() et décider de leur position et angle de visée initiaux sur le plateau de jeu.

Il est facile de déplacer les véhicules dans la méthode act() en les décalant à l’aide de la méthode move(). Lorsqu’un véhicule roulant de gauche à droite sort de l’écran à droite, on le fait réapparaître à gauche et lorsqu’un véhicule roulant en sens inverse disparaît à gauche de l’écran, on le fait réapparaître à droite. Rappelez-vous qu’un acteur de jeu (en l’occurrence les véhicules) peut posséder des coordonnées correspondant à une position hors écran.

from gamegrid import *

# ---------------- class Car ----------------
class Car(Actor):
    def __init__(self, path):
        Actor.__init__(self, path)
    
    def act(self):
        self.move()
        if self.getX() < -100:
            self.setX(1650)
        if self.getX() > 1650:
            self.setX(-100)
     
def initCars():
    for i in range(20):
        car = Car("sprites/car" + str(i) + ".gif")
        if i < 5:
            addActor(car, Location(350 * i, 100), 0)
        if i >= 5 and i < 10:
            addActor(car, Location(350 * (i - 5), 220), 180)
        if i >= 10 and i < 15:
            addActor(car, Location(350 * (i - 10), 350), 0)
        if i >= 15:
            addActor(car, Location(350 * (i - 15), 470), 180)


makeGameGrid(800, 600, 1, None, "sprites/lane.gif", False)
setSimulationPeriod(50)
initCars()
show()
doRun()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

Pour les jeux d’arcade, on utilise habituellement une grille de jeu dont les cellules font 1 pixel de largeur. On effectue le rendu de la scène du jeu 20 fois par seconde en fixant la période de simulation à 50 ms, ce qui permet un mouvement relativement fluide. Des saccades sporadiques peuvent survenir en raison d’un manque de puissance du CPU. En effet, du fait que le rendu graphique s’effectue sans accélération graphique et que le CPU est limité au niveau des traitements graphiques, la période de simulation peut difficilement être abaissée en-dessous de 50 ms.

 

 

DÉPLACER LA GRENOUILLE AVEC LES FLÈCHES DIRECTIONNELLES

 

On peut maintenant songer à incorporer la grenouille dans le jeu. Elle doit apparaître au bas de l’écran et se mouvoir à l’aide des flèches directionnelles du clavier.

Comme elle est également un acteur, il faut commencer par écrire la classe Frog dérivant de la classe Actor. Il suffit d’y définir le constructeur __init__ sans se soucier de la méthode move() qui est inutile puisque les déplacements de la grenouille sont commandés par les événements clavier déclenchés par le joueur. On définit le gestionnaire d’événements clavier par la fonction de rappel onKeyRepeated,

que l’on enregistre à l’aide du paramètre nommé keyRepeated lors de la création de la grille de jeu avec makeGameGrid(). Ce gestionnaire d’événement onKeyRepeated sera non seulement appelé lors de la pression d’une touche du clavier, mais également, de manière répétée, lorsque la touche sera maintenue enfoncée.

 On détermine la touche enfoncée au sein du gestionnaire d’événement onKeyRepeated et on déplace la grenouille de 5 cellules (5 pixels) dans la direction correspondante.

from gamegrid import *

# ---------------- class Frog ----------------
class Frog(Actor):
  def __init__(self):
      Actor.__init__(self, "sprites/frog.gif")

# ---------------- class Car ----------------
class Car(Actor):
    def __init__(self, path):
        Actor.__init__(self, path)
    
    def act(self):
        self.move()
        if self.getX() < -100:
            self.setX(1650)
        if self.getX() > 1650:
            self.setX(-100)
     
def initCars():
    for i in range(20):
        car = Car("sprites/car" + str(i) + ".gif")
        if i < 5:
            addActor(car, Location(350 * i, 100), 0)
        if i >= 5 and i < 10:
            addActor(car, Location(350 * (i - 5), 220), 180)
        if i >= 10 and i < 15:
            addActor(car, Location(350 * (i - 10), 350), 0)
        if i >= 15:
            addActor(car, Location(350 * (i - 15), 470), 180)

def onKeyRepeated(keyCode):
    if keyCode == 37: # left
        frog.setX(frog.getX() - 5)
    elif keyCode == 38: # up
        frog.setY(frog.getY() - 5)
    elif keyCode == 39: # right
        frog.setX(frog.getX() + 5)
    elif keyCode == 40: # down
        frog.setY(frog.getY() + 5)


makeGameGrid(800, 600, 1, None, "sprites/lane.gif", False, 
             keyRepeated = onKeyRepeated)
setSimulationPeriod(50);
frog = Frog()
addActor(frog, Location(400, 560), 90)
initCars()
show()
doRun()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

On pourrait également capturer les événements clavier à l’aide des fonctions de rappel keyPressed(e) et keyReleased(e). À la différence de keyRepeated(code), il faudrait alors récupérer le code de touche à partir du paramètre événement e par l’appel e.getKeyCode(). De plus, les événements keyPressed(e) et keyReleased(e) sont moins adaptés dans ce jeu car ils causent un délai entre la première pression de la touche et le moment où la répétition des événements de pression est déclenchée.

Pour connaître le code des touches directionnelles, il faut concocter un petit programme de test qui les affiche sur la sortie standard :


from gamegrid import *

def onKeyPressed(e):
    print "Pressed: ", e.getKeyCode()

def onKeyReleased(e):
    print "Released: ", e.getKeyCode()

makeGameGrid(800, 600, 1, None, "sprites/lane.gif", False, 
  keyPressed = onKeyPressed, keyReleased = onKeyReleased)
show()

 

 

ÉVÉNEMENTS DE COLLISION

 

Il est relativement simple de détecter les collisions entre acteurs. Il suffit de spécifier, lors de la création d’un véhicule car, que la grenouille doit déclencher un événement particulier lors d’une collision avec ce véhicule, à l’aide de l’appel

frog.addCollisionActor(car)

Ceci va faire que chaque collision déclenche automatiquement la méthode collide() de la classe Frog. Il suffit de traiter l’événement de collision de manière appropriée dans cette méthode collide(), en faisant par exemple sauter la grenouille à sa position initiale au bas de l’écran.

from gamegrid import *

# ---------------- class Frog ----------------
class Frog(Actor):
    def __init__(self):
        Actor.__init__(self, "sprites/frog.gif")
        self.setCollisionCircle(Point(0, -10), 5)

    def collide(self, actor1, actor2):
        self.setLocation(Location(400, 560))
        return 0
 
# ---------------- class Car ----------------
class Car(Actor):
    def __init__(self, path):
        Actor.__init__(self, path)
    
    def act(self):
        self.move()
        if self.getX() < -100:
            self.setX(1650)
        if self.getX() > 1650:
            self.setX(-100)

def initCars():
    for i in range(20):
        car = Car("sprites/car" + str(i) + ".gif")
        frog.addCollisionActor(car)
        if i < 5:
            addActor(car, Location(350 * i, 100), 0)
        if i >= 5 and i < 10:
            addActor(car, Location(350 * (i - 5), 220), 180)
        if i >= 10 and i < 15:
            addActor(car, Location(350 * (i - 10), 350), 0)
        if i >= 15:
            addActor(car, Location(350 * (i - 15), 470), 180)

def onKeyRepeated(keyCode):
    if keyCode == 37: # left
        frog.setX(frog.getX() - 5)
    elif keyCode == 38: # up
        frog.setY(frog.getY() - 5)
    elif keyCode == 39: # right
        frog.setX(frog.getX() + 5)
    elif keyCode == 40: # down
        frog.setY(frog.getY() + 5)

makeGameGrid(800, 600, 1, None, "sprites/lane.gif", False, 
     keyRepeated = onKeyRepeated)
setSimulationPeriod(50)
frog = Frog()
addActor(frog, Location(400, 560), 90)
initCars()
show()
doRun()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

La méthode collide() n’est en l’occurrence pas une fonction de rappel mais une méthode de la classe Actor redéfinie dans la classe Frog. C’est la raison pour laquelle il n’est pas nécessaire d’enregistrer la méthode collide en tant que fonction de rappel avec un paramètre nommé.

Par défaut, un événement collision est déclenché lorsque les rectangles circonscrits aux sprites se recoupent. Il est cependant possible de modifier la forme, la taille et la position de la zone de détection de collision pour s’adapter à l’image du sprite. A cet effet, on peut utiliser les méthodes suivantes de la classe Actor :

Methode Collision area

setCollisionCircle(centerPoint, radius)

Cercle de centre et de rayon donnés (en pixels)
setCollisionImage() Pixels non transparents du sprite associé à l’acteur. Ne fonctionne qu’avec un partenaire de collision disposant d’une zone de collision en cercle, ligne ou point
setCollisionLine(startPoint, endPoint) Segment droit défini par les points start et end
setCollisionRectangle(center, width, height) Rectangle de centre center, de largeur width et de hauteur height
setCollisionSpot(spotPoint) Point dont les coordonnées sont fixées de manière relative au centre du sprite associé à l’acteur

Toutes les méthodes utilisent un système de coordonnées en pixels relatif dont l’origine se trouve au centre du sprite. L’axe Ox est orienté vers la droite et l’axe Oy est orienté vers le bas.


La taille de l’image de la grenouille est de 71 x 41 pixels. Ainsi, il est par exemple possible d’ajouter les lignes suivantes au constructeur de la classe Frog

self.setCollisionCircle(Point(0, -10), 5)

afin de définir une zone de collision circulaire de 5 pixels de rayon autour de la tête de la grenouille. Un véhicule doit donc entrer dans ce cercle pour déclencher une collision avec la grenouille.

 
Comme les collisions sont mises en mémoire cache pour des raisons de performance, il peut être nécessaire de redémarrer TigerJython pour que les changements au niveau des zones de détection soient prises en compte.

 

 

GESTIONNAIRE DE JEU ET SON

 

Dans le domaine des jeux de société, il est souvent nécessaire de désigner une personne comme « maître du jeu » responsable de veiller au respect des règles, de distribuer les points et de désigner le vainqueur à l’issue de la partie. De manière similaire, il est également plus judicieux de ne pas attribuer à un acteur en particulier le soin de gérer le déroulement du jeu mais de programmer cet aspect dans une partie indépendante du programme. La partie principale du programme est bien adaptée à cet effet puisque son exécution se poursuit après l’initialisation du jeu. Il suffit d’ajouter à la fin du programme principal une boucle qui va périodiquement tester l’état du jeu et y réagir de manière appropriée. Il faut cependant veiller à insérer un délai avec delay() entre chaque itération de la boucle pour éviter de surcharger inutilement le processeur qui ne pourrait plus s’occuper des autres parties du jeu comme le mouvement des acteurs. Cette boucle devrait se terminer lors de la fermeture de la fenêtre de jeu, ce qui a comme consequence que isDisposed() retourne True. Le gestionnaire de jeu peut par exemple limiter le nombre de vies de la grenouille ou compter et afficher le nombre de traversées réussies et ratées.

Il est souvent assez délicat de gérer les conditions de fin de jeu car il faut considérer de nombreux cas différents. On veut aussi souvent pouvoir jouer plusieurs parties de suite après un « game over » sans devoir redémarrer tout le jeu.

N’hésitez pas à utiliser les connaissances acquises dans le chapitre Son pour insérer des effets sonores et ainsi améliorer le « game play ». Le plus simple est d’utiliser la fonction playTone().

from gamegrid import *

# ---------------- class Frog ----------------
class Frog(Actor):
    def __init__(self):
        Actor.__init__(self, "sprites/frog.gif")

    def collide(self, actor1, actor2):
        global nbHit
        nbHit += 1
        playTone([("c''h'a'f'", 100)])
        self.setLocation(Location(400, 560))
        return 0

    def act(self):
        global nbSuccess
        if self.getY() < 15:
            nbSuccess += 1
            playTone([("c'e'g'c''", 200)])
            self.setLocation(Location(400, 560))
 
# ---------------- class Car ----------------
class Car(Actor):
    def __init__(self, path):
        Actor.__init__(self, path)
    
    def act(self):
        self.move()
        if self.getX() < -100:
            self.setX(1650)
        if self.getX() > 1650:
            self.setX(-100)

def initCars():
    for i in range(20):
        car = Car("sprites/car" + str(i) + ".gif")
        frog.addCollisionActor(car)
        if i < 5:
            addActor(car, Location(350 * i, 90), 0)
        if i >= 5 and i < 10:
            addActor(car, Location(350 * (i - 5), 220), 180)
        if i >= 10 and i < 15:
            addActor(car, Location(350 * (i - 10), 350), 0)
        if i >= 15:
            addActor(car, Location(350 * (i - 15), 470), 180)

def onKeyRepeated(keyCode):
    if keyCode == 37: # left
        frog.setX(frog.getX() - 5)
    elif keyCode == 38: # up
        frog.setY(frog.getY() - 5)
    elif keyCode == 39: # right
        frog.setX(frog.getX() + 5)
    elif keyCode == 40: # down
        frog.setY(frog.getY() + 5)

makeGameGrid(800, 600, 1, None, "sprites/lane.gif", False, 
    keyRepeated = onKeyRepeated)
setSimulationPeriod(50)
setTitle("Frogger")
frog = Frog()
addActor(frog, Location(400, 560), 90)
initCars()
show()
doRun()

# Game supervision
maxNbLifes = 3
nbHit = 0
nbSuccess = 0
while not isDisposed():   
    if nbHit + nbSuccess == maxNbLifes:  # game over
        addActor(Actor("sprites/gameover.gif"), Location(400, 285))
        removeActor(frog)
        doPause()
    setTitle("#Success: " + str(nbSuccess) + " #Hits " + str(nbHit))
    delay(100)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

Le comptage des traversées réussies avec nbSuccess et des échecs avec nbHit s’effectue dans la classe Frog. Ceci explique pourquoi ces variables doivent être déclarées comme globales.

Lors de la fin du jeu, une image portant l’inscription « Game Over » est insérée, la grenouille est supprimée, le cycle de simulation est stoppé avec doPause() et, finalement, la boucle du gestionnaire de jeu est interrompue avec break. On aurait aussi pu utiliser un acteur textuel de la classe TextActor pour afficher « Game Over », ce qui permettrait de changer le texte lors de l’exécution.
rate = nbSuccess / (nbSuccess + nbHit)
ta = TextActor("  Game Over: Success Rate = " + str(rate) + " % ",  
     DARKGRAY, YELLOW, Font("Arial", Font.BOLD, 24))
addActor(ta, Location(200, 287))

 

 

EXERCICES

 

1.


Remplacer l’image de fond et les vieilles voitures avec des images d’animaux qui nagent dans une rivière (Crocodiles, etc.).

2.

Introduire un système de gestion des points et un temps limite pour effectuer la traversée : chaque traversée réussie rapporte 5 points, chaque collision fait perdre 5 points. Dépasser la limite de temps ramène la grenouille à sa position d’origine et fait perdre 10 points.


3.

Étendre le jeu en faisant tourner votre imagination et en implémentant quelques nouveautés sympathiques.