Author Topic: User-Customizable Keybinding System  (Read 3122 times)

0 Members and 1 Guest are viewing this topic.

Offline johannhowitzer

  • Forum Regular
  • Posts: 118
    • View Profile
User-Customizable Keybinding System
« on: November 11, 2020, 03:38:37 pm »
For the game I'm designing, naturally I wanted players to be able to choose their inputs.
The following is a very modular, adaptable system I developed; you should be able to use it
in your own projects with very few changes.

Essentially, a bunch of keyboard codes are stored in an array, and the settings chosen by
the user are stored as indices of that array, in another array, and then referenced by
several functions that can be used in very simple ways at the surface level of your program.

Here's the stuff that goes in the header:


Code: [Select]
' References for press function and hold array
const up_key        =  1
const down_key      =  2
const left_key      =  3 ' These are the things the player can do in your program.
const right_key     =  4 ' So for my game, we have directional buttons, typical
const weapon_key    =  5 ' controller face buttons, two weapon selects that would
const shield_key    =  6 ' probably be bound to shoulder buttons, and enter and esc,
const engine_key    =  7 ' which are a non-rebindable failsafe to prevent the user
const wpn_sel_right =  8 ' getting somehow stuck in a menu.
const wpn_sel_left  =  9
const enter_key     = 10 ' Enter and esc should be the last two values.
const esc_key       = 11

const key_ref_count = 66 ' Number of usable keys on the keyboard, not including shift-modified
dim shared key_ref(key_ref_count)             as long  ' Contains codes of all bindable keys
dim shared ucase_key_ref(key_ref_count)       as long  ' The corresponding uppercase version of key_ref()
dim shared key_name$(key_ref_count)                    ' Names of keys, for menus, dialogue, etc

const keybind_count = 11 ' Number of user actions, including unbindable enter and esc
dim shared keybind_name$(keybind_count)                ' Name of gameplay functions - "WEAPON", "UP" etc
dim shared keybind_ref(keybind_count)         as _byte ' Current keybindings - contains key_ref index
dim shared default_keybind_ref(keybind_count) as _byte ' Defaults in case user wants to reset
dim shared keybind_edit_ref(keybind_count)    as _byte ' Used for editing keybind assignments,
                                                       ' so the user won't be confused in the menu

call set_key_ref

' Input tracking flags
dim shared hold(keybind_count)       as _byte ' Set true when pressed, false when released
                                              ' This allows the program to know what was pressed last frame

const text_tag_keybind = 1 ' Used in an optional routine at the end


With this in place, the next thing is to set up all the keyboard code data that will be used
behind the scenes.  This is a good boilerplate version that I'm using, it should be easy to
add and remove codes as you see fit.  Their order does not matter, but remember to change
the key_ref_count constant, and adjust your defaults accordingly.  Most notably, this set
is missing the F1-F12 keys.


Code: [Select]
sub set_key_ref

' All lowercase codes and names for display in program
key_ref(1)  =     97: key_name$(1)  = "A"
key_ref(2)  =     98: key_name$(2)  = "B"
key_ref(3)  =     99: key_name$(3)  = "C"
key_ref(4)  =    100: key_name$(4)  = "D"
key_ref(5)  =    101: key_name$(5)  = "E"
key_ref(6)  =    102: key_name$(6)  = "F"
key_ref(7)  =    103: key_name$(7)  = "G"
key_ref(8)  =    104: key_name$(8)  = "H"
key_ref(9)  =    105: key_name$(9)  = "I"
key_ref(10) =    106: key_name$(10) = "J"
key_ref(11) =    107: key_name$(11) = "K"
key_ref(12) =    108: key_name$(12) = "L"
key_ref(13) =    109: key_name$(13) = "M"
key_ref(14) =    110: key_name$(14) = "N"
key_ref(15) =    111: key_name$(15) = "O"
key_ref(16) =    112: key_name$(16) = "P"
key_ref(17) =    113: key_name$(17) = "Q"
key_ref(18) =    114: key_name$(18) = "R"
key_ref(19) =    115: key_name$(19) = "S"
key_ref(20) =    116: key_name$(20) = "T"
key_ref(21) =    117: key_name$(21) = "U"
key_ref(22) =    118: key_name$(22) = "V"
key_ref(23) =    119: key_name$(23) = "W"
key_ref(24) =    120: key_name$(24) = "X"
key_ref(25) =    121: key_name$(25) = "Y"
key_ref(26) =    122: key_name$(26) = "Z"
key_ref(27) =     49: key_name$(27) = "1"
key_ref(28) =     50: key_name$(28) = "2"
key_ref(29) =     51: key_name$(29) = "3"
key_ref(30) =     52: key_name$(30) = "4"
key_ref(31) =     53: key_name$(31) = "5"
key_ref(32) =     54: key_name$(32) = "6"
key_ref(33) =     55: key_name$(33) = "7"
key_ref(34) =     56: key_name$(34) = "8"
key_ref(35) =     57: key_name$(35) = "9"
key_ref(36) =     48: key_name$(36) = "0"
key_ref(37) =      9: key_name$(37) = "TAB"
key_ref(38) = 100304: key_name$(38) = "L SHIFT"
key_ref(39) = 100306: key_name$(39) = "L CTRL"
key_ref(40) =     32: key_name$(40) = "SPACE"
key_ref(41) =     96: key_name$(41) = "~"
key_ref(42) =      8: key_name$(42) = "BKSP"
key_ref(43) = 100303: key_name$(43) = "R SHIFT"
key_ref(44) = 100305: key_name$(44) = "R CTRL"
key_ref(45) =  20992: key_name$(45) = "INSERT"
key_ref(46) =  21248: key_name$(46) = "DELETE"
key_ref(47) =  18176: key_name$(47) = "HOME"
key_ref(48) =  20224: key_name$(48) = "END"
key_ref(49) =  18688: key_name$(49) = "PG UP"
key_ref(50) =  20736: key_name$(50) = "PG DOWN"
key_ref(51) =  19200: key_name$(51) = "LEFT"
key_ref(52) =  19712: key_name$(52) = "RIGHT"
key_ref(53) =  18432: key_name$(53) = "UP"
key_ref(54) =  20480: key_name$(54) = "DOWN"
key_ref(55) =     45: key_name$(55) = "-"
key_ref(56) =     61: key_name$(56) = "="
key_ref(57) =     91: key_name$(57) = "["
key_ref(58) =     93: key_name$(58) = "]"
key_ref(59) =     92: key_name$(59) = "\"
key_ref(60) =     59: key_name$(60) = ";"
key_ref(61) =     39: key_name$(61) = chr$(39) ' Apostrophe
key_ref(62) =     44: key_name$(62) = ","
key_ref(63) =     46: key_name$(63) = "."
key_ref(64) =     47: key_name$(64) = "/"
key_ref(65) =     13: key_name$(65) = "ENTER"
key_ref(66) =     27: key_name$(66) = "ESC"

' Matching uppercase codes for replacement - false if none
ucase_key_ref(1)  =  65 ' A
ucase_key_ref(2)  =  66 ' B
ucase_key_ref(3)  =  67 ' C
ucase_key_ref(4)  =  68 ' D
ucase_key_ref(5)  =  69 ' E
ucase_key_ref(6)  =  70 ' F
ucase_key_ref(7)  =  71 ' G
ucase_key_ref(8)  =  72 ' H
ucase_key_ref(9)  =  73 ' I
ucase_key_ref(10) =  74 ' J
ucase_key_ref(11) =  75 ' K
ucase_key_ref(12) =  76 ' L
ucase_key_ref(13) =  77 ' M
ucase_key_ref(14) =  78 ' N
ucase_key_ref(15) =  79 ' O
ucase_key_ref(16) =  80 ' P
ucase_key_ref(17) =  81 ' Q
ucase_key_ref(18) =  82 ' R
ucase_key_ref(19) =  83 ' S
ucase_key_ref(20) =  84 ' T
ucase_key_ref(21) =  85 ' U
ucase_key_ref(22) =  86 ' V
ucase_key_ref(23) =  87 ' W
ucase_key_ref(24) =  88 ' X
ucase_key_ref(25) =  89 ' Y
ucase_key_ref(26) =  90 ' Z
ucase_key_ref(27) =  33 ' !
ucase_key_ref(28) =  64 ' @
ucase_key_ref(29) =  35 ' #
ucase_key_ref(30) =  36 ' $
ucase_key_ref(31) =  37 ' %
ucase_key_ref(32) =  94 ' ^
ucase_key_ref(33) =  38 ' &
ucase_key_ref(34) =  42 ' *
ucase_key_ref(35) =  40 ' (
ucase_key_ref(36) =  41 ' )
ucase_key_ref(41) = 126 ' Tilde with shift
ucase_key_ref(55) =  95 ' _
ucase_key_ref(56) =  43 ' +
ucase_key_ref(57) = 123 ' {
ucase_key_ref(58) = 125 ' }
ucase_key_ref(59) = 124 ' |
ucase_key_ref(60) =  58 ' :
ucase_key_ref(61) =  34 ' "
ucase_key_ref(62) =  60 ' <
ucase_key_ref(63) =  62 ' >
ucase_key_ref(64) =  63 ' ?

' Default keybinds
keybind_name$(up_key)        = "UP":            default_keybind_ref(up_key)        = 53
keybind_name$(down_key)      = "DOWN":          default_keybind_ref(down_key)      = 54
keybind_name$(left_key)      = "LEFT":          default_keybind_ref(left_key)      = 51
keybind_name$(right_key)     = "RIGHT":         default_keybind_ref(right_key)     = 52
keybind_name$(weapon_key)    = "WEAPON/OK":     default_keybind_ref(weapon_key)    = 24 ' x
keybind_name$(shield_key)    = "SHIELD/CANCEL": default_keybind_ref(shield_key)    =  3 ' c
keybind_name$(engine_key)    = "ENGINE":        default_keybind_ref(engine_key)    = 26 ' z
keybind_name$(wpn_sel_right) = "NEXT WEAPON":   default_keybind_ref(wpn_sel_right) =  6 ' f
keybind_name$(wpn_sel_left)  = "PREV WEAPON":   default_keybind_ref(wpn_sel_left)  =  4 ' d
default_keybind_ref(enter_key) = 65 ' Enter and Esc are static and cannot be rebound
default_keybind_ref(esc_key)   = 66

' On launch, start with defaults
for n = 1 to keybind_count
   keybind_ref(n) = default_keybind_ref(n)
next n

end sub


The next thing you'll need are the basic functions for use in your code for input processing.
These are designed to be very clean and intuitive.


Code: [Select]
function press(b) ' Returns true if a key is currently pressed
press = _keydown(key_ref(keybind_ref(b)))
u& = ucase_key_ref(keybind_ref(b))
if u& <> false then
   if _keydown(u&) = true then press = true
end if
end function


function new_press(b) ' Returns true if a key is pressed this frame, but wasn't last frame
new_press = false
if press(b) = true and hold(b) = false then new_press = true
end function


sub update_hold ' Store state of all keys when moving to next frame
for b = 1 to keybind_count
   hold(b) = press(b)
next b
end sub


sub set_hold(p) ' Override all last-frame states - useful for avoiding slurring with new_press
for b = 1 to keybind_count
   hold(b) = p
next b
end sub


At this point, everything's in place for you to use your default settings in your program.
The only thing left is to provide the user with a way to edit the settings directly.
The following menu routine makes use of a little function I made called wrap(),
so I've included it as well.


Code: [Select]
sub keybind_menu

call set_hold(true) ' Prevent slurring inputs from whatever was happening before

kc = keybind_count - 2 ' Exclude Enter and Esc

' Copy keybinds to editing array
for n = 1 to kc
   keybind_edit_ref(n) = keybind_ref(n)
next n

c = 1 ' Starting cursor position

do
   _limit 60

   ' Menu display goes here, however you like.
   ' Options available to the user, as coded here, are:
   ' - The user functions in sequence, from 1 to kc
   ' - Reset to defaults, located at kc + 1
   ' - Exit menu, located at kc + 2
   ' Use keybind_name$(1 to kc) to show names of keybind slots
   ' Use key_name$(keybind_ref(1 to kc)) to show names of keys

   ' My game's menu uses a bunch of assets specific to the game,
   ' but here's a basic menu using print statements.
   cls
   x1 = 3: x2 = 20
   for n = 1 to kc
      locate n, x1: print keybind_name$(n)
      locate n, x2: print key_name$(keybind_ref(n))
   next n
   locate kc + 1, x1: print "Reset to default"
   locate kc + 2, x1: print "Exit"
   locate c, 1: print ">"

   _display


   if new_press(up_key) = true and new_press(down_key) = false then
      c = wrap(c - 1, 1, kc + 2)
   elseif new_press(down_key) = true and new_press(up_key) = false then
      c = wrap(c + 1, 1, kc + 2)

   else
      if new_press(esc_key) = true then exit do ' Leave menu and apply settings

      if new_press(enter_key) = true or new_press(weapon_key) = true then
         if c = kc + 2 then
            ' Leave menu and apply settings
            exit do

         elseif c = kc + 1 then
            ' Reset to defaults, you may want a confirmation for this
            for n = 1 to keybind_count
               ' Reset both, changes keybinds in menu immediately
               keybind_ref(n)      = default_keybind_ref(n)
               keybind_edit_ref(n) = default_keybind_ref(n)
            next n

         else
            ' User selected this gameplay function, await a new assignment

            call set_hold(true) ' Prevent slurring inputs
            do
               _limit 60

               ' Draw whatever indication of the keybind slot being assigned
               locate c, 1: print " "
               locate c, x2 - 2: print ">"

               _display

               if new_press(esc_key) = true then exit do ' Cancel

               ' Bind key when any valid key is pressed, avoiding duplicates
               for n = 1 to key_ref_count - 2 ' Excludes enter and esc
                  dupe = false
                  for n1 = 1 to kc
                     if keybind_edit_ref(n1) = n then dupe = true
                  next n1

                  if ucase_key_ref(n) <> false then
                     uc = _keydown(ucase_key_ref(n))
                  end if
                  if _keydown(key_ref(n)) = true or uc = true then
                     if dupe = false then keybind_edit_ref(c) = n
                     exit do
                  end if
               next n
               call update_hold
            loop

         end if
      end if
   end if

   call update_hold
loop

' Copy new settings to keybind array
for n = 1 to keybind_count - 2
   keybind_ref(n) = keybind_edit_ref(n)
next n

end sub


function wrap(n, l1, h1)
' n is adjusted back within lower(l) and upper(h) bounds similar to mod operator
l = l1: h = h1 ' make sure h is never less than l, this also prevents division by zero
if h1 < l1 then
   l = h1: h = l1
end if
x = (l - n) / ((h - l) + 1)
if x <> int(x) then x = x + 1
wrap = n + (int(x) * ((h - l) + 1))
end function


In general, use press() to check for keys that are down, new_press() to check for fresh
keypresses, put an update_hold() call at the END of every input loop, and use set_hold(true)
before the loop to avoid having new_press() slur inputs from a previous process.
You can see most of this implemented in the menu routine above.

Due to everything having built-in names, you can dynamically alter text to reflect
the user's settings with this sort of routine below, which takes a text marker with
a value attached, and converts it into the relevant text based on the keybinding settings.


Code: [Select]
function text_tag_replace$(t1$, f)
' Flag parameter invokes specific tag set, false to use all

t$ = t1$

if f = false or f = text_tag_keybind then
   ' Look for every "#kb01" etc in string and replace with key_name$
   n = scan_text(0, lcase$(t$), "#kb")
   do while n <> 0
      i  = val(mid$(t$, n + 3, 2))
      t$ = text_replace$(t$, key_name$(keybind_ref(i)), n, 5)
      n  = scan_text(n, lcase$(t$), "#kb")
   loop

end if
text_tag_replace$ = t$

end function


function text_replace$(t$, r$, p, l)
' p = position of section to replace, l = its length
text_replace$ = left$(t$, p - 1) + r$ + right$(t$, len(t$) - p - (l - 1))
end function


function scan_text(p1, t$, d$)
p = p1
do
   p = p + 1
   if p > len(t$) - (len(d$) - 1) then
      scan_text = 0
      exit function
   end if
loop until mid$(t$, p, len(d$)) = d$
scan_text = p
end function


So, with the above routine, let's say you wanted to have a character say the following:

"Do a barrel roll!  (Press LEFT or RIGHT twice)"


You would write their text as follows, given the values assigned to our constants at the top:

"Do a barrel roll!  (Press #kb03 or #kb04 twice)"


Then, in the display routine, enclose the text in the function, such as:

t$ = "Do a barrel roll!  (Press #kb03 or #kb04 twice)"
print text_tag_replace$(t$, text_tag_keybind)


Now, if the user changes the settings to typical WASD directional control, the program will
alter the text it displays, printing the following:

"Do a barrel roll!  (Press A or D twice)"


Enjoy!

Offline johannhowitzer

  • Forum Regular
  • Posts: 118
    • View Profile
Re: User-Customizable Keybinding System
« Reply #1 on: November 11, 2020, 03:39:22 pm »
Also, I wanted to check in and see if the shift bug with _KEYDOWN has been solved,
or will be soon.  Until now, I haven't bothered to do all the work necessary to convert
this input system to using _KEYHIT, since I had many other things to work on.

However, now my game is close to demo release, and if _KEYDOWN isn't going to be fixed soon,
I'd like to know so I can make an informed decision about how to proceed.  I've been keeping
my eyes open around the forum, but haven't seen any news about this fix.  Thanks!