├── 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 |
--------------------------------------------------------------------------------