February 28, 2011

ISSUE 7: Do you read me, HAL?

5. More elaborate State Machines

Panda3D does actually include classes specifically for FSMs. We have opted not to use them at this stage (our machine and logic is really quite simple, implemented with an ‘if’ statement!) but may revert to them in the future if needed. If you are interested we recommend viewing the Panda3D Manual Page for FSMs. If you’re feeling adventurous, try and adapt the code we have provided to use the FSM classes from the Panda3D library. A short example is shown below:

from direct.fsm.FSM import FSM
 
class EnemyFSM(FSM):
    def __init__(self):
        FSM.__init__(self, 'EnemyFSM')
 
    def enterPursue(self):
        # accelerate etc.
 
    def exitPursue(self):
        # exit actions

    def enterAttack(self):
        # attack
 
    def exitAttack(self):
        # exit actions
 
myfsm = EnemyFSM()
myfsm.request('Pursue')

6. Screen resolutions

Does your game support multiple screen resolutions? Yes, it does! Changing screen resolution can be done quite easily in code. There is also a global configuration file for Panda3D called config.prc that sets the defaults (a future topic) and you can also have a separate .prc file per project/game (again, a future topic). In code, you can simply open up volume1/game.py and add, after your import statements, the lines:

from panda3d.core import loadPrcFileData
loadPrcFileData('','win-size 1024 768')

In the example given, the screen is set to a width of 1024 and a height of 768. We suggest keeping with standard screen resolutions and suggest keeping the resolution relatively low: 640×480 (VGA), 800×600 (SVGA), 1024×768 (XGA), 1280×768 (WXGA) or 1280×960 (SXGA-).

We tested in a number of other resolutions too and the game worked perfectly. The only instances of any problems we could find pertained to the on screen text messages. We fixed this via modifying the makeStatusLabel method in game.py to read as follows:


    def makeStatusLabel(self, i):
        """ Create a status label at the top-left of the screen,
        Parameter 'i' is the row number """
        return OnscreenText(style=2, fg=(.5,1,.5,1), pos=(-1.75,0.92-(.08 * i)), \
                               align=TextNode.ALeft, scale = .08, mayChange = 1)

While we are here, this method really belongs in our Game GUI and not the main logic. Open up gui.py and move the makeStatusLabel method from game.py (ArcadeFlightGame) into gui.py (GameGUI). Then, in the ArcadeFlightGame constructor, move the lines:

        self.statusLabel = self.makeStatusLabel(0)
        self.collisionLabel = self.makeStatusLabel(1)

…to after the declaration of self.gui and change them to read:

        self.statusLabel = self.gui.makeStatusLabel(0)
        self.collisionLabel = self.gui.makeStatusLabel(1)

You will also need to add imports to gui.py:

from direct.gui.OnscreenText import OnscreenText
from panda3d.core import TextNode

…the same imports can be removed now from game.py.

Since we have covered default parameters in this Issue we may as well quickly cover named parameters! Look at the call to OnscreenText in makeStatusLabel. The parameters are named. This helps code readability but also means parameters can be in a different order. This approach is used quite often when there are many parameters and many have default values (i.e., you only want to pass a few values in, not all, so we make it clear what we are passing).

Would you like to try the game running in full screen? You can do it via adding the line:

loadPrcFileData('', 'fullscreen t') 

…after the import. 🙂 You can also change the title bar for when in Windowed mode (it currently reads ‘Panda’ as we’re sure you noticed):

loadPrcFileData("", "window-title All Known Threats - MGF Volume 1") 

7. Picture in Picture (PIP) and taking Screenshots

Picture in Picture

We thought it might be nice to have a ‘picture in picture’ feature to the game. Let the player see the game as normal but, at the same time, some ‘mini screens’ showing a left, right and backward looking viewpoint of the action. It’s actually quite easy to do!

First, our updated constructor in game.py declares the new cameras:

    def __init__(self):
        ShowBase.__init__(self)
        self.debug = False
        self.maxdistance = 400
        self.player = AlliedFlanker(self.loader,self.render,self.taskMgr)
        self.world = GameWorld(1024,self.loader,self.render,self.camera)
        self.gui = GameGUI(self.render2d)
        self.statusLabel = self.gui.makeStatusLabel(0)
        self.collisionLabel = self.gui.makeStatusLabel(1)
        self.taskMgr.add(self.updateTask, "update")
        self.keyboardSetup()

        # performance and map to player so can't fly beyond visible terrain
        self.player.setMaxHeight(self.maxdistance)

        self.leftCam = base.makeCamera(base.win, \ 
                            displayRegion = (0.79, 0.99, 0.01, 0.21)) 
        self.rightCam = base.makeCamera(base.win, \ 
                              displayRegion = (0.79, 0.99, 0.23, 0.43)) 
        self.backCam = base.makeCamera(base.win, \ 
                              displayRegion = (0.79, 0.99, 0.45, 0.65)) 
        if self.debug == False:
            self.camLens.setFar(self.maxdistance)
            self.leftCam.node().getLens().setFar(self.maxdistance*2.0)
            self.rightCam.node().getLens().setFar(self.maxdistance*2.0)
            self.backCam.node().getLens().setFar(self.maxdistance*2.0)
        else:
            base.oobe()

        self.camLens.setFov(60)
        self.leftCam.node().getLens().setFov(60)
        self.leftCam.node().setActive(False)
        self.rightCam.node().getLens().setFov(60)
        self.rightCam.node().setActive(False)
        self.backCam.node().getLens().setFov(60)
        self.backCam.node().setActive(False)

        self.setupCollisions()
        self.textCounter = 0

        newGameSound = base.loader.loadSfx("volume1/sounds/gamestart.wav")
        newGameSound.play()

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

Next, we update our keyboardSetup method. We will use the ‘p’ key for PIP and will only show PIP while the key is held down. Once released, we will again hide the PIP cameras. The code below also adds support for the ‘s’ key which we will use for taking screen shots in a moment:

        self.keyMap = {"left":0, "right":0, "climb":0, "fall":0, \
                        "accelerate":0, "decelerate":0, "fire":0, \ 
                        "cam":0, "pip":0, "shot":0}
        self.accept("p", self.setKey, ["pip",1])
        self.accept("p-up", self.setKey, ["pip",0])
        self.accept("s", self.setKey, ["shot",1])
        self.accept("s-up", self.setKey, ["shot",0])

In your actionInput method, activate/deactivate the cameras:

        if (self.keyMap["pip"]!=0 and self.enemyCam == False):
            self.leftCam.node().setActive(True)
            self.rightCam.node().setActive(True)
            self.backCam.node().setActive(True)
        else:
            self.leftCam.node().setActive(False)
            self.rightCam.node().setActive(False)
            self.backCam.node().setActive(False)

All we need to do now is control what these cameras are looking at. Modify the updateCamera method as follows:

    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)
            self.player.lookAtMeLeft(self.leftCam)
            self.player.lookAtMeRight(self.rightCam)
            self.player.lookAtMeBack(self.backCam)

The supporting methods need to be added to AlliedFlanker in player.py as shown below:

    def lookAtMeLeft(self,camera):
        camera.setPos(self.player,0,16,3)
        camera.setHpr(self.player,-180,-20,0)

    def lookAtMeRight(self,camera):
        camera.setPos(self.player,0,-16,3)
        camera.setHpr(self.player,0,-20,0)

    def lookAtMeBack(self,camera):
        camera.setPos(self.player,-9,1.5,3)
        camera.setHpr(self.player,-90,-30,0)

That’s it for PIP! Give it a try by launching the game then holding down the ‘P’ key. Finally, to add in screen shot support, add the following to actionInput in game.py:

        if (self.keyMap["shot"]!=0):
            base.screenshot()
            self.keyMap["shot"] = 0

Run the game, press the ‘S’ key. Now quit the game. You should find a new image file in the main game directory! A screen shot of the game as it was when you pressed the ‘S’ key! Simples. 🙂

8. Wrap up and Summary

A lot of ground covered! We introduced AI and implemented a simple state machine for our enemy. We need to add enemy collisions (and sounds, and damage, and firing!). We have also added a nice PIP feature along with the ability to take screen shots. Quite a productive Issue!

Goto Issue 8 >>>