7.5 SPRITE ANIMATION

 

 

INTRODUCTION

 

When working with the game library JGameGrid, all tokens should be derived from the class Actor so that they already include many important features and capabilities without any necessary programming effort. However, they get their specific appearance loaded from an image file, called a sprite.

Actors in a game are animated in various ways: they move across the game area and change their appearance in the process, e.g. their posture or expression. For this reason, an Actor object can be assigned any number of different sprite images that are distinguished by an integer index (the sprite ID). This is simpler than modeling Actors with different sprites via class derivation.

Game tokens also often change their place, direction, and rotation angle. The rotation angle should automatically be adjusted to the direction of the movement. In JGameGrid, for efficiency reasons, one has to already specify at their definition whether Actors can be rotated and which sprite images they are assigned. The latter are, at the creation of the Actor object, loaded into an image buffer that also contains the rotated images. At runtime, the images therefore do not have to be loaded from the hard drive or otherwise transformed, which would decrease the performance. By default, 60 sprite images are generated for every 6 degrees of rotation. JGameGrid uses an animation concept also available in other game libraries, particularly Greenfoot [more... Greenfoot is a programming learning system based on Java, with built-in program editor (BlueJ)] .

Fundamental animation principle:

The method act(), defined for the class Actor(), has an empty definition part, and so it returns immediately. The user-defined derived actors then override act() and thereby implement the specific behavior of the actor.

When adding a character to the game window with addActor(), it will be inserted into an act-order list (ordered by Actor classes). An internal game loop (in this case also called a simulation cycle) periodically runs through this list and subsequently calls all the actors' specific act() methods due to polymorphism.

For this ingenious principle to work, the actors have to be cooperative, i.e. act() must have short running code. Loops and delays have especially catastrophic effects, since other actors must wait for their own call of act().

The drawing of the sprite images happens according to the following principle. In the game loop, the images of all Actors are copied into a screen buffer according to the order in the paint-order list and finally rendered in the game window. The order of execution thus determines the visibility of the sprite images: images of later actors cover the ones of all previously drawn actors, they lie above them, so to speak. Since the actors are added to the paint-order list when addActor() is called, sprites added later will lie above the others. The Act-Order List and the Paint-order list can be changed, in particular, an actor with setOnTop () request to be added to the top and his act() executes first.

Although any number of sprite images can be assigned at the initialization of an Actor, they cannot be changed at runtime Do you need Rapidly changing sprite images, such as a caption, so it is possible to produce the Actor only at run-time as a dynamic actor using conventional graphics functions.

PROGRAMMING CONCEPTS:
Simulation cycle, cooperative code, factory class, static variable, decoupling

 

 

MOVING A BOW AND SHOOTING ARROWS

 

You want to shoot arrows that move on a natural trajectory (parabola) using a crossbow that you control with the keyboard. You will use these arrows later to slice flying fruit in half.

You write a class Crossbow that is derived from the class Actor. When calling the constructor of the base class Actor, you use True to say that it is a rotatable actor. The value 2 indicates that there are 2 sprite images, namely one with a cocked crossbow that has an arrow attached to it and the other for a relaxed crossbow without an arrow. The image files are automatically searched for under the name sprites/crossbow_0.gif and sprites/crossbow_1.gif  and are found in the distribution of TigerJython.

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

The crossbow is controlled with keyboard events: You can change the direction using the cursor up/down keys and you can shoot the arrow with the spacebar. The callback keyCallback() is registered in makeGameGrid() as keyPressed.

The arrow class Dart already gets a bit more complicated, as the arrows have to move on a parabolic trajectory in an x-y coordinate system, with the horizontal x-axis and the vertical y-axis pointing down. The trajectory is not determined by a curve equation, but rather iteratively as a change in the short time dt. It is known from kinematics that the new speed coordinates (vx', vy') and the new location coordinates (px', py') after the time difference dt are calculated as follows (g = 9.81m/s^2 is the gravitational acceleration):

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

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

You determine the starting values (initial conditions) in the method reset(), which is automatically called when you add the Dart instance to the game area.

You can give the arrow a new location and direction in act(). To save some resources, you remove it from the board as soon as it is outside of the visible window and then bring the crossbow into the firing position again.


 

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()
Highlight program code (Ctrl+C to copy, Ctrl+V to paste)

 

 

MEMO

 

When calling the constructor of the class Actor you indicate whether the actor is rotatable and whether it is assigned more than one sprite image. [more...The sprite images are at this time from the hard disk into a sprite buffer
loaded, which also contains all the rotated images.
]

You rotate the direction of the arrow continuously into the direction of the velocity so that it has a natural flight appearance.

 

 

FRUIT FACTORY AND MOVING FRUITS

 

Your program should use three different types of fruits: melons, oranges and strawberries. The fruits are continuously generated in a random order and then move from the upper right edge to the left with a randomly varied horizontal speed on a parabolic trajectory. The three different types of fruit have many similarities and just a few small differences. It would therefore not be a good idea to derive the classes Melon, Orange, and Strawberry directly from Actor, because you would have to re-implement the shared properties in each class which leads to frowned-upon duplicated code. In this situation, it is appropriate to define a helper class Fruit where the the similarities can be implemented and where the specific fruits Melon, Orange, and Strawberry can be derived from..

You delegate the generation of fruit to a type of class called factory class. Although it does not have a sprite image, you can (also) derive it from Actor so that act() can be used to produce new fruits. A Factory class has a specific feature: Although it produces multiple fruits, there is only a single instance [more...We call such a class in the theory of design patterns a singleton]. Because of this, it is not common to include a constructor which is intended for the creation of multiple instances. Factory classes therefore have a method called create() (or a similarly meaningful name), that creates a single object of the class and returns it as a function value. Each subsequent call of create() then merely provides the already created factory instance.

Since the method create() is invoked without an instance, it must be statically defined with @staticmethod.

At the creation of the FruitFactory, the maximum number of fruits that the factory can create is specified in the variable capacity. Also, each Actor can call setSlowDown() to slow down the calling frequency of 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()
Highlight program code (Ctrl+C to copy, Ctrl+V to paste)

 

 

MEMO

 

In a static method, the parameter self is not available. Therefore, all variables assigned in create() must be static variables (the class name is prepended) [more... The creation of an instance of Fruit Factory with the constructor should be banned.
In addition, it should also not be allowed to make an instance of Fruit,
since only instances of the derived classes have meaning
].

Certain functions or methods may still be incompletely coded in a development phase. You can, for example, merely write out to the console that they have been called. You do this here by printing "Factory expired". With the adding of actors in the GameGrid using addActor(), the image buffer is automatically rendered on the screen so that the actor is immediately visible. As soon as the simulation cycle is started, the rendering happens at every cycle anyway. That is why in this case, you should use addActorNoRefresh() since rendering too frequently can cause the screen to flicker.

 

 

ASSEMBLING AND DEALING WITH COLLISIONS

 

The two program parts just written may well have been developed by two research groups. The next task is to merge these parts, which is not always easy. However, if the programming style is consistent and mostly decoupled as it is here, merging the code is significantly easier.

Additionally, you will incorporate a new functionality where the fruits are cut in half when they are hit by an arrow. We have already prepared this, as the fruits have two sprite images: one for the whole fruit and one for the halved fruit.

As you already know, collisions between actors are detected by a collision event. For this, you determine what the possible collision partners are for each actor. Consider the following: when creating an arrow, all currently existing fruits are potential collision partners.

However, do not forget that more fruits are added during the movement of the arrow. That is why you also need to declare all existing arrows (maybe there is only one) as collision partners when creating a fruit.

In JGameGrid you can also pass addCollisionActors() a whole list of actors as collision partners. With getActors(class) you will get a list with all the actors of the specified class, which you can pass to 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()
Highlight program code (Ctrl+C to copy, Ctrl+V to paste)

 

 

MEMO

 

Once you have declared the collision partners of your actor with addCollisionActor() or addCollisionActors(), you have to insert the method collide() in the class of the actor which is automatically called at each collision. The return value must be an integer that determines how many simulation cycles collision will now be deactivated (in this case 0). A number greater than 0 is sometimes necessary so that the two partners have time to separate before collisions become active again.

Collision areas are the surrounding rectangles of the sprite image by default (of course they are rotated along with the rotation of the actors). For the dart, you could also set the collision area to a circle around the arrowhead, so that the fruits that collide with the back part of the arrow do not get halved.

setCollisionCircle(Point(20, 0), 10) 

 

 

DISPLAYING THE GAME STATE AND DEALING WITH GAME OVER

 

For dessert, you refine the code by incorporating a game score and user information. The easiest way is to write them out in a status bar.

As you already know, it is favorable to implement a game supervisor in the main part of the program. It should write out the number of the hit and missed fruits and end the game when the fruit factory reaches its capacity. It shows the final score, generates a Game Over actor, and prevents the game from continuing on.

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)
Highlight program code (Ctrl+C to copy, Ctrl+V to paste)

 

 

MEMO

 

Most user actions should not be allowed at Game Over. The easiest way to implement this is to introduce a flag isGameOver = True with which you prohibit the actions using a premature return in the corresponding functions and methods.

You should still be allowed to move the crossbow at Game Over, but not shoot.

 

 

EXERCISES

 

1.

Count the number of arrows and restrict it to a reasonable maximum number. Once you have used up the specified amount of arrows, the game will also be over. Add appropriate status information too.

 

2.

Add a point score system for the halving of the fruits:

Melon: 5 points
Orange: 10 points
Strawberry: 15 points

 

3.

Make it so that when you press the Enter key after Game Over, the game starts over.

 

4.

Expand or modify the game with some of your own ideas.