# help with character rotation

I’m making my character rotate using wasd (status[6-9]). The problem is, I want him to rotate the short way around all the time. It’s fine if the character doesn’t jump from 0 to 360 degrees, but if he does, he spins the long way around. In general if he spins more than 180 degrees, he does it the long way. It might be easier to understand what I mean by running the whole program (Legends.py), but the problem code, called every frame, is below:

``````#rotate character
#from angle A
self.fromAngle=int(self.megaman.getH())

#to angle B
if self.status[6] and self.status[7]: self.toAngle=45
elif self.status[7] and self.status[8]: self.toAngle=135
elif self.status[8] and self.status[9]: self.toAngle=225
elif self.status[6] and self.status[9]: self.toAngle=315

elif self.status[6]: self.toAngle=0
elif self.status[7]: self.toAngle=90
elif self.status[8]: self.toAngle=180
elif self.status[9]: self.toAngle=270

#difference
dAB=(self.fromAngle-self.toAngle)

def spinRight():
self.megaman.setH(self.fromAngle-self.spinSpeed)

def spinLeft():
self.megaman.setH(self.fromAngle+self.spinSpeed)

if dAB>0:spinRight()
if dAB<0:spinLeft()``````

Thanks for any help! I’m sure this type of problem has been encountered before. The angles are hard coded too, so if the camera rotation changes, the controls will be off by the amount the camera rotated around the character

Try this:

``````        self.toAngle = PythonUtil.fitDestAngle2Src(self.fromAngle, self.toAngle)
#difference
dAB=(self.fromAngle-self.toAngle) ``````

Of course you also need to:

``from direct.showbase import PythonUtil``

at the top.

PythonUtil.fitDestAngle2Src() is a handy function that will return the second parameter modified to be within +/- 180 degrees of the first angle.

David

``````if dAB>0:spinRight()
if dAB<0:spinLeft()``````

this doesnt always work cuz lets say my fromAngle = 17 and my toAngle =270

The shortest way would be to turn right but when you get the difference it is -253
Those if statements tell your guy to turn left

You need to figure the distanceof going one way(preferrably left) and base it on that

something like this:

``````if (toAngle > fromAngle):
Dist = math.abs(fromAngle-toAngle)
if (fromAngle > toAngle):
fromAngle -= 360
Dist = math.abs(fromAngle - toAngle)

if (Dist > 180):
turnRight()
else:
turnLeft()
``````

Great! Thanks david, and thatguy. It works perfectly. I wanted the controls to be the same no matter where the camera is, so I tried something like:

``if self.status[6]: self.toAngle=0+camera.getH()``

but the PythonUtil makes the character jitter back and forth, especially at larger camera angles(>5). Is there another way to control the character direction with different camera angles?

never mind, I solved it accidentally using nodes. I love them Sam I Am.

I know this is an old thread, but maybe we can solve this once and for all?
The challenge: Rotate an object over a period of time so that if faces another object. Throwing in some keywords: Euler angles, quaternions, dot products, tasks, fitDestAngle2Src.

I tried using a dummy node and lookAt() and Euler angles, but the heading of the objects is basically random. Tried drwr’s quaternion solution. Failed or didn’t get it or both.

I tried and failed:

``````def decideAndTurn(self):
randomangle = randrange(-180,180,10) # give us a random turn, this is a span to go, not a point to reach
currentH = self.ShipBase.ShipModel.getH() # get the current heading
randomH = currentH + randomangle # add or substract the target heading
targetH = fitDestAngle2Src(currentH, randomH) # see API reference

print "currentH", currentH, "randomangle", randomangle, "targetH", targetH, "+/- 180"
# let's what we got

if randomangle <= 0: #	left  H +
if randomangle <= -90:
else: #	 right H -
if randomangle >= 90:
# lets assume startGoing() starts acceleration in the reached direction

### turn functions

# turn left ... the current heading is smaller then the target heading
# for example -120 < -30 or 20 < 160
# so turn left until the targetH is hit from below

def turnLeft(self, targetH, task): # if getH < 0 set H +
elapsed = globalClock.getDt()
if self. Object.getH() < targetH:
self. Object.setKey("turnleft", 1)
self. Object.setKey("turnleft", 0)

# turn right ... the current heading is larger then the target heading
# for example 120 > 30 or -20 > -160
# so turn right until the targetH is hit from above

def turnRight(self, targetH, task): # if getH > 0 set H -
elapsed = globalClock.getDt()
if self.Object.getH() > targetH:
self. Object.setKey("turnright", 1)
self. Object.setKey("turnright", 0)

### function similar to the Asteroids sample

if (self.Object.keyMap["turnleft"]!=0):
self.Object.setH(self.Object, + elapsed * TURN_FACTOR) #TURN FACTOR - LARGER IS FASTER
if (self.Objects.keyMap["turnright"]!=0):
self.Objects.setH(self.Object, - elapsed * TURN_FACTOR) #TURN FACTOR - LARGER IS FASTER``````

Somebody please explain why this does not work and maybe give a bullet proof solution via a dot product. rdb is surely right when he says that was the safest solution, but frankly I just don’t get it.

This problem doesn’t seem to be that closely related to the OP–he was asking about turning based on user input, which is different than turning to face an object.

I also wonder what you’re referring to by “drwr’s quaternion solution,” since my proposal in this thread has nothing to do with quaternions.

Finally, it seems that the code you’ve posted is far more complex than you need for the simple problem of something rotating to face another object.

Why not just use an interval to turn your object over time? An interval can be used to turn an object to face any angle you like over any period of time you like. If you want the object to turn to face another object, it just means you need to figure out first what rotation you need for that.

You can use lookAt() for this:

``````origHpr = self.Object.getHpr()
self.Object.lookAt(self.ShipBase.ShipModel)
targetHpr = self.Object.getHpr()
self.Object.setHpr(origHpr)``````

And once you have a target rotation, you can construct an interval to apply it:

``````ival = self.Object.hprInterval(5, hpr = targetHpr)
ival.start()``````

Or, to avoid the problem with HPR angles mentioned in this thread, perform the interval using quaternions internally instead (which is legal even though you’re dealing exclusively with HPR angles):

``````ival = self.Object.quatInterval(5, hpr = targetHpr)
ival.start()``````

David

Maybe it’s not close to the OP, but why not have a thread for people to study? And I’m sure turning over time, instead of just snipping into the right heading is what he meant and what 99% of the users want.

Yes, your quat solution was here. I am looking for a flexible solution. Let’s assume that randomangle in my codebit is come from anywhere, from a lookAt check, something random or whatever.

I don’t like intervals. What if the target moves? Call the interval from the outside to finish after a positive success check? Could get you into attribute problems. At the end of the interval check for success, fails, decide new turn, start new interval, repeat?

I would like a task that reproduces itself. Get the angle to a target, decide the rotation direction, start turning in that direction. Every frame, check if the target is reached, if not, decide the direction and start turn further. If the target rotates around the object faster than it can turn, it spins forever. If the target is reached, kill the thread and spawn some other action.

OK, it means you have to reimplement what the lerp is doing for you automatically. A lerp is fundamentally a smooth adjustment of a value over time, and it comes down to the basic lerp equation: A + (B - A) * t, where t is a value 0 to 1. Each frame, you have to compute A, B, and t appropriately.

Here’s a sample program that demonstrates this in practice. See if you can understand how it works:

``````from direct.directbase.DirectStart import *
from pandac.PandaModules import *
from direct.interval.IntervalGlobal import *
from direct.showbase.PythonUtil import fitSrcAngle2Dest
import random

# Smiley can wander at random around a [-4 .. 4] cube.
smileyRange = 4
smileyTime = (0.5, 3)
def chooseSmileyPlace():
target = (random.uniform(-smileyRange, smileyRange),
random.uniform(-smileyRange, smileyRange),
random.uniform(-smileyRange, smileyRange))
moveTime = random.uniform(*smileyTime)
waitTime = random.uniform(*smileyTime)
ival = Sequence(smiley.posInterval(moveTime, target),
Wait(waitTime),
Func(chooseSmileyPlace))
ival.start()

smiley.reparentTo(render)
chooseSmileyPlace()

# Now a task to keep the "camera" model looking at smiley.
cameraSpeed = 10 # degrees per second
# What direction are we facing now?
origHpr = camera.getHpr()

# What direction do we want to be facing?
axis.lookAt(smiley)
targetHpr = axis.getHpr()

# Make the rotation go the shortest way around.
origHpr = VBase3(fitSrcAngle2Dest(origHpr[0], targetHpr[0]),
fitSrcAngle2Dest(origHpr[1], targetHpr[1]),
fitSrcAngle2Dest(origHpr[2], targetHpr[2]))

# How far do we have to go from here?
delta = max(abs(targetHpr[0] - origHpr[0]),
abs(targetHpr[1] - origHpr[1]),
abs(targetHpr[2] - origHpr[2]))
if delta == 0:
# We're already looking at the target.

# Figure out how far we should rotate in this frame, based on the
# distance to go, and the speed we should move each frame.
t = globalClock.getDt() * cameraSpeed / delta

# If we reach the target, stop there.
t = min(t, 1.0)

# Now use simple componentwise lerp to rotate just a little bit
# from the current direction towards the target direction.  This
# is the basic lerp equation, to compute a value on the line
# somewhere between origHpr and targetHpr.  If t == 0, this
# computes origHpr; if t == 1, this computes targetHpr.  If t is
# anywhere in between, it computes a point proportionately
# between.
newHpr = origHpr + (targetHpr - origHpr) * t
camera.setHpr(newHpr)

# The large, colored axis shows the direction the camera is
# trying to look.
axis.reparentTo(render)

# The small, gray axis shows the direction the camera is actually
# looking.
axis2.reparentTo(camera)
axis2.setScale(0.5)
axis2.setColor(0.8, 0.8, 0.8, 1)

camera.reparentTo(render)

# Also, start the viewpoint a little bit back so we can see what's
# going on.
base.trackball.node().setPos(0, 20, 0)
run()``````

Well, here we go again …
I thought this would work, but the projectiles are flying in a chain around the target, but hardly hit it.

``````given: projvel (float), projturn (int - degree per second), projnode (node)

targetnode = self.ShipObjects[int(projnode.getTag("targetid"))].ShipModel
targeter = projnode.getPythonTag("targeter")
origH = projnode.getH()
targeter.lookAt(targetnode)
targetH = targeter.getH()
origH = fitSrcAngle2Dest(origH, targetH) # src dest
deltaH = targetH - origH
delta = globalClock.getDt() * projturn
if deltaH == 0:
elif deltaH > 0:
newH = origH - delta
else:
newH = origH + delta
projnode.setH(newH)
mccodebin.setVelocity(projnode, currentDir)
self.positionObject(projnode)``````

What am I doing wrong? The ship is supposed to spawn the rocket, and it turns onto the target while proceeding.

Well, ThomasEgi (thx!) brought me to this:

``````targetnode = self.ShipObjects[int(projnode.getTag("targetid"))].ShipModel
targeter = projnode.getPythonTag("targeter")
origH = projnode.getH()
targeter.lookAt(targetnode)
targetH = targeter.getH()
targetH = fitDestAngle2Src(origH, targetH) # src dest

if targetH - origH > elapsed*projturn:
targetH = origH - elapsed*projturn
if targetH - origH < -elapsed*projturn:
targetH = origH + elapsed*projturn
projnode.setH(targetH)
projnode.setY(projnode, projvel/projnode.getSx() * -elapsed)``````

but the behavavior is similar to what is in this video: filefront.com/17190250/Panda3D_20100810.mp4/

shouldnt it be more like + and - swapped?

``````
if targetH - origH > elapsed*projturn:
targetH = origH + elapsed*projturn
if targetH - origH < -elapsed*projturn:
targetH = origH - elapsed*projturn``````

if it’s still circeling around your target you might want to replace the setY with setX

No, I don’t think that’s it. The behavior isn’t any different. Yeah I know that sounds weird. This is …

filefront.com/17192462/Panda3D_20100811.mp4

… what I am getting with this code:

``````targetnode = self.ShipObjects[int(projnode.getTag("targetid"))].ShipModel
targeter = projnode.getPythonTag("targeter")
origH = projnode.getH()
targeter.lookAt(targetnode)
targetH = targeter.getH()
targetH = fitDestAngle2Src(origH, targetH) # src dest

if targetH - origH > elapsed*projturn:
targetH = origH + elapsed*projturn
if targetH - origH < -elapsed*projturn:
targetH = origH - elapsed*projturn
projnode.setH(targetH)

projnode.setY(projnode, projvel/projnode.getSx() * -elapsed)``````

But -setY should be correct. +setY and the missiles travel backwards, -setX or +setX and they go sideways.

I’m trying hard to find a pattern here, but I don’t see one. Some rockets hit the target fine. Others steer upon the target and then drift away. Others don’t take the right direction from the beginning.

Sometimes I am thinking that once a rocket roughly faces the target it should maybe not try to adjust its course anymore. Like once you are within a certain corridor, dont go left or right anymore. On the other hand, if this was relevant, the rocket should wiggle left and right while closing in on the target, which right now it doesnt.

this one does the trick for me.

``````        elapsed = globalClock.getDt()
projturn = 180
origH = model.getH()-90
self.model.lookAt(dummy)
targetH = model.getH()
targetH = fitDestAngle2Src(origH, targetH) # src dest
print targetH
if targetH - origH > elapsed*projturn:
targetH = origH + elapsed*projturn
if targetH - origH < -elapsed*projturn:
targetH = origH - elapsed*projturn
model.setH(targetH+90)
model.setX(model, 5 * elapsed)``````

there “model” is my rocket-node, and “dummy” is the target node.
if your rocket travels sidewards you may have to rotate the geom in your editor or by code.

if the rocket can turn fast enough it’ll hit the target, if not it’ll miss and orbit around it until it’s able to hit it.

Well, still doesnt work. Here’s the current code:

``````if projturn > 0 and int(projnode.getTag("targetid")) in self.ShipObjects.keys():
targetnode = self.ShipObjects[int(projnode.getTag("targetid"))].ShipModel
targeter = projnode.getPythonTag("targeter")
targeter.lookAt(targetnode)
targetH = targeter.getH()
origH = projnode.getH()
fittargetH = fitDestAngle2Src(origH, targetH) # src dest

if fittargetH - origH > elapsed*projturn:
newtargetH = origH + elapsed*projturn
projnode.setH(newtargetH)
elif fittargetH - origH < -elapsed*projturn:
newtargetH = origH - elapsed*projturn
projnode.setH(newtargetH)
else:
pass

projnode.setY(projnode, projvel/projnode.getSx() * -elapsed)

I mean your code makes sense, so it should work. When I comment all the relevant code and just use

``projnode.lookAt(targetnode)``

every frame I have to use

``projnode.setY(projnode, projvel/projnode.getSx() * elapsed)``

to get the projectile moving. In that case the movement is perfect, the missiles hit the target in a straight line. But I tried all kinds of variant, -setX, +setX, -setY and +setY, but only -setY works when I use the targetH stuff.

By the way when I use

``````axis = loader.loadModel('zup-axis.egg')
axis.reparentTo(projnode)``````

the green Y axis points along the movement / facing axis of the projectiles.

Isnt that a case where one should use dot product stuff? I mean to figure out if the target is left or right of the missile. That’s what I had before.

When I use something close to your code:

``````if projturn > 0 and int(projnode.getTag("targetid")) in self.ShipObjects.keys():

targetnode = self.ShipObjects[int(projnode.getTag("targetid"))].ShipModel
targeter = projnode.getPythonTag("targeter")
origH = projnode.getH()
projnode.lookAt(targetnode)
targetH = projnode.getH()

fittargetH = fitDestAngle2Src(origH, targetH) # src dest
print "origH: ", origH, "targetH: ", targetH, "fittargetH: ", fittargetH, "deltaH", fittargetH-origH

if fittargetH - origH > elapsed*projturn:
newtargetH = origH + elapsed*projturn
projnode.setH(newtargetH+90)
elif fittargetH - origH < -elapsed*projturn:
newtargetH = origH - elapsed*projturn
projnode.setH(newtargetH+90)
else:
pass

projnode.setX(projnode, projvel/projnode.getSx() * elapsed)

unguided rockets (only using the setX formula, ignoring the stuff above) come out sideways left of the model, facing in its direction.
Guided rockets just twitch and turn on the spot, going nowhere.

Problem solved, ThomasEgi once again giving the right hints. He had it working with +setX, I wanted -setY. This one works:

``````if projturn > 0 and int(projnode.getTag("targetid")) in self.ShipObjects.keys():

targetnode = self.ShipObjects[int(projnode.getTag("targetid"))].ShipModel
origH = projnode.getH()-180
projnode.lookAt(targetnode)
targetH = projnode.getH()
targetH = fitDestAngle2Src(origH, targetH) # src dest

if targetH - origH > elapsed*projturn:
targetH = origH + elapsed*projturn
elif targetH - origH < -elapsed*projturn:
targetH = origH - elapsed*projturn
else:
pass
projnode.setH(targetH+180)
projnode.setY(projnode, projvel/projnode.getSx() * -elapsed)``````

Here’s mine, without fitDestAngle2Src, but by limiting the lookAt resulting hpr to the target with clampScalar, to all 3 axis.

``````from pandac.PandaModules import *
from direct.interval.IntervalGlobal import *
from direct.showbase.DirectObject import DirectObject
from random import choice
from copy import copy
import direct.directbase.DirectStart
import os

base.cTrav = CollisionTraverser('')
base.cTrav.setRespectPrevTransform(1)
CHPusher = CollisionHandlerFluidPusher()
CHEvent = CollisionHandlerEvent()

class Proj(DirectObject):
lastID = -1
def __init__(self,target):
self.target = target
self.targetExist = True
self.maxSpeed = self.speed = 70.
self.turn = self.speed*3
self.p.reparentTo(render)
self.p.setP(180)
self.p.setY(2)
self.p.setScale(.1,.25,.1)
self.p.flattenLight()
maxb = self.p.getTightBounds()[1]
Proj.lastID+=1
collName = 'proj%s'%Proj.lastID
self.collNP = self.p.attachCollisionSphere(collName, 0,maxb[1]*.5,0, maxb[1]*.2, PROJ2SHIP, GHOST)
self.p.setPos(player,0,player.getChild(0).getTightBounds()[1][1],0)
self.p.setHpr(player,0,0,0)
self.p.setColor(Vec4(1,1,0,1))
self.p.setTextureOff(1)
self.acceptOnce('%s-hit-ship'%collName,self.hit)
self.probe = self.p.attachNewNode('')

dt = globalClock.getDt()
if self.targetExist and self.target.getErrorType()==NodePath.ETRemoved:
self.targetExist = False
if self.targetExist:
self.probe.lookAt(self.target)
degThreshold = self.turn*dt
origHpr = self.probe.getHpr()
d = copy(origHpr)
for i in range(3):
d[i] = clampScalar(-degThreshold,degThreshold,d[i])
self.p.setHpr(self.p,d)
dist = self.p.getDistance(self.target)
# within this range, and not immediately after launched, and not head on,
# slow down smoothly, otherwise go full speed
if dist<30 and task.time>.5 and (d-origHpr).lengthSquared()>500:
self.speed = max(10,self.maxSpeed * dist/30.)
elif self.speed<self.maxSpeed:
self.speed+=15*dt
# after the target is destroyed, accelerate to maxSpeed
elif self.speed<self.maxSpeed:
self.speed+=15*dt
speed = self.speed*dt
self.p.setFluidY(self.p,speed)

def cleanup(self):
base.cTrav.removeCollider(self.collNP)
print 'CLEANUP',self.collNP.getName()
self.p.removeNode()
self.ignoreAll()

def hit(self,ce):
into = ce.getIntoNodePath()
if into.hasNetPythonTag('hit'):
into.getNetPythonTag('hit')()
self.cleanup()

class Ship:
def __init__(self):
global s
if choice([0,1]):
self.s.reparentTo(shipsParent)
self.s.getChild(0).setScale(3)
csNP = self.s.attachCollisionSphere('ship', 0,0,0, 3, SHIP2SHIP,PROJ2SHIP)
else:
self.s.reparentTo(shipsParent)
self.s.getChild(0).setScale(7,3,3)
b = self.s.getTightBounds()
dim = b[1]-b[0]
coll = CollisionNode('ship')
box = CollisionBox(*b)
boxNP = self.s.attachNewNode(coll)
csNP = self.s.attachCollisionSphere('bounds', 0,0,0, dim.length()*.5, SHIP2SHIP,GHOST)
self.s.setY(25)
self.s.setPythonTag('hit',self.hit)
self.health = 1
s=self

def hit(self):
self.health -= .25
self.s.setColor(1,self.health,self.health,1)
if self.health<=0:
self.destroyed()

def destroyed(self):
global s
if self.s.hasPythonTag('hit'):
self.s.clearPythonTag('hit')
base.cTrav.removeCollider(self.s.find('+CollisionNode'))
self.s.setTransparency(1)
Sequence(
Parallel(
self.s.scaleInterval(.2,Vec3(2)),
self.s.colorScaleInterval(.2,Vec4(1,1,1,0))
),
Func(self.s.removeNode),
Wait(1),
Func(spawnShip)
).start()
s=None

def spawnShip():
global s
s = Ship()

def fire():
if s is None: return
Proj(s.s)

dt = globalClock.getDt()
move = playerMove[0]-playerMove[1]
rotate = playerMove[2]-playerMove[3]
if move:
player.setY(player,move*20*dt)
if move<0:
rotate*=-1
if rotate:
player.setH(player,rotate*120*dt)

def toggleShipMovement():
if shipsParentIval.isPlaying():
shipsParentIval.pause()
else:
shipsParentIval.resume()

shipsParent = render.attachNewNode('')
shipsParentIval = shipsParent.hprInterval(4,Vec3(360,0,0))
shipsParentIval.loop()
spawnShip()

player.reparentTo(render)
player.setTextureOff(1)
coll = CollisionNode('player')
box = CollisionBox(*player.getTightBounds())
playerColl = player.attachNewNode(coll)

playerMove = Vec4(0)
base.accept('f',fire)
base.accept('arrow_up',playerMove.setX,[1])
base.accept('arrow_up-up',playerMove.setX,[0])
base.accept('arrow_down',playerMove.setY,[1])
base.accept('arrow_down-up',playerMove.setY,[0])
base.accept('arrow_left',playerMove.setZ,[1])
base.accept('arrow_left-up',playerMove.setZ,[0])
base.accept('arrow_right',playerMove.setW,[1])
base.accept('arrow_right-up',playerMove.setW,[0])
base.accept('space',toggleShipMovement)
base.accept('escape',os._exit,[0])