Many mobile games need arcade-like controls, and implementing a virtual joystick is a quick and easy solution.
Here is a quick and simple way to implement a virtual joystick.
Table of Contents
Background
For arcade-like games played on mobile devices, you need arcade-like controls.
Joystick
Arcade games typically implement a joystick, allowing movement in 8 basic directions.
An arcade joystick has a “dead zone”, which has the effect of dampening trivial input. The dead zone is important, because it allows the player to be at rest without interpreting the player’s at-rest state as input.
Outside the dead zone, the player can push the joystick up or down, and left or right. Up/down and left/right can be combined, such as UP+LEFT, to form 8 basic directions – the four cardinal directions, plus four diagonals.
An arcade joystick allows smooth transitions, but also provides exclusivity, meaning that you can’t push “LEFT” and “RIGHT” at the same time, nor “UP” and “DOWN” at the same time. Likewise, any given diagonal is exclusive from all other diagonals.
Exclusivity means that the game doesn’t require as much code in order to interpret the user’s input, because the user’s input is limited to one of 9 exclusive states (idle, plus the four cardinal states, plus the four diagonals).
Button Array
The first handheld games used individual buttons in a “cross” configuration.
This provided a dead zone in the center, where the player could rest their thumb, and each of the four cardinal directions had a unique button.
Unfortunately, buttons are not as feasible or user friendly as a joystick:
- Buttons do not provide exclusivity. You CAN press LEFT and RIGHT at the same time, for example.
- No smooth transition. Shifting directions requires lifting your thumb and placing it in a new location.
- Diagonals don’t work well. Pressing a diagonal requires pushing two buttons at the same time. Although diagonals can be implemented as four more discreet buttons, this can result in the player inadvertently pressing two or more buttons at the same time, which the game sees as ambiguous.
In addition to the physical limitations, software buttons follow a complex event lifecycle.
- The button starts in an IDLE state.
- When pressed, the button generates a DOWN event.
- When released, the button generates an UP event.
- When a DOWN is followed by an UP, the button generates a CLICK event.
- After a CLICK event, the button returns to an IDLE state.
Because of the complex event lifecycle, using a button array becomes even more complicated, because each DOWN and UP event must be individually tracked, and the game must react accordingly.
DPad
In the early 80’s, Nintendo came up with the perfect solution: The DPad (“Digital” pad).
The DPad has all of the advantages of an arcade joystick, with the simplicity and compact design of a button array:
- Exclusivity – LEFT and RIGHT are exclusive, as are UP and DOWN. Any diagonal is exclusive from all other diagonals, just like an arcade stick.
- Smooth transition – without lifting a thumb, the player can easily transition from any direction to any other direction.
- Diagonals – unlike a button array, the player can easily push two cardinal directions at once in order to move diagonally.
- Dead zone – allows the player to place the controls in an idle state without lifting the thumb.
As a physical input device, the DPad works well. Implemented as a virtual input device, however, it doesn’t translate very well.
For example, a physical DPad has four physical buttons that determine 8 possible direction inputs, as well as an idle state.
As we’ve seen, buttons don’t translate very well to the virtual world.
VPad (Virtual Pad)
Enter the Virtual Pad (VPad). A VPad is simply a picture of a DPad, where input to the game is a series of TOUCH events.
The VPad provides all of the advantages of a DPad, plus it’s easy for a game to interpret the player’s input.
Anatomy of a VPad
A VPad, or Virtual Pad, is a picture that represents a DPad control, but responds to TOUCH events.
Unlike a DPad, where each button has its own event lifecycle, there is only one type of event supported by a VPad – the TOUCH event.
Each TOUCH event provides the X,Y location where the player placed their thumb.
Once a TOUCH event has occurred, either the player can lift their thumb, or drag it to another location, resulting in a second TOUCH event with a new X,Y location.
When the player lifts their thumb, the control returns to an idle state.
In addition to the four cardinal directions and four diagonals, we can programmatically implement a dead zone as well, allowing the player to place the control in an idle state without lifting their thumb.
More precisely, we can represent this structure as a grid:
When a touch event occurs, the game simply needs to determine in what square the touch event occurred. The resulting input is +1, 0, or -1 for X, and +1, 0, or -1 for Y, indicating the state of each independent axis.
Converting a Touch Event to Game Input
When a TOUCH event occurs, it returns an X,Y pair that ranges from (0,0), which is the upper-left coordinate of the VPad, up to the VPad’s width and height (ControlWidth,ControlHeight).
So, X will range from 0 to ControlWidth, while Y will range from 0 to ControlHeight.
Ideally, we want to convert this to a directionality for X (-1, 0, or +1) and a directionality for Y (-1, 0, or +1).
Since there are 3 “X” sections and 3 “Y” sections (3×3 grid), we can use the following formula to convert a TOUCH event to directionality:
XI = X * 3 / ControlWidth -1 YI = Y * 3 / ControlHeight -1
For each X and Y value, we start by multiplying the X value by 3, and then dividing by the control’s width or height respectively.
This returns a value in the range of 0 to 2.
The last step is to subtract 1, shifting both ranges from -1 to +1.
Virtual Analog Pad (VAPad)
Suppose you want to scale the player’s input. For example, if the player touches slightly right, the player’s avatar might walk. If the player touches the far right, the player’s avatar might run. This allows 5 possible states for X, and 5 possible states for Y:
The resulting structure is a 5×5 grid, where there are two possible magnitudes for each of the 8 basic directions, plus there are 8 additional directions (16 total).
XI = X * 5 / ControlWidth - 2 YI = Y * 5 / ControlHeight - 2
This can be scaled up to any number of slices:
XI = X * GridWidth / ControlWidth - INT(GridWidth/2) YI = Y * GridHeight / ControlHeight - INT(GridHeight/2)
Compared to a VPad, which supports 8 basic directions and an idle state, a VAPad can be scaled up using any odd number, to support N*N possible states.
X States: N Y States: N Total States: N * N Total Directions: N * 2 + (N-2) * 2 Sensitivity: INT(N / 2)
The effects of Clamping
Clamping limits the range of an output variable.
For example, let’s say that we want a smaller dead zone without changing the range of inputs.
A clamping function works like this:
XI = MIN( RangeX, MAX( -RangeX, X * GridWidth / ControlWidth - INT(GridWidth / 2) ) YI = MIN( RangeY, MAX( -RangeY, Y * GridWidth / ControlWidth - INT(GridHeight / 2) )
Clamping makes the dead zone smaller. For example, if we start with a Grid size of 5 x 5, we can implement clamping at -1 to 1:
XI = MIN( 1, MAX( -1, X * 5 / ControlWidth - INT(5 / 2)) YI = MIN( 1, MAX( -1, Y * 5 / ControlHeight - INT(5 / 2))
Clamping allows you to reduce the dead zone, and combine possible output states.
Creating Acceleration
We can create a non-linear relationship between input and output.
For example, we can use the same linear grid to feed an exponentiation function.
Capture the sign (in pseudocode): IF X=0 SX=0 ELSE IF X<0 SX=-1 ELSE SX=1 If your compiler supports the conditional operator (C, Java): SX = (X=0)?0:((X<0)?-1:1) Use the Absolute value of X, then apply the sign. XI = ABS(X)^N * SX
Since there is no square root of a negative number, you must separate the sign of the value from its magnitude.
Once the magnitude has been exponentiated, multiply by the sign in order to obtain directionality.
Exponential scaling can provide the feeling of acceleration, where the player’s input is linear, but the magnitude is raised to some specific power.
For example, if X is in the range of -5 to +5, and we raise that to the 1.7 power, the resulting output will be X in the range of -15 to +15.
If the player touches (3,5), the resulting output is (3^1.7, 5^1.7) = (6,15), creating the impression of acceleration.
Conclusion
Using a VAPad provides a quick and simple way to allow the player to have responsive and intuitive controls in a mobile game.