Archer aiming at a moving target

Question

I’m making a medival battle simulator. one of the units in this game are archers, and more often than not, they have to be hitting some moving enemies.
I have already figured out most of the math for this, however It is still problematic.

here’s the code:

# closest enemy
var e : CharacterBody3D = findClosestEnemy()

# arrow speed
var s : float = 0.5

# target global Position relative to the archer
var P : Vector3 = e.global_position - global_position

# the velocity of the enemy
var V : Vector3 = e.velocity


# solving some quadratics
var a : float = V.dot(V) - (s * s)
var b : float = 2.0 * V.dot(P)
var c : float = P.dot(P)
var discriminant : float = b * b - 4.0 * a * c

var expectedPos : Vector3 = global_position

# if the targert is moving away faster than the bullet can reach it
if discriminant < 0:
	expectedPos = e.global_position
else:
	var t : float = (-b - sqrt(discriminant))/(2*a)
	expectedPos = e.global_position + V * t

When i try to run this code though, the archer shoots the arrow only 1/3 of the way to the enemy.

note: the white cylinder in front of the archer is the expected position

please help. I have been trying to fix this problem for the past month.

If the cylinder is the expected position, where’s the problem? It looks like it goes exactly to the expected position.

The cylinder’s position is only a visualization of where the arrow will land after being shot. I need the arrow to hit the enemy. So if it worked correctly, the cylinder would be somewhere close to the enemy’s position

What are those quadratics that are being solved?

Yep. I’ll explain all I know right now

The archer is trying to shoot an arrow at the enemy, however, the arrow is slow and takes some time to reach a target that is far away. If we where to simply shoot at the enemy’s current position, by the time the arrow reaches that spot the enemy would have already moved away.

Here, I’m trying to solve for the position the enemy would be at by the time the arrow hits him, and the time the arrow takes to reach that position.

The expected position is where the enemy would be when the arrow reaches it. The formula for that being:

Expected position = enemy.current position + enemy.velocity* time

We can only use this formula when we have found the time it takes the arrow to reach the enemy. This however requires some mathematics, and I got the code above.

Well then, the calculated time might be too short if the arrow doesn’t go all the way to the enemy.

I’ve tried plugging this into desmos and I don’t think that is the case. It probably has something to do with how godot’s transforms work.

If expectedPos is correctly calculated and you assign it to cylinder’s global_position, the cylinder should show up exactly at the calculated position. So either you don’t position the cylinder correctly or expectedPos is not correctly calculated. Since the spear also goes to the position of the cylinder, the only reasonable conclusion is that expectedPos is not correctly calculated.

var discriminant : float = b * b - 4.0 * a * c

Does Godot follow order of operations? I’d check to make sure the discriminat is evaluated correctly.

I have changed it to this, but I’m getting the same results, so I don’t think that’s the problem.

var discriminant : float = (b * b) - (4.0 * a * c)

I have been thinking about it and it probably has something to do with how I’m getting the enemy’s velocity.
In this example, the enemies velocity is -2 m/s along the x-axis and the distance between the archer and the knight being 100 m. if this were true, it would only take the knight 50 frames to get to the archer, but in the game, the knight moves much slower than that.

This is how the knight moves:

const WalkSpeed : float = 2

# targetPos is the archer's position
func move() -> void:
	var t : Vector3 = (-global_position+targertPos).normalized()
	velocity = t * WalkSpeed
	
	move_and_slide()

maybe in the move_and_slide() function, the knight isn’t actually moved -2 m/s along the x-axis. would there be a reason for this?

It would take the knight 50 seconds not 50 frames.

sorry, I meant -2 metres/frame not -2 metres/second.

Also, I figured it out. after doing some research, i realised the move_and_slide() function automaticly multiplies velocity by delta, so when i set the velocity to -2 metres/frame, it actually moves -2*delta metres/frame.

to counter act this, I changed the knights move function so it divides the walkspeed by delta before being multiplied by delta in move_and_slide():

func move(delta) -> void:
	var t : Vector3 = (-global_position+targertPos).normalized()
	velocity = t * WalkSpeed / delta
	
	move_and_slide()

I also reduced the knights walkspeed to 2/60.

and now the archer shoots at the knight properly.

thanks for the help.

A quick question, isn’t that targetPos (or e.global_position in the original post) grounded to the floor? I just wonder whether you also would want to add a little bit of Z for a clean headshot.

All the objects in this game are grounded to the floor. there is no difference in height. This is to improve on performance, since there will be hundreds of archers firing arrows at the same time.

And for headshots, I won’t bother.

For this type of game, adding extra damage for headshots doesn’t really make that much sense since the player isn’t the one firing the arrow.