December 12, 2010

ISSUE 4: What goes up…

3. Explosions

What happens when the player crashes? Ultimately, we’ll be looking at a system of ‘lives’ leading to the classic ‘game over’. Prior to that, we need to make it so that when the player dies we display an explosion and then take a ‘reset’ action. Add the following method to your code:

    def resetPlayer(self): 
        self.player.show() 
        self.player.setPos(self.world,self.startPos) 
        self.player.setHpr(self.world,self.startHpr) 
        self.speed = self.maxspeed/2 

Now, we have lines similar to this in our constructor already. Good code does not duplicate, it reuses. So, find the corresponding lines in your constructor that set the position, orientation and speed and remove them. Then, in their place, add a call to our new method. You will also need to define ‘startPos’ and ‘startHpr as they are used in the method above. The whole ‘block’ in our constructor now looks like this:

        self.maxspeed = 100.0 
        self.startPos = Vec3(200,200,65) 
        self.startHpr = Vec3(225,0,0) 
        self.player = self.loader.loadModel("alliedflanker.egg") 
        self.player.setScale(.2,.2,.2) 
        self.player.reparentTo(self.render) 
        self.resetPlayer() 

We’ll show you our full code at the end of the Issue in case you have problems or simply wish to compare. Notice also that in our method we’ve added a bit of speed at the start. Rather than the game starting with a stationary player we are opting to start the game with the player flying at a moderate speed. The “Vec3” is new. A Vec3 is a 3-component vector. Both setPos and setHpr will accept a Vec3 as a parameter as you can see in the resetPlayer method.

Run your game and crash into the terrain. You should find the game ‘starts again’.

Next, we need an explosion. We’re opting for a simple and easy trick for this one. We are going to use a model that is a small square. The model will be textured by a ‘red ring’ though you can edit the image to be whatever you like! When the player ‘explodes’ we will hide the player model, display the red ring in its place, and over a number of frames scale the red ring to be larger and larger giving an exploding effect. The model is about as simple as a model can be. In fact, it’s that simple that in Panda3D we could create it in a few lines of code and we’ll discuss that possibility in the future. In this Issue, however, we instead giving the files for download. Download the model and the image below and place them in your game directory:

(your may need to right-click and save-as depending on your browser/operating system)

In your constructor, load the explosion ring as follows:

        # load the explosion ring 
        self.explosionModel = loader.loadModel('explosion') 
        self.explosionModel.reparentTo(self.render) 
        self.explosionModel.setScale(0.0) 
        self.explosionModel.setLightOff() 
        # only one explosion at a time: 
        self.exploding = False

By setting the scale of the ring to zero, it is not visible in the game by default. Did you notice something unusual in the code snippet? When we called loadModel we simply said ‘explosion’ and not ‘explosion.egg’.

Panda3D has a configuration variable for the ‘default model extension’ which is set to .egg. You will see why when we reach the stage of packaging the game for distribution. We have been specifying file extension thus far because it made the code a little easier for new developers to understand. With this in mind:

  • Update every call in your code that references an egg file.
  • Remove the file extension.
  • Run your game again, it should still work perfectly fine!
  • NOTE: this only works for egg files, you will still need the file extension for .bam files!

While we are here, we can tidy up our game directory. Create a sub-directory where your game resides called ‘models’. Move all of your egg files, bam files and their images into this directory. Ours looks like this:

+models
    alliedflanker.egg
    alliedflanker.jpg
    clouds.png
    explosion.egg
    explosion.png
    sky.egg
    square.egg
    water.png
    world.bam

Yours should look the same though you will currently be missing the “square.egg” file. In your game top-level directory you should now only have one file – your .py file. It’s all a lot tidier and easier to work with! You will need to again update your loadModel calls to match (e.g. loadModel(‘models/sky’)). Notice also that there is no height map or colour map image file. When we converted our GeoMipTerrain into a bam file in Issue 2 we removed the need for the image files.

Now to make the explosion happen. Again, some house keeping first. Remove all of the code in your updatePlayer method that related to enforcing world boundaries. Move this code into a new method called applyBoundaries as follows:

    def applyBoundaries(self): 
        if (self.player.getZ() > self.maxdistance): 
            self.player.setZ(self.maxdistance) 
        # should never happen once we add collision, but in case: 
        elif (self.player.getZ() < 0): 
            self.player.setZ(0) 

        # and now the X/Y world boundaries: 
        boundary = False        
        if (self.player.getX() < 0): 
            self.player.setX(0) 
            boundary = True 
        elif (self.player.getX() > self.worldsize): 
            self.player.setX(self.worldsize) 
            boundary = True 
        if (self.player.getY() < 0): 
            self.player.setY(0) 
            boundary = True 
        elif (self.player.getY() > self.worldsize): 
            self.player.setY(self.worldsize) 
            boundary = True 

        # lets not be doing this every frame... 
        if boundary == True and self.textCounter > 30: 
            self.statusLabel.setText("STATUS: MAP END; TURN AROUND") 
        elif self.textCounter > 30: 
            self.statusLabel.setText("STATUS: OK") 

        if self.textCounter > 30: 
            self.textCounter = 0 
        else: 
            self.textCounter = self.textCounter + 1 

It’s mostly the same as the code was when it was in updatePlayer. Though we have added a new section at the end (beginning “lets not be doing this every frame”). The new section simply adds a status indicator on the screen. It will read “OK” or, if the gamer attempts to fly off the end of our world, it will advise them to “TURN AROUND”. The text counter we added previously in our constructor. The purpose is simple. Updating the on screen text every single frame is just not necessary. This is again a ‘performance tuning’ tweak. We have opted to update the on screen text every 30 frames. This is far ‘easier’ for your CPU with little noticeable impact to the player.

Now change your updatePlayer method. It should roughly read as per below (we have omitted the bulk of the code for clarity as it is unchanged):

    def updatePlayer(self): 
        # scale, climb, bank, speed factor 
        # (CODE OMITTED IN SNIPPET)

        # Climb and Fall 
        # (CODE OMITTED IN SNIPPET)

        # Left and Right 
        # (CODE OMITTED IN SNIPPET)

        # throttle control 
        # (CODE OMITTED IN SNIPPET)

        # move forwards - our X/Y is inverted, see the issue 
        if self.exploding == False: 
            self.player.setX(self.player,-speedfactor) 
            self.applyBoundaries() 

It’s the last 3 lines we are interested in. All of our boundary checking has been removed from the updatePlayer method. Instead, we call our new method ‘applyBoundaries’. We also check our ‘exploding’ variable. A boolean variable (boolean = True or False). We do not want the player to move while exploding, hence the check. If the player is not moving, there’s no need to check the boundaries either. Hence both statements are nested in this IF statement.

Return now to your updateTask method and adjust it to look like this:

    def updateTask(self, task): 
        self.updatePlayer() 
        self.updateCamera() 

        self.collTrav.traverse(self.render) 
        for i in range(self.playerGroundHandler.getNumEntries()): 
            entry = self.playerGroundHandler.getEntry(i) 
            if (self.debug == True): 
                self.collisionLabel.setText("DEAD:"+str(globalClock.getFrameTime())) 
            if (self.exploding == False): 
                self.player.setZ(entry.getSurfacePoint(self.render).getZ()+10) 
                self.explosionSequence() 
        return Task.cont 

Essentially, when the player collides and the player is not already exploding, begin exploding. It would look horrible without this check as an explosion would commence for every detected collision. We only want one collision! Notice also we bump the players height a little. The player has collided with the terrain and we want to begin an explosion around the player. But that would result in an explosion ‘within the terrain’ (because that’s where the player is!). So, we simply bump the position height a little so the explosion will be nice and visible.

You will, of course, need the explosionSequence method. Here it is along with a new method it relies on called ‘expandExplosion’:

    def explosionSequence(self): 
        self.exploding = True 
        self.explosionModel.setPosHpr( Vec3(self.player.getX(),self.player.getY(), \ 
                               self.player.getZ()), Vec3( self.player.getH(),0,0)) 
        self.player.hide() 
        taskMgr.add( self.expandExplosion, 'expandExplosion' ) 

    def expandExplosion( self, Task ): 
        # expand the explosion rign each frame until a certain size 
        if self.explosionModel.getScale( ) < VBase3( 60.0, 60.0, 60.0 ): 
            factor = globalClock.getDt() 
            scale = self.explosionModel.getScale() 
            scale = scale + VBase3( factor*40, factor*40, factor*40 ) 
            self.explosionModel.setScale(scale) 
            return Task.cont 
        else: 
            self.explosionModel.setScale(0) 
            self.exploding = False 
            self.resetPlayer() 
            # and it stops the task 

The first thing we do is set exploding to True. As noted previously, ‘one explosion at a time please’ for the player. We then set the position and orientation of the explosion model to match the player. Next, hide the player (it would look awful strange if there was an explosion and the player model remained on screen unaffected!). You may have been wondering why we added a “show” call in our resetPlayer method, now you know! After this, we begin the explosion sequence by adding a task to the task manager. This should all be quite familiar now based on what we covered in previous issues. The method to handle the update task is expandExplosion.

expandExplosion checks the scale of the explosion model (remember, it begins at zero). If it is less than 60 we scale the explosion up. In other words, we want to draw the explosion growing from a size of zero to 60. 60 is just an arbitrary number we picked that gave good results, feel free to change it to suit your game! If it has reached the maximum scale, we put it back to zero (make the explosion disappear), set exploding to false, and reset the player. Notice that we do not ‘return’ anything in that scenario, this achieves the result of removing the task from the task manager (because we are done exploding!). In the case where we are still growing the explosion, we return ‘Task.cont’, thus knowing expandExplosion will get called the next frame.

Run the game, crash the player and you should now see a nice explosion before the player is reset to the start position.