(Note: This is a dry run for a YouTube series I'm planning on putting together and is a work in progress.)
A common complaint against PC games nowadays is a lack of proper input remapping in games. We see triple-A games and indie games alike where being able to remap controls in a friendly manner is either not an available option, or is set up as a flow ("press Up, now press Down, now press Left, etc.") with no clue how many controls there are or any way of seeing what your controls are set to.
In this article, we're going to look at the history of input processing in video games and see how that history led us to where we are today, so let's start with...
Analog Input
In the beginning, there was Pong. You had two potentiometers as controls and a button for reset/start. Once a frame, the value of the potentiometer would be converted into a binary representation of its current resistance value via a process called ADC (
analog to
digital
conversion). That value would be stored in a certain memory location. The code back then had to be really small, so it was hardcoded to look for that location. Usually, the reset/start button just reset the entire device, and the device would start with the game "running" when you first turned it on.
Code back then was small and simple because it had to be. In pseudo-assembly...
LDX $PADDLE0
This would get compiled out to two bytes: the instruction to load the X register, and a byte offset from zero where the ADC value from the potentiometer was stored. However, we then went to...
Atari Joysticks
The original Atari joysticks were amazing. You had an eight-directional joystick and an action button per player! Wow, such progress. Arcade games at the time used extremely similar code to what we'll be talking about here.
Joysticks were a set of switches under the hood. When you pressed the action button or any of the four cardinal directions, a switch was closed. If you pressed in a diagonal, two switches were closed. For example, if you pressed diagonal up-left, the switches for up and left would be closed.
This will seem a bit weird, but if a switch was open, it was registered as a 1 in a bitmask. If the joystick wasn't being pressed in a direction, it would return 15 (b1111). Pressing up would mask off bit 0 and return 14 (b1110). Down would mask off bit 1 and return 13 (b1101). Left would mask off bit 2 and right would mask off bit 3. The action button would be bound to a different memory address usually, and would return 1 if not pressed, and 0 if pressed.
So, if you wanted to see if a player was pressing in a certain direction, you'd do something similar in pseudo-assembly...
LDA b0001 // Load the accumulator with the bit we want to test
BIT $STICK0 // Check stick 1 to see if the bits in the accumulator are set
BNE $UP // Branch if the bit test failed (meaning the joystick was pressed)
This wouldn't take a lot of memory. The load would be two bytes, the bit test would be three bytes, and the BNE call would be three bytes, for a total of eight bytes to check for up. Given that Atari 2600 games only had 4KB of space, you can see that input processing would take some space.
Now let's jump to...
Nintendo Gamepads
If you take a look at a Nintendo gamepad port, you may notice that it only has seven pins. However, your Nintendo gamepad has eight buttons (the four directions, A, B, select and start). Yes, this made input processing more difficult. Long story short, every frame a "latch" signal would be sent to the controller asking for input data. Once that "latch" signal was received, the developer would have to read from a certain memory location eight times in a row. Each time, they'd get a single bit of data: either a 1 for a button being pressed, or a 0 for a button not being pressed. Input processing for controller 1 would look like this pseudocode:
byte pad0 = 0;
for (int i = 0; i < 8; i++) {
pad0 = pad0 * 2;
pad0 &= (*paddlestate & 1);
}
This would get you all the inputs into the pad0 variable, and then you could bit-test. SNES controllers worked the same way, except they had 16 bits in their controller entry (even though they only had twelve buttons).
A game like
Super Mario Bros. (which was only 32KB), where only one player was playing at a time, could probably just switch the paddlestate pointer from controller 1 to controller 2 and keep the rest of the code the same.
One of the major benefits of Nintendo consoles was that most games had the same basic controls. A jumped, B shot, etc. There wasn't a requirement that it be this way (yet), but standard controls helped players quickly get going.
Now at this point, you're probably screaming, "Michael, you ignorant bint, this is all console crap. We already know that consoles are why we don't have decent controller mapping on console ports." To which I reply, "No, that's not the only reason, and I'm getting to PC's in a moment, say...now."
DOS Games
Now, while controller inputs were handled in a fairly straightforward fashion on the consoles of the era, the PC was pretty much the wild west. There was an
optional joystick interface, but you couldn't rely on everyone having access to a joystick. There was a controller everyone had to have, though...the PC keyboard.
When you'd press or release a key, an interrupt would fire and code in a keyboard interrupt handler would execute to handle the keypress. Most applications would let the default keyboard interrupt handler do its work and just pass the final processed keypress along, but games would often jump in and "hook" the handler so they could do their own processing on key down and key up.
Each keypress would generate one or two scan codes. For example, pressing the down arrow key would send hex 0xE0, 0x48. Releasing the down arrow key would send hex 0xD0. If a game developer properly hooked the keyboard handler, they could keep a memory location up to date with whether or not a key they cared about was being pressed.
Most DOS games didn't offer keyboard remapping. Since the size and shape of keyboards was pretty much standard, it was common for games to ship with
keyboard overlays so you'd have a reminder of which key was which. However, keyboards for other countries often caused problems.
Some games did offer keyboard remapping, but it was almost always through an external config utility. Gamers would open a small utility that would let them pick the keys of their choice, and it would save the key down/key up codes from that press to a config file. The game would load that config file and use that data in its keyboard hook, and the game, rather than check the input directly, would check an internal data structure to see if an input was set. You'd end up with code like this in your keyboard hook...
struct Input {
bool Left;
bool Right;
bool Fire;
...
}
Input newInput = prevInput;
newInput.Left = (scanCode == LeftUp ? false : (scanCode == LeftDown ? true : newInput.Left));
...
This would allow people to compare previous inputs to new input and act accordingly. Other developers moved to an input event system.
vector<InputEvent> events;
if (scanCode == LeftUp) events.push_back (InputEvent_LeftUp);
...
The guys who had moved to event-based input were in great position when it came to moving to...
Windows Games
Coming soon...
- Lots of variation in controllers for PC at this point, but no standards
- Success of Quake makes in-game keyboard rebinding popular
- DirectInput, Sidewinder Joystick and MechWarrior 2 bring about a joystick renaissance
- Microsoft focus on gaming and purchasing of game developers leads to a positive feedback loop of features for DirectX
- Input event mapping built into DirectInput
- Rise of USB, elimination of dedicated gamepad port
- XInput, the PC gamepad renaissance
- Death of DirectInput and the near-death of the joystick