The New Direction
This update is a little different than what we have done up to now. This marks a small change of content on the blog, which will from this post be more in-depth about Baltica’s development and focused toward other developers more than our core audience.
With that out of the way: we’re gonna dive into the magical world of predictive projectiles today.
In a strategy game; a RTS like Age of Empires 2, or a unrelated genre game; action adventures like Dark Souls and The Legend of Zelda: Breath of the Wild, enemies fire projectiles at your characters. The general idea is exhibited in all the examples: an AI determines that an arrow (or other projectile) should be fired at a target. However, if the target is moving, the AI has exactly 0% chance of hitting if the shot is aimed at the target’s current location. In AoE2, all ranged units do this until a specific technology is researched, as demonstrated below:
As demonstrated, when the technology is researched, the arrows fire at the target’s predicted position, giving them a greatly increased hit rate against a moving target. In Baltica, we wanted to control the ranged units so they could accurately hit moving targets, while also controlling how well they would be able to. So whether you want some or all of your units to fire with position prediction, how would you actually go about implementing it?
First, we cheat
When calculating projectile trajectories, we opted to fake physics. What does that mean, to fake physics? Well, instead of calculating a projectile’s gravity, launch angle, initial velocity (and, if you’re really insane: air resistance), we simply send the projectile on a parabolic arc directly towards the target. This makes it infinitely less troublesome for us to sort out how exactly a unit aims. It’s not that I flunked calculus – though that might be part of it – faking it well is probably better than the real thing, unless you’re intentionally aiming for realism.
This way, we can assign any point to a projectile, and know for certain that that projectile will hit that point at some time in the future. We can even easily know when that will happen by dividing its initial distance with its speed in the horizontal plane. (Remembering to take frame time into account). Here is the actual Unity C# implementation:
hoirizPos = Vector3.MoveTowards(hoirizPos, impactTarget, initialSpeed * Time.deltaTime * 60f); float distanceDelta = Vector3.Distance(hoirizPos, start); float normDelta = distanceDelta - (Vector3.Distance(start, impactTarget) / 2f); float startDelta = Vector3.Distance(start, impactTarget) - (Vector3.Distance(start, impactTarget) / 2f); float yPos = (-0.1f * parabolaArc * Mathf.Pow(normDelta, 2f)) - (-0.1f * parabolaArc * Mathf.Pow(startDelta, 2f));
Then, we throw math at it!
You’d think this is a really simple problem, right? If you know the target’s speed and direction (ie. its velocity vector), you could just aim wherever the target is going to be then, right? But then the question becomes when? The projectile itself has travel time too, which we must take into account (this is where faking physics starts to come in real handy).
In this example assume the projectile has a constant velocity 50m/s and the target is 10m away. (The units are unimportant here, and in the implementation the Unity unit is in fact an arbitrary length.) We get that the travel time is 0.2 seconds for the arrow to reach the target’s current position. Then we can use the target’s speed and calculate where the target will be in .2 seconds and aim there! …or?
The archer misses
A recursive problem?
The most astute of you might have noticed from the illustration above, that the new trajectory is in fact longer than the original, momentarily direct path. This means, of course, that the trajectory will still lag behind its target. Now you could use some simple trigonometry to calculate the length of the new trajectory – then calculate the new travel time – then make a new prediction based on that travel time, however, the same problem will simply repeat itself – albeit on a smaller scale. You would get closer and closer to a good prediction using this method, however it’s neither very pretty or performant. There is a certain specific math for this kind of problem in calculus, if we want to model reality as close as possible – but do we?
Let me guess, more cheating?
Making games, as we know, is all about tricks and cheats. In fear of repeating myself; a good approximation can be better than the real, accurate calculation. For this problem, I employed a general approximation. First, we establish a heading function that returns 2 when a angle is 0°, 1 when it is 90° or 270°, and 0 when it is 180°. This means that we get a bigger number when the target is moving away from us, and a smaller number when its moving towards us. This can easily be done by taking the cosine of the angle between the origin of the projectile, the target, and the target’s speed vector, then adding 1. We then take some constant and multiply it by this heading function to get a really close estimate to where our target is going to be.
float a = 1f + Mathf.Cos(Vector3.Angle(closestOnDefender - transform.position, attackTarget.transform.forward)); aimTarget = attackTarget.transform.position + (attackTarget.GetComponent<NavMeshAgent>().velocity * t * a * Time.smoothDeltaTime);
(t is the calculated travel time for the projectile)
He shoots, he scores!
We can’t wait to show you more of the inner workings of Baltica in the coming weeks!
Øivin M, Milk Carton Games