Something that’s been on my mind recently is the implementation of aim assist in fixed-perspective
games like Hades and Death’s Door, when aiming with an analog stick.
When playing these games, I’ve had experiences where I felt a disconnect
between what I wanted to do, and what the game’s aim assist thought I wanted to do.
Fundamentally, I felt like my issue was that many of these games implement more
of an aim automation system than aim assist. The effect is much more pronounced in Hades,
where the weapon can snap by as much as 90 degrees, wrestling control away from the player.
In Death’s Door, the effect is much more subtle, as the game never visualizes the
correction it applies to the user input, and doesn’t correct by more than 15 degrees.
For the sake of this video, I reverse engineered the aim assist code in both of these games,
because I think it highlights the types of gameplay design problems
that aim assist attempts to address. I’ll then present my own thoughts on
how to better formulate the problem of aim-assist in a way that makes it easier
to think about how to balance things like player intention, skill, and gameplay feel.
Hades is a fast-paced game where players often find themselves surrounded by fast moving enemies
In general, every enemy must be dealt with before the player can progress to the next room
In this context, aiming is often secondary to hitting, and so the aim assist in Hades
appears to have been designed to ask the question “What are the targets that the player might want
to hit in this context”, and, “Which one is the closest to the player’s aim direction?”
In theory, this ensures that any target the game selects will always make
incremental progress to clearing the room. Weapons which require line of sight will
only snap to targets that have line of sight. Weapons won’t snap to enemies that are off screen,
that are currently invulnerable, or that are outside of the range of the player’s attack.
For each potential target, the game uses the current gameplay context to make its best estimate
of whether that target is valid for aiming or not. For the targets which pass, the game selects
the target whose direction is closest to the player’s input direction, and whose distance
to the player and arc distance to the player’s input direction falls below some threshold.
In Death’s Door, the aim assist is formulated in the exact opposite order.
It first asks, “What are the hittable objects closest to the player’s aim direction”,
then, “Which one is the player most likely to want to hit?”
This reversal is interesting because it allows for multiple layers of context sensitivity.
For example, puzzle elements have higher priority than nearby enemies,
which in turn have higher priority than faraway enemies.
Because both of these games snap the player input directly onto a target,
any mis-snap can lead to an instantly frustrating experience for the player.
This is mainly an issue when the player has a very specific target in mind, because the incongruity
between player intention and avatar action leads to a very noticeably jarring experience.
A common failure mode is when the player intends to shoot at a target which is
slightly out of range, but the aim assist erroneously corrects the player aim to a
completely different in-range target. This incongruity between intention and
action can be especially frustrating if aiming at the wrong target doesn’t make any tactical sense,
such as prioritizing a low-threat enemy instead of a high-threat enemy,
using a high-power attack on a weak enemy, or using an AOE attack on a lone enemy.
Aim assist is something that I want to incorporate in my own game,
and through some early prototyping, testing, and subsequent reflection,
I’ve come up with a few ideas that I’ve consolidated into what I think is a pretty
flexible framework for thinking about the problem and potentially addressing some of these issues.
Let’s represent player input direction and avatar orientation
as the x and y axes on a 2D phase graph. The map between player input and avatar
orientation is a function, f(x)
When f is the identity function f(x)=x
player input is mapped to avatar orientation directly without modification.
The avatar points in exactly the direction of input, as is the case when there’s no aim assist.
When f(x) is not equal to x, it describes the transformation
from the player input space to the avatar orientation space.
Now let’s consider a potential target near the player.
The target has a particular angular position and size from the player avatar’s perspective
The angular position changes as the target moves around the player avatar.
The angular size changes with distance, and is also correlated with the target’s physical size.
When f(x)=x, the range of avatar orientations that are on target
is equal to the range of controller inputs that are on target.
However, if we select f(x) to widen the target region, a greater range of controller inputs
get mapped to on-target avatar orientations.
In more mathematical language, we can call this the preimage
of the target region B under the function f, and we can think of it as being
the virtual size of the target from the perspective of the player’s controller.
Even for a physically small target, we can choose to maintain a large virtual target,
making it easier for the player to input an angle that points to somewhere on the target.
In this way, we can imagine constructing f(x) to always maintain a minimum preimage
size of each target, no matter their actual size or distance.
This allows us to set a skill floor for how hard we want it to be to aim at targets.
Another way of analyzing this function is by looking at its slope.
Regions of low slope represent regions where the function makes
it easier to aim by decreasing sensitivity.
They must be balanced by regions of high slope,
to ensure the total domain and range fill up the full 360 degrees.
Beyond this constraint, we can make many different decisions for the design of f(x)
depending on our goal as a gameplay designer. For example, we may like to smooth the curve to
reduce the jarring effect of a sudden change of sensitivity as we aim toward a target.
Perhaps for a precision weapon, we want the player to feel a change in
sensitivity at the center of the target no matter how large it is,
so we design f to always flatten the slope at the center of targets.
Perhaps we want to limit the extent to which we enlarge the virtual size
of targets on higher difficulty settings. Or to only provide the feeling of locking
on target without actually affecting the difficulty of hitting targets at all.
Maybe we decide that for predictability, we want to apply the same correction to all targets no
matter how big or small they are. And in each case, we can globally
decide how much assistance to provide by blending it with the identity function.
As a point of comparison, the sort of direct snapping aim assist approach we’ve previously seen
looks something like this in our visualization. We see that this indeed produces large virtual
targets, which any useful aim assist function should do.
However, the direct snapping approach also produces large output discontinuities,
and sets of avatar orientations whose preimage is completely empty, representing
directions that are impossible to aim at. Consequently, any aim assist system using
the snapping method must be highly robust in its target identification,
as any failure of the system results in catastrophic gameplay consequences.
The generalization of aim assist to include these smoother functions thus represents a
relaxing of the responsibility of the aim assist algorithm,
while still addressing the fundamental problem of compensating for imperfect player inputs.
In our search for the perfect function that satisfies our game design goals,
we may like to enforce a couple more restrictions. As previously mentioned we would like the domain
and range of the function to be all the angles between 0 and 360 degrees,
with a circular topology. We might like to enforce
that the slope of the function is never negative, or larger than some maximum.
We might also like to limit the magnitude of the correction to some reasonable value.
Beyond these simple constraints, we may have other constraints we’d like to enforce.
It might make sense to specify that perfect aim on target should never result in any correction.
And maybe we’d like to explicitly specify the sensitivity of f at each target depending on the target.
It may seem like a pointless complication to define all of these things mathematically, but there is some advantage
in at least thinking a little bit about it. In my opinion, mathematics is the universal
language of abstraction, and so if we understand the mathematics of our problem,
we can often find a wealth of solutions beyond our immediate application that we can make use of.
For example, these last two constraints look a lot like specifying function values
and derivatives at a set of points, which is a problem solvable by cubic spline interpolation.
However, we’d quickly realize that cubic splines don’t guarantee monotonicity,
which violates our second constraint.
A quick search leads us to monotone cubic interpolation, which handily solves our problem.
There are still many open problems that I haven’t addressed by this point,
but that deserve some thought when implementing such a system in a game.
How do we handle it when targets occlude one another, if the projectile requires line of sight?
How do we handle moving targets if the projectile has a finite travel speed?
How should we handle targets moving in and out of range or line of sight during aiming?
Should the aim assist correction magnitude
change depending on how fast the player’s analog input changes?
Does there need to be any sort of balancing between mouse and controller input?
I personally think these types of questions map very naturally to the
paradigm of thinking about aim assist as a function from player input to avatar output.
The production burden of this video has already greatly exceeded my expectation though,
so I’ll have to leave further exploration of the subject to you.
If you’re a game designer who’s worked on the aim assist problem before,
I’d be interested to hear your thoughts and experiences, things I’ve missed,
things that did or didn’t work out.
In any case, as always, thanks for watching.