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:
' 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.
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.
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.
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.
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!