7.3 ARCADE GAMES, FROGGER

 

 

INTRODUCTION

 

Many computer games found on game consoles and the Internet consist of images moving over a background. Sometimes even the background moves too, especially in situations where the game characters move to the edge of the window allowing the player to have the impression of a scenario that is substantially larger than the display window. Even though game animation requires a lot of computational power, it is generally easy to understand: at time points in quick succession, the so-called game loop recalculates the screen content, copies the background and the images of the game characters into an invisible image buffer, and then displays (renders) it as a whole in the window. With anything more than around 25 frames per second, you will see a smooth, flowing movement, but with anything less the movement will be jerky.

In many games, characters interact through collisions, and therefore dealing with collisions is fundamental. Well developed game libraries such as JGameGrid provide programmers with built-in collision detection under the use of event models. One defines the potential collision partners and the system automatically calls a callback when a collision occurs.

PROGRAMMING CONCEPTS: Game design, sprite, actor, collision, supervisor

 

 

GAME-SCENARIO

 

When developing a computer game it is important that you first think of a game scenario that is as detailed as possible, and then write out notes as functional program specifications. Often your goals are set too high in your first attempt, and so you should try to simplify the game a bit so that you can develop executable subversions that can then gradually be expanded. The trick is to write the program as generally as possible so that you will not have to modify the existing code with following extensions. Instead, you will only need to add to it. However, this rarely succeeds right off the bat, even with professional programmers, and it is thus common that feelings of euphoria and frustration lie close together when developing a game. This will make your pleasure and satisfaction even greater once you can finally show off your very own personal computer game and let people play it.

Along the way of becoming a competent game programmer, it is advisable to learn by developing well-known games that you can implement in your own personal way using your own sprite images. It is not very important that these games are readily available in the training phase, because it is not about playing a lot, but rather about learning how it is developed. One well-known game is Frogger. It has the following fun scenario:


A frog tries to move across a busy road and get to a pond. If it hits a car, the frog loses its life. The goal is to use the cursor keys to bring it safely across the road.

You should implement four lanes: two of the lanes with trucks and buses moving in opposite directions, and the other two with vintage cars (see the adjacent image).
 

There are two possible development paths available: you can either first implement the movement of the frog, or the movement of the vehicles. After this, you add the collision mechanism and the calculation of the game points, as well as the end of the game (game over).

In GameGrid the vehicles are modeled as instances of the class Car derived from Actor. The movement of the vehicles is programmed in the method act().

Use the images car0.gif,..car19.gif as sprites, located in the distribution of TigerJython. You can, of course, also use your own images (they should be a maximum of 70 pixels high and 200 pixels wide with a transparent background).

With arcade games it is common to use a game board with a size of 1 pixel, i.e. the grid corresponds to the pixel grid. Choose 800 x 600 pixels as a window size and display the road scenario using the background image lane.gif with the size 801 x 601 pixels. Generate 20 car objects in the function initCars() and think about where and with which viewing direction you want to add them to the game board.

Moving the cars with the method act() is easy: you push them on with move(), and let the cars moving from left to right jump to the far left as soon as they have driven out of the window, and similarly let the cars moving from right to left jump to the right. Remember that the location coordinates of actors can also be outside of the screen.

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

 

 

MEMO

 

Usually a game grid with a cell size of 1 pixel is used in arcade games (pixel game).
The game scene is rendered 20 times per second in a simulation period lasting 50 ms, resulting in a relatively good flow of movement. Sporadic jerks are caused by a computer's low processing power. Moreover no special graphics accelerator is used. Due to the limit of computing power, the simulation period cannot be significantly reduced

 

 

MOVING THE FROG WITH THE CURSOR KEYS

 

Now you can start thinking about how to incorporate the frog into the game. It should first appear at the bottom of the screen, and should then move with the up, down, left, and right cursor keys.

Since the frog is also an Actor, first write the class Frog which you derive from Actor. You do not need any methods besides the constructor, since the frog is moved by keyboard events. For this, define the callback onKeyRepeated, that you register through the call makeGameGrid() with the parameter named keyRepeated. This callback is not only called once when you press the key, but also periodically if you hold it down.

You test the key code in the callback and move the frog 5 steps further accordingly.

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

 

 

MEMO

 

In order to capture keyboard events, you can also register the callbacks keyPressed(e) and keyReleased(e). In contrast to keyRepeated(code), the key code must be fetched from the parameter e with e.getKeyCode(). Moreover, keyPressed(e) is less suitable in this game because there is a delay after pressing and holding down the button until the following press events are triggered.

If you do not know the key codes, you should write a small test program that writes them out:


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

 

 

COLLISION EVENTS

 

The procedure to detect collisions between actors is simple: When generating a vehicle car, you say that the frog should trigger an event when colliding with a car, using the method

frog.addCollisionActor(car)

The collision event triggers the method collide(), located in the class Frog. There, you treat the event according to your own wishes, for example you could make the frog jump back to the starting position.

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

 

 

MEMO

 

The method collide() is not an actual callback, but rather a method of the class Actor that is overridden in Frog. This is why you do not need to register collide() with a named parameter.

By default, the collision event is triggered when the bounding rectangles of the sprite images overlap. However, you can also change the collision areas in shape, size, and position to fit the sprite image. For this, you can use the following methods of the class Actor:

Methode Collision area

setCollisionCircle(centerPoint, radius)

circle with a given center and radius (in pixels)
setCollisionImage() non-transparent image pixels (only with a partner that has a circle, line, or point as a collision area)
setCollisionLine(startPoint, endPoint) line between the given start and end points
setCollisionRectangle(center, width, height) rectangle with a given center, width, and height
setCollisionSpot(spotPoint) an image pixel

All methods use a relative pixel coordinate system with the zero point in the center of the sprite image, a positive x-axis going to the right, and a positive y-axis going down.


The frog image is 71 x 41 pixels in size. So, for example, you can add the following to the constructor of Frog

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

so that a vehicle has to drive over the circle with a radius of 5 pixels around the head of the frog to trigger a collision event.

 
(Since the collision area is cached for efficiency reasons, it may be necessary to restart TigerJython so that your changes are taken into effect.)

 

 

GAME SUPERVISOR AND SOUND

 

In many games it is necessary that an "independent game supervisor" is made responsible for the compliance with the game rules, the distribution of points, and the proclamation of the winner at game over. Similar to daily life, it is also better if the task is not allocated to a character in the game, but rather to an independent part of the program. The main part of the program is especially well suited for this, which continues to runs after the initialization of the game. Add a loop to the end of the existing program so that it periodically checks the game and reacts accordingly. You should, however, not implement a very tight loop without a delay(), as this will unnecessarily waste computing power, which can lead to delays in the remaining execution of the program. The loop should stop when the game window is closed and isDisposed() returns True. The supervisor can, for example, limit the number of attempts and also count and display the number of successful and unsuccessful crossings of the road.

Dealing with the game over situation is often especially tricky, since one has to consider different variants. It is also often the case that you want to play the game several times without restarting the program.

You can use your knowledge from the chapter Sound to include sound effects. The easiest way to do this is to use the function 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)
Highlight program code (Ctrl+C to copy, Ctrl+V to paste)

 

 

MEMO

 

The counting of successes with nbSuccess and failures with nbHit takes place in the class Frog. This is why these variables have to be declared as globals.

At game over an Actor image with a text is inserted, the frog is removed, the simulation cycle is stopped with doPause(), and finally, the supervisor loop is left with break. You could also use a TextActor, which makes it possible to adjust the text at runtime.

rate = nbSuccess / (nbSuccess + nbHit)
ta = TextActor("  Game Over: Success Rate = " + str(rate) + " % ",  
     DARKGRAY, YELLOW, Font("Arial", Font.BOLD, 24))
addActor(ta, Location(200, 287))

 

 

EXERCISES

 

1.


Replace the background image and the vintage car photos with animal images that swim in a river (crocodiles, etc.).

2.

Introduce a scoring system and a time limit for the crossing: Each successful crossing should give you 5 points, each hit should take away 5 points. Exceeding the time limit should minus 10 points and put the frog back at the starting point.


3.

Add some of your own ideas to the game.