Page 1

ISSUE 4: What goes up…

Add collisions.  Add gravity. Add explosions. Add water. Add camera tricks. Improve fog, lighting, sky. Issue 4, all done!
  • Delicious
  • Digg
  • Reddit
  • StumbleUpon
  • Twitter

Issue 4 already? Time flies! In this one we’re going to do a bit of housekeeping. As a project grows you often need to refactor (rewrite) parts of your code to keep the code scalable and readable. Our approach is what you might consider ‘rapid application development’ (RAD). An alternative approach would have been to sit down and work out the whole game in advance – before writing any code – but the MGF aim is “results, fast” so the more traditional ‘waterfall’ approach is out the window! We want fun while we learn. You’ll also be adding collisions to your game, explosions, gravity, water effects and a bit of camera trickery. As promised in Issue 3 we will also explain and improve the fog, lighting and sky.

1. Imports and Modules

Unlike last time, where we gave you the whole code in advance, we are opting this time to make changes to the code one at a time. Don’t worry – we will give the full code listing at the end of the issue! To begin with, update the start of your code, your ‘imports’ to look like this instead:

    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

Quite a few additions, but all will be used by the end of the issue! So what exactly are these imports doing? In Python, there a number of ways to import what you need. So let’s start with the simplest. Open up a text editor and enter the following code:

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

Save the file in a directory somewhere (not the same one as your game, as this is illustrative only). Save it as ‘mgfutil.py’. The file name is important! Now, in the same directory, create a ‘test.py’ file containing the following:

        # 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

Save it. Now run it:

python test.py

It should run and complete without any errors or warnings. Hopefully this example helps clarify. The “import mgfutil” caused the Python interpreter* to look for mgfutil.py and load it. It’s that simple. In our game, you see examples using the dot notation (e.g. import direct.showbase.ShowBase). The dots are used to give directory paths. If you had saved mgfutil.py in a sub-directory called “libs” you would have to “import libs.mgfutil”.

The code you saved in mgfutil.py is considered in Python to be a ‘module’. A module is simply a collection of Python definitions and statements saved in a ‘.py’ file.

* Interpreters and Compilers: The Python Interpreter is a computer program, it is the python command you execute from the command line. Its job is to execute the instructions it is given (your code!). Try running Python without specifying a .py file – you will get an interactive prompt where you can enter (and execute) Python code one line at a time! The alternative to an interpreted language such as Python is a compiled language. With compiled languages your code is translated to machine code the computer can directly understand (e.g., an EXE file on Microsoft Windows). This is done via a compiler.

Which is best?

It depends. Compiled languages tend to be faster but platform specific. Interpreted languages tend to be cross platform. Take the Panda3D engine for instance. It is written in a compiled language for speed (C++ in this instance). That’s why, in Issue 1, you had to download the Panda3D version for your operating system. Writing applications using Panda3D is done using Python as we know (though it can also be done in C++). Your Python code will work just fine on Windows, Linux, Mac and more.

There is a third option that is essentially ‘somewhere inbetween’. Java is an example of such a language. The code is compiled to a platform independent ‘bytecode’ instead of machine code. This is then executed via the Java Virtual Machine (JVM). The JVM is the platform specific portion that translates the bytecode to machine code.

But wait – we have all manner of includes in our game code, but not that much in our game directory, so what’s going on? Python uses a path – a set of directories to search in order to find anything you might import. If you enter the code below in a new file, save it as showpath.py, and execute it, it will print your path out:

    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

The Panda3D installation you did in Issue 1 will have made sure the engine libraries (modules!) are in the Python path. For example, on our install under Linux, the file ShowBase file is here:

/usr/local/share/panda3d/direct/showbase/ShowBase.py

Ok, a more complex example. Notice in your test code that to create an MGFMaths object you had to write:

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

In Python, and many other languages, a concept exists called the ‘namespace’. Think of it as dividing up code into named blocks! To access MGFMaths we have to grab it from the mgfutil namespace. The code that is in mgfutil is NOT by default accessible in your main program. It is in a different namespace. The concept is important – particularly in large applications using many libraries. What would happen if there was no namespace and you imported two modules – both of which provided an MGFMaths class? Unlikely given what we named our class, but that’s because we like to think about ‘good naming’ when we code. 🙂

This is fine. But suppose now that MGFMaths provided lots of different functionality – 10s of classes. Some we need, some we don’t. Our code would be littered all over the place with “mgfutil.”. Every time we use anything from the mgfutil module we have to prefix the code.

Now update your test.py file 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

Notice this time the “from” syntax. This is saying “from the mgfutil module import the MGFMaths class into our namespace”. Hence, later in the code, you note we did not have to prefix creating the object (myMath = MGFMaths).

….and that’s what you are seeing in many of our game import statements. Notice later in your game code that the AmbientLight object is simply created (in your createEnvironment method) – no prefixes – because we used a from/import at the start of our code.

Don’t worry too much about this next one for now, but you can also assign an import to a variable:

    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

There is one more import approach which we’ll cover for completeness but strongly discourage you from using it!

from direct.showbase.ShowBase import *

The asterix means “everything”. In our game we only “import ShowBase” – it’s the only thing we want from the module. Importing “*” would import every all other classes/functions/data the module provided into our current namespace. A bad idea. Remember this: good code comes when we divide and conquer.

Quick Nav: Next page |

Index: page 1 | page 2 | page 3 | page 4 | page 5 | page 6 |
All: View full Issue on one Page