Panda3D gamedev tutorial 3


Collisions between objects in games, are an absurdly important point. Important because it is something crucial to game mechanics, and also for being a picky topic in computational terms. And this technical factor forces us to some reflections.

To calculate object’s collisions, with efficiency, involves a series of complex algorithms – which we will not get any deeper – that consume a lot of computational power. And in order to make this work possible in real time, is mandatory to simplify our object’s geometric descriptions. For example: In case we have a human head, collisions can not be calculated for every single head detail, like eyes, libs, cheek. We must simplify our head shape for a simples sphere, or tube, or some other very simple geometry shape. That is how collision can be calculated in real time.

These simple shapes, are called collision solids. Each game engine, has it’s own collision solids, and we must used them in this simplification process.
As an example, I made use of some spheres to describe a human body:

simplificacaoColisao

The image above, is just an example, this little project won’t need such description. At the moment we will need just one simple sphere close to our avatar’s feet, in order to detect collision between our avatar and the platform.

In Panda3D, we must set some objects required to collision calculations, the Collision Traverser, and the Collision Handler. Let’s add these objects, set some initial configuration, and solve the problems latter. Yes, we will face some issues! But all solvable.
First the file Panda-tut-00.py:

[code lang=”python”]
from pandac.PandaModules import loadPrcFileData
loadPrcFileData(”,’show-frame-rate-meter 1′)

import direct.directbase.DirectStart
from pandac.PandaModules import *

from direct.task.Task import Task
from direct.showbase.DirectObject import DirectObject

from avatar import Avatar

#game class
class Game(DirectObject):
def __init__(self):
#init collider system
traverser = CollisionTraverser()
base.cTrav = traverser

base.pusher = CollisionHandlerPusher()
base.pusher.addInPattern(“%fn-into-%in”)
base.pusher.addOutPattern(“%fn-out-%in”)

#load platform model
self.plataforma = loader.loadModel(“../assets/eggs/plataformaBase”)
self.plataforma.reparentTo(render)
self.plataforma.setPos(0,10,0)
#load background sky
self.wall = loader.loadModel(‘../assets/eggs/papelParede’)
self.wall.reparentTo(render)
self.wall.setScale(1)
self.wall.setPos(-5, 30, -2)
#avatar
avatar = Avatar()
avatar.setCollision(traverser)

game = Game()
run()
[/code]

What’s new here:
– In function __init__(self), we created the collision traverser and the collision handler (base.pusher)
– When the avatar is created, we have the line: avatar.setCollision(traverser). Here is where the avatar starts to set it’s collision properties.

Now, the Avatar class:

[code lang=”python”]
from direct.showbase.DirectObject import DirectObject
from direct.actor.Actor import Actor
from direct.task import Task
from pandac.PandaModules import *

#class Avatar
class Avatar(DirectObject):
def __init__(self):
self.persona = render.attachNewNode(‘persona’)
self.personaActor = Actor(‘../assets/eggs/personagem.egg’,
{‘idle’:’../assets/eggs/personagem-parado’,
‘run’ :’../assets/eggs/personagem-correr’,
‘jump’:’../assets/eggs/personagem-pular’}
)
self.personaActor.setScale(1)
self.personaActor.setPos(0,0,5)
self.personaActor.reparentTo(self.persona)

self.persona.setPos(0,10,0)
self.persona.setScale(.15)
self.persona.setH(90)

self.state = { ‘flying’:True,
‘left’ :False,
‘right’ :False
}

taskMgr.add(self.movement, “Avatar Movement”)

#capture keyboard events
self.accept(‘arrow_left’, self.changeState, [‘left’,True])
self.accept(‘arrow_left-up’, self.changeState, [‘left’,False])
self.accept(‘arrow_right’, self.changeState, [‘right’,True])
self.accept(‘arrow_right-up’, self.changeState, [‘right’,False])

def setCollision(self, trav):
#colision nodes and solids
self.ball0 = CollisionSphere(0,0,1.5,1.5)
self.ball0NP = self.persona.attachNewNode(CollisionNode(‘ball0CN’))
self.ball0NP.node().addSolid(self.ball0)
self.ball0NP.show()

trav.addCollider(self.ball0NP, base.pusher)
base.pusher.addCollider(self.ball0NP, self.persona)

def movement(self, task):
if(self.state[‘flying’] == True):
self.persona.setZ( self.persona.getZ() – 0.1 )
if(self.persona.getZ() < -2.3):
self.state[‘flying’]=False
#move sideways
else:
if(self.state[‘left’]):
self.persona.setX( self.persona.getX() – 0.1 )
elif(self.state[‘right’]):
self.persona.setX( self.persona.getX() + 0.1 )
return Task.cont

def changeState(self, key, value):
self.state[key] = value
[/code]

What’s new here:
– At the first lines, we have: from pandac.PandaModules import * . Here the tools needed for collision detection are imported.
– Between function __init__ and function movement, we get function setCollision.
– In function setCollision two node were created as children of persona node.
grafo04

setCollision function, is where avatar will gain it’s collision solid and a collision node, which is crucial to make collisions possible.

After this changes, you will see that the avatar will move in a very weird way through platform, but will be easy to realize that it is now creeping around the platform, in a very weird way, but that is the natural behavior of collision handler pusher, and we will fix it soon.

An import detail! SPECIALLY WHEN USING YOUR OWN 3D MODELS!!!

Til now, we set collision objects to our avatar, NOTHING WAS DONE TO CONFIGURE COLLISIONS FOR OUR PLATFORM!!
Actually, the plataforma.egg file were already configured for collisions, that’s why we did not configured it. Let’s take a look how it was made, and you will see how to make it in your own models.

We could set some collision solids for our platform, but for educational matter, I chose Collision Mesh. Collision Mesh, is how we can set our 3d model, as collision solid, with one limitation: your model must be simple and not concave!
So, with a simple and not concave 3D model in a 3D software, you must convert it to Panda3D format. After this process, you will have a .egg file, which is basically a structured text file like an XML, that you can open in any simple text editor, and modify it as you like. In order to make collisions with those .egg models we need to set the line <Collide>{Polyset keep descend} for each group.

The .egg structure is:
– first, information about materials in geometry;
– than a Group, is defined;
– the Group definition starts with it’s matrix (information about it’s position and rotation);
– than we have a list (Vertex Pool), with all vertices that make the geometry;
– finally, a list of the polygons made with the vertices listed above.

So the line <Collide>{Polyset keep descend}, must me included right down the group declaration. Take a look:

[code lang=”xml”]
piso {
{Polyset keep descend}
{
{
1.000000 0.000000 0.000000 0.000000
0.000000 11.226257 0.000000 0.000000
0.000000 0.000000 0.182287 0.000000
0.000000 0.000000 0.000000 1.000000
}
}
piso {
0 {
1.000000 11.226256 -0.182287
}
1 {
1.000000 -11.226257 -0.182287
}
[…]
22 {
-1.000000 11.226261 -0.182287
}
23 {
-1.000000 11.226257 0.182287
}
}
{
{ Material }
{ 0.000000 0.000000 -1.000000 }
{ 0 1 2 3 { piso } }
}
[ … ]
{
{ Material }
{ 0.000003 1.000000 0.000012 }
{ 20 21 22 23 { piso } }
}
}
[/code]

Back to implementation —————

Before start the implementation, let’s think about the CollisionHandlerPusher, and how collisions will fit out game.
As said before, the CollisionHandlerPusher, do not allow objects to pass through each other, so collision pusher, pushes the object back, or aside.

But another possibility with it is, generate collision events! Collision Handler Pusher, is a sub class of Collision Handler Event, so we can make event generation when objects collide.
In Panda-tut-00.py, lines 20 and 21, is the point where we set Collision Handler Pusher to generate collisions events.

Implementation —————

Now, our basic collision is set up, but we must work on some issues.
The first collision we have is our avatar hitting the platform. At the moment, we make it stop falling using a Z axis numeric clause. But to make it right, let’s make the collision, change our avatar state.

To start with, let’s kill lines 49 e 50, in avatar class:
[code lang=”python”]
if(self.persona.getZ() < -2.3):
self.state[‘flying’]=False
[/code]

We must change the self.state[‘flying’] to false, through collision between our sphere and the platform.
Now let’s create a new accept, for collision events:
[code lang=”python”]
self.accept(‘ball0CN-into-plataforma’, self.changeState, [‘flying’,False])
[/code]

In this line above, we say to Panda3D, to notify avatar when the ball0CN hits the ‘plataforma’. And the function self.chageState, will be called to treat the event.
To deeper understanding of this collision event generation, check this link.

But now we have a logic problem. All functions that deals with collisions, must receive the collision informations, as an argument. So we need to prepare our function to receive such argument. The problem lays on the fact that self.changeState, is also used to treat simple events (keyboard events), which do not demand and special argument. And if we insert a new argument in that function, and the function was called, and do not pass all required arguments, an error will be generated, and the software will be killed.

Fortunately, there is a trick to solve this problem in Python. We will insert a new argument in that function, but a default value will be set to it, in case nothing were passed to the function. In this case, let’s insert a new argument called ‘col’ (small version for ‘collision’), and give to it a default value of ‘None’ (null in python):
[code lang=”python”]
def changeState(self, key, value, col=None):
self.state[key] = value
[/code]

At the end of this process, run the program, and you will see that the avatar stops falling, thanks to collision detection. But we still have a problem, the avatar seems to slide over the platform! That is a effect of Handler Pusher. To solve this problem we will “hard” set our avatar X and Y position.
Take a look at the final code:

[code lang=”python”]
from direct.showbase.DirectObject import DirectObject
from direct.actor.Actor import Actor
from direct.task import Task
from pandac.PandaModules import *

#class Avatar
class Avatar(DirectObject):
def __init__(self):
self.persona = render.attachNewNode(‘persona’)
self.personaActor = Actor(‘../assets/eggs/personagem.egg’,
{‘idle’:’../assets/eggs/personagem-parado’,
‘run’ :’../assets/eggs/personagem-correr’,
‘jump’:’../assets/eggs/personagem-pular’}
)
self.personaActor.setScale(1)
self.personaActor.setPos(0,0,5)
self.personaActor.reparentTo(self.persona)

self.persona.setPos(0,10,0)
self.persona.setScale(.15)
self.persona.setH(90)

self.state = { ‘flying’:True,
‘left’ :False,
‘right’ :False
}
self.lastXpos = 0;

taskMgr.add(self.movement, “Avatar Movement”)

#capture keyboard events
self.accept(‘arrow_left’, self.changeState, [‘left’,True])
self.accept(‘arrow_left-up’, self.changeState, [‘left’,False])
self.accept(‘arrow_right’, self.changeState, [‘right’,True])
self.accept(‘arrow_right-up’, self.changeState, [‘right’,False])
self.accept(‘ball0CN-into-plataforma’, self.changeState, [‘flying’,False])
self.accept(‘ball0CN-out-plataforma’, self.changeState, [‘flying’,True])

def setCollision(self, trav):
#colision nodes and solids
self.ball0 = CollisionSphere(0,0,1.5,1.5)
self.ball0NP = self.persona.attachNewNode(CollisionNode(‘ball0CN’))
self.ball0NP.node().addSolid(self.ball0)
self.ball0NP.show()

trav.addCollider(self.ball0NP, base.pusher)
base.pusher.addCollider(self.ball0NP, self.persona)

def movement(self, task):
if(self.state[‘flying’] == True):
self.persona.setZ( self.persona.getZ() – 0.06 )
#move sideways
else:
if(self.state[‘left’]):
self.persona.setX( self.persona.getX() – 0.1 )
elif(self.state[‘right’]):
self.persona.setX( self.persona.getX() + 0.1 )
else:
self.persona.setX(self.lastXpos)
if(self.persona.getY() != 10):
self.persona.setY(10)
return Task.cont

def changeState(self, key, value, col=None):
self.state[key] = value
if(value == 0):
self.lastXpos = self.persona.getX()
[/code]

Basically, in function movement, we check if avatar’s Y position is different than 10, if is true, we set it to 10. It will solve the avatar sliding in Y axis. But it still slide on X axis. To solve this, we create a variable lastXpos, which will position the avatar in the proper X position. When the avatar is in touch with the platform, and not moving to right or left, it will be positioned in lastXpos. This variable is updated, every time the avatar will stop moving: See function self.changeState, when the ‘value’ is 0(zero), the lastXpos is updated with the avatar’s current position.

Leave a Reply

Your email address will not be published. Required fields are marked *