We're going to add two kinds of animation to our cycle. We'll make the discs at the front and rear of the cycle rotate in accordance with the throttle setting, and we'll make the cycle lean left and right when it turns. Because we don't want the CollisionRays attached to the cycle to lean as well, we'll have to make accommodations for that.

  1. Open CycleClass_00.py in the Chapter09 folder.
  2. Add this line to our import statements to give us access to Actors:
    from direct.actor.Actor import Actor
    
  3. Scroll down to the setupVarsNPs() method and look for the section where we use if statements to position self.root and load the correct model. That section looks like the following code:
    if(startPos == 1):
    self.root.setPos(5,0,0)
    self.cycle = loader.loadModel("../Models/RedCycle.bam")
    elif(startPos == 2):
    self.root.setPos(-5,-5,0)
    self.cycle = loader.loadModel("../Models/BlueCycle.bam")
    elif(startPos == 3):
    self.root.setPos(5,-10,0)
    self.cycle = loader.loadModel("../Models/GreenCycle.bam")
    elif(startPos == 4):
    self.root.setPos(-5,-15,0)
    self.cycle = loader.loadModel("../Models/YellowCycle.bam")
    
  4. Delete that section and replace it with the following code:
    self.cycle = self.root.attachNewNode("Cycle")
    if(startPos == 1):
    self.root.setPos(5,0,0)
    self.model = loader.loadModel("../Models/RedCycle.bam")
    self.turret = loader.loadModel("../Models/RedTurr.bam")
    elif(startPos == 2):
    self.root.setPos(-5,-5,0)
    self.model = loader.loadModel("../Models/BlueCycle.bam")
    self.turret = loader.loadModel("../Models/BlueTurr.bam")
    elif(startPos == 3):
    self.root.setPos(5,-10,0)
    self.model = loader.loadModel("../Models/GreenCycle.bam")
    self.turret = loader.loadModel("../Models/GreenTurr.bam")
    elif(startPos == 4):
    self.root.setPos(-5,-15,0)
    self.model = loader.loadModel("../Models/YellowCycle.bam")
    self.turret = loader.loadModel("../Models/YellowTurr.bam")
    self.mounts = Actor("../Models/Mounts.egg")
    self.model.reparentTo(self.cycle)
    self.mounts.reparentTo(self.model)
    turretMount = self.mounts.exposeJoint(None,
    "modelRoot", "Turret")
    fdMount = self.mounts.exposeJoint(None,
    "modelRoot", "FrontDisc")
    rdMount = self.mounts.exposeJoint(None,
    "modelRoot", "RearDisc")
    self.fd = loader.loadModel("../Models/Disc.bam")
    self.rd = loader.loadModel("../Models/Disc.bam")
    self.turret.reparentTo(turretMount)
    self.fd.reparentTo(fdMount)
    self.rd.reparentTo(rdMount)
    
  5. Scroll down a little to where we set the cycle's speed, shield, and other variables. Right beneath that there is a line that attaches self.cycle to self.root. Delete that line and put the following two lines in its place:
    self.turning = None
    self.lean = 0
    
  6. Keep moving down until we're in setupCollisions. Look for the following line:
    self.shieldCNP = self.cycle.attachNewNode(self.shieldCN)
    
  7. Change that line from self.cycle to self.model so it looks like this:
    self.shieldCNP = self.model.attachNewNode(self.shieldCN)
    
  8. Head down to the cycleControl() method and find the short block of code that turns the cycle according to user input. It looks like the following code:
    if(self.inputManager.keyMap["right"] == True):
    self.turn("r", dt)
    elif(self.inputManager.keyMap["left"] == True):
    self.turn("l", dt)
    
  9. Edit that code so it looks like the following code:
    if(self.inputManager.keyMap["right"] == True):
    self.turn("r", dt)
    self.turning = "r"
    elif(self.inputManager.keyMap["left"] == True):
    self.turn("l", dt)
    self.turning = "l"
    else:
    self.turning = None
    
  10. Now, skip down to the move() method. Add the following code to the bottom of the method, right above the return statement:
    currentLean = self.model.getR()
    if(self.turning == "r"):
    self.lean += 2.5
    if(self.lean > 25): self.lean = 25
    self.model.setR(self.model,
    (self.lean - currentLean) * dt * 5)
    elif(self.turning == "l"):
    self.lean -= 2.5
    if(self.lean < -25): self.lean = -25
    self.model.setR(self.model,
    (self.lean - currentLean) * dt * 5)
    else:
    self.lean = 0
    self.model.setR(self.model,
    (self.lean - currentLean) * dt * 5)
    self.fd.setH(self.fd, 5 + (20 * self.throttle))
    self.rd.setH(self.rd, -5 + (-20 * self.throttle))
    
  11. We've got one more change to make to this file. Go all the way down to the bottom of the file to the destroy() method and add these five lines right underneath the call to self.cycle.removeNode():
    self.mounts.delete()
    self.model.removeNode()
    self.turret.removeNode()
    self.fd.removeNode()
    self.rd.removeNode()
    
  12. Resave the file as CycleClass_01.py and run WorldClass_00.py from the command prompt. The other files for this chapter have already been updated to look for CycleClass_01.py so we don't need to edit any of them.
Time for action - animating our cycles

That brings some new life to our cycle! There's one thing we should talk about in this example that we haven't really discussed yet: the reason why we made self.cycle an empty NodePath and changed the model to be self.model. Remember the CollisionRays we made back in Chapter 6, The World in Action: Handling Collisions and how they are children self.cycle? Well, when we lean the cycle during a turn, we don't want those CollisionRays to lean with it, but the CollisionRays still have to turn with the cycle so that the front ray stays in front and the back ray stays at the back. For all that to work out, we need to separate the turning from the leaning, and that's why we are using two NodePaths now. Since we want the CollisionSpheres for the shield to lean with the cycle, we made them children of self.model instead of self.cycle.