December 12, 2010

ISSUE 4: What goes up…

2. Collisions, Debugging and the For-Loop

While we are currently able to fly our player around the world, there are no collisions. We can fly straight through any mountain or terrain. This would be fine if we were writing Ghost Busters! But, well, we’re not. 🙂 Our player needs to be able to crash (and burn, and die).

First, however, open up your code and somewhere near the start of your constructor, add the following lines (in our code, we added it right after the call to the ShowBase constuctor):

        self.debug = True
        self.statusLabel = self.makeStatusLabel(0) 
        self.collisionLabel = self.makeStatusLabel(1) 

The ‘makeStatusLabel’ method is new and does not exist. Below your constuctor, define it as follows:

    def makeStatusLabel(self, i): 
        return OnscreenText(style=2, fg=(.5,1,.5,1), pos=(-1.3,0.92-(.08 * i)), \
		align=TextNode.ALeft, scale = .08, mayChange = 1) 

We’re not going to discuss this method in detail, but you can read about the OnscreenText routine over here. The backslash you will recall we used in the Issue 3 code too. It simply helps layout/display by spreading one line of code over several.

What we are doing here is adding a ‘debug mode’ into the game. Essentially, whenever you want to ‘run in debug mode’ just change self.debug value to be ‘True’. When you wish to return to normal (or ‘release’) mode just change it back to ‘False’.

Next, at the end of your constructor, after the call to createEnvironment add:

        self.setupCollisions() 
        self.textCounter = 0 

Elsewhere in your code, define the setupCollisions method as follows:

    def setupCollisions(self): 
        self.collTrav = CollisionTraverser() 

        self.playerGroundSphere = CollisionSphere(0,1.5,-1.5,1.5) 
        self.playerGroundCol = CollisionNode('playerSphere') 
        self.playerGroundCol.addSolid(self.playerGroundSphere) 

        # bitmasks 
        self.playerGroundCol.setFromCollideMask(BitMask32.bit(0)) 
        self.playerGroundCol.setIntoCollideMask(BitMask32.allOff()) 
        self.world.setCollideMask(BitMask32.bit(0)) 

        # and done 
        self.playerGroundColNp = self.player.attachNewNode(self.playerGroundCol) 
        self.playerGroundHandler = CollisionHandlerQueue() 
        self.collTrav.addCollider(self.playerGroundColNp, self.playerGroundHandler) 

        # DEBUG 
        if (self.debug == True): 
            self.playerGroundColNp.show() 
            self.collTrav.showCollisions(self.render) 

Notice the checks against our new debug variable. The collision code might look complicated and we’d have to agree that collisions in Panda3D are not immediately intuitive. It amounts to:

  1. Create a collision traverser. A traverser does the actual work of checking for collisions. It maintains a list of active world objects. When writing your code, it is best to think in terms of ‘from objects’ (or ‘colliders’) and ‘into objects’ (or ‘collide-able’). In our game, the ‘from object’ is the player while the ‘into object’ is the terrain.
  1. Create a collision sphere. The sphere is a kind of Collision Solid. A collision solid is an invisible object created purely for testing collisions. What we need to do is create a sphere and attach it to the player. Whenever that sphere intersects with the terrain, we have a collision (crash). The parameters we passed were 3 centering values (cx,cy,cz) that detail how the sphere will be positioned relative to the player (when we attach it to the player below) and a fourth value for the size of the sphere.
  1. Create a collision node. A collision node can hold any number of collision solids.
  1. Add the sphere to that newly created node. Our collision solid (sphere) has to be held by a collision node.
  1. Setup our bitmasks. Bitmasks are a common source of confusion. A ‘mask’ is used in what we call a bitwise operation. A BitMask32 as used in our code indicates a 32-bit number. Deep down in the heart of your CPU everything is in binary. Binary numbers (or ‘base-2’) use only zeros and ones (or ‘off’ and ‘on’). For example, the number 201202 in binary reads:
    0000 0000 0000 0011 0001 0001 1111 0010
    We are not, at this stage, going to discuss the mechanics of the binary number system but those interested can read more here. It is very likely we will return to this and explain in more depth in a future Issue. What we do need to understand though is that a 32-bit number has 32 binary digits each being zero or one. When we set bit ‘masks’ we are manipulating the digits (known as ‘bits’). For example:

    BitMask32.allOff() = 0000 0000 0000 0000 0000 0000 0000 0000 BitMask32.bit(0) = 0000 0000 0000 0000 0000 0000 0000 0001 BitMask32.bit(1) = 0000 0000 0000 0000 0000 0000 0000 0010 BitMask32.bit(2) = 0000 0000 0000 0000 0000 0000 0000 0100  

    Each object in Panda3D has two masks – the FROM and the INTO as we discussed above (this can also be considered as ‘source’ and ‘destination’). Objects will not be considered for collision if the masks do not match. It is because of this we set our player and world bitmasks the same (bit zero, the first digit of our binary number reading right to left). In other words, the player and the world can collide!

    setCollideMask is used to set the INTO collision mask on the world (which subsequently sets the mask on all of its child nodes – node paths are discussed later in this Issue). We can also be more granular than this by applying the collision mask on the actual collision node using setIntoCollideMask and setFromCollideMask. Our code illustrates both mechanisms.

    In short, we use bit masks to filter our collisions. This will be increasingly important as we add more objects to our world in future issues.

  1. Attach the collision node to the player. Where the player goes, the collision sphere goes. When we call ‘attachNewNode’ a node path is returned and stored in the variable ‘playerGroundColNp’ (again, node paths are discussed later in this Issue).
  1. Create a collision handler queue for the newly created node. Collisions are stored in queues. There can, on occasion, be many many collisions. You don’t want to miss any, so we put them in a queue to be processed later in our code.
  1. Add that queue to the traverser. We add the new queue and a reference to collision node. When it comes time for the traverser to check for collisions, it now knows to check our new collision node (player into terrain) and if there’s a result – store it in our collision queue.

You can, as always, add the new method wherever makes the code feel ‘easier’/more maintainable to you. There are ‘conventions’ and approaches to this. In Industry/commercially, developers quite often follow a ‘style guide’ that dictates the order methods should be in so everyone is ‘one the same page’ (likewise with where variable declarations go etc.).

Collision and OOBE

Finally, we need to check for collisions in our updateTask method. Change your code to look like this:

    def updateTask(self, task): 
        self.updatePlayer() 
        self.updateCamera() 

        self.collTrav.traverse(self.render) 
        for i in range(self.playerGroundHandler.getNumEntries()): 
            entry = self.playerGroundHandler.getEntry(i) 
            if (self.debug == True): 
                self.collisionLabel.setText("DEAD:"+str(globalClock.getFrameTime())) 
            # we will later deal with 'what to do' when the player dies 

Our collision solid (sphere) has to be held by a collision node. We call the traverser to find collisions. Any collisions will be in our queue. We look at the queue and process each entry in it. To do this we are using a ‘loop’. Loops are very common in programming and there are several types. We are using a ‘for-loop’.
– A loop is used to to make a section of code repeat a number of times.
– A ‘for loop’ allows us to repeat a section of code a number of times with different values.

In this case, we declare a variable called “i”. Because “i” is declared as part of the loop it only exists while the loop is executing. The variable does not exist outside of the loop. You might recall our discussion of local variables in Issue 3. This is similar only even more restrictive. Where variables do/do not exist is something we call ‘scope’. Scope is important as it avoids unwanted interactions (code elsewhere in our program might set the value of “i” to something unwanted).

The value of “i” is set to “range( self. playerGroundHandler. getNumEntries() )”. Range is a function in the Python language that generates lists of arithmetic progressions. We tell it our range is “getNumEntries”; That is, however many entries are in the queue. If there were 3 collisions in the queue range would generate the list “0,1,2”. Notice we start counting at zero! This is another very common thing you will run into with programming! The code below (and indented further than) the ‘for i….’ line would be executed 3 times. Once with “i” set to zero, once with i=1 and one final time with i=2.

For each iteration of the loop we call getEntry – this retrieves the collision event from the queue. We pass “i” to getEntry. Taking our example of 3 collisions again, getEntry would get called 3 times, with the values 0, 1 and finally 2. We are specifying exactly which collision event we want to pull from the queue. Again, notice we count from zero!. In our code, we don’t actually do anything with our ‘entry’, it’s enough for us to know a collision has happened. In future issues we will look more closely at what information you can retrieve about a collision.

Now, if you run your game in debug mode and fly into the terrain, you should see a message appear at the top-left of your screen saying the player is dead (along with the current time). Turn off the debug mode and you will see nothing displayed on the screen. This is how we want things to be – we will return to “what to do when the player is dead” shortly. For now, we have just added an on screen indication (that only appears in debug mode, because it should not be present in the final game!).

Also, while in debug mode, you should see a visual indication of the collision happening and you should also be able to see the collision sphere itself around the player.

TIP 1: If collisions are happening before the player hits the ground the likely cause is the size of your collision sphere. Enable the game debug mode (which is turn calls ‘showCollisions’) to view the sphere and adjust its size in code until it works correctly.

TIP 2: If collisions are happening and not being caught (e.g., flying through the ground), see the discussion in Issue 5 regarding the use of ‘Fluid’ movements and “respect previous transform” on the collision traverser.