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.
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.
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.
Let’s go through this recipe’s tasks:
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")
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)
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)
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)
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
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.
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.
In the sample code, we used CollisionPlane and CollisionSphere, but there are several more shapes available:
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:
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.
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!
The following steps are required to work with Panda3D’s built-in physics system:
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()
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)
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)
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)
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.