The plan is to set up our game to have a demo race going on the start screen, and create a new race when we click a menu button. That means we'll need to be able to dynamically construct and destroy races, so we should make a Race
class for that. To further expand our menu use, we'll create a confirmation dialog menu so the player can let the game know when he's ready for the race to start. Roll up those sleeves; it's time to get busy again.
RaceClass_01.py
.from TrackClass_01 import Track from CycleClass_01 import Cycle
__init__
method. Here's the code for it:class Race: def __init__(self, inputManager): self.inputManager = inputManager self.cycles = [] self.track = None
__init__
method we just made:def createDemoRace(self): self.destroyRace() self.track = Track() self.cycles.append(Cycle(self.inputManager, self.track, 1, "Bert", ai = True)) self.cycles.append(Cycle(self.inputManager, self.track, 2, "Ernie", ai = True)) self.cycles.append(Cycle(self.inputManager, self.track, 3, "William", ai = True)) self.cycles.append(Cycle(self.inputManager, self.track, 4, "Patrick", ai = True)) self.setCameraHigh(self.cycles[0]) self.startRace(1) return
def createRace(self): self.destroyRace() self.track = Track() self.cycles.append(Cycle(self.inputManager, self.track, 1, "Bert")) self.cycles.append(Cycle(self.inputManager, self.track, 2, "Ernie", ai = True)) self.cycles.append(Cycle(self.inputManager, self.track, 3, "William", ai = True)) self.cycles.append(Cycle(self.inputManager, self.track, 4, "Patrick", ai = True)) self.setCameraFollow(self.cycles[0]) return
def setCameraFollow(self, cycle): base.camera.reparentTo(cycle.dirNP) base.camera.setPos(0, -15, 3) return def setCameraHigh(self, cycle): base.camera.reparentTo(cycle.dirNP) base.camera.setPos(0, 30, 30) base.camera.lookAt(cycle.root) return
def startRace(self, delay): taskMgr.doMethodLater(delay, self.startCycles, "Start Cycles") return def startCycles(self, task): for C in self.cycles: C.active = True return task.done
Race
class only needs one more method, and then it's done. We need a way to remove a race that's already in existence, so we'll make a destroyRace()
method for that:def destroyRace(self): if(self.track != None): self.track.destroy() for C in self.cycles: C.destroy() del self.cycles[0:4] return
Track
class. Save RaceClass_01.py
and open the TrackClass_00.py
file in the Chapter08
folder. Track
class is a method that will destroy it. Here's the code for that; put it at the bottom of the file:def destroy(self): self.track.removeNode() self.planet.removeNode() self.groundCol.removeNode() self.skySphere.removeNode() self.dirLight.removeNode() self.ambLight.removeNode() self.trackLanes.destroy() self.trackLanes = None self.skySphere = None render.setLightOff() return
removeNode()
on self.planet
, but our track doesn't have a self.planet
. Well, we're going to add one now. Put the following two lines of code in our Track
class's __init__
method, right below the lines where we load up the model for the track:self.planet = loader.loadModel("../Models/Planet.egg") self.planet.reparentTo(render)
Track
. Save the file as TrackClass_01.py
and open up the CycleClass_00.py
file from the Chapter08
folder. We need to make some changes to this file, too. __init__
method that will allow a player-controlled cycle to identify the fact that it doesn't have a CycleAI
object. This is important for the clean-up method we'll be adding. Put the following line of code right above the line where we add the cycleControl()
method to the task manager:self.ai = None
Race
class now. Scroll down to the setupVarsNPs()
method and look for the lines that deal with the camera. Delete them. setupVarsNPs()
method, let's add a new variable that the cycle can use to turn on and off its throttle and turning controls. We'll need that variable to make it impossible for the cycle to move when it isn't supposed to. Add the following line to the bottom of setupVarsNPs():
self.active = False
Track
class has a whole bunch of polygons, and we don't want each cycle's glow light to be trying to illuminate that mass of geometry when it isn't necessary. Move down to the setupLight()
method and look at the end of it for the line where we set render to accept the light. Delete that line, and put in the following two lines instead:self.cycle.setLight(self.glow) self.track.track.setLight(self.glow)
task.done
. We need to set our cycleControl()
task to do that when the time is right, or our clean-up method won't work. Add these two lines to the very beginning of the cycleControl()
method, before we even get dt:
if(self.cycle == None): return task.done
def destroy(self): self.root.removeNode() self.cycle.removeNode() self.dirNP.removeNode() self.refNP.removeNode() self.trackNP.removeNode() self.shieldCNP.removeNode() self.gRayCNP.removeNode() self.glow.removeNode() self.cycle = None if(self.ai != None): self.ai = None return
Cycle
class, too. Save the file as CycleClass_01.py
and let's move on to the Menu
class. Open MenuClass_01.py
, the file we made earlier this chapter. initMenu()
. We'll number It 3
because it uses the Menu3
object from the menuGraphics
egg. Right below our code for a type 0
menu, in the initMenu()
method, add the following block of code. Note that it's very, very similar to the code we used to create the type 0
menu, but with a few minor differences. The most major change is the use of the menu title.if(type == 3): self.frame = DirectFrame( geom = self.menuGraphics.find("**/Menu3"), relief = None, scale = (1.5,1,1.5), frameColor = (1,1,1,.75), parent = base.aspect2d) framePadding = .1 height = self.frame.getHeight()/2 - framePadding self.title = DirectLabel(text = self.title, text_font = self.fonts["silver"], text_fg = (1,1,1,.75), relief = None, text_align = TextNode.ACenter, text_scale = .065, parent = self.frame, pos = (0,0,height)) for N in range(len(self.items)): xPos = 0 zPos = -(height / (len(self.items)-1)) * N self.buttons.append(DirectButton( command = self.activateItem, extraArgs = [N], geom = (self.menuGraphics.find("**/BttnNormal"), self.menuGraphics.find("**/BttnPushed"), self.menuGraphics.find("**/BttnNormal"), self.menuGraphics.find("**/BttnNormal")), relief = None, clickSound = None, rolloverSound = None, parent = self.frame, pos = (xPos, 0, zPos))) self.items[N] = DirectLabel(text = self.items[N], text_font = self.fonts["silver"], text_fg = (1,1,1,.75), relief = None, text_align = TextNode.ACenter, text_scale = .035, parent = self.buttons[N]) self.items[N].setPos(0,0,-self.items[N].getHeight()/2)
MenuClass_02.py
and open up WorldClass_01.py
from the Chapter08
folder. The World
class doesn't use the Cycle
or Track
classes anymore, so delete those two imports. Update the import of the Menu
class to use the new file. Then, add this new import:from RaceClass_01 import Race
Track
or Cycle
classes. __init__
method:def createStartMenu(self): menu = Menu(self.menuGraphics, self.fonts, self.inputManager) menu.initMenu([0,None, ["New Game","Quit Game"], [[self.race.createRace, self.createReadyDialogue], [base.userExit]], [[None,None],[None]]])
def createReadyDialogue(self): menu = Menu(self.menuGraphics, self.fonts, self.inputManager) menu.initMenu([3,"Are you ready?", ["Yes","Exit"], [[self.race.startRace],[self.race.createDemoRace]], [[3],[None]]])
__init__
method, add a call to the createStartMenu()
method. We also need to create an instance of the Race
class and start the demo race. Here's the code:self.race = Race(self.inputManager) self.race.createDemoRace() self.createStartMenu()
printTest()
method any more. So just delete it. WorldClass_02.py
. We don't need to worry about clean up for any of the other classes; their clean-up methods have been added for this chapter and are all ready to go. If you're curious about them, just open the files and take a look. Run our new World
class file from the command prompt. Be aware that it's going to take a while to load; Panda3D has to process Planet.egg
, which is a pretty big file. Since Panda3D caches eggs, the game will load faster in the future.After the program loads and you see the preceding screen, click on the New Game button on the menu to proceed to the screen as shown in the following screenshot:
Now, when we load our game we see an overhead view of a demo race in progress. When we click on the New Game button, a new race is loaded and a dialog box comes up. Menu's in action!
That was a lot of work, but really there wasn't much new stuff. We only saw two pieces of Panda3D that we hadn't seen before. The first was the call to removeNode()
that we used in the clean-up methods we created. This is another method of NodePath
that removes the NodePath
from the scene graph, so the scene graph doesn't retain a reference to it, and also erases the NodePath
from the variable it's stored in. If we call removeNode()
on self.cycle
, for example, the NodePath
that self.cycle
points to will be taken out of the scene graph, along with all its children, and self.cycle
will be set to **removed**
so the NodePath
it used to contain can be garbage collected. Feel free to print out a NodePath
that had removeNode()
called on it to see that for yourself.
If we wanted to remove the NodePath
from the scene graph while still retaining the reference to it, we would use detachNode
instead. That only removes the NodePath
from the scene graph.
The other new call we saw was base.userExit
. This method causes the game to close itself.