Input
Overview
Input in fighting games requires quick transmission between players, precision and the ability to track the state input stream over a certain period of time rather than simply consuming it.
This page will go over which of player input is captured by Unity and how these values are subsequently evaluated and computed in Quantum.
Input Struct
It is generally recommended to keep the input struct as small as possible to keep the bandwidth low. This is pushed to the extreme in the Fighting Sample by holding a single enum InputFlag
as part of the input.
chsarp
input {
InputFlag inputFlag;
}
The InputFlag
enum definition explicitly assigns power of two values thus allowing the InputFlag
to be manipulated using bitwise operations. This enables an InputFlag
to hold multiple input values simultaneously.
C#
// Input flags used to represent the directional and button inputs
enum InputFlag {
NONE = 0,
Left = 1,
Right = 2,
Down = 4,
Up = 8,
LP = 16, // light punch
LK = 32, // light kick
HP = 64, // heavy punch
HK = 128, // heavy kick
Directions = 15, // Used by the systems to extract the input directions from the input struct
Buttons = 240, // Used by the systems to extract the button input from the input struct
}
Unity Side
The Fighting template implements two different input interfaces, mobile (UI Buttons) and pc (keyboard and gamepad). Depending on the platform the build is running on, one or the other input will be taken into account.
Although the input interfaces vary, the input logic remains the same across devices.
Input Logic
The input logic is set up in the QuantumDemoInput.cs
script and subscribes to the PollInput
callback.
Keyboard Input
Each button set up in the KeycodeSet
struct in QuantumDemoInput.cs
is represented by a boolean value. When the boolean evaluates to true, the value held by the InputFlag
parameter in the Input struct is updated accordingly.
C#
if (UInput.GetKey(Left)){
input.inputFlag |= InputFlag.Left;
}
Mobile Input
The mobile input is split into two parts; a UI script called MobileJoystick
which handles the button press’ value and the logic in QuantumDemoInput.cs
which holds references to all active MobileJoystick
scripts and consumes their value when the input is polled.
MobileJoystick
presents two flag parameters to allow for movement in all 8 directions; diagonal up right is represented by the Quantum.InputFlag.Up
and Quantum.InputFlag.Right
flag. If the flag1
and flag2
are set to the same value, then it is considered a unique input. Bitwise operations allow for the value assignment to remain identical regardless of each UI button’s settings.
C#
public class MobileJoystick : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler{
public Quantum.InputFlag flag1;
public Quantum.InputFlag flag2;
public Quantum.InputFlag FlagValue { get; private set; }
public void OnPointerEnter(PointerEventData eventData){
FlagValue = flag1 | flag2;
}
public void OnPointerExit(PointerEventData eventData){
FlagValue = Quantum.InputFlag.NONE;
}
}
QuantumDemoInput
simply iterates over the array of known MobileJoystick
scripts and uses a bitwise or to add together the values held by each button.
C#
for (int i = 0; i < mobileButtons.Length; i++){
input.inputFlag |= mobileButtons[i].FlagValue;
}
Quantum Side
On the simulation side (Quantum), the input needs to be tracked across multiple frames to allow for both combos triggered by sequential inputs over several frames and charge attacks requiring a continuous button press.
The input sequences are defined at edit time using the InputCommandData
Asset and tracked at runtime using the InputBuffer
component.
InputCommandData
The InputCommandDataBase
is an abstract asset class. It is used to define concrete assets that will be implementing TestCmdPair()
specifically to match their needs.
C#
public abstract unsafe partial class InputCommandDataBase{
/// The parameter set for the forward and backward version of the input. Should be a fixed point parameter.
public CAParameters forDirectionParameter;
public CAParameters backDirectionParameter;
/// The forward and backward version of the command
public InputFlag[] forDirectionInputSequence;
public InputFlag[] backDirectionInputSequence;
public void TestCmdPair(Frame f, InputBuffer* i, int side, CustomAnimator* a, ref QList<InputValue> dSeq);
protected abstract bool TestCmd(Frame f, InputBuffer* iB, int side, InputFlag[] flagArray, ref QList<InputValue> directionalSequence);
}
The concrete implementation included in the Fighting Sample are:
InputCommandData
; and,ChargeInputCommandData
.
InputCommandData
is used to activate special moves that players trigger by performing a sequence of inputs. ChargeInputCommandData
are used to define input sequences which require charging up over several frame by holding the same input.
InputBuffer
Input needs to be tracked for each player separately as part of the frame. The most convenient and flexible way to go about this is by having a component on each player controlled entity dedicated to this purpose; that is the approach used by the Fighting Sample.
The InputBuffer
component is used to keep track of 3 different things:
- the input direction;
- the button presses; and,
- the amount of time for which an input has been pressed or released.
The InputBuffer
is updated by the InputBufferSystem
.
C#
component InputBuffer{
list<InputValue> directionalSequence;
list<InputValue> buttonSequence;
array<InputTimer>[8] inputTimers;
}
The InputValue
type is a simple struct defined in the DSL. It contains a copy of the InputFlag
and the frame number it is associated with. Since directional input and action button sequences are manipulated separately, they are also tracked separately in the InputBuffer
component; this is achieved by keeping two InputValue
lists.
C#
struct InputValue{
InputFlag inputFlag;
Int32 frame;
}
The amount of time a certain input has been pressed or released is tracked using the InputTimer
struct.
C#
struct InputTimer{
FP timeDown;
FP timeUp;
}
InputBufferSystem
The InputBufferSystem
has two purposes:
- Check the current
InputFlag
in the input struct; and, - Update the
InputBuffer
components.
Before evaluating the current input’s InputFlag, the directional and button inputs are separated.
C#
InputFlag dirFlag = input->inputFlag & ~(InputFlag.Buttons);
InputFlag btnFlag = input->inputFlag & ~(InputFlag.Directions);
This split facilitates tracking the actions taken by the player to trigger move sets. First UpdateInput()
updates the InputTimer
in the InputBuffer
component by checking whether an input is being held down:
- Pressed: the
InputTimer.TimeDown
for that input is increased. - Not Pressed: if the input is not held down during that frame and the reset tolerance for it has been exceeded, the
InputTimer.TimeDown
for that input is reset. This is done in order to enable charge moves (e.g.: charge back for a few frames, then press forward and a button).
After all the InputTimers
have been updated, the InputBufferSystem
updates the CustomAnimator
s parameters with those new values.
The last step, the InputBufferSystem
extracts the input sequence list held by the InputBufferComponent
. The directional values of the current input are checked against the previous InputValues
held in the component and updated if the two values differ. The list is then passed as a parameter to the TestCmdPair()
method in all InputCommandData
assets associated with each players’ fighter.
This implementation combines the aforementioned aspects to allow players to enter a sequence of inputs with a tolerance between the changes (approx. 10 frames by default).
For example, if the player’s fighter holds for instance the InputCmd_QuarterCircle
asset, this would trigger the QCF_F
animation parameters and execute the associated behaviours if the input sequence was made of these values:
C#
{frame = 80, value = 4} // Down
{frame = 90, value = 6} // Down + Right
{frame = 100, value = 2} // Right
This would then register as a positive input sequence since the command buffer is 32 inputs.
Triggering Special Moves
Special moves are set up at edit time in the Unity Animator and baked into the CustomAnimatorGraph
Asset. At runtime, the InputBufferSystem
simply updates the parameters of the CustomAnimator
components.
If the transition parameters are all fulfilled when the CustomAnimatorUpdater
calls Update()
on the CustomAnimator
components (see CustomAnimatorSystem
), then the moves are triggered. Upon leaving the current state and entering the new state, the SignalOnAnimatorStateExit
and ISignalOnAnimatorStateEnter
are fired for the respective states.
N.B.: The parameters updated in the InputBufferSystem
are all of type FixedPoint
. This is done so players who do not perform new inputs for several frames, do not accidentally perform an attack based on input cached from prior frames.
The Uppercut_LP state for example can be triggered from AnyState as long as the following conditions are fulfilled simultaneously:
- The motion on the z-axis (ZMotionF) is greater than 0 and less than 0.15;
- The light punch input time is greater than 0 and less than 0.15;
- The character is currently grounded (i.e. InAir is false); and,
- The character is currently in a state that allows them to perform a special attack (i.e. CanSpecialAtk is true).
While the FixedPoint
parameters are continuously updated in the InputBufferSystem
, either directly in the system or as a result of the input sequence passed into the InputCommandData
; the boolean parameters on the other hand are part of the FighterAnimatorState
and are set when the character first enters the current state.