I'm creating a python program where a chaser runs after a target. I have a function that moves the target, and I'm trying to make a function that outputs the chaser the angle it needs to move at to intercept it given the positions of itself and the target, the direction of the target, and the speed of itself and the target (they always run at top speed, and their speeds may or may not be different). The below image that I found describes pretty well what it looks like with an important caveat:
The caveat is that, like the image shows, I'm using an absolute angle system. 0 radians is the 3 o'clock position, continuing to 2pi radians as you go counter-clockwise. Thus, up and to the right is pi/4, straight ahead is pi/2, down and to the right is 7pi/4, etc.
However, in my game, sometimes the chaser is behind the target, and sometimes the target is moving to the left. These configurations break the formula. Either the chaser moves in the wrong direction, or there is a domain error with arcsine or arccosine. If you are curious, my WIP function that attempts to deal with this by using many formulas, but still doesn't work (it fails in the same way) is at the bottom of this post.
My question is this: how do I get this to work for all configurations of the chaser and the target? I've seen solutions using various techniques but they all either break in the same way mine does (when the configuration is anything other than the chaser moving down-right and the target moving up-right, the chaser moves in the wrong direction and/or raises a domain error with arcsine or arccosine), or they use a relative angle system.
I think the reason why the absolute angle system I am using is problematic is that you are given the targets angle, but the relationship between the target angle and the angles inside the triangle are different depending on if the target angle is between 0 and pi/2, pi/2 and pi, pi and 3pi/2, or 3pi/2 and 2pi. In my "P.S." I have an example of what I mean, if it doesn't make sense.
My question is simply: How do I make this function? Have I done it mostly right, and I just need to make a small change? Am I thinking about this totally wrong? Is there something the other posts do differently than my code that allows me to use their answers (how do I use relative angles in python)?
Any insight, solutions, or advice is appreciated!
P.S.
Below is an example of why the absolute angle measuring system I'm using is problematic, and why I tried to use many different formulas in my code (refer to the image I linked above for a diagram):
Let's call the angle formed between the y axis and the line connecting the chaser and the target "angle Beta". Angle beta can be calculated by taking the distance in each direction of the chaser and target- I'll call these distances deltaY and deltaX- and using the law of sines.
Beta = arcsin(deltaX/d) <-- d is the distance between chaser and target.
Therefore, angle Alpha = pi/2 - targetAngle - Beta
Now, imagine that we flip the image across the y-axis. Suddenly, pi - targetAngle - Beta is nothing. In this configuration,
Alpha = -pi/2 + targetAngle - Beta
The formula is different, because when we rotate the image, targetAngle changes- it has an absolute reference, not a relative one.
I hope this explanation of my problem is clear- if it isn't please respond telling me what part you don't understand so that I can clarify- I may also be wrong!
def chaseRec(self, target):
self.targetxpos = target.xpos
self.targetypos = target.ypos
self.targetspeed = target.speed
self.targetangle = target.currang - m.pi/2
print("target ang:", self.targetangle)
#gets all of the target information
self.deltaY = abs(self.ypos-self.targetypos)
self.deltaX = abs(self.xpos-self.targetxpos)
self.w = m.sqrt((self.deltaY)**2 + (self.deltaX)**2)
self.d = m.asin(self.deltaY/self.w)
self.z = m.pi/2 - self.d + self.targetangle
#Below line gives error
self.x = m.asin((self.targetspeed * m.sin(self.z)) / self.speed)
#Computes the location of (mx,my), the meeting point. Uses this to choose which formula to use to compute self.currang
if self.targetangle >= 0 and self.targetangle <= m.pi/2:
self.mx = self.targetxpos + self.targetspeed*m.sin((m.pi/2)-self.targetangle)
self.my = self.targetypos + self.targetspeed * m.cos((m.pi / 2) - self.targetangle)
elif self.targetangle > m.pi/2 and self.targetangle <= m.pi:
self.mx = self.targetxpos - self.targetspeed * m.sin((3*m.pi / 2) - self.targetangle)
self.my = self.targetypos + self.targetspeed * m.cos((3*m.pi / 2) - self.targetangle)
elif self.targetangle > m.pi and self.targetangle <= 3*m.pi/2:
self.mx = self.targetxpos - self.targetspeed * m.sin((3 * m.pi / 2) - self.targetangle)
self.my = self.targetypos - self.targetspeed * m.cos((3 * m.pi / 2) - self.targetangle)
else:
self.mx = self.targetxpos + self.targetspeed * m.sin((3 * m.pi / 2) - self.targetangle)
self.my = self.targetypos - self.targetspeed * m.cos((3 * m.pi / 2) - self.targetangle)
#Computes chaser angle
if self.mx > self.xpos:
if self.my > self.ypos:
self.currang = -1* self.d + self.x
else:
self.currang = 2*m.pi - self.d + self.x
else:
self.currang = m.pi + self.d - self.x
#moves the defender according to currang
self.xpos += ((self.speed * framelength) / 2) * m.cos(self.currang)
print("Xpos:", self.xpos)
self.ypos += ((self.speed * framelength) / 2) * m.sin(self.currang)
print("Ypos:", self.ypos)
EDIT: I've almost solved this, but my function breaks when chaser is faster than target, among other things. Thus, below is only a partial answer.
I think I just made my function work. I used the formulas from Jeffery Hantin's response to this post:
2d game : fire at a moving target by predicting intersection of projectile and unit
Below is my new function, for those who are interested, complete with a lot of useless print statements I used for debugging. The function takes into account the fact that angles are absolute in python. I have not tested it TOO extensively, but I haven't been able to break it yet. When it can't catch up, it just runs forwards.
def chaseRec(self, target):
self.targetxpos = target.xpos
self.targetypos = target.ypos
self.targetspeed = target.speed
self.targetangle = target.currang
print("target ang:", self.targetangle)
print("Xpos1:", self.xpos)
print("Ypos1:", self.ypos)
self.dX = False
self.dY = False
print('ds: ', self.dX, self.dY)
#gets all of the target information
#Calculates abs(deltaY)
if self.targetangle <= m.pi/2:
self.deltaY = self.targetspeed * m.sin(self.targetangle) # 23 * 1
elif self.targetangle <=m.pi:
self.deltaY = self.targetspeed * m.sin(m.pi-self.targetangle)
self.dX = True
elif self.targetangle <=3*m.pi/2:
self.deltaY = self.targetspeed * m.sin(self.targetangle-m.pi)
#If deltaY, deltaX is actually negative
self.dY = True
self.dX = True
else:
self.deltaY = self.targetspeed * m.sin(self.targetangle - 3*m.pi/2)
self.dY = True
#Calculates abs(deltaX)
self.deltaX = m.sqrt(self.targetspeed**2 - self.deltaY**2)
#Quadratic Parameters
self.a = self.deltaX**2 + self.deltaY**2 - self.speed**2
self.b = 2 * (self.deltaX * (self.targetxpos - self.xpos) + self.deltaY * (self.targetypos - self.ypos))
self.c = (self.targetxpos - self.xpos)**2 + (self.targetypos - self.ypos)**2
self.disc = self.b**2 - (4* self.a * self.c)
print('data:', self.disc, self.a, self.b, self.c)
print('data2:', self.deltaX, self.deltaY)
#Selects root, then finds where to aim
if self.disc < 0:
self.currang = m.pi/2
print('data:', self.disc, self.a, self.b, self.c)
print('data2:', self.deltaX, self.deltaY)
else:
self.r1 = (-1 *self.b + m.sqrt(self.disc)) / (2 * self.a)
self.r2 = (-1 * self.b - m.sqrt(self.disc)) / (2 * self.a)
print('rs: ', self.r1, self.r2)
if self.r1 < 0 and self.r2 <0:
self.currang = m.pi/2
elif self.r1 <= self.r2:
if self.dX and self.dY:
self.mx = -1 * self.r1 * self.deltaX + self.targetxpos
self.my = -1 * self.r1 * self.deltaY + self.targetypos
elif self.dX:
self.mx = -1 * self.r1 * self.deltaX + self.targetxpos
self.my = self.r1 * self.deltaY + self.targetypos
elif self.dY:
self.mx = self.r1 * self.deltaX + self.targetxpos
self.my = -1 * self.r1 * self.deltaY + self.targetypos
else:
self.mx = self.r1 * self.deltaX + self.targetxpos
self.my = self.r1 * self.deltaY + self.targetypos
else:
if self.dX and self.dY:
self.mx = -1 * self.r2 * self.deltaX + self.targetxpos
self.my = -1 * self.r2 * self.deltaY + self.targetypos
elif self.dX:
self.mx = -1 * self.r2 * self.deltaX + self.targetxpos
self.my = self.r2 * self.deltaY + self.targetypos
elif self.dY:
self.mx = self.r2 * self.deltaX + self.targetxpos
self.my = -1 * self.r2 * self.deltaY + self.targetypos
else:
self.mx = self.r2 * self.deltaX + self.targetxpos
self.my = self.r2 * self.deltaY + self.targetypos
#computes currang
print("meeting point: ", self.mx, self.my)
self.theta = m.atan(abs(self.my - self.ypos) / abs (self.mx - self.xpos))
print('theta: ', self.theta)
if self.mx > self.xpos:
if self.my > self.ypos:
self.currang = self.theta
else:
self.currang = 2 * m.pi - self.theta
else:
if self.my > self.ypos:
self.currang = m.pi - self.theta
else:
self.currang = m.pi + self.theta
print("Chaser ang:", self.currang)
#finds self.currang, while avoiding domain errors
self.xpos += ((self.speed * framelength) / 2) * m.cos(self.currang)
print("Xpos:", self.xpos)
self.ypos += ((self.speed * framelength) / 2) * m.sin(self.currang)
print("Ypos:", self.ypos)
#moves the defender according to currang