Customizeable Keyboard Controls for Games

After making games in processing for a while, I and many of you have realized that processing’s keyboard input is not that great for games, especially games when remapping controls is a desired feature. I made a class that can be plunked into any sketch and can be used for input with any key on the keyboard except for a few keys. You can use this with the three main mouse buttons as well.
This can be used for multiplayer by making multiple ControlManager objects, or you could use a single object to control every player at once. It depends on how you implement it.

I’m releasing this code here because 1. I don’t know how to make a library and 2. this is only one class, do I need a library for it?

The instructions for use are in the comment at the top. You don’t have to credit me if you use this in your sketches. If there are bugs, please let me know. There are some criticisms you could make about this system’s efficiency because I am iterating through lists a LOT when changing controls, but it’s not that bad.

/*
 Control Manager
 by Yehoshua Halle
 
 This is a class that manages keyboard input and mouse button input with the intent that they will be used in a game.
 
 Quick Start
 1. Create a Control Manager Object
 2. Use setMapping(String name, char input) to add a new mapping or change an old one
 3. Call the method "updatePressed(keyCode)" method at the beginning of the keyPressed() method
 4. Call the method "updateReleased(keyCode)" method at the beginning of the keyReleased() method
 5. Call the method "updateMousePressed(mouseButton)" at the beginning of the mousePressed() method
 6. Call the method "updateMouseReleased(mouseButton)" at the begining of the mouseReleased() method
 7. Use getControl(String name) to get the current state of a mapping
 
 Mapping controls
 letter characters and all typed characters can be input directly into the char argument in setMapping, like 'a' for the A key or ' ' for the space bar
 non-typed keys have these options: UP, DOWN, LEFT, RIGHT, SHIFT, CONTROL, BACKSPACE, TAB, RETURN, ENTER, and DELETE
 For mouse buttons, use the constants LEFTMOUSE, MIDDLEMOUSE, and RIGHTMOUSE for the char argument
 if you want to allow the player to input a button in void keyPressed, use 'keyCode' for input
 
 More Methods and Info
 
 void setMapping(String name, char input)
 - input should be a character as it appears with the 'keyCode' reserved word: 'a' for the A key or LEFT for left arrow, etc.
 - if allowConflicts is false (which is its default value), then attempting to set the button of a mapping to a button that is already mapped to something else will
 swap the two buttons if the target mapping already has a button, or set the old mapping to null 
 - Be sure to check for null in your code with the hasNull() method
 
 void updatePressed(keyCode) and updateReleased(keyCode)
 - the argument in these methods should be the 'keyCode' reserved word, but 'key' can be used if you don't want to use non-type buttons like shift and the arrow keys
 
 void updateMousePressed(mouseButton) and updateMouseReleased(mouseButton)
 - the argument must be mouseButton
 
 boolean getControl()
 - if the mapped button is not mapped (null), then this function will always return false
 
 void setDefault()
 - saves the current mappings temporarily
 
 void restoreDefault()
 - restores the saved default mapping if one exists
 
 boolean hasNull()
 - returns true when at least one mapping has no button mapped to it. This can result when two mappings conflict and allowConflicts is false.
 
 String getMappingName(String name)
 - returns a readable version of the button mapped to a mapping if the mapping exists.
 - for example, a mapping called "jump" is mapped to ' ', which is the space bar, but it's not easy to read. getMappingName("jump") would return "Space" instead of ' '
 
 String getMappingNameDirectly(keyCode)
 - returns a readable version of the key passed in the argument
 - for this to work well, pass this method keyCode or an equivalent value like SHIFT or UP
 - this is useful when you want to tell a player how the program interprets their direct input without mapping it
 
 void changeMappingName(String oldName, String newName)
 - changes the name of a mapping to something else
 
 void removeMapping(String name)
 - removes a mapping that matches the given name
 
 void clearMappings()
 - removes all mappings
 
 Limitations:
 - For keyboard input and mouse buttons ONLY, no mouse movement or scroll wheel and no gamepad input.
 - Windows, Alt, Escape, and Function keys are not usable!
 - Only intended for use in processing, not java in general
 */
public final static int LEFTMOUSE = '{';
public final static int MIDDLEMOUSE = '}';
public final static int RIGHTMOUSE = '|';
public class ControlManager {
  private HashMap<String, Character> mappings;
  private HashMap<String, Character> defaults;
  private HashMap<String, Boolean> controls;
  public boolean allowConflicts = false;
  public ControlManager() {
    mappings = new HashMap<String, Character>();
    controls = new HashMap<String, Boolean>();
    defaults = null;
  }
  public ControlManager(int numMappings) {
    mappings = new HashMap<String, Character>(numMappings);
    controls = new HashMap<String, Boolean>(numMappings);
    defaults = null;
  }
  //IMPORTANT METHODS (the ones you must use in your program)
  public void setMapping(String name, int raw) {
    char fixed = keyFix(raw);
    if (!allowConflicts) { //assumes 1:1 mapping is not violated already (if it was, you have a problem and should call resetMappings() to reset your controls)
      if (mappings.containsValue(fixed)) { //if value is already used, there is a conflict
        for (HashMap.Entry<String, Character> entry : mappings.entrySet()) { //find which mapping conflicts
          if (entry.getValue().equals(fixed)) { //if this entry matches the conflicting value, use its key
            if (mappings.containsKey(name)) { //if controls already has a mapping using this name
              mappings.replace(entry.getKey(), mappings.get(name)); //swap controls
            } else mappings.replace(entry.getKey(), null); //otherwise, clear the old value and create the new one
          }
        }
      }
    }
    mappings.put(name, fixed);
    finalizeControls();
  }
  public void updatePressed(int raw) {
    char fixed = keyFix(raw);
    for (HashMap.Entry<String, Character> entry : mappings.entrySet()) {
      if (entry.getValue() == null) continue;
      if (entry.getValue().equals(fixed)) {
        controls.replace(entry.getKey(), true);
      }
    }
  }
  public void updateReleased(int raw) {
    char fixed = keyFix(raw);
    for (HashMap.Entry<String, Character> entry : mappings.entrySet()) {
      if (entry.getValue() == null) continue;
      if (entry.getValue().equals(fixed)) {
        controls.replace(entry.getKey(), false);
      }
    }
  }
  public void updateMousePressed(int button) {
    if (button == LEFT) {
      updatePressed(LEFTMOUSE);
    }
    if (button == CENTER) {
      updatePressed(MIDDLEMOUSE);
    }
    if (button == RIGHT) {
      updatePressed(RIGHTMOUSE);
    }
  }
  public void updateMouseReleased(int button) {
    if (button == LEFT) {
      updateReleased(LEFTMOUSE);
    }
    if (button == CENTER) {
      updateReleased(MIDDLEMOUSE);
    }
    if (button == RIGHT) {
      updateReleased(RIGHTMOUSE);
    }
  }
  public boolean getControl(String name) {
    if (controls.keySet().contains(name))
      return controls.get(name);
    else
      return false;
  }
  public void setDefault() {
    defaults = new HashMap<String, Character>(mappings.size());
    for (HashMap.Entry<String, Character> entry : mappings.entrySet()) {
      defaults.put(entry.getKey(), entry.getValue());
    }
  }
  public void restoreDefault() {
    if (defaults == null) return;
    mappings = new HashMap<String, Character>(defaults.size());
    for (HashMap.Entry<String, Character> entry : defaults.entrySet()) {
      mappings.put(entry.getKey(), entry.getValue());
    }
    finalizeControls();
  }
  public boolean hasNull() {
    return mappings.values().contains(null);
  }
  //OTHER METHODS (optional to use, but useful when using a GUI to set controls)
  public void changeMappingName(String oldName, String newName) {
    if (mappings.keySet().contains(oldName)) {
      mappings.put(newName, mappings.get(oldName));
      mappings.remove(oldName);
      finalizeControls();
    }
  }
  public void removeMapping(String name) {
    if (mappings.keySet().contains(name)) {
      mappings.remove(name);
      finalizeControls();
    }
  }
  public void clearMappings() {
    mappings.clear();
    controls.clear();
  }
  public String getMappingName(String name) {
    //Gets a readable name of the control mapped to this name
    if (controls.keySet().contains(name))
      return getKeyName(mappings.get(name));
    else
      return "None";
  }
  public String getMappingNameDirectly(int raw) {
    //Gets a readable name of the control that this character would result in
    return(getKeyName(keyFix(raw)));
  }
  public HashMap<String, String> getMappings() {
    HashMap<String, String> map = new HashMap<String, String>();
    for (String k : mappings.keySet()) {
      map.put(k, getMappingName(k));
    }
    return map;
  }
  //UNDER THE HOOD METHODS (you can't use these, sorry)
  private void finalizeControls() {
    //keeps controls in sync with mappings, ABSOLUTELY NECESSARY FOR CORE FUNCTIONALITY
    //automatically called at the end of every method that changes mappings
    //it couldn't hurt to call this yourself
    controls.clear();
    for (String name : mappings.keySet()) {
      controls.put(name, false);
    }
  }
  private String getKeyName(Character in) {
    //Gets a readable name of the given "fixed" character
    //Returns strings in Title Case for most diverse manipulation
    if (in == null) { //if conflicts are not allowed, there is a chance 'in' could be null
      return "Empty";
    }
    switch(in) {
    case 'U':
      return "Up Arrow";
    case 'D':
      return "Down Arrow";
    case 'L':
      return "Left Arrow";
    case 'R':
      return "Right Arrow";
    case 'S':
      return "Shift";
    case 'C':
      return "Ctrl";
    case ' ':
      return "Space";
    case '{':
      return "Left Mouse";
    case '}':
      return "Middle Mouse";
    case '|':
      return "Right Mouse";
    case 'T':
      return "Tab";
    case 'E':
      return "Enter";
    case 'B':
      return "Backspace";
    case 'Q':
      return "Delete";
    default:
      return str(in).toUpperCase();
    }
  }
  private Character keyFix(int raw) {
    if (raw == UP) {
      return 'U';
    }
    if (raw == DOWN) {
      return 'D';
    }
    if (raw == LEFT) {
      return 'L';
    }
    if (raw == RIGHT) {
      return 'R';
    }
    if (raw == SHIFT) {
      return 'S';
    }
    if (raw == CONTROL) {
      return 'C';
    }
    if (raw == TAB) {
      return 'T';
    }
    if (raw == ENTER) {
      return 'E';
    }
    if (raw == RETURN) {
      return 'E';
    }
    if (raw == BACKSPACE) {
      return 'B';
    }
    if (raw == DELETE) {
      return 'Q';
    }
    if (raw == LEFTMOUSE) {
      return '{';
    }
    if (raw == MIDDLEMOUSE) {
      return '}';
    }
    if (raw == RIGHTMOUSE) {
      return '|';
    }
    char pkey = str((char) raw).toLowerCase().charAt(0);
    switch(pkey) {
    case '~':
      pkey = '`';
      break;
    case '!': 
      pkey = '1';
      break;
    case '@': 
      pkey = '2';
      break;
    case '#': 
      pkey = '3';
      break;
    case '$': 
      pkey = '4';
      break;
    case '%': 
      pkey = '5';
      break;
    case '^': 
      pkey = '6';
      break;
    case '&': 
      pkey = '7';
      break;
    case '*': 
      pkey = '8';
      break;
    case '(': 
      pkey = '9';
      break;
    case ')': 
      pkey = '0';
      break;
    case '_': 
      pkey = '-';
      break;
    case '+': 
      pkey = '=';
      break;
    case '{': 
      pkey = '[';
      break;
    case '}': 
      pkey = ']';
      break;
    case '|': 
      pkey = '\\';
      break;
    case ':': 
      pkey = ';';
      break;
    case '"': 
      pkey = '\'';
      break;
    case '<': 
      pkey = ',';
      break;
    case '>': 
      pkey = '.';
      break;
    case '?':
      pkey = '/';
      break;
    default: 
      break;
    }
    return pkey;
  }
}

5 Likes

After gaining more experience, this code seems overcomplicated and slow. It does have the feature of being able to map multiple actions to the same buttons, but I struggle to think of a situation where that would be useful. If you want an actually easy way to have simple keyboard input, you can just use an array of booleans to store your key presses:

// 256 length to have 1 index for every value in a char
boolean[] keys = new boolean [256];

void setup() {
  // Do your thing
}

void draw() {
  // Continue doing your thing
}

/*
 * Helper method that returns on true if a key is pressed.
 * Important note that letters only work with CAPITAL letters.
 * The method also works for other buttons like the arrow keys and shift (the key constants).
 */
boolean keyDown(char input) {
  int index = (int) input;
  if(index < 0 || index > 255) return false;
  return keys[index];
}

void keyPressed() {
  if(keyCode < 256) keys[keyCode] = true;
}

void keyReleased() {
  if(keyCode < 256) keys[keyCode] = false;
}

It’s quick to implement, easy to understand, and works for game keyboard controls.
Know its limitations, though. It can’t distinguish capital or lowercase letters and not every key on the keyboard can be detected (see keyCode for more info).
As with most processing games, you’re left to make everything custom. While this code is convenient, I encourage you to test it to understand it, experiment with adding new features, and combine it with other input methods (if necessary) to make your game the best it can be.

1 Like