7.5 SPRITES ANIMÉS

 

 

INTRODUCTION

 

Lorsque l’on travaille avec la bibliothèque JGameGrid, tous les acteurs devraient être dérivés de la classe Actor pour qu’ils réutilisent automatiquement de nombreuses fonctionnalités et capacités incluses cette dernière sans aucun effort de programmation. Leur apparence propre doit cependant être chargée depuis un fichier image spécifique appelé sprite.

Les personnages de jeu sont animés de différentes manières : ils bougent à travers la surface de jeu en changeant parfois d’apparence, de posture ou d’expression. De ce fait, on peut attribuer à un objet acteur de nombreux sprites différents que l’on distingue par un indice entier, l’ID du sprite. Cette technique est plus simple que de spécialiser les différentes apparences sous forme de classes dérivées. Les personnages de jeu modifient donc souvent leur position, direction de mouvement et angle de rotation. L’orientation du sprite devrait être automatiquement ajustée à la direction de son mouvement. Avec JGameGrid, il faut spécifier dès la création de l’acteur, pour des raisons de performance, si ce dernier peut subir des rotations et, le cas échéant, quels sont les sprites correspondant aux différentes orientations possibles. Ces derniers sont, dès l’instanciation de la classe Actor, chargés dans une mémoire tampon qui contient également les différentes rotations des images. Ceci permet d’éviter de devoir, lors de l’exécution du jeu, charger les images depuis le disque dur ou de leur appliquer des transformations, ce qui dégraderait les performances. Par défaut, pour chaque sprite chargé depuis une image, 60 images de sprites sont générées pour des angles de rotations entre 0° et 360°, par incréments de 6°. JGameGrid fait usage d’un concept d’animation également disponible dans d’autres bibliothèques de développement de jeu, notamment Greenfoot [plus... Greenfoot est un système d’apprentissage de la programmation basé sur Java qui inclut également un éditeur de code, BlueJ] .

Principe fondamental de l’animation:

La méthode act(), telle que définie dans la classe Actor(), possède un corps vide et retourne donc immédiatement. Pour définir des acteurs personnalisés, il faut donc créer des classes dérivées de Actor et y redéfinir la méthode act() pour implémenter leur comportement propre.

Lors de l’ajout d’un acteur à la fenêtre de jeu avec addActor(), celui-ci va être inséré dans une liste actOrder, ordonnée selon la classe de l’acteur. Une boucle de jeu interne, en l’occurrence appelée cycle de simulation, va parcourir périodiquement cette liste et appeler de manière séquentielle la méthode act() propre à chaque acteur grâce au mécanisme de polymorphisme d’héritage.

Pour que ce principe astucieux fonctionne correctement, il est nécessaire que les acteurs adoptent un  comportement coopératif : leur méthode act() doit se contenter d’exécuter un code qui retourne très rapidement. L’insertion de boucles ou de délais dans la méthode act(), ne serait-ce que d’un seul acteur, entraîne des effets catastrophiques puisque les autres acteurs devront ainsi attendre les uns sur les autres pour voir leur méthode act() être traitée.

Il faut avoir conscience du mécanisme qui régit le rendu des images de sprite des acteurs présents dans le jeu. Dans la boucle de jeu, les sprites de chaque acteur sont copiés dans une mémoire tampon regroupant les images de tous les acteurs selon l’ordre spécifié par la liste paintOrder. C’est à partir de cette mémoire tampon que le rendu des acteurs est effectué dans la fenêtre de jeu. L’ordre de rendu des acteurs détermine donc leur visibilité : le sprite des derniers acteurs dessinés va se situer au premier plan tandis que celui des premiers acteurs sera en arrière-plan. Du fait que les acteurs sont insérés dans la liste paintOrder selon l’ordre d’ajout avec addActor(), les acteurs ajoutés en dernier apparaîtront au premier plan. Heureusement, autant l’ordre de la liste actOrder que celui de paintOrder peuvent être modifiés lors de l’exécution. En particulier, un acteur peut demander à être placé en tête de liste avec setOnTop() pour que son sprite apparaisse au premier plan et que sa méthode act() soit invoquée en priorité

Bien qu’il soit possible d’assigner un nombre arbitraire de sprites à un acteur lors de sa création, il n’est ensuite plus possible de les modifier lors de l’exécution. Si un acteur (par exemple un texte de légende) doit changer d’apparence dynamiquement lors de l’exécution du jeu, il est possible de générer l’acteur dynamiquement grâce aux fonctions graphiques habituelles.

CONCEPTS DE PROGRAMMATION:
Cycle de simulation, code coopératif, fabrique de classe, variable statique (de classe), découplage

 

 

BOUGER UNE ARBALÈTE ET UNE FLÈCHE

 

Développons un jeu dans lequel le joueur bouge, à l’aide des flèches directionnelles, une arbalète (crossbow) qui tire des flèches (arrows) empruntant une trajectoire naturelle (parabole). Les flèches devront atteindre des fruits volant pour les pourfendre.

On définit une classe Crossbow qui dérive de la classe Actor. Pour indiquer qu’il s’agit d’un acteur pouvant subir des rotations, on indique True lors de l’appel du constructeur de la classe de base. Le troisième paramètre, en l’occurrence l’entier 2, indique qu’il y a deux images de sprite associées avec l’arbalète, l’une représentant l’arbalète tendue et chargée d’une flèche et l’autre représentant l’arbalète au repos et dépourvue de flèche. Les images correspondantes sont automatiquement cherchées sous le nom sprites/crossbow_0.gif et sprites/crossbow_1.gif dans la distribution de TigerJython.

Actor.__init__(self, True, "sprites/crossbow.gif", 2)

L’arbalète est contrôlée par des événements clavier : on change son orientation avec les touches haut/bas et on la déclenche avec la touche espace. La fonction de rappel keyCallback() qui sert de gestionnaire d’événements est enregistrée lors de l’appel à makeGameGrid() avec le paramètre nommé keyPressed.

La classe Dart modélisant les flèches est un peu compliquée du fait que ces dernières doivent se mouvoir sur une trajectoire parabolique dans un système de coordonnées xOy dont l’axe Ox est horizontal vers la droite et l’axe Oy vertical et orienté vers le bas. La trajectoire n’est pas déterminée par une équation de courbe mais plutôt parcourue itérativement par de petits changements temporels dt. On sait de la cinématique que les nouvelles composantes du vecteur vitesse (vx', vy') et du vecteur position (px', py') sont déterminées après chaque incrément temporel dt comme suit, où g = 9.81m/s^2 est l’accélération de la pesanteur à la surface de la terre :

vx' = vx
vy' = vy + g * dt

px'= px + vx * dt
py' = py + vy * dt

On détermine les valeurs initiales dans la méthode reset() qui est appelée automatiquement lors de l’ajout d’une instance de Dart à la fenêtre de jeu.

On peut donner à la flèche une nouvelle position et orientation dans sa méthode act(). Pour ne pas gaspiller de ressources de calcul, il faut supprimer la flèche du jeu sitôt qu’elle sort de la fenêtre et la replacer sur l’arbalète en position de tir.


 

from gamegrid import *
import math

# ------------------- class Crossbow -----------------------
class Crossbow(Actor):
    def __init__(self):
        Actor.__init__(self, True, "sprites/crossbow.gif", 2)

# ------ class Dart ----------------
class Dart(Actor):
    def __init__(self, speed):
        Actor.__init__(self, True, "sprites/dart.gif")
        self.speed = speed
        self.dt = 0.005 * getSimulationPeriod()

    # Called when actor is added to GameGrid
    def reset(self):
        self.px = self.getX()
        self.py = self.getY()
        self.vx = self.speed * math.cos(math.radians(self.getDirection()))
        self.vy = self.speed * math.sin(math.radians(self.getDirection()))
        
    def act(self):
        self.vy = self.vy + g * self.dt
        self.px = self.px + self.vx * self.dt
        self.py = self.py + self.vy * self.dt
        self.setLocation(Location(int(self.px), int(self.py)))
        self.setDirection(math.degrees(math.atan2(self.vy, self.vx)))
        if not self.isInGrid():
            self.removeSelf()
            crossbow.show(0) # Load crossbow

# ------ End of class definitions --------------------
        
def keyCallback(e):
    code = e.getKeyCode()   
    if code == KeyEvent.VK_UP:
        crossbow.setDirection(crossbow.getDirection() - 5)
    elif code == KeyEvent.VK_DOWN:
        crossbow.setDirection(crossbow.getDirection() + 5)
    elif code == KeyEvent.VK_SPACE:
        if crossbow.getIdVisible() == 1: # Wait until crossbow is loaded
            return
        crossbow.show(1) # crossbow is released
        dart = Dart(100)
        addActorNoRefresh(dart, crossbow.getLocation(), 
                                crossbow.getDirection())   

screenWidth = 600
screenHeight = 400
g = 9.81

makeGameGrid(screenWidth, screenHeight, 1, False, keyPressed = keyCallback)
setTitle("Use Cursor up/down to target, Space to shoot.")
setBgColor(makeColor("skyblue"))
crossbow = Crossbow()
addActor(crossbow, Location(80, 320))
setSimulationPeriod(30)
doRun()
show()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

Lors de l’appel du constructeur de la classe Actor, on peut indiquer si l’acteur va subir des rotations et s’il faut lui associer plusieurs images de sprite. Les images de sprite sont chargées à ce moment depuis le disque et placées dans une mémoire tampon qui contiendra également toutes les différentes rotations des images.

On règle sans cesse l’orientation de la flèche pour qu’elle corresponde à la direction de son vecteur vitesse dans le but de rendre son vol plus naturel.

 

 

FABRIQUE DE FRUITS ET FRUITS EN MOUVEMENT

 

Notre programme va utiliser trois différents types de fruits : des melons, des oranges et des fraises. Ceux-ci sont générés en continu de manière aléatoire et se déplacent depuis le coin supérieur droit de la fenêtre vers la gauche avec une vitesse horizontale initiale aléatoire et suivant une trajectoire parabolique. Ces trois différents types de fruits partagent de nombreuses caractéristiques communes et ne comportent que quelques différences mineures. Il ne serait de ce fait pas une bonne idée de dériver ces trois classes directement de la classe de base Actor car cela impliquerait de coder les méthodes qui leur seraient communes dans chacune des classes, ce qui constituerait une redondance de code à éviter à tout prix. Dans cette situation, il est plus judicieux de définir une classe auxiliaire et intermédiaire Fruit dans laquelle sont implémentées les fonctionnalités communes et de laquelle les classes spécifiques Melon, Orange et Strawberry peuvent être dérivées.

On délègue la création d’un fruit à la classe FruitFactory d’un genre particulier : il s’agit d’une classe fabrique. Bien que celle-ci ne possède pas d’image de sprite, on peut néanmoins la dériver de la classe Actor de telle sorte que sa méthode act() puisse être utilisée pour générer de nouveaux fruits. Cette classe fabrique a une particularité : bien qu’elle produise de nombreux fruits, il n’y aura qu’une seule instance de cette classe dans tout le jeu [plus...Une telle classe est appelée Singleton]. De ce fait, on ne définit généralement pas de constructeur pour une telle classe puisqu’elle n’est pas destinée à engendrer plusieurs instances. On définit plutôt une méthode nommée create() ou de manière similaire qui crée une unique instance de la classe et la retourne comme valeur de retour. Tous les appels subséquents à cette méthode create(), au lieu de recréer une nouvelle instance, ne vont faire que de renvoyer la référence à l’instance précédemment créée.

Comme la méthode create() est invoquée sans passer par une instance particulière, il s’agit d’une méthode de classe qui doit de ce fait être décorée avec @staticmethod.

Lors de la création de la FruitFactory, le nombre maximum de fruits que la fabrique peut créer est spécifié dans une variable capacity. De plus, chaque Actor peut invoquer la méthode setSlowDown() pour ralentir la fréquence d’appels à la méthode act().


 


from gamegrid import *
from random import randint, random

# ---------- class Fruit ------------------------
class Fruit(Actor):
    def  __init__(self, spriteImg, vx):
        Actor.__init__(self, True, spriteImg, 2) # rotatable, 2 sprites
        self.vx = vx
        self.vy = 0

    def reset(self): # Called when Fruit is added to GameGrid
        self.px = self.getX()
        self.py = self.getY()
    
    def act(self):
        self.movePhysically()
        self.turn(10)

    def movePhysically(self):
        self.dt = 0.002 * getSimulationPeriod()
        self.vy = self.vy + g * self.dt # vx = const
        self.px = self.px + self.vx * self.dt
        self.py = self.py + self.vy * self.dt
        self.setLocation(Location(int(self.px), int(self.py)))
        self.cleanUp()
 
    def cleanUp(self):
        if not self.isInGrid(): 
            self.removeSelf()

# ------ class Melon -----------
class Melon(Fruit):
    def __init__(self, vx):
        Fruit.__init__(self, "sprites/melon.gif", vx)
 
# ------ class Orange -----------
class Orange(Fruit):
    def __init__(self, vx):
        Fruit.__init__(self, "sprites/orange.gif", vx)

# ------ class Strawberry -----------
class Strawberry(Fruit):
    def __init__(self, vx):
        Fruit.__init__(self, "sprites/strawberry.gif", vx)

# ------------------- class FruitFactory -------------------
class FruitFactory(Actor):
    myFruitFactory = None
    myCapacity = 0
    nbGenerated = 0

    @staticmethod
    def create(capacity, slowDown):
        if FruitFactory.myFruitFactory == None:
            FruitFactory.myCapacity = capacity
            FruitFactory.myFruitFactory = FruitFactory()
            FruitFactory.myFruitFactory.setSlowDown(slowDown)  
                 # slows down act() call for this actor
        return FruitFactory.myFruitFactory

    def act(self): 
        if FruitFactory.nbGenerated == FruitFactory.myCapacity:
            print("Factory expired")
            return
   
        vx = -(random() * 20 + 30)
        r = randint(0, 2)
        if r == 0:
            fruit = Melon(vx)
        elif r == 1:
            fruit = Orange(vx)
        else:
            fruit = Strawberry(vx)
        FruitFactory.nbGenerated += 1
        y = int(random() * screenHeight / 2)
        addActorNoRefresh(fruit, Location(screenWidth-50, y), 180)

# ------ End of class definitions --------------------

FACTORY_CAPACITY = 20
FACTORY_SLOWDOWN = 35
screenWidth = 600
screenHeight = 400
g = 9.81

makeGameGrid(screenWidth, screenHeight, 1, False)
setTitle("Use Cursor up/down to target, Space to shoot.")
setBgColor(makeColor("skyblue"))
factory = FruitFactory.create(FACTORY_CAPACITY, FACTORY_SLOWDOWN)
addActor(factory, Location(0, 0))  # needed to run act()
setSimulationPeriod(30)
doRun()
show()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

Dans une méthode statique, il n’y a pas de paramètre self. De ce fait, les variables créées au sein de la méthode create() doivent être des variables statiques (leur nom doit être préfixé du nom de la classe) [plus...L’instanciation de la classe FruitFactory devrait être interdite.
Il ne faudrait pas non plus pouvoir créer une instance de la classe Fruit
puisque seules les instances concrètes des classes dérivées font sens en
tant qu’objets
].

Lors de l’ajout d’acteurs dans la grille de jeu avec addActor(), les images présentes dans la mémoire tampon sont automatiquement affichées à l’écran de sorte que l’acteur est immédiatement visible. Dès que le cycle de simulation est démarré, le rendu est de toute manière effectué à chaque cycle de simulation. C’est la raison pour laquelle, dans le cas présent, il faut utiliser addActorNoRefresh() pour éviter qu’un rendu trop fréquent n’entraine des scintillements à l’écran.

 

 

GESTION ET INTÉGRATION DES COLLISIONS

 

Les deux parties du programme que nous venons de mettre en place auraient pu être développées indépendamment par deux équipes de développeurs distincts. Si le style de programmation est cohérent et en majorité découplé à l’exemple de notre code, il est presque un jeu d’enfant de fusionner les deux parties.

Nous allons donc mettre ces deux pièces ensemble et ajouter au passage une nouvelle fonctionnalité permettant aux fruits d’être fendus lorsqu’ils sont atteints par la flèche. Le terrain est déjà préparé puisque les fruits disposent de deux images de sprite : l’une pour le fruit entier et l’autre pour le fruit fendu.

Comme vous le savez, les collisions entre acteurs sont détectées par des événements de collision et leur gestion nécessite d’identifier, pour chaque acteur, les partenaires de collision potentiels. En ce qui concerne la flèche, il s’agit de tous les fruits existants et visibles à l’écran.

Il ne faut cependant pas oublier que davantage de fruits sont ajoutés à la grille de jeu pendant le vol de la flèche. Ceci exige de déclarer également toutes les flèches (même s’il n’y en a probablement qu’une seule) comme partenaires de collision lors de la création d’un fruit.

Avec JGameGrid,  il est également possible de passer toute une liste d’acteurs à la fonction addCollisionActors() en tant que partenaires de collision. La fonction getActors(class) permet d’obtenir une liste de tous les acteurs appartenant à la classe mentionnée que l’on peut passer à addCollisionActors().

 

crossbow

 

from gamegrid import *
from random import randint, random
import math

# ---------- class Fruit ------------------------
class Fruit(Actor):
    def  __init__(self, spriteImg, vx):
        Actor.__init__(self, True, spriteImg, 2) 
        self.vx = vx
        self.vy = 0
        self.isSliced = False

    def reset(self): # Called when Fruit is added to GameGrid
        self.px = self.getX()
        self.py = self.getY()
    
    def act(self):
        self.movePhysically()
        self.turn(10)

    def movePhysically(self):
        self.dt = 0.002 * getSimulationPeriod()
        self.vy = self.vy + g * self.dt
        self.px = self.px + self.vx * self.dt
        self.py = self.py + self.vy * self.dt
        self.setLocation(Location(int(self.px), int(self.py)))
        self.cleanUp()
  
    def cleanUp(self):
        if not self.isInGrid(): 
            self.removeSelf()

    def sliceFruit(self):
        if not self.isSliced:
            self.isSliced = True
            self.show(1)
      
    def collide(self, actor1, actor2):
       actor1.sliceFruit()
       return 0

# ------ class Melon -----------
class Melon(Fruit):
    def __init__(self, vx):
        Fruit.__init__(self, "sprites/melon.gif", vx)

# ------ class Orange -----------
class Orange(Fruit):
    def __init__(self, vx):
        Fruit.__init__(self, "sprites/orange.gif", vx)

# ------ class Strawberry -----------
class Strawberry(Fruit):
    def __init__(self, vx):
        Fruit.__init__(self, "sprites/strawberry.gif", vx)

# ------------------- class FruitFactory -------------------
class FruitFactory(Actor):
    myCapacity = 0
    myFruitFactory = None
    nbGenerated = 0
        
    @staticmethod
    def create(capacity, slowDown):
        if FruitFactory.myFruitFactory == None:
            FruitFactory.myCapacity = capacity
            FruitFactory.myFruitFactory = FruitFactory()
            FruitFactory.myFruitFactory.setSlowDown(slowDown)  
        return FruitFactory.myFruitFactory

    def act(self): 
        self.createRandomFruit()

    def createRandomFruit(self):
        if FruitFactory.nbGenerated == FruitFactory.myCapacity:
            print("Factory expired")
            return
   
        vx = -(random() * 20 + 30)
        r = randint(0, 2)
        if r == 0:
            fruit = Melon(vx)
        elif r == 1:
            fruit = Orange(vx)
        else:
            fruit = Strawberry(vx)
        FruitFactory.nbGenerated += 1
        y = int(random() * screenHeight / 2)
        addActorNoRefresh(fruit, Location(screenWidth-50, y), 180)
        # for a new fruit, the collision partners are all existing darts
        fruit.addCollisionActors(toArrayList(getActors(Dart)))

# ------------------- class Crossbow -----------------------
class Crossbow(Actor):
    def __init__(self):
        Actor.__init__(self, True, "sprites/crossbow.gif", 2)

# ------ class Dart ----------------
class Dart(Actor):
    def __init__(self, speed):
        Actor.__init__(self, True, "sprites/dart.gif")
        self.speed = speed
        self.dt = 0.005 * getSimulationPeriod()

    # Called when actor is added to GameGrid
    def reset(self):
        self.px = self.getX()
        self.py = self.getY()
        dx = math.cos(math.radians(self.getDirectionStart()))
        self.vx = self.speed * dx
        dy = math.sin(math.radians(self.getDirectionStart()))
        self.vy = self.speed * dy
        
    def act(self):
        self.vy = self.vy + g * self.dt
        self.px = self.px + self.vx * self.dt
        self.py = self.py + self.vy * self.dt
        self.setLocation(Location(int(self.px), int(self.py)))
        self.setDirection(math.degrees(math.atan2(self.vy, self.vx)))
        if not self.isInGrid():
            self.removeSelf()
            crossbow.show(0) # Load crossbow

    def collide(self, actor1, actor2):
        actor2.sliceFruit()
        return 0

# ------ End of class definitions --------------------
        
def keyCallback(e):
    code = e.getKeyCode()   
    if code == KeyEvent.VK_UP:
        crossbow.setDirection(crossbow.getDirection() - 5)
    elif code == KeyEvent.VK_DOWN:
        crossbow.setDirection(crossbow.getDirection() + 5)
    elif code == KeyEvent.VK_SPACE:
        if crossbow.getIdVisible() == 1: # Wait until crossbow is loaded
            return
        crossbow.show(1) # crossbow is released
        dart = Dart(100)
        addActorNoRefresh(dart, crossbow.getLocation(), 
                                crossbow.getDirection())
        # for a new dart, the collision partners are all existing fruits
        dart.addCollisionActors(toArrayList(getActors(Fruit)))

 
FACTORY_CAPACITY = 20
FACTORY_SLOWDOWN = 35
screenWidth = 600
screenHeight = 400
g = 9.81

makeGameGrid(screenWidth, screenHeight, 1, False, keyPressed = keyCallback)
setTitle("Use Cursor up/down to target, Space to shoot.")
setBgColor(makeColor("skyblue"))
factory = FruitFactory.create(FACTORY_CAPACITY, FACTORY_SLOWDOWN)
addActor(factory, Location(0, 0))  # needed to run act()
crossbow = Crossbow()
addActor(crossbow, Location(80, 320))
setSimulationPeriod(30)
doRun()
show()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

Une fois que les partenaires de collision d’un acteur sont déclarés, il faut redéfinir la méthode collide() dans la classe de cet acteur pour qu’elle soit appelée lors de chaque collision. La valeur de retour doit être un nombre entier indiquant pendant combien de cycles de simulation, la détection de collision doit être suspendue (en l’occurrence 0). Un nombre supérieur à 0 est parfois nécessaire pour que les deux partenaires aient le temps de se séparer avant qu’une nouvelle collision soit détectée.

Notez bien que dans la définition de la méthode collide(self, actor1, actor2), actor1 est l’acteur de la classe dans laquelle collide() est définie.

Par défaut, la zone de détection de collisions correspond au rectangle circonscrit à l’image du sprite. Ce rectangle est évidemment tourné avec l’image lors des rotations. On pourrait définir la zone de collision des flèches comme un cercle de faible rayon autour de leur pointe, pour éviter que les fruits ne soient fendus lors d’une collision avec la queue d’une flèche.

setCollisionCircle(Point(20, 0), 10) 

 

 

AFFICHAGE DE L’ÉTAT DU JEU ET GESTION DE LA FIN DU JEU

 

Pour le dessert, raffinons encore notre jeu en y ajoutant des informations à destination de l’utilisateur ainsi que le décompte des points. Le plus simple est de les écrire dans la barre d’état de la fenêtre de jeu.

Comme nous l’avons déjà vu, il est judicieux d’implémenter les règles du jeu dans un gestionnaire de jeu indépendant des acteurs dans le programme principal. Ce dernier affiche le nombre de fruits touchés, le nombre de ceux qui ont été manqués et termine le jeu lorsque la fabrique de fruits est épuisée. Le gestionnaire affiche également le score final, génère un acteur textuel Game Over et empêche le jeu de se poursuivre.

from gamegrid import *
from random import random, choice
import math

# ---------- class Fruit ------------------------
class Fruit(Actor):
    def  __init__(self, spriteImg, vx):
        Actor.__init__(self, True, spriteImg, 2) 
        self.vx = vx
        self.vy = 0
        self.isSliced = False

    def reset(self): # Called when Fruit is added to GameGrid
        self.px = self.getX()
        self.py = self.getY()
    
    def act(self):
        self.movePhysically()
        self.turn(10)

    def movePhysically(self):
        self.dt = 0.002 * getSimulationPeriod()
        self.vy = self.vy + g * self.dt
        self.px = self.px + self.vx * self.dt
        self.py = self.py + self.vy * self.dt
        self.setLocation(Location(int(self.px), int(self.py)))
        self.cleanUp()
   
    def cleanUp(self):
        if not self.isInGrid(): 
            if not self.isSliced:
                FruitFactory.nbMissed += 1
            self.removeSelf()

    def sliceFruit(self):
        if not self.isSliced:
            self.isSliced = True
            self.show(1)
            FruitFactory.nbHit += 1
      
    def collide(self, actor1, actor2):
       actor1.sliceFruit()
       return 0

# ------ class Melon -----------
class Melon(Fruit):
    def __init__(self, vx):
        Fruit.__init__(self, "sprites/melon.gif", vx)
  
# ------ class Orange -----------
class Orange(Fruit):
    def __init__(self, vx):
        Fruit.__init__(self, "sprites/orange.gif", vx)

# ------ class Strawberry -----------
class Strawberry(Fruit):
    def __init__(self, vx):
        Fruit.__init__(self, "sprites/strawberry.gif", vx)

# ------------------- class FruitFactory -------------------
class FruitFactory(Actor):
    myCapacity = 0
    myFruitFactory = None
    nbGenerated = 0
    nbMissed = 0
    nbHit = 0
        
    @staticmethod
    def create(capacity, slowDown):
        if FruitFactory.myFruitFactory == None:
            FruitFactory.myCapacity = capacity
            FruitFactory.myFruitFactory = FruitFactory()
            FruitFactory.myFruitFactory.setSlowDown(slowDown)  
        return FruitFactory.myFruitFactory

    def act(self): 
        self.createRandomFruit()

    @staticmethod
    def createRandomFruit():
        if FruitFactory.nbGenerated == FruitFactory.myCapacity:
            return
        vx = -(random() * 20 + 30)
        fruitClass = choice([Melon, Orange, Strawberry])
        fruit = fruitClass(vx)
        FruitFactory.nbGenerated += 1
        y = int(random() * screenHeight / 2)
        addActorNoRefresh(fruit, Location(screenWidth-50, y), 180)
        # for a new fruit, the collision partners are all existing darts
        fruit.addCollisionActors(toArrayList(getActors(Dart)))
        print(type(getActors(Dart)))

# ------------------- class Crossbow -----------------------
class Crossbow(Actor):
    def __init__(self):
        Actor.__init__(self, True, "sprites/crossbow.gif", 2)

# ------ class Dart ----------------
class Dart(Actor):
    def __init__(self, speed):
        Actor.__init__(self, True, "sprites/dart.gif")
        self.speed = speed
        self.dt = 0.005 * getSimulationPeriod()

    # Called when actor is added to GameGrid
    def reset(self):
        self.px = self.getX()
        self.py = self.getY()
        dx = math.cos(math.radians(self.getDirectionStart()))
        self.vx = self.speed * dx
        dy = math.sin(math.radians(self.getDirectionStart()))
        self.vy = self.speed * dy
        
    def act(self):
        if isGameOver:
            return
        self.vy = self.vy + g * self.dt
        self.px = self.px + self.vx * self.dt
        self.py = self.py + self.vy * self.dt
        self.setLocation(Location(int(self.px), int(self.py)))
        self.setDirection(math.degrees(math.atan2(self.vy, self.vx)))
        if not self.isInGrid():
            self.removeSelf()
            crossbow.show(0) # Load crossbow

    def collide(self, actor1, actor2):
        actor2.sliceFruit()
        return 0

# ------ End of class definitions --------------------
        
def keyCallback(e):
    code = e.getKeyCode()   
    if code == KeyEvent.VK_UP:
        crossbow.setDirection(crossbow.getDirection() - 5)
    elif code == KeyEvent.VK_DOWN:
        crossbow.setDirection(crossbow.getDirection() + 5)
    elif code == KeyEvent.VK_SPACE:
        if isGameOver:
            return
        if crossbow.getIdVisible() == 1: # Wait until crossbow is loaded
            return
        crossbow.show(1) # crossbow is released
        dart = Dart(100)
        addActorNoRefresh(dart, crossbow.getLocation(), crossbow.getDirection())
        # for a new dart, the collision partners are all existing fruits
        dart.addCollisionActors(toArrayList(getActors(Fruit)))

 
FACTORY_CAPACITY = 20
FACTORY_SLOWDOWN = 35
screenWidth = 600
screenHeight = 400
g = 9.81
isGameOver = False

makeGameGrid(screenWidth, screenHeight, 1, False, keyPressed = keyCallback)
setTitle("Use Cursor up/down to target, Space to shoot.")
setBgColor(makeColor("skyblue"))
addStatusBar(30)
factory = FruitFactory.create(FACTORY_CAPACITY, FACTORY_SLOWDOWN)
addActor(factory, Location(0, 0))  # needed to run act()
crossbow = Crossbow()
addActor(crossbow, Location(80, 320))
setSimulationPeriod(30)
doRun()
show()

while not isDisposed() and not isGameOver:
   # Don't show message if same 
   oldMsg = ""
   msg = "#hit: "+str(FruitFactory.nbHit)+" #missed: "+str(FruitFactory.nbMissed)
   if  msg != oldMsg:
        setStatusText(msg)
        oldMsg = msg
   if FruitFactory.nbHit + FruitFactory.nbMissed == FACTORY_CAPACITY:
       isGameOver = True
       removeActors(Dart)
       setStatusText("You smashed " + str(FruitFactory.nbHit) + " out of " 
       + str(FACTORY_CAPACITY) + " fruits")
       addActor(Actor("sprites/gameover.gif"), Location(300, 200))
        
   delay(100)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

La plupart des actions utilisateur devraient être interdites après le Game Over. Le plus simple pour implémenter ces restrictions est d’introduire un fanion global isGameOver = True utilisé pour effectuer un return conditionnel prématuré dans le gestionnaire des événements clavier et dans la méthode act() de la flèche.

L’utilisateur devrait par contre toujours être en mesure de bouger l’arbalète après le Game Over mais pas de tirer des flèches.

 

 

EXERCISES

 

1.

Compter le nombre de flèches utilisées durant la partie et fixer un maximum de flèches disponibles. Lorsque toutes les flèches ont été épuisées, le jeu s’arrête également. Ajouter l’information appropriée dans la barre d’état.

 

2.

Ajouter une récompense pour chaque fruit fendu :
Melon: 5 points
Orange: 10 points
Fraise: 15 points

 

3.

Faire en sorte que le jeu redémarre lors d’une pression sur la touche espace après le Game Over. S’assurer que l’utilisateur ne puisse pas redémarrer le jeu involontairement en demandant une confirmation.

 

4.

Étendre ou modifier le jeu selon votre imagination.