While working on DEBASER‘s enemy AI, I found a bug that is so miraculous that I almost want it to be a feature. Due to a series of coincidences, I have unintentionally programmed my enemies to become contact jugglers.
Beautiful, isn’t it? They can even do it with longer items:
There’s no animation or trickery here, this phenomenon is completely physics based. The enemy’s collider is juggling the item.
This juggling is the result of an enemy trying to pick up a gun that is on his head.
To perform any action, the enemy starts by moving to the target (the gun) and performing the action (picking up the gun). However, for the performance stage to begin, the enemy must be within a certain range of the item. This distance is measured from the root transform of the enemy, located at his feet (after all, items usually fall to the ground). The problem with this scenario is that this range happens to be slightly shorter than the height of the enemy. So the enemy can’t pick up the item until he arrives, but he can’t get there if the item is on his head: it’s a catch-22.
You may ask, “but why doesn’t the gun fall off the enemy? Surely it’s not being balanced on its exact center of mass?“
That’s a perfectly reasonable, yet incorrect assumption: the enemy is balancing the item exactly on its center of mass. Does this mean my enemy AI has become self aware? No. The reason for this goes back to my system for picking up items. Most gun / swords models have their origin located in the handle:
This means that if we were standing close to the tip of this gun, we can’t pick it up because the distance is being evaluated all the way from the handle. To solve this problem, I needed to evaluate distances from some kind of center. Since all item drops have a Rigidbody component and approximately accurate colliders, I figured I would just use the center of mass if it was available.
I’m currently working on an FPS called DEBASER. In this game, until very recently in development, the enemies had 100% accuracy when shooting, so long as you were visible by the time they pulled the trigger. Obviously, this is both unrealistic and makes the game unreasonably hard. After some playtesters complained, I began searching for a solution. For simplicity, I decided that I would use a probabilistic model to determine if the enemy misses or hits to player according to RNG.
As a starting point, I figured that a good model for shot accuracy should take two variables into account: distance and speed of the target. If you were trying to shoot something, you would much rather it be close to you and not moving, than far away and zig-zagging.
An immediate problem with this model is that speed can be misleading. It’s difficult to hit a target running around you in a circle, but it’s easy if they’re running directly towards you. This is because you only have to account for movement that changes the angle you must shoot at. It follows that we would be better suited to consider the angular velocity of the target relative to the shooter.
Above is a basic diagram showing how we get the angular velocity of the player relative to the enemy. Note that v is the tangential velocity, which is found by projecting the velocity vector of the player onto the plane whose normal is the facing vector of the enemy:
The Difficulty Model
As an intermediate to the Accuracy model, let’s first model “difficulty.” Difficulty (D) will be a number from 0 to infinity, where 0 means it is 100% likely that the enemy will hit the player and infinity means it is 0% likely that the enemy will succeed. Difficulty should increase as range and angular velocity increase, like so:
To get an intuition for how this function behaves, let’s graph it with D on the y axis and r on the x axis. v will raise and lower.
Notice that the difficulty starts at 0 when the distance is zero. Intuitively, this makes sense. However, the function is asymptotic: it converges to a number, not infinity. This makes no sense, as difficulty should always be rising as distance increases. The reason for this convergence is that, as you get farther away, angular velocity shrinks so fast that it cancels out the growth of r. The solution to this is to add a “helper” constant (c) to the angular velocity at all times. This will ensure that it always approaches infinity.
Above, you can see the effect that increasing the helper constant has on the limits of the Difficulty model.
With that, our Difficulty model is complete:
The Accuracy Model
Our final accuracy model should represent the probability from 0 to 1 that the shot will be accurate. Because Difficulty can be anywhere from 0 to infinity, we should start by establishing two more constants to confine our results: L = the difficulty below which accuracy is 100%. U is the difficulty above which accuracy is 0%.
Above we see L and U in the context of the current difficulty model. To create an accuracy model, we need to form a piecewise function. The behavior when D is above and below L and U has already been specified above, but what about in-between? We don’t want this function to have any gaps in it, so what we can do is interpolate from 1 to 0 based on how far D has traveled throughout the region between U and L. The math behind this interpolation gets a bit messy but the overall concept is simple. The resulting Accuracy model is as follows:
Accounting for Human Stupidity
Humans are bad at probability (well, maybe it’s just me…); as I play-tested this model, I noticed that the AI was still very accurate. At 50% accuracy they were still very often taking me out on the first try. In a deliberate departure from realism, I added another constant, h, which I’ve affectionately named the Constant of Human Stupidity. The final model simply applies h as an exponent to A. Along with making it easier to avoid getting shot, this constant has the effect of making the model look more exponential for higher values of h. Below is the result of raising and lowering h.
Earlier today I had to spend a whole 20 minutes coming up with this function. It’s like a modulo function, but instead of looping back to zero every cycle, it transitions back down to zero. It cycles every 2(n-1) elements. There’s probably a proper name for it, but I can’t figure it out.
In case anyone else would rather google it than spend time figuring it out, here it is:
cycle = 2 * (n - 1)
i = i % cycle
if i >= n:
return cycle - i
I hope this is helpful to a fellow lazy person in the future.