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.
|
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 :
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()
|
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()
|
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()
|
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 :
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.
|
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)
|
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 |
|