collision avoidance

Here is a script that is useful in conjunction with pathfinding for a rts-like game. There are static obstacles and moving bots. The bots have a sort of awereness of what is in front of them and try to avoid it.

Pressing c will change the camera angle.
Pressing t will move the target.

It requires a couple of models in a folder called media, all are available from this site (ralph, firehydrant, smiley), or you can easily modify it to use your own.

import direct.directbase.DirectStart
from pandac.PandaModules import *
from direct.actor.Actor import Actor
from direct.task.Task import Task
import sys,random
from math import *

class bot:
        def __init__(self,selfname='Ralph',model='models/ralph',run='models/ralph-run',
                        walk='models/ralph-walk',maxspeed=3,maxturnspeed=6,hprs=(180,0,0,1,1,1),
                        sPos=(random.uniform(-150,150),random.uniform(-150,150),0),
                        awareness=1,traverser=base.cTrav):

                self.MainPiv = NodePath(PandaNode(selfname))
                self.MainPiv.reparentTo(render)

                self.maxspeed = maxspeed        #maxspeed that the unit can go
                self.maxturnspeed = maxturnspeed#the maximum turn speed the unit can do
                self.cancelAction = 0      #time until action count times out
                self.DoAction = 0            #time still left to turn (prevents clumping)
                self.action = 0                 #0-nothing,1-right,-1-left
                self.speed = 0                  #are we moving?
                self.animation = 0              #what animation is playing

                self.model = Actor(model,{"run":run,"walk":walk})
                self.model.setHprScale(*hprs)
                self.model.wrtReparentTo(self.MainPiv)

                height = getHeight(self.model)
                half_height = height/2
                width = getWidth(self.model)
                half_width = width/2
                length = getLength(self.model)
                half_length = length/2
                if half_width > half_length:
                        radius = half_width
                else:
                        radius = half_length
                #add a collisiontube to each character that can only be collided into
                ctube = CollisionTube(0,0,height-radius,0,0,radius,radius)
                self.ctubepath = self.MainPiv.attachNewNode(CollisionNode('ctnode'))
                self.ctubepath.node().addSolid(ctube)
                self.ctubepath.node().setFromCollideMask(BitMask32.allOff())
                self.ctubepath.node().setIntoCollideMask(BitMask32.bit(0))

                #add 2 collision segments so the characters "know" what is in front of them
                #one on the right side, and one on the left side
                self.collided = {'left':0,'right':0}
                #left
                clseg = CollisionSegment(-half_width,radius,half_height,-half_width,awareness+radius,half_height)
                self.clsegpath = self.MainPiv.attachNewNode(CollisionNode('clnode'))
                self.clsegpath.node().addSolid(clseg)
                self.clsegpath.node().setFromCollideMask(BitMask32.bit(0))
                self.clsegpath.node().setIntoCollideMask(BitMask32.allOff())
                #right
                crseg = CollisionSegment(half_width,radius,half_height,half_width,awareness+radius,half_height)
                self.crsegpath = self.MainPiv.attachNewNode(CollisionNode('crnode'))
                self.crsegpath.node().addSolid(crseg)
                self.crsegpath.node().setFromCollideMask(BitMask32.bit(0))
                self.crsegpath.node().setIntoCollideMask(BitMask32.allOff())

                #set up collision queue so that everything works out hunky-dory :)
                self.chandler = CollisionHandlerQueue()
                base.cTrav.addCollider(self.clsegpath,self.chandler)
                base.cTrav.addCollider(self.crsegpath,self.chandler)

                self.prevtime = 0
                self.MainPiv.setPos(*sPos)
                taskMgr.add(self.update,'update for: '+selfname)
                
                #self.crsegpath.show()
                #self.clsegpath.show()
                #self.ctubepath.show()

        def update(self,task):
                elapsed = (task.time - self.prevtime)*10

                collided = self.chandler.getNumEntries()

		#action count decreases every frame.  If the bot is no longer colliding, the cancelactioncount
		#will decrease.  This makes sure that the bot will turn 8 frames past when it is colliding.
                if collided==0 and self.DoAction > 0:
                        self.cancelAction -= 1
                        if self.cancelAction < -8:
                                self.DoAction = 0
                else: self.cancelAction = 0

                if self.DoAction > 0:
                        if self.action ==-1: self.MainPiv.setH(self.MainPiv,-self.maxturnspeed*elapsed)
                        elif self.action == 1: self.MainPiv.setH(self.MainPiv,self.maxturnspeed*elapsed)
                else:
                        if collided > 0:
                                #find collisions, only want the closest one
                                self.chandler.sortEntries()
                                for i in range(self.chandler.getNumEntries()):
                                        entry = self.chandler.getEntry(i)
                                        if entry.hasInto():
                                                break
                                yaw = deltaYaw(entry.getFromNodePath().getParent(),entry.getIntoNodePath().getParent())
                                if yaw > 0: self.action = -1
                                else: self.action = 1
                                self.speed = 0
                                self.DoAction = 1
                                self.cancelAction = 0
                        else:
                                #no collision, continue to waypoint
                                yaw = deltaYaw(self.MainPiv,target)
                                #self.MainPiv.setH(self.MainPiv,yaw*self.maxturnspeed)
                                if yaw > 4:
                                        self.MainPiv.setH(self.MainPiv,self.maxturnspeed*elapsed)
                                elif yaw < -4:
                                        self.MainPiv.setH(self.MainPiv,-self.maxturnspeed*elapsed)
                                if abs(yaw) > 90:
                                        self.speed = 0

                                self.speed +=.15
                                if self.speed > self.maxspeed: 
                                        self.speed = self.maxspeed
                                        if self.animation != 1:
                                                self.model.loop('run')
                                                self.animation = 1
                                elif (self.speed > (self.maxspeed/2.0)):
                                        if self.animation != 2:
                                                self.model.loop('walk')
                                                self.animation = 2
                                else:
                                        if self.animation != 0:
                                                self.animation = 0
                                                self.model.pose('walk',5)

                self.MainPiv.setY(self.MainPiv,self.speed*elapsed)

                self.prevtime = task.time
                return task.cont

class barrel:
        def __init__(self,sPos):
                self.model = loader.loadModel('models/firehydrant')
                self.model.setPos(*sPos)
                self.model.setScale(2.5)
                self.model.reparentTo(render)

                height = getHeight(self.model)
                half_height = height/2
                width = getWidth(self.model)
                half_width = width/2
                length = getLength(self.model)
                half_length = length/2
                if half_width > half_length:
                        radius = half_width
                else:
                        radius = half_length

                ctube = CollisionTube(0,0,1,0,0,-1,1)
                cpath = self.model.attachNewNode(CollisionNode('barrel'))
                cpath.node().addSolid(ctube)
                cpath.node().setFromCollideMask(BitMask32.allOff())
                cpath.node().setIntoCollideMask(BitMask32.bit(0))

def getHeight(model):
       pt1, pt2 = model.getTightBounds()
       modelZDim = pt2.getZ() - pt1.getZ()
       return modelZDim

def getWidth(model):
       pt1, pt2 = model.getTightBounds()
       modelXDim = pt2.getX() - pt1.getX()
       return modelXDim

def getLength(model):
       pt1, pt2 = model.getTightBounds()
       modelYDim = pt2.getY() - pt1.getY()
       return modelYDim

def deltaYaw(obj1,obj2):
        point = obj1.getRelativePoint(render,Vec3(obj2.getX(),obj2.getY(),0))
        return degrees(atan2(point.getY(),point.getX()))-90

def update_target():
        target.setPos(Vec3(random.uniform(-150,150),random.uniform(-150,150),0))

def update_camera():
        global view
        view += 1
        if view > 2: view = 0
        
        if view == 0:
                base.camera.setHpr(0,0,0)
                base.camera.setPos(0,-30,10)
        elif view == 1:
                base.camera.setHpr(0,-90,0)
                base.camera.setPos(0,0,100)
        elif view == 2:
                base.camera.setPos(0,-90,0)
                base.camera.setPos(0,0,200)

base.cTrav = CollisionTraverser('main')

random.seed(globalClock.getFrameTime())
target = loader.loadModel('models/smiley')
target.reparentTo(render)
base.accept('t',update_target)
base.setFrameRateMeter(True)
base.setBackgroundColor(0,0,0)
base.accept('esc',sys.exit)

for i in range(0,50):
        c = barrel((random.uniform(-150,150),random.uniform(-150,150),0))
for i in range(0,10):
        b = bot(sPos=(random.uniform(-150,150),random.uniform(-150,150),0))

base.disableMouse()
base.camera.reparentTo(b.MainPiv)
base.camera.setPos(0,-30,10)
view = 0
base.accept('c',update_camera)
run()