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.

  1. Open a blank document in NotePad++ and save it as RaceClass_01.py.
  2. As always, when creating a new class, we'll start with the imports. Add this code to our blank file:
    from TrackClass_01 import Track
    from CycleClass_01 import Cycle
    
  3. To get us rolling, we'll need the class definition and the __init__ method. Here's the code for it:
    class Race:
    def __init__(self, inputManager):
    self.inputManager = inputManager
    self.cycles = []
    self.track = None
    
  4. That's a nice easy start. Next, we'll create a method that will start a demo race for us. Add the following code below the __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
    
  5. Moving right along, we'll put in a method to create a regular race. The code is pretty similar to what we just added:
    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
    
  6. Next, we'll put in a couple of methods to set up the camera. They're pretty short and simple.
    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
    
  7. The next two methods we want to add will give us the ability to delay the start of a race so it isn't in progress the instant it's loaded.
    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
    
  8. Our 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
    
  9. We're going to edit some of our other files to make all this work. We'll start with the Track class. Save RaceClass_01.py and open the TrackClass_00.py file in the Chapter08 folder.
  10. The main thing we need to add to our 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
    
  11. You may have noticed that our destroy method calls 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)
    
  12. That's it for the 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.
  13. To start things off, we're going to make a small edit to our __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
    
  14. The next thing we need to do is remove the camera control stuff from this class, since we're handing that over to the Race class now. Scroll down to the setupVarsNPs() method and look for the lines that deal with the camera. Delete them.
  15. While we're in the 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
    
  16. The next modification is a performance optimization. The planet model we're loading in our 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)
    
  17. The most reliable way to remove a task from the task manager is by having it return 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
    
  18. The last change we'll make to our cycle will be the clean-up method. Paste the following code at the very bottom of the file:
    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
    
  19. That's it for the 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.
  20. The only change we're making to this class is to add a new menu type in 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)
    
  21. Resave this file as 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
    
  22. Scroll down and delete all the lines that create instances of the Track or Cycle classes.
  23. Now, we're going to move the lines that create our menu into their own method. Either cut them, paste them, and edit them, or just delete them and add the following new code to the file, right after the __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]]])
    
  24. We also want a method to create our dialog menu. Add the following code below the method we just created:
    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]]])
    
  25. At the end of the __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()
    
  26. We don't need the printTest() method any more. So just delete it.
  27. Resave the file as 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.
Time for action - using menus

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:

Time for action - using menus