January 19, 2011

ISSUE 6: Know your Enemy

3. Coding with the Enemy

Create a new file in your game directory volume1/enemies.py, contents as follows:

from .player import AlliedFlanker
import random

class EnemyFlanker(AlliedFlanker):
    def __init__(self,loader,scenegraph,taskMgr):
        # take an ally and make it join the dark side...
        AlliedFlanker.__init__(self,loader,scenegraph,taskMgr)
        tex = loader.loadTexture('volume1/models/enemyflanker.jpg')
        self.player.setTexture(tex,1)

    def teleport(self,boundingBox):
        # random X/Y within bounds
        self.player.setX(random.randint(0,boundingBox))
        self.player.setY(random.randint(0,boundingBox))
        self.player.setZ(100)

For the most part, that is it! Look at the code above and notice it inherits from the AlliedFlanker class we already wrote. It then calls the parent constructor and then “does a bit extra” (i.e., introduces code specific to the enemy). Once again we see the power of object oriented programming. Why reinvent the wheel? We’ve already coded a functional flying aircraft, why not reuse it? You’ll notice also the constructor swaps out the texture for our new enemy version (the “,1” at the end of the setTexture call tells Panda3D we are replacing the texture). Beyond that, all we have added is a teleport routine to position the enemy randomly on the terrain (but always making sure it is above the terrains highest point; not actually that important as you’ll see in a future Issue that we simply won’t allow the enemy to collide with the terrain!). “randint” is provided via “random” (hence the import) and generates a psuedo-random number between two values (we say psuedo because if you drill down a little in Computer Science you will learn that there isn’t actually such a thing as ‘random’!). As an example, randint(0,5) would return a value of 0, 1, 2, 3 or 4. Note, not 5!

Turns out our enemies aren’t all that different from ourselves (political?).

All of the other changes are in volume1/game.py. First, add a new import:

from .enemies import EnemyFlanker

Now, add the following to the constructor:

        self.level = 1; self.score = 0; self.baddies = list()
        self.startLevel()
        self.enemyCam = False; self.currentEnemy = 0;

We set a level of 1 for the game, a player score of zero. We declare a list to hold baddies – initially empty. We call startLevel, a new method and we also set an enemyCam and currentEnemy variables. The last two will not make sense until the next section. Notice we have also done something new here – you can have multiple ‘lines’ of Python on one line if you separate the statements by semi-colons. Useful sometimes. Now add the startLevel method, the code being:

    def startLevel(self):
        if (self.level == 1):
           self.spawnBadGuys(2,1) # two level 1 baddies please
        # else - other levels, perhaps splash screen/menu injects here

Which, as you probably guessed, requires the addition of a spawnBadGuys method:

    def spawnBadGuys(self,howMany,skillLevel):
        # note we're not doing anything with skill level yet, as there's only 1 level!
        for i in range(0,howMany):
            self.baddies.append(EnemyFlanker(self.loader,self.render,self.taskMgr))
        # jump start em
        for baddy in self.baddies:
           baddy.teleport(self.world.getSize())

If you follow the code you will note that startLevel checks what level the game is on. Based on that, it spawns bad guys. Bad guys are created and then teleported to a random position (within world boundaries). If you run the game there are now two enemies. They don’t move yet and will simply hover where positioned. If you’re having trouble finding them (very likely as they teleport to random position) you can try:

  • Enabling the debug mode we build in previously.
  • Commenting out the teleport. The enemies will sit where the player starts! Fly out a bit and turn back.
  • Add some camera switching (see “By the power of…” below).
  • Get the RADAR on the HUD working (see the next page of this Issue!

4. By the power of Inheritance!

Ok, shockingly bad He-Man reference. 🙂 We digress. Recall back in Issue 5 we added a ‘lookAtMe’ method to the player? Well, the enemy has inherited this too! So with a bit of code tweaking, we should be able to build in, quite easily, a camera toggle. We’re going to let the player use the tab key on the keyboard to cycle through camera positions – including their own and the ‘lookAtMe’ position for each enemy (the so called ‘enemy camera’).

Update your keyboard setup in game.py to look like this:

    def keyboardSetup(self):
        self.keyMap = {"left":0, "right":0, "climb":0, "fall":0, \
                        "accelerate":0, "decelerate":0, "fire":0, "cam":0}
        self.accept("escape", sys.exit)
        self.accept("a", self.setKey, ["accelerate",1])
        self.accept("a-up", self.setKey, ["accelerate",0])
        self.accept("z", self.setKey, ["decelerate",1])
        self.accept("z-up", self.setKey, ["decelerate",0])
        self.accept("arrow_left", self.setKey, ["left",1])
        self.accept("arrow_left-up", self.setKey, ["left",0])
        self.accept("arrow_right", self.setKey, ["right",1])
        self.accept("arrow_right-up", self.setKey, ["right",0])
        self.accept("arrow_down", self.setKey, ["climb",1])
        self.accept("arrow_down-up", self.setKey, ["climb",0])
        self.accept("arrow_up", self.setKey, ["fall",1])
        self.accept("arrow_up-up", self.setKey, ["fall",0])
        self.accept("space", self.setKey, ["fire",1])
        self.accept("space-up", self.setKey, ["fire",0])
        self.accept("tab", self.setKey, ["cam",1])
        self.accept("tab-up", self.setKey, ["cam",0])
        base.disableMouse() # or updateCamera will fail!

The changes are on lines 3 and 19-20 above. All we have done is add support for capturing the TAB key on the keyboard being pressed. Now, at the end of your actionInput method, add:

        if (self.keyMap["cam"]!=0):
            if (self.enemyCam == False):
                self.enemyCam = True
                self.currentEnemy = 0
            else:
                # "drops off" see below
                self.currentEnemy = self.currentEnemy + 1
            # stop uber fast switching, have to key again
            self.keyMap["cam"] = 0

Remember, we want the TAB key to cycle from the players view through each of the enemies view. So, if we’re currently in enemy cam mode, switch to the next enemy (increment currentEnemy). There’s a danger we’ll run out of enemies and need to go back to the player cam, this is handled in updateCamera (see below). If we’re not already in enemy camera mode, switch to it and set currentEnemy to zero (the first enemy in our list of baddies). Notice we do one final thing – if the TAB key has been pressed we deal with it but then set the value of our keyMap to indicate it is not pressed. This is simply to stop rapid camera switching when you hold the TAB key down. Instead, you must repeatedly press TAB to cycle in the game. Finally, update your updateCamera method to check what viewpoint to show:

    def updateCamera(self):
        if self.enemyCam == True:
           if len(self.baddies) > self.currentEnemy:
               self.baddies[self.currentEnemy].lookAtMe(self.camera)
           else:
               self.enemyCam = False
        else:
            self.player.lookAtMe(self.camera)

Run the game and you should now find you can switch views via the pressing the tab key. Result!