Specifically, we'll carry out the following modifications:
As usual, begin by creating the lab8 directory:
cd games mkdir lab8 cd lab8Then copy/save today's lab files: ship.gif, star.gif, backdrop.gif, game8.py.
Make the python files executable and open the game8.py
chmod u+x *.py gedit game8.py &
This first part is just a maintenance simplification: instead of having numeric values scattered throughout the code for scaling up/down the size and speed of the ship we will use a single variable within the game objects.
First, this involves adding the definition in the game object class, e.g.
class GameObject(pygame.sprite.Sprite): scaleFactor = 1.5 |
Second, in the routines changeSpeed and changeScale we replace the old numeric resizing of self.velocity and self.scale:
self.scale = self.scale / 1.25 becomes self.scale = self.scale / self.scaleFactor self.scale = self.scale * 1.25 becomes self.scale = self.scale * self.scaleFactor self.velocity = self.velocity * 2 becomes self.velocity = self.velocity * self.scaleFactor self.velocity = self.velocity / 2 becomes self.velocity = self.velocity / self.scaleFactor |
Next, we'll let each game object have an integer id value associated with it, and allow the game to assign id's when the object is first created.
This requires an update to the ship constructor (__init__ routine):
def __init__(self, image, x, y, direction, speed, id): pygame.sprite.Sprite.__init__(self) self.shipID = id |
It also means the main routine needs to pass along values when the constructors are first called, e.g. 10001, 10002, 10003 in the calls below:
GameObject('ship.gif', 80, 100, 270, 3, 10000), GameObject('ship.gif', 480, 120, 180, 0, 10001), GameObject('ship.gif', 520, 320, 90, 0, 10002), |
Having done this, the various output routines can now name which specific ship is being manipulated (helps in debugging and reporting), e.g.:
def changeSpeed(self, change): print "Ship", self.shipID, " changing speed" |
In addition to the ship id, we'll also add a ship health stat, that starts off at 100 and ultimately gets reduced during collisions.
This involves adding one more line of code to the game object constructor (__init__ routine):
self.health = 100 |
The various AI ships will need to know which ship currently belongs to the player, so they can chase it appropriately.
We used to use the field selected in the game objects to keep track of whether the current ship was player controlled or not, now we'll simply use that to keep track of specifically which ship the player controls.
This means a minor change to the __init__ routine:
self.selected = False becomes self.selected = None |
The changeSelected routine also needs to change:
def changeSelected(self, selObj): # keep track of which object is now selected self.selected = selObj |
Next, the event handler needs to change to tell all ships whenever the player picks a new ship to control, e.g.
objList[selObj].changeSelected(False) and objList[selObj].changeSelected(True) get replaced with for obj in objList: obj.changeSelected(objList[selObj]) |
Finally, the main handling routine needs to change to tell all ships which one the player initially controls, e.g.
gameObjList[selectedObj].changeSelected(True) becomes for obj in gameObjList: obj.changeSelected(gameObjList[selectedObj]) |
Within the game object update routine, if the current object is AI controlled then we'll have it check to see if it is currently colliding with the player ship.
If there is a collision, then have the player's ship lose some health, and end the game if the player's health drops below 0, e.g.
# if this isn't the player controlled ship... if not (self == self.selected): # check for collisions with the player's ship if self.rect.colliderect(self.selected.rect): # decrement the player ship's health by 10 self.selected.health = self.selected.health - 10 # end the game if the player's health goes below 0 if self.selected.health < 0: print "***GAME OVER***" # pause the game for 5 seconds pygame.time.delay(5000) # then exit sys.exit() |
We can add another snippet to the code from step 5, having the AI ship jump to a random location after the collision:
# since the game didn't end, teleport the AI someplace random self.x = random.randint(16, scrWidth-16) self.y = random.randint(16, scrHeight-16) |
Of course, this means that we need to specify that the update routine uses globals scrWidth and scrHeight, e.g.
def update(self): global scrWidth, scrHeight |
This also means that we need to import the random module along with the others at the beginning of the program, e.g.
import pygame, sys, random |
Finally, this requires that, during the gameSetup routine, we initialize the random number generator. This is done with a built-in seed routine:
def gameSetup(): # initialize the random number generator random.seed() |
The interesting bit is getting the AI to chase the player around the map. To do so, we'll add (in GameObject) a routine called chasePlayer that tries to match the player's zoom level and speed, and figures out roughly which direction the AI should be facing to be pointed "at" the player's current ship.
# the chasePlayer routine adjusts an AI object's current facing, # zoom, and speed to pursue the player-controlled object def chasePlayer(self): # start matching the player's speed if self.velocity < self.selected.velocity: self.velocity = self.velocity * self.scaleFactor if self.velocity == 0: self.velocity = 1 elif self.velocity > self.selected.velocity: self.velocity = self.velocity / self.scaleFactor # start matching the player's zoom factor if self.scale < self.selected.scale: self.scale = self.scale * self.scaleFactor elif self.scale > self.selected.scale: self.scale = self.scale / self.scaleFactor # change facing to roughly point at the player ship if self.selected.x < self.x: if self.selected.y < self.y: # the player's ship is NW of the AI ship self.facingDir = 90 else: # the player's ship is NE of the AI ship self.facingDir = 180 else: if self.selected.y < self.y: # the player's ship is SW of the AI ship self.facingDir = 0 else: # the player's ship is SE of the AI ship self.facingDir = 270 |
Next, we need to decide how often the AI will update it's pursuit path.
In the grand scheme of things, the relative positioning of the AI and player ship won't change hugely from update to update, since those are only 40 or 50 milliseconds apart.
As a result, it might be more efficient to give the AI a chance of updating its chase routine each time the update routine is called, e.g. in the update routine just after it figures out this is an AI ship...
if not (self == self.selected): # have a 10% chance of updating our pursuit plotting # every time update runs ... chance = random.randint(0,100) if (chance < 10): self.chasePlayer() |
It might also be advisable for the AI ships to re-think their pursuit plotting whenever the player switches ships, e.g.
def changeSelected(self, selObj): # keep track of the newly selected player ship self.selected = selObj # if we're an AI, start chasing the new player ship if not (self = selObj): self.chasePlayer() |
This might be a bit easier to keep track of if we have the ships 'wrap-around' when they go off screen edges, which is a basic modification to the game object update routine:
# ensure positions wrap-around if self.x > scrWidth: self.x = 1 elif self.x < 0: self.x = scrWidth - 1 if self.y > scrHeight: self.y = 1 elif self.y < 0: self.y = scrHeight - 1 |
#! /usr/bin/python import sys, pygame, random # ===================================================================== # GLOBAL VARIABLES # - list all variables that need to be globally accessible # define the size of the pygame display window scrSize = scrWidth, scrHeight = 640,480 gameScreen = None # the main display screen backImage = None # the loaded background image scrRefreshRate = 25 # the pause (in milliseconds) between updates keepPlaying = True # flag to identify if the game should continue # ===================================================================== # SETUP ROUTINE # - initializes the pygame display screen and background image def gameSetup(): # specify the global variables the setup routine needs to access global gameScreen, scrSize, backImage # initialize the randon number generator random.seed() # initialize pygame pygame.init() # initialize the display screen gameScreen = pygame.display.set_mode(scrSize) # load and display the background image backImage = pygame.image.load('backdrop.gif') gameScreen.blit(backImage, (0,0)) # ===================================================================== # GAME OBJECT # controls the basic movable in-game objects (ships in this case) # # All objects share a common scaling factor, used to determine # how rapidly objects change their speed and zoom factors # # Each game object has a number of additional properties: # # origImage: the loaded image to represent that object # image: the current display image for the object # (rotated and zoomed appropriately from the original) # position = x,y: the x,y coordinates of the object # facingDir: the direction the object is currently facing # velocity: the horizontal/vertical distance covered # by the object each game step # scale: the scaling (zoom) factor currently used for the object # selected: a reference to the ship or object the player has # currently selected # shipID: a unique integer id for the object # # Each game object also has several actions that can be applied to it: # # __init__ : the initialization routine for the object # update: the routine applied each step to update the # object's current location and image # changeFacing: a routine to provide a new facing direction # for the object # changeSpeed: a routine to increase or decrease the object's # current velocity # changeScale: a routine to zoom in/out on the object # (scaling the size of its image) # changeSelected: a routine to identify which object # the player has currently selected # chasePlayer: a routine to update the speed and facing # of AI-controlled ships to chase the player ship class GameObject(pygame.sprite.Sprite): # the shared scaling factor scaleFactor = 1.5 # the constructor (initialization routine) for the # movable game objects def __init__(self, image, x, y, direction, speed, id): # initialize a pygame sprite for the object pygame.sprite.Sprite.__init__(self) # record the object's unique id self.shipID = id # initialize the object's health at 100% self.health = 100 # load an image for the object self.origImage = pygame.image.load(image) self.image = self.origImage # set up the initial position for the object self.position = self.x, self.y = x, y # set up the initial direction for the object self.facingDir = direction # set up the initial scale (zoom) for the object self.scale = 0.333 # set up the initial speed for the object self.velocity = speed # initially treat all objects as unselected self.selected = None # the update routine adjusts the object's current position and # image based on its speed and direction def update(self): # note that we'll use the global screen width/height variables global scrWidth, scrHeight # calculate the object's new position based on its old position, # and its current facing and velocity if (self.facingDir == 0): self.x = self.x + self.velocity self.y = self.y - self.velocity elif (self.facingDir == 90): self.x = self.x - self.velocity self.y = self.y - self.velocity elif (self.facingDir == 180): self.x = self.x - self.velocity self.y = self.y + self.velocity elif (self.facingDir == 270): self.x = self.x + self.velocity self.y = self.y + self.velocity # ensure positions "wrap around" if self.x > scrWidth: self.x = 1 elif self.x < 0: self.x = scrWidth - 1 if self.y > scrHeight: self.y = 1 elif self.y < 0: self.y = scrHeight - 1 self.position = self.x, self.y # update (rotate and zoom) the image for the object self.image = pygame.transform.rotozoom(self.origImage, self.facingDir, self.scale) # position the image correctly self.rect = self.image.get_rect() self.rect.center = self.position # if this object is NOT currently the player controlled one # (i.e. not the currently selected object) then have # check to see if there has been a collision with the # player ship, otherwise have a 10% chance of the AI # updating its speed/facing to chase the player if not (self == self.selected): # check for collisions with the player if self.rect.colliderect(self.selected.rect): # decrement the player ship's health by 10 self.selected.health = self.selected.health - 10 print self.shipID, "collision with player, health now:", self.selected.health # if the player's health drops below 0 end the game if self.selected.health < 0: print "**************************" print "**************************" print "**************************" print "*****GAME OVER!!!!!!!*****" print "**************************" print "**************************" print "**************************" pygame.time.delay(5000) sys.exit() # otherwise randomly jump this ship somewhere else self.x = random.randint(16, scrWidth-16) self.y = random.randint(16, scrHeight-16) # otherwise have a 10% chance of performing # an update on our pursuit plotting else: chance = random.randint(0,100) if chance < 10: self.chasePlayer() # the turning routine turns the ship 90 degrees clockwise if # the turning direction is right ('r') or 90 degrees # counterclockwise if the turning direction is left ('l') def changeFacing(self, dir): # if the direction is left then # turn the object 90 degrees counterclockwise if (dir == 'l'): self.facingDir = self.facingDir + 90 if self.facingDir >= 360: self.facingDir = self.facingDir - 360 # otherwise, if the direction is right then # turn the object 90 degrees counterclockwise elif (dir == 'r'): self.facingDir = self.facingDir - 90 if self.facingDir < 0: self.facingDir = self.facingDir + 360 # the speed change routine increases the object's velocity # if the change is '+', or decreases the velocity # if the change is '-' def changeSpeed(self, change): # if the change is '+' then double the current speed # (or increase to 1 if the speed used to be 0) if (change == '+'): self.velocity = self.velocity * self.scaleFactor if self.velocity == 0: self.velocity = 1 # otherwise, if the change is '-' then cut the # current speed in half elif (change == '-'): self.velocity = self.velocity / self.scaleFactor # the zoom routine zooms in on the object (makes it larger) # if the scale is '+' or zooms out (makes it smaller) # if the scale is '-' def changeScale(self, scale): # zoom in on the object if the scale is '+' if (scale == '+'): self.scale = self.scale * self.scaleFactor # otherwise zoom out if the scale is '-' elif (scale == '-'): self.scale = self.scale / self.scaleFactor # the object selection routine notifies the object # which object has just been selected def changeSelected(self, selObj): # keep track of which object is now selected self.selected = selObj # if this is an AI object, make sure you adjust # to chase the new player object if not (selObj == self): self.chasePlayer() # the chasePlayer routine adjusts an AI object's current facing, # zoom, and speed to pursue the player-controlled object def chasePlayer(self): # start matching the player's speed if self.velocity < self.selected.velocity: self.velocity = self.velocity * self.scaleFactor if self.velocity == 0: self.velocity = 1 elif self.velocity > self.selected.velocity: self.velocity = self.velocity / self.scaleFactor # start matching the player's zoom factor if self.scale < self.selected.scale: self.scale = self.scale * self.scaleFactor elif self.scale > self.selected.scale: self.scale = self.scale / self.scaleFactor # change facing to roughly point at the player ship if self.selected.x < self.x: if self.selected.y < self.y: # the player's ship is NW of the AI ship self.facingDir = 90 else: # the player's ship is NE of the AI ship self.facingDir = 180 else: if self.selected.y < self.y: # the player's ship is SW of the AI ship self.facingDir = 0 else: # the player's ship is SE of the AI ship self.facingDir = 270 # ===================================================================== # EVENT HANDLING ROUTINE # - processes any pending in-game events, # returns which object is currently selected # (since this can be changed by some events) # the routine expects to be given a list of the game objects # currently available (objList) and which object is # currently selected/controlled by the player (selObj) def processEvents(objList, selObj): # specify which global variables the routine needs access to global keepPlaying # process each pending event for event in pygame.event.get(): # if the user closed the window set keepPlaying to False # to tell the game to quit playing if event.type == pygame.QUIT: keepPlaying = False # check if the user has pressed a key elif event.type == pygame.KEYDOWN: # the escape and q keys quit the game if event.key == pygame.K_ESCAPE: keepPlaying = False elif event.key == pygame.K_q: keepPlaying = False # the left and right arrows turn the currently selected ship # counterclockwise or clockwise, respectively elif event.key == pygame.K_LEFT: print 'turning left with ship ', selObj objList[selObj].changeFacing('l') elif event.key == pygame.K_RIGHT: print 'turning right with ship ', selObj objList[selObj].changeFacing('r') # the up and down arrows cause the currently selected ship # to speed up or slow down, respectively elif event.key == pygame.K_UP: print 'speeding up ship ', selObj objList[selObj].changeSpeed('+') elif event.key == pygame.K_DOWN: print 'slowing down ship ', selObj objList[selObj].changeSpeed('-') # the plus and minus keys zoom in/out on the currently # selected ship elif event.key == pygame.K_EQUALS: print 'zooming in on ship ', selObj objList[selObj].changeScale('+') elif event.key == pygame.K_MINUS: print 'zooming out on ship ', selObj objList[selObj].changeScale('-') # the tab key scrolls through the list of ships, # changing which one is currently selected elif event.key == pygame.K_TAB: # change to the next object in the list selObj = selObj + 1 if (selObj >= len(objList)): selObj = 0 # notify all objects that there is a new selection, # specifying which one is now the selected object for obj in objList: obj.changeSelected(objList[selObj]) print 'selected ship ', selObj # return the currently selected object number # (may have been updated by one of the events processed) return selObj # ===================================================================== # MAIN GAME CONTROL ROUTINE # - sets up the game and runs the main game update loop # until instructed to quit def main(): # identify any global variables the main routine needs to access global gameScreen, backImage, scrRefreshRate, keepPlaying # initialize pygame and the game's display screen gameSetup() # create the list of screen update sections (rectangles) updateSections = None # create an array of objects to add to the display, # giving each of them # an image, xcoord, ycoord, facing, speed, and unique id gameObjList = [ GameObject('ship.gif', 80, 100, 270, 3, 10000), GameObject('ship.gif', 480, 120, 180, 0, 10001), GameObject('ship.gif', 520, 320, 90, 0, 10002) ] # indicate which game object is currently 'selected' selectedObj = 0 for obj in gameObjList: obj.changeSelected(gameObjList[selectedObj]) # create a group for the game objects objGroup = pygame.sprite.RenderUpdates(*gameObjList) # run the main game loop keepPlaying = True while keepPlaying: # handle any pending events # (processEvents needs the list of objects that might # be affected, and which object is currently selected) selectedObj = processEvents(gameObjList, selectedObj) # update the display of the object groups objGroup.clear(gameScreen, backImage) # run the updates on each object in the group objGroup.update() # update the display rectangles updateSections = objGroup.draw(gameScreen) # update the buffered display pygame.display.update(updateSections) # switch to the new display image pygame.display.flip() # pause before initiating the next loop cycle pygame.time.delay(scrRefreshRate) # ===================================================================== # INITIATE THE GAME # - calls the main() routine if __name__ == "__main__": main() |