One of the first things I like to do when I start a new project is Input Handling. I think it’s generally an underappreciated and underdeveloped aspect in a lot of games. Input is one of the core parts of how you play a game and you will use it constantly during development.
First of all we should define what we want our hotkey system to be able to do:
- Full custimizability (any hotkey can be bound to any function)
- Any combination of keys/buttons should be valid
- Should feature double tapping
- On press, on release and while holding should be seperate hotkeys
- All these should be combinable
Lets start with the basics because of the first point we can’t hardcode anything. For my implementation I use a map where the key is a hotkey and the value is a array of functions. It’s an array of functions because it should be possible for one hotkey to be bound to multiple functions. For now a function is just an Enum value, we’ll need to add more to it later to deal with certain things. As for the hotkey the what exactly is in this class depends on what the engine provides but in generally it’ll include.
- The key/button
- Other buttons held.
- Is it doubleTapped?
- is the key pressed/held/released?
We also need to make our hotkey class hashable and include a comparison operator so that we can use it as a key in our map. The result is we can query our hotkey map with a hotkey and get a list of functions back. Now when you receive an input you make a hotkey with this input and use it to get the functions that needs to be executed. We also use a library to serialize this hotkey as a json file which is how we store hotkeys.
Now this is when we run into our first issue, holding down a key doesn’t generate events in the way you would expect. This is because when you hold a key on Windows after a short delay it just sends a key pressed event every few miliseconds (maybe the engine you use handles this for you). The fix is pretty simple: we need to keep track of which keys are held ourselves.
In most engines keys/buttons will be represented by an enumarator which is really just a number. So all keys/buttons are just a number from 0 to ~255. To keep track of keys held we make an array of booleans that is the same size as the number of keys. When a key is pressed we cast the enum to a number and set the value to true, when it released we set it to false. So now we have a list of keys held. Then we just generate a hotkey every loop for every hotkey held. We also add every hotkey held as a list to every hotkey generated by an input event.
Note that while you are holding this hotkey the “on press” version of this hotkey is still being generated, it’s not really obvious if this is a problem or not, it’s something that only occurs when you bind a function to the “on press” and “while held” version of the same key. I can’t think of any existing game where something like that was used so it’s not clear if this behaviour is expected or not. Because of that I’ll just leave it as is.
When I mention something being a problem I really mean it’s not the expected behaviour based on how the hotkeys worked in other/similar games.
Now we run into another problem, lets say you are holding one hotkey to move the camera and another one to pause the game. Moving the camera is set as holding a key and pausing as pressing a key. Because all held keys are added to every generated hotkey and the held keys change the hash we can’t pause the game while holding a key since the pause key is set as just pressing one key. Again it’s not super obvious what the expected behaviour is here, there is always exceptions and weird edge cases. But I think the most obvious solution is the use the valid hotkey with the most modifiers.
To occomplish this we need to go through every combination of held keys and test (test if it’s actually bound to a function) each hotkey and use the one with the most modifiers. However what do we do if we find two valid hotkeys with the same amount of modifiers? I tried to think of cases where this would actually happen but couldn’t think of any, so I left the implementation as is which ended up being just picking the first hotkey that is found (going through the hotkeys from most modifiers to least). It might seem more obvious to have both hotkeys trigger however this introduces another issue of what order these trigger in and since a lot of these hotkeys modify similar things for example different ways of selecting units having both trigger ends up being just as arbitrary as just picking one.
Also if someone wants both hotkeys to trigger all they need to do is create a new hotkey that includes all the modifiers of both hotkeys and bind both functions to that new hotkey. It is important to note that if a key is held for a modifier and that key is also used as a held key it will trigger the held version and the modifier version. Again it’s not clear if this is the expected behaviour or not, so we’ll leave it as is.
Next thing is doubletapping hotkeys. To detect if a hotkey is doubletapped we need to keep an array of hotkeys and timers. Everytime a hotkey is created we add it to an array with the current time and compare it to the rest of the array, if we find the same hotkey within a certain time difference it means it doubletapped. Also if the entry in the array is past the doubletap timer we remove it. Seems simple enough but this creates more issues.
For example if we have a “doubletap” and “regular” version of the same key defined should the regular version trigger on the second click? Again, expected behaviour, i don’t think it should, which is already how it works with our implementation so that’s fine.
However what if only the regular version of the hotkey is defined and we doubletap that key would we expect it to trigger twice (which it would not do)? So for every doubletapped key we need to check if there is a regular version defined and use that one if the doubletap version is not defined.
What about the first click should it trigger the regular version on the first click if you end up doubletapping, outside of the issues of implenting this, I don’t think it should work like that.
Now putting together doubletapping and modifiers. When we are checking combinations of modifiers, if a key is doubletapped, we need to check for every combination if a regular version is defined, if the doubletapped version is not defined. Now is where things really get complicated what if you doubletap a hotkey while holding another key, the doubletapped version of that key is not defined with the modifier but without the modifier it is. I guess the question is do we favor doubletapping over amount of modifier and if so by how much? Again I find it hard to think of a situation where does would happen but it makes sense to me to favor amount of modifiers.
User Interface Functions
There’s is one last thing that I wanna talk about which is UI functions. Many RTS/tactical/strategy game will have some kind of UI that is clickable, the way the hotkey system is designed, clicking on the UI counts as a hotkey. Initially I added an extra field to my hotkeys to indicate if they where a UI hotkey or not. However it makes more sense to add this field to functions instead, since functions are really the ones that are limited to UI or not. That is to say a function that handles “click a button” only makes sense if the mouse is over the inventory.
So we end up adding two fields to functions
nonUIOnly. These function indicate which functions should be triggered, some functions it only makes sense if the mouse is over the UI, some only make sense if it’s not, and some functions can always be triggered. To make this work all we need to do is check the mouse position and match it to the right hotkeys.
Now to combine this with everything else! Everytime we check a hotkey we need to also check if the mouse is in the right place for the function to actually trigger otherwise ignore the hotkey and move on to the next one. So every step of the way, including the doubletap check, we need add an extra check to see if it’s the right type of function.
Putting it all together
Here’s a diagram that illustrates the above process. This is what I’ll be following for Divine Conquest.
Well that’s it, there might still be more weird edge cases that I haven’t considered, but this should be a very robust and consistent hotkey system that is fully modifiable.