It’s common to use an analog input method (control stick, trigger button) to control a digital system. Maybe you want to control menus with an analog stick. Maybe you want to detect a tap or double-tap of the analog stick. Or, as is the focus of this article, maybe you want to fire a bullet with an analog trigger. Getting this right requires more subtlety than you might expect.
While INVERSUS isn’t shipping with shooting bound to an analog trigger, it did start out that way. You can get some insight into why I changed schemes in my article on the parry system (although it might merit its own article), but the important thing to make clear is that it wasn’t changed due to trigger feel; the triggers felt great.
Let’s assume we have successfully preprocessed our controller data and have a clean floating point value from 0.0 to 1.0 representing trigger pressure. Zero represents a released trigger and one represents a fully pressed trigger.
My specific goal was to implement a single-shot fire when the player pulled in the trigger, but I’m going to abstract that goal a bit. What I really want is to translate the analog button input into a digital button input. A digital button is either in a pressed state or a released state. If we can evaluate the same pressed/released status of an analog button then it should be easy to bind it to any game action that takes digital input. For shooting, we can fire the bullet when the state transitions from released to pressed.
So how do we translate our analog button to a digital button?
Divide and Conquer (kinda)
The dumb simple thing to start out with is picking a pressure threshold that divides the range into pressed and released states. For example, if the pressure is below 0.5 it is released, otherwise it is pressed.
If you’ve ever played a game that did this (not that common) or tried this yourself, you might notice that right around the threshold input gets fickle. It’s too easy to cross the threshold back and forth by accident. It’s like pressing a digital button with a shaky hand.
Two Thresholds Are Better Than One
The common fix for our single threshold implementation is to split it up into separate thresholds for transitioning from released to pressed and pressed to released. For example, we could say that when the button is in the released state, it transitions to the pressed state if the pressure grows above 0.6. When it is in the pressed state, it transitions to the released state if pressure drops below 0.4. There is now this buffer zone of 0.2 in the middle that your finger can sloppily move into without accidentally releasing or pressing the button.
This is a more common implementation to see out in the wild. It requires more care to notice and subsequently respect when it goes wrong. It might be an hour into playing before one of your shots doesn’t fire because you pressed the trigger to 0.5 instead of 0.6. This is even more common when trying to fire in rapid succession. I swear I fired five times and not four! It’s easy to place the blame on the player, but often times the blame should be put on the game. We can do a better job interpreting the player’s input.
Pressure Deltas and Moving Thresholds
People aren’t great at knowing what pressure value they are inputting. When someone tries to fire a shot, they don’t think about pulling the trigger to reach a pressure value of 0.6. They just think about pulling the trigger. More specifically, they are thinking about pulling the trigger some amount relative to whatever pressure it is currently at. The game should be thinking in similar terms.
Let’s change our framework to define a change in pressure that will transition from the released to pressed and a change for transitioning from pressed to released. Let’s start with 0.3 and -0.2 respectively (tune these for your game). These deltas will be used to dynamically define the thresholds that cause a state transition. Because the deltas are relative values, let’s define what they are relative to.
When in the released state, our transition threshold will be 0.3 above the minimum pressure that ever gets encountered. For example, let’s walk through a scenario where we entered the released state with a pressure of 0.2.
First, we initialize our minimum to to the current pressure of 0.2. The threshold to enter the pressed state is now 0.5 based on the 0.2 minimum plus the 0.3 delta.
On the next frame, we poll a pressure of 0.1 and update our minimum accordingly. The threshold to enter the pressed state is now 0.4 based on the 0.1 minimum plus the 0.3 delta.
On the next frame, we poll a pressure of 0.2 which doesn’t affect the minimum. The threshold to enter the pressed state remains at 0.4 based on the 0.1 minimum plus the 0.3 delta.
On the next frame, we poll a pressure of 0.4 which doesn’t affect the minimum. The threshold remains at 0.4 again and we trigger our transition into the pressed state.
When in the pressed state, our transition threshold will be 0.2 below the maximum pressure that ever gets encountered. This works just like the released example above except in the opposite direction.
Before we can claim victory, there are some edge cases to discuss. You’ll notice that in the example, I chose separate deltas for pressing and releasing. This isn’t required, but I do think that making the release quicker/easier feels better because players are more conscious of how far they pulled in against the physical resistance of the button. They are less aware of how far they let pressure off. Regardless of whether or not that is optimal for feel, let’s look at the implications of using asymmetric deltas.
Because we chose a longer pull than release, there is a section near full pressure in which we can release the button but not have enough room to press it again. Specifically, if the button is fully pressed we are at 1.0. Now release pressure by our delta of -0.2 to enter the released state. This puts us at 0.8 and puts our threshold for re-entering the pressed state to 1.1 because of the 0.3 delta. Unfortunately, we can’t actually input a value of 1.1 so we need to release all the way down to 0.7 before we have enough room to fire again. We can have a similar problem with releasing near 0.0 when the press delta is lower than the release delta.
Can we do better? I think so. Let’s clamp our thresholds into the [0,1] range. This means for cases where the press delta is larger than the release delta, we can always fire from the released state, but when near 1.0 we sometimes don’t need to press as far. That said, the physical player input hits a limit as the mechanical button can no longer be pulled in so it doesn’t feel that odd to the player.
Note that if your release threshold is tuned extremely small compared to your press threshold (e.g. 0.01 and 0.3), it can be preferable to take an alternate approach that requires some minimum pull delta near 1.0.
As I’ve said before, if you can make an accurate translation of the player’s intent through the language of the controller, you can make a tight controlling game. This is one more step along that path.