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:
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.
|
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 px'= px + vx * dt
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()
|
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 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.
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()
|
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. 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().
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()
|
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)
|
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 |
|