12 min read

Panda3D 1.7 Game Developer’s Cookbook

Panda3D 1.7 Game Developer's Cookbook

Over 80 recipes for developing 3D games with Panda3D, a full-scale 3D game engine

In a video game, the game world or level defines the boundaries within which the player is allowed to interact with the game environment. But how do we enforce these boundaries? How do we keep the player from running through walls?

This is where collision detection and response come into play.

Collision detection and response not only allow us to keep players from passing through the level boundaries, but also are the basis for many forms of interaction. For example, lots of actions in games are started when the player hits an invisible collision mesh, called a trigger, which initiates a scripted sequence as a response to the player entering its boundaries.

Simple collision detection and response form the basis for nearly all forms of interaction in video games. It’s responsible for keeping the player within the level, for crates being pushable, for telling if and where a bullet hit the enemy.

What if we could add some extra magic to the mix to make our games even more believable, immersive, and entertaining? Let’s think again about pushing crates around: What happens if the player pushes a stack of crates? Do they just move like they have been glued together, or will they start to tumble and eventually topple over?

This is where we add physics to the mix to make things more interesting, realistic, and dynamic.

In this article, we will take a look at the various collision detection and physics libraries that the Panda3D engine allows us to work with. Putting in some extra effort, we will also see that it is not very hard to integrate a physics engine that is not part of the Panda3D SDK.

Using the built-in collision detection system

Not all problems concerning world and player interaction need to be handled by a fully fledged physics API—sometimes a much more basic and lightweight system is just enough for our purposes. This is why in this recipe we dive into the collision handling system that is built into the Panda3D engine.

Getting ready

This recipe relies upon the project structure created in Setting up the game structure (code download-Ch:1), Setting Up Panda3D and Configuring Development Tools.

How to do it…

Let’s go through this recipe’s tasks:

  1. Open Application.py and add the include statements as well as the constructor of the Application class:

    from direct.showbase.ShowBase import ShowBase
    from panda3d.core import *
    import random

    class Application(ShowBase):
    def __init__(self):
    ShowBase.__init__(self)
    self.cam.setPos(0, -50, 10)
    self.setupCD()
    self.addSmiley()
    self.addFloor()
    taskMgr.add(self.updateSmiley, “UpdateSmiley”)

    
    
  2. Next, add the method that initializes the collision detection system:

    def setupCD(self):
    base.cTrav = CollisionTraverser()
    base.cTrav.showCollisions(render)
    self.notifier = CollisionHandlerEvent()
    self.notifier.addInPattern(“%fn-in-%in”)
    self.accept(“frowney-in-floor”, self.onCollision)

    
    
  3. Next, implement the method for adding the frowney model to the scene:

    def addSmiley(self):
    self.frowney = loader.loadModel(“frowney”)
    self.frowney.reparentTo(render)
    self.frowney.setPos(0, 0, 10)
    self.frowney.setPythonTag(“velocity”, 0)

    col = self.frowney.attachNewNode(CollisionNode(“frowney”))
    col.node().addSolid(CollisionSphere(0, 0, 0, 1.1))
    col.show()
    base.cTrav.addCollider(col, self.notifier)

    
    
  4. The following methods will add a floor plane to the scene and handle the collision response:

    def addFloor(self):
    floor = render.attachNewNode(CollisionNode(“floor”))
    floor.node().addSolid(CollisionPlane(Plane(Vec3(0, 0, 1),
    Point3(0, 0, 0))))
    floor.show()

    def onCollision(self, entry):
    vel = random.uniform(0.01, 0.2)
    self.frowney.setPythonTag(“velocity”, vel)

    
    
  5. Add this last piece of code. This will make the frowney model bounce up and down:

    def updateSmiley(self, task):
    vel = self.frowney.getPythonTag(“velocity”)
    z = self.frowney.getZ()
    self.frowney.setZ(z + vel)
    vel -= 0.001
    self.frowney.setPythonTag(“velocity”, vel)
    return task.cont

    
    
  6. Hit the F6 key to launch the program:

    Panda3D Game Development tutorial

How it works…

We start off by adding some setup code that calls the other initialization routines. We also add the task that will update the smiley’s position.

In the setupCD() method, we initialize the collision detection system. To be able to find out which scene objects collided and issue the appropriate responses, we create an instance of the CollisionTraverser class and assign it to base.cTrav. The variable name is important, because this way, Panda3D will automatically update the CollisionTraverser every frame. The engine checks if a CollisionTraverser was assigned to that variable and will automatically add the required tasks to Panda3D’s update loop.

Additionally, we enable debug drawing, so collisions are being visualized at runtime. This will overlay a visualization of the collision meshes the collision detection system uses internally.

In the last lines of setupCD(), we instantiate a collision handler that sends a message using Panda3D’s event system whenever a collision is detected. The method call addInPattern(“%fn-in-%in”) defines the pattern for the name of the event that is created when a collision is encountered the first time. %fn will be replaced by the name of the object that bumps into another object that goes by the name that will be inserted in the place of %in. Take a look at the event handler that is added below to get an idea of what these events will look like.

After the code for setting up the collision detection system is ready, we add the addSmiley() method, where we first load the model and then create a new collision node, which we attach to the model’s node so it is moved around together with the model. We also add a sphere collision shape, defined by its local center coordinates and radius. This is the shape that defines the boundaries; the collision system will test against it to determine whether two objects have touched.

To complete this step, we register our new collision node with the collision traverser and configure it to use the collision handler that sends events as a collision response.

Next, we add an infinite floor plane and add the event handling method for reacting on collision notifications. Although the debug visualization shows us a limited rectangular area, this plane actually has an unlimited width and height. In our case, this means that at any given x- and y-coordinate, objects will register a collision when any point on their bounding volume reaches a z-coordinate of 0. It’s also important to note that the floor is not registered as a collider here. This is contrary to what we did for the frowney model and guarantees that the model will act as the collider, and the floor will be treated as the collidee when a contact between the two is encountered.

While the onCollision() method makes the smiley model go up again, the code in updateSmiley() constantly drags it downwards. Setting the velocity tag on the frowney model to a positive or negative value, respectively, does this in these two methods. We can think of that as forces being applied. Whenever we encounter a collision with the ground plane, we add a one-shot bounce to our model. But what goes up must come down, eventually. Therefore, we continuously add a gravity force by decreasing the model’s velocity every frame.

There’s more…

This sample only touched a few of the features of Panda3D’s collision system. The following sections are meant as an overview to give you an impression of what else is possible. For more details, take a look into Panda3D’s API reference.

Collision Shapes

In the sample code, we used CollisionPlane and CollisionSphere, but there are several more shapes available:

  • CollisionBox: A simple rectangular shape. Crates, boxes, and walls are example usages for this kind of collision shape.
  • CollisionTube: A cylinder with rounded ends. This type of collision mesh is often used as a bounding volume for first and third person game characters.
  • CollisionInvSphere: This shape can be thought of as a bubble that contains objects, like a fish bowl. Everything that is outside the bubble is reported to be colliding. A CollisionInvSphere may be used to delimit the boundaries of a game world, for example.
  • CollisionPolygon: This collision shape is formed from a set of vertices, and allows for the creating of freeform collision meshes. This kind of shape is the most complex to test for collisions, but also the most accurate one. Whenever polygon-level collision detection is important, when doing hit detection in a shooter for example, this collision mesh comes in handy.
  • CollisionRay: This is a line that, starting from one point, extends to infinity in a given direction. Rays are usually shot into a scene to determine whether one or more objects intersect with them. This can be used for various tasks like finding out if a bullet shot in the given direction hit a target, or simple AI tasks like finding out whether a bot is approaching a wall.
  • CollisionLine: Like CollisionRay, but stretches to infinity in both directions.
  • CollisionSegment: This is a special form of ray that is limited by two end points.
  • CollisionParabola: Another special type of ray that is bent. The flying curves of ballistic objects are commonly described as parabolas. Naturally, we would use this kind of ray to find collisions for bullets, for example.

Collision Handlers

Just like it is the case with collision shapes for this recipe, we only used CollisionHandlerEvent for our sample program, even though there are several more collision handler classes available:

  • CollisionHandlerPusher: This collision handler automatically keeps the collider out of intersecting vertical geometry, like walls.
  • CollisionHandlerFloor: Like CollisionHandlerPusher, but works in the horizontal plane.
  • CollisionHandlerQueue: A very simple handler. All it does is add any intersecting objects to a list.
  • PhysicsCollisionHandler: This collision handler should be used in connection with Panda3D’s built-in physics engine. Whenever a collision is found by this collision handler, the appropriate response is calculated by the simple physics engine that is built into the engine.

Using the built-in physics system

Panda3D has a built-in physics system that treats its entities as simple particles with masses to which forces may be applied. This physics system is a great amount simpler than a fully featured rigid body one. But it still is enough for cheaply, quickly, and easily creating some nice and simple physics effects.

Getting ready

To be prepared for this recipe, please first follow the steps found in Setting up the game structure (code download-Ch:1). Also, the collision detection system of Panda3D will be used, so reading up on it in Using the built-in collision detection system might be a good idea!

How to do it…

The following steps are required to work with Panda3D’s built-in physics system:

  1. Edit Application.py and add the required import statements as well as the constructor of the Application class:

    from direct.showbase.ShowBase import ShowBase
    from panda3d.core import *
    from panda3d.physics import *

    class Application(ShowBase):
    def __init__(self):
    ShowBase.__init__(self)
    self.cam.setPos(0, -50, 10)
    self.setupCD()
    self.setupPhysics()
    self.addSmiley()
    self.addFloor()

    
    
  2. Next, add the methods for initializing the collision detection and physics systems to the Application class:

    def setupCD(self):
    base.cTrav = CollisionTraverser()
    base.cTrav.showCollisions(render)
    self.notifier = CollisionHandlerEvent()
    self.notifier.addInPattern(“%fn-in-%in”)
    self.notifier.addOutPattern(“%fn-out-%in”)
    self.accept(“smiley-in-floor”, self.onCollisionStart)
    self.accept(“smiley-out-floor”, self.onCollisionEnd)

    def setupPhysics(self):
    base.enableParticles()
    gravNode = ForceNode(“gravity”)
    render.attachNewNode(gravNode)
    gravityForce = LinearVectorForce(0, 0, -9.81)
    gravNode.addForce(gravityForce)
    base.physicsMgr.addLinearForce(gravityForce)

    
    
  3. Next, implement the method for adding a model and physics actor to the scene:

    def addSmiley(self):
    actor = ActorNode(“physics”)
    actor.getPhysicsObject().setMass(10)
    self.phys = render.attachNewNode(actor)
    base.physicsMgr.attachPhysicalNode(actor)

    self.smiley = loader.loadModel(“smiley”)
    self.smiley.reparentTo(self.phys)
    self.phys.setPos(0, 0, 10)

    thrustNode = ForceNode(“thrust”)
    self.phys.attachNewNode(thrustNode)
    self.thrustForce = LinearVectorForce(0, 0, 400)
    self.thrustForce.setMassDependent(1)
    thrustNode.addForce(self.thrustForce)

    col = self.smiley.attachNewNode(CollisionNode(“smiley”))
    col.node().addSolid(CollisionSphere(0, 0, 0, 1.1))
    col.show()
    base.cTrav.addCollider(col, self.notifier)

    
    
  4. Add this last piece of source code that adds the floor plane to the scene to Application.py:

    Application.py:
    def addFloor(self):
    floor = render.attachNewNode(CollisionNode(“floor”))
    floor.node().addSolid(CollisionPlane(Plane(Vec3(0, 0, 1),
    Point3(0, 0, 0))))
    floor.show()

    def onCollisionStart(self, entry):
    base.physicsMgr.addLinearForce(self.thrustForce)

    def onCollisionEnd(self, entry):
    base.physicsMgr.removeLinearForce(self.thrustForce)

    
    
  5. Start the program by pressing F6:

    Panda3D Game Development tutorial

How it works…

After adding the mandatory libraries and initialization code, we proceed to the code that sets up the collision detection system. Here we register event handlers for when the smiley starts or stops colliding with the floor. The calls involved in setupCD() are very similar to the ones used in Using the built-in collision detection system. Instead of moving the smiley model in our own update task, we use the built-in physics system to calculate new object positions based on the forces applied to them.

In setupPhysics(), we call base.enableParticles() to fire up the physics system. We also attach a new ForceNode to the scene graph, so all physics objects will be affected by the gravity force. We also register the force with base.physicsMgr, which is automatically defined when the physics engine is initialized and ready.

In the first couple of lines in addSmiley(), we create a new ActorNode, give it a mass, attach it to the scene graph and register it with the physics manager class. The graphical representation, which is the smiley model in this case, is then added to the physics node as a child so it will be moved automatically as the physics system updates.

We also add a ForceNode to the physics actor. This acts as a thruster that applies a force that pushes the smiley upwards whenever it intersects the floor. As opposed to the gravity force, the thruster force is set to be mass dependant. This means that no matter how heavy we set the smiley to be, it will always be accelerated at the same rate by the gravity force. The thruster force, on the other hand, would need to be more powerful if we increased the mass of the smiley.

The last step when adding a smiley is adding its collision node and shape, which leads us to the last methods added in this recipe, where we add the floor plane and define that the thruster should be enabled when the collision starts, and disabled when the objects’ contact phase ends.

LEAVE A REPLY

Please enter your comment!
Please enter your name here