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!
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.
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:
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.
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).
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.