├── Input.lua └── README.md /Input.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2018 SSYGEN 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | ]]-- 22 | 23 | local input_path = (...):match('(.-)[^%.]+$') .. '.' 24 | local Input = {} 25 | Input.__index = Input 26 | 27 | Input.all_keys = { 28 | " ", "return", "escape", "backspace", "tab", "space", "!", "\"", "#", "$", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", 29 | "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", "@", "[", "\\", "]", "^", "", "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", 30 | "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "capslock", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "printscreen", 31 | "scrolllock", "pause", "insert", "home", "pageup", "delete", "end", "pagedown", "right", "left", "down", "up", "numlock", "kp/", "kp*", "kp-", "kp+", "kpenter", 32 | "kp0", "kp1", "kp2", "kp3", "kp4", "kp5", "kp6", "kp7", "kp8", "kp9", "kp.", "kp,", "kp=", "application", "power", "f13", "f14", "f15", "f16", "f17", "f18", "f19", 33 | "f20", "f21", "f22", "f23", "f24", "execute", "help", "menu", "select", "stop", "again", "undo", "cut", "copy", "paste", "find", "mute", "volumeup", "volumedown", 34 | "alterase", "sysreq", "cancel", "clear", "prior", "return2", "separator", "out", "oper", "clearagain", "thsousandsseparator", "decimalseparator", "currencyunit", 35 | "currencysubunit", "lctrl", "lshift", "lalt", "lgui", "rctrl", "rshift", "ralt", "rgui", "mode", "audionext", "audioprev", "audiostop", "audioplay", "audiomute", 36 | "mediaselect", "brightnessdown", "brightnessup", "displayswitch", "kbdillumtoggle", "kbdillumdown", "kbdillumup", "eject", "sleep", "mouse1", "mouse2", "mouse3", 37 | "mouse4", "mouse5", "wheelup", "wheeldown", "fdown", "fup", "fleft", "fright", "back", "guide", "start", "leftstick", "rightstick", "l1", "r1", "l2", "r2", "dpup", 38 | "dpdown", "dpleft", "dpright", "leftx", "lefty", "rightx", "righty", 39 | } 40 | 41 | function Input.new() 42 | local self = {} 43 | 44 | self.prev_state = {} 45 | self.state = {} 46 | self.binds = {} 47 | self.functions = {} 48 | self.repeat_state = {} 49 | self.sequences = {} 50 | 51 | -- Gamepads... currently only supports 1 gamepad, adding support for more is not that hard, just lazy. 52 | self.joysticks = love.joystick.getJoysticks() 53 | 54 | -- Register callbacks automagically 55 | local callbacks = {'keypressed', 'keyreleased', 'mousepressed', 'mousereleased', 'gamepadpressed', 'gamepadreleased', 'gamepadaxis', 'wheelmoved', 'update'} 56 | local old_functions = {} 57 | local empty_function = function() end 58 | for _, f in ipairs(callbacks) do 59 | old_functions[f] = love[f] or empty_function 60 | love[f] = function(...) 61 | old_functions[f](...) 62 | self[f](self, ...) 63 | end 64 | end 65 | 66 | return setmetatable(self, Input) 67 | end 68 | 69 | function Input:bind(key, action) 70 | if type(action) == 'function' then self.functions[key] = action; return end 71 | if not self.binds[action] then self.binds[action] = {} end 72 | table.insert(self.binds[action], key) 73 | end 74 | 75 | function Input:pressed(action) 76 | if action then 77 | for _, key in ipairs(self.binds[action]) do 78 | if self.state[key] and not self.prev_state[key] then 79 | return true 80 | end 81 | end 82 | 83 | else 84 | for _, key in ipairs(Input.all_keys) do 85 | if self.state[key] and not self.prev_state[key] then 86 | if self.functions[key] then 87 | self.functions[key]() 88 | end 89 | end 90 | end 91 | end 92 | end 93 | 94 | function Input:released(action) 95 | for _, key in ipairs(self.binds[action]) do 96 | if self.prev_state[key] and not self.state[key] then 97 | return true 98 | end 99 | end 100 | end 101 | 102 | function Input:sequence(...) 103 | local sequence = {...} 104 | if #sequence <= 1 then error("Use :pressed instead if you only need to check 1 action") end 105 | if type(sequence[#sequence]) ~= 'string' then error("The last argument must be an action") end 106 | if #sequence % 2 == 0 then error("The number of arguments passed in must be odd") end 107 | 108 | local sequence_key = '' 109 | for _, seq in ipairs(sequence) do sequence_key = sequence_key .. tostring(seq) end 110 | 111 | if not self.sequences[sequence_key] then 112 | self.sequences[sequence_key] = {sequence = sequence, current_index = 1} 113 | 114 | else 115 | if self.sequences[sequence_key].current_index == 1 then 116 | local action = self.sequences[sequence_key].sequence[self.sequences[sequence_key].current_index] 117 | for _, key in ipairs(self.binds[action]) do 118 | if self.state[key] and not self.prev_state[key] then 119 | self.sequences[sequence_key].last_pressed = love.timer.getTime() 120 | self.sequences[sequence_key].current_index = self.sequences[sequence_key].current_index + 1 121 | end 122 | end 123 | 124 | else 125 | local delay = self.sequences[sequence_key].sequence[self.sequences[sequence_key].current_index] 126 | local action = self.sequences[sequence_key].sequence[self.sequences[sequence_key].current_index + 1] 127 | 128 | if (love.timer.getTime() - self.sequences[sequence_key].last_pressed) > delay then self.sequences[sequence_key] = nil end 129 | for _, key in ipairs(self.binds[action]) do 130 | if self.state[key] and not self.prev_state[key] then 131 | if (love.timer.getTime() - self.sequences[sequence_key].last_pressed) <= delay then 132 | if self.sequences[sequence_key].current_index + 1 == #self.sequences[sequence_key].sequence then 133 | self.sequences[sequence_key] = nil 134 | return true 135 | else 136 | self.sequences[sequence_key].last_pressed = love.timer.getTime() 137 | self.sequences[sequence_key].current_index = self.sequences[sequence_key].current_index + 2 138 | end 139 | else 140 | self.sequences[sequence_key] = nil 141 | end 142 | end 143 | end 144 | end 145 | end 146 | end 147 | 148 | local key_to_button = {mouse1 = '1', mouse2 = '2', mouse3 = '3', mouse4 = '4', mouse5 = '5'} 149 | local gamepad_to_button = {fdown = 'a', fup = 'y', fleft = 'x', fright = 'b', back = 'back', guide = 'guide', start = 'start', 150 | leftstick = 'leftstick', rightstick = 'rightstick', l1 = 'leftshoulder', r1 = 'rightshoulder', 151 | dpup = 'dpup', dpdown = 'dpdown', dpleft = 'dpleft', dpright = 'dpright'} 152 | local axis_to_button = {leftx = 'leftx', lefty = 'lefty', rightx = 'rightx', righty = 'righty', l2 = 'triggerleft', r2 = 'triggerright'} 153 | 154 | function Input:down(action, interval, delay) 155 | if action and delay and interval then 156 | for _, key in ipairs(self.binds[action]) do 157 | if self.state[key] and not self.prev_state[key] then 158 | self.repeat_state[key] = {pressed_time = love.timer.getTime(), delay = delay, interval = interval, delay_stage = true} 159 | return true 160 | elseif self.repeat_state[key] and self.repeat_state[key].pressed then 161 | return true 162 | end 163 | end 164 | 165 | elseif action and interval and not delay then 166 | for _, key in ipairs(self.binds[action]) do 167 | if self.state[key] and not self.prev_state[key] then 168 | self.repeat_state[key] = {pressed_time = love.timer.getTime(), delay = 0, interval = interval, delay_stage = false} 169 | return true 170 | elseif self.repeat_state[key] and self.repeat_state[key].pressed then 171 | return true 172 | end 173 | end 174 | 175 | elseif action and not interval and not delay then 176 | for _, key in ipairs(self.binds[action]) do 177 | if (love.keyboard.isDown(key) or love.mouse.isDown(key_to_button[key] or 0)) then 178 | return true 179 | end 180 | 181 | -- Supports only 1 gamepad, add more later... 182 | if self.joysticks[1] then 183 | if axis_to_button[key] then 184 | return self.state[key] 185 | elseif gamepad_to_button[key] then 186 | if self.joysticks[1]:isGamepadDown(gamepad_to_button[key]) then 187 | return true 188 | end 189 | end 190 | end 191 | end 192 | end 193 | end 194 | 195 | function Input:unbind(key) 196 | for action, keys in pairs(self.binds) do 197 | for i = #keys, 1, -1 do 198 | if key == self.binds[action][i] then 199 | table.remove(self.binds[action], i) 200 | end 201 | end 202 | end 203 | if self.functions[key] then 204 | self.functions[key] = nil 205 | end 206 | end 207 | 208 | function Input:unbindAll() 209 | self.binds = {} 210 | self.functions = {} 211 | end 212 | 213 | local copy = function(t1) 214 | local out = {} 215 | for k, v in pairs(t1) do out[k] = v end 216 | return out 217 | end 218 | 219 | function Input:update() 220 | self:pressed() 221 | self.prev_state = copy(self.state) 222 | self.state['wheelup'] = false 223 | self.state['wheeldown'] = false 224 | 225 | for k, v in pairs(self.repeat_state) do 226 | if v then 227 | v.pressed = false 228 | local t = love.timer.getTime() - v.pressed_time 229 | if v.delay_stage then 230 | if t > v.delay then 231 | v.pressed = true 232 | v.pressed_time = love.timer.getTime() 233 | v.delay_stage = false 234 | end 235 | else 236 | if t > v.interval then 237 | v.pressed = true 238 | v.pressed_time = love.timer.getTime() 239 | end 240 | end 241 | end 242 | end 243 | end 244 | 245 | function Input:keypressed(key) 246 | self.state[key] = true 247 | end 248 | 249 | function Input:keyreleased(key) 250 | self.state[key] = false 251 | self.repeat_state[key] = false 252 | end 253 | 254 | local button_to_key = { 255 | [1] = 'mouse1', [2] = 'mouse2', [3] = 'mouse3', [4] = 'mouse4', [5] = 'mouse5', 256 | ['l'] = 'mouse1', ['r'] = 'mouse2', ['m'] = 'mouse3', ['x1'] = 'mouse4', ['x2'] = 'mouse5' 257 | } 258 | 259 | function Input:mousepressed(x, y, button) 260 | self.state[button_to_key[button]] = true 261 | end 262 | 263 | function Input:mousereleased(x, y, button) 264 | self.state[button_to_key[button]] = false 265 | self.repeat_state[button_to_key[button]] = false 266 | end 267 | 268 | function Input:wheelmoved(x, y) 269 | if y > 0 then self.state['wheelup'] = true 270 | elseif y < 0 then self.state['wheeldown'] = true end 271 | end 272 | 273 | local button_to_gamepad = {a = 'fdown', y = 'fup', x = 'fleft', b = 'fright', back = 'back', guide = 'guide', start = 'start', 274 | leftstick = 'leftstick', rightstick = 'rightstick', leftshoulder = 'l1', rightshoulder = 'r1', 275 | dpup = 'dpup', dpdown = 'dpdown', dpleft = 'dpleft', dpright = 'dpright'} 276 | 277 | function Input:gamepadpressed(joystick, button) 278 | self.state[button_to_gamepad[button]] = true 279 | end 280 | 281 | function Input:gamepadreleased(joystick, button) 282 | self.state[button_to_gamepad[button]] = false 283 | self.repeat_state[button_to_gamepad[button]] = false 284 | end 285 | 286 | local button_to_axis = {leftx = 'leftx', lefty = 'lefty', rightx = 'rightx', righty = 'righty', triggerleft = 'l2', triggerright = 'r2'} 287 | 288 | function Input:gamepadaxis(joystick, axis, newvalue) 289 | self.state[button_to_axis[axis]] = newvalue 290 | end 291 | 292 | return setmetatable({}, {__call = function(_, ...) return Input.new(...) end}) 293 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # boipushy 2 | 3 | An input module for LÖVE. Simplifies input handling by abstracting them away to actions, 4 | enabling pressed/released checks outside of LÖVE's callbacks and taking care of gamepad input as well. :) 5 | 6 |
7 | 8 | ## Usage 9 | 10 | ```lua 11 | Input = require 'Input' 12 | ``` 13 | 14 |
15 | 16 | ### Creating an input object 17 | 18 | ```lua 19 | function love.load() 20 | input = Input() 21 | end 22 | ``` 23 | 24 | You can create multiple input objects even though you can get by most of the time with just a single global one. If your game supports multiple players locally then it's probably a good idea to create a different input object for each player, although it's not necessary as long as bindings between players don't collide. 25 | 26 |
27 | 28 | ### Binding keys to actions 29 | 30 | ```lua 31 | input:bind('1', 'print') 32 | ``` 33 | 34 | The example above binds the `'1'` key to the `'print'` action. This means that in our code we can check for the `'print'` action being pressed with `input:pressed('print')`, for instance, and that function would return true on the frame that we pressed the `'1'` key on the keyboard. This layer of indirection between keyboard and action allows our gameplay focus to only speak in terms of actions, which means that it doesn't have to care about which method of input is being used or if the key bindings were changed to something else. 35 | 36 | ```lua 37 | input:bind('s', function() print(2) end) 38 | input:bind('mouse1', 'left_click') 39 | ``` 40 | 41 | On top of action strings we can also bind anonymous functions. In this case, whenever we press the `'s'` key on the keyboard `2` would be printed to the console. Additionally, we can bind mouse and gamepad buttons in the same. 42 | 43 |
44 | 45 | ### Checking if an action is pressed/released/down 46 | 47 | ```lua 48 | function love.update(dt) 49 | if input:pressed('print') then print('The 1 key was pressed on this frame!') end 50 | if input:released('print') then print('The 1 key was released on this frame!') end 51 | if input:down('left_click') then print('The left mouse button is being held down!') end 52 | end 53 | ``` 54 | 55 | `pressed`, `released` and `down` are the main functions of the library. Both `pressed` and `released` only return true on the frame when that event happened, while `down` returns true on every frame that the key bound to the action is being held down. 56 | 57 |
58 | 59 | ### Triggering events on intervals if an action is held down 60 | 61 | The `down` function can accept additional arguments to trigger events on an interval basis. For instance: 62 | 63 | ```lua 64 | function love.update(dt) 65 | if input:down('print', 0.5) then print(love.math.random()) end 66 | end 67 | ``` 68 | 69 | The example above will print a random number every 0.5 seconds from when the `'print'` action key was held down. This is useful in a number of situations, like if you want your player to be able to only perform some action (like shooting projectiles) according to some cooldown. 70 | 71 | Additionally, a third argument can be passed which represents a delay for the event triggering to start. For instance: 72 | 73 | ```lua 74 | function love.update(dt) 75 | if input:down('print', 0.5, 2) then print(love.math.random()) end 76 | end 77 | ``` 78 | 79 | The example above will print a random number every 0.5 seconds after 2 seconds have passed from when the `'print'` action key was held down. This behavior can be seen in action whenever you're typing something and you hold a key down, for instance. If you hold the `x` key down, first `x` will be typed once, there will be a small amount of time (like say 0.3s) where nothing happens, and then a lot of `x` will come following really fast. 80 | 81 |
82 | 83 | ### Sequences 84 | 85 | The `sequence` function allows you to check for sequences of buttons pressed within an interval of each other. For instance: 86 | 87 | ```lua 88 | function love.update(dt) 89 | if input:sequence('right', 0.5, 'right') then 90 | -- dash right 91 | end 92 | end 93 | ``` 94 | 95 | In the example above the `sequence` function will return true when the `'right'` action is pressed twice, and the second key press happened within 0.5s of the first. This is useful for simple things like dashing, but also for more complex sequences like combos. This function must be started and finished with an action, and between each action there should be the interval limit. 96 | 97 |
98 | 99 | ### Unbinding a key 100 | 101 | ```lua 102 | input:unbind('1') 103 | input:unbind('s') 104 | input:unbind('mouse1') 105 | ``` 106 | 107 | Unbinding keys simply disconnects them from their actions. You can also use `input:unbindAll()` to unbind all bound keys. 108 | 109 |
110 | 111 | ### Key/mouse/gamepad Constants 112 | 113 | Keyboard constants are unchanged from [here](https://www.love2d.org/wiki/KeyConstant), but mouse and gamepad have been changed to the following: 114 | 115 | ```lua 116 | -- Mouse 117 | 'mouse1' 118 | 'mouse2' 119 | 'mouse3' 120 | 'mouse4' 121 | 'mouse5' 122 | 'wheelup' 123 | 'wheeldown' 124 | 125 | -- Gamepad 126 | 'fdown' -- fdown/up/left/right = face buttons: a, b... 127 | 'fup' 128 | 'fleft' 129 | 'fright' 130 | 'back' 131 | 'guide' 132 | 'start' 133 | 'leftstick' -- left stick pressed or not (boolean) 134 | 'rightstick' -- right stick pressed or not (boolean) 135 | 'l1' 136 | 'r1' 137 | 'l2' -- returns a value from 0 to 1 138 | 'r2' -- returns a value from 0 to 1 139 | 'dpup' -- dpad buttons 140 | 'dpdown' 141 | 'dpleft' 142 | 'dpright' 143 | 'leftx' -- returns a value from -1 to 1, the left stick's horizontal position 144 | 'lefty' -- same for vertical 145 | 'rightx' -- same for right stick 146 | 'righty' 147 | ``` 148 | 149 |
150 | 151 | ### LICENSE 152 | 153 | You can do whatever you want with this. See the license at the top of the main file. 154 | --------------------------------------------------------------------------------