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()