December 29, 2010

ISSUE 5: A New Hope

2. Our New Design

….is by no means our final design! Yes, refactoring again. 🙂 We are going to do enough to make our game supportable for the foreseeable future. Again, as we have said before, there is a balance between over and under engineering a solution. We also do not want to run too far ahead as we realise there have been some fairly heavy concepts to absorb in our short five Issues to date!

New Design

The diagram above summarises our aim and design. Our application already inherits from ShowBase so that should not be new (the hollow triangle in the diagram). We have also shown that ShowBase actually inherits from DirectObject. This is new! DirectObject and ShowBase are both parts of the Panda3D library. You can start an application by inheriting from either. We chose ShowBase way back in Issue 1 because it does what DirectObject does “plus a bit extra”. The key parts of the diagram are our changes (from ArcadeFlightGame down in the diagram). In short they amount to:

  • Get rid of the MyApp class and replace that with an ArcadeFlightGame class. The new class is responsible for our game logic.
  • Add an AlliedFlanker class to deal with everything player related. Moving, exploding, rotating (etc.). The new ArcadeFlightGame class should simply have to call methods (behaviours) on an object of type AlliedFlanker.
  • Add a GameWorld class to deal with the terrain, lighting, sky and anything else pertaining to the ‘world’. The ArcadeFlightGame class should simply create a GameWorld object.

3. Refactor!

Again, as we pointed out above, watch out for the other changes and improvements we are making as we refactor! First, create a new folder somewhere on your PC. Copy your models and textures into the this new directory following the folder hierarchy/structure below:

New Folder Structure

You should already have all the files for the ‘models’ directory. For the Python (.py) files, just create empty files in a text editor for now (keeping to the file names as per above). We will provide the code for each file below along with some explanations.

newgamefolder/volume1/__init__.py
You actually leave this as a completely empty file! It tells the Python Interpreter that the directory Volume 1 is to be treated as a ‘package’. A package is a collection of modules. This is needed for the ‘dotted module name’ import syntax to work (as discussed right at the start of Issue 4).

newgamefolder/volume1/environment.py

from panda3d.core import AmbientLight, DirectionalLight, Vec4, Fog
from panda3d.core import Texture, TextureStage
from pandac.PandaModules import CompassEffect
from pandac.PandaModules import VBase4, TransparencyAttrib
from direct.interval.LerpInterval import LerpTexOffsetInterval, LerpPosInterval

class GameWorld():
    def __init__(self,size,loader,scenegraph,camera):
        self.worldsize = size
        self.loader = loader
        self.render = scenegraph
        self.camera = camera
        self.world = self.loader.loadModel("volume1/models/world.bam")
        # the model is 1024 already, so we scale accordingly:
        self.world.setScale(self.worldsize/1024)
        self.world.setPos(0,0,0)
        self.world.reparentTo(self.render)
        self.__createEnvironment()

    # private method
    def __createEnvironment(self):
        # Fog
        expfog = Fog("scene-wide-fog")
        expfog.setColor(0.5,0.5,0.5)
        expfog.setExpDensity(0.002)
        self.render.setFog(expfog)

        # Our sky
        skysphere = self.loader.loadModel('volume1/models/blue-sky-sphere')
        skysphere.setEffect(CompassEffect.make(self.render))
        skysphere.setScale(0.08)
        # NOT render or you'll fly through the sky!:
        skysphere.reparentTo(self.camera) 

        # Our lighting
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor(Vec4(.6, .6, .6, 1))
        self.render.setLight(self.render.attachNewNode(ambientLight))

        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setColor(VBase4(0.8, 0.8, 0.5, 1))
        dlnp = self.render.attachNewNode(directionalLight)
        dlnp.setPos(0,0,260)
        dlnp.setHpr(225,-60,0)#lookAt(self.player)
        self.render.setLight(dlnp)

        # water
        self.water = self.loader.loadModel('volume1/models/square.egg')
        self.water.setSx(self.worldsize* 2)
        self.water.setSy(self.worldsize*2)
        self.water.setPos(self.worldsize/2,self.worldsize/2,18) # sea level
        self.water.setTransparency(TransparencyAttrib.MAlpha) 
        nTS = TextureStage('1')
        self.water.setTexture(nTS,self.loader.loadTexture('volume1/models/water.png'))
        self.water.setTexScale(nTS,4)
        self.water.reparentTo(self.render)
        LerpTexOffsetInterval(self.water,200,(1,0),(0,0),textureStage=nTS).loop()

    def setGroundMask(self,mask):
        self.world.setCollideMask(mask)

    def setWaterMask(self,mask):
        self.water.setCollideMask(mask)

    def getSize(self):
        return self.worldsize


For the most part, this should look reasonably familiar. We have just moved all the environment related code out of MyApp into its own class called GameWorld. There are a few new methods at the bottom that are new and you will see why they are needed as we progress. We’re essentially wrapping up the environment code and providing an interface via those new methods. You’ll notice that the __createEnvironment method name starts with two underscores (__). This makes the method a private method. Private methods can only be used ‘within the class itself’. The constructor calls __createEnvironment and that is fine. But this would be an error:

someVar = GameWorld()
someVar.__createEnvironment() # would NOT be allowed

Private methods are useful in the situation where you feel a class needs a method – but only for internal use. You can do the same with member variables, make them private via a double-underscore prefix. Sadly, in Python, it’s a bit of a hack under the hood. The double-underscore prefix actually causes something called name mangling (to be discussed in the future!). Private variables, with a bit of code magic, can still always be accessed and modified. Other languages are stricter about this. Methods that are not private are termed ‘public’. Some other languages offer further levels of control (e.g., C++ allows for ‘protected’ variables and methods), Python does not.