├── .gitattributes ├── .gitignore ├── readme.md └── tactile.lua /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | For anyone coming here from awesome-love2d (or anywhere else), if you need an input library, give [Baton](https://github.com/tesselode/baton) a shot first! It's the successor to Tactile and is much better, at least in my opinion. 2 | 3 | # Tactile 4 | 5 | Tactile is an input library for LÖVE that bridges the gap between different input methods and types. In Tactile, there is no distinction between buttons and analog controls - controls are both buttons and axes at the same time. 6 | 7 | ```lua 8 | Control = { 9 | Horizontal = tactile.newControl() 10 | :addAxis(tactile.gamepadAxis(1, 'leftx')) 11 | :addButtonPair(tactile.keys 'left', tactile.keys 'right'), 12 | Fire = tactile.newControl() 13 | :addButton(tactile.gamepadButtons(1, 'a')) 14 | :addButton(tactile.keys 'x') 15 | } 16 | 17 | function love.update(dt) 18 | Control.Horizontal:update() 19 | Control.Fire:update() 20 | 21 | player.x = player.x + player.speed * Control.Horizontal() * dt 22 | if Control.Fire:isDown() then 23 | player:shoot() 24 | end 25 | end 26 | ``` 27 | 28 | ## Table of contents 29 | 30 | - [Overview](#overview) 31 | - [Usage](#usage) 32 | - [API](#api) 33 | - [License](#license) 34 | 35 | ## Overview 36 | 37 | Tactile has two types of objects: 38 | - **Controls**: A control represents a distinct action in the game. For example, you might make "horizontal" and "vertical" controls for movement using the arrow keys or analog stick, and "primary" and "secondary" controls for the A and B button. 39 | - **Detectors**: A detector is a function that checks for a certain kind of input. These are split up into three types: 40 | - **Axis detectors**: An axis detector checks for an analog input. For example, a function that returned the value of a gamepad axis would be an axis detector. 41 | - **Button detectors**: A button detector checks the state of a single button. 42 | - **Button pair detectors**: A button pair detector uses two buttons to represent an axis. One button represents the negative end of an axis, and the other represents the positive end. 43 | 44 | ### Controls 45 | Controls contain a series of detectors and use them to act as both a button and an axis. The most important function is `Control:getValue`, which runs through all of the detectors in order and uses them to calculate a value between -1 and 1. 46 | - If the detector is an axis detector, the resulting value will be whatever number the axis detector returns. 47 | - If the detector is a button detector, the resulting value will be 0 if the button detector returns `false` and 1 if the button detector returns `true`. 48 | - If the detector is a button pair detector... 49 | - If both or neither the negative and positive detectors return `true`, the resulting value will be 0. 50 | - If only the negative detector returns `true`, the resulting value will be -1. 51 | - If only the positive detector returns `true`, the resulting value will be 1. 52 | - Each detector will override the values of the previous one as long as they are non-zero (i.e., their absolute value is greater than the deadzone) 53 | 54 | Controls also act as buttons, so they can be "down" or not "down". They're considered to be "down" if `Control:getValue` is a non-zero number. Furthermore, controls can be "down" in a certain direction, meaning `Control:getValue` is less than `-deadzone` or greater than `deadzone`. They also keep track of whether they were pressed or released in the current frame. 55 | 56 | ### Examples 57 | That was all very abstract. What does this mean? Well, here are some examples of common ways to use Tactile. For these examples, let's assume that we've set up the controls like this: 58 | 59 | ```lua 60 | Control = { 61 | Horizontal = tactile.newControl() 62 | :addAxis(tactile.gamepadAxis(1, 'leftx')) 63 | :addButtonPair(tactile.keys('a', 'left'), tactile.keys('d', 'right')), 64 | Vertical = tactile.newControl() 65 | :addAxis(tactile.gamepadAxis(1, 'lefty')) 66 | :addButtonPair(tactile.keys('w', 'up'), tactile.keys('s', 'down')), 67 | Fire = tactile.newControl() 68 | :addAxis(tactile.gamepadAxis(1, 'triggerleft')) 69 | :addAxis(tactile.gamepadAxis(1, 'triggerright')) 70 | :addButton(tactile.gamepadButtons(1, 'a')) 71 | :addButton(tactile.keys 'x') 72 | } 73 | ``` 74 | 75 | First, let's think about movement. This is the perfect time to use controls like axes. The `Horizontal` and `Vertical` controls have the left analog stick, arrow keys, and WASD mapped to them, so you can easily do something like this: 76 | 77 | ```lua 78 | player.x = player.x + Control.Horizontal:getValue() * player.speed * dt 79 | player.y = player.y + Control.Vertical:getValue() * player.speed * dt 80 | ``` 81 | 82 | Since `Control:getValue()` always returns a number between -1 and 1, the player will move at a speed and in a direction that makes sense given the input. 83 | 84 | Now let's think about shooting. This is something that's handled by a button input. We'll use the `Fire` control: 85 | 86 | ```lua 87 | if Control.Fire:isDown() then 88 | player:shoot() 89 | end 90 | ``` 91 | 92 | That's all we have to do! The `Fire` control has the `X` key, `A` button on the gamepad, and left and right triggers mapped to it. If `X` or `A` are pushed down, or if either trigger is pushed down more than halfway, the `Control.Fire` will register as being pushed down. 93 | 94 | One more example: menu controls. This is the sneaky one! It's obvious to use `Horizontal` and `Vertical` as axes and `Fire` as a button, but for menus, we need to use the analog stick and the arrow keys as button presses to move a cursor around. But since controls are both axes and buttons, this is already set up for us. We'll use the `dir` argument of `Control:pressed` to detect button presses in certain directions. 95 | 96 | ```lua 97 | if Control.Horizontal:pressed(-1) then 98 | -- move the cursor to the left 99 | end 100 | if Control.Horizontal:pressed(1) then 101 | -- move the cursor to the right 102 | end 103 | if Control.Vertical:pressed(-1) then 104 | -- move the cursor up 105 | end 106 | if Control.Vertical:pressed(1) then 107 | -- move the cursor down 108 | end 109 | ``` 110 | 111 | ## Usage 112 | 113 | Place tactile.lua somewhere in your project. To use it, do: 114 | ```lua 115 | local tactile = require 'path.to.tactile' 116 | ``` 117 | 118 | ## API 119 | 120 | ### Controls 121 | 122 | #### `Control = tactile.newControl()` 123 | Creates and returns a new control. 124 | 125 | Controls have the following properties: 126 | - `deadzone` (number) - the deadzone amount. Detectors with an absolute value less than the deadzone will be ignored. 127 | 128 | #### `Control:addAxis(f)` 129 | Adds an axis detector to the control. 130 | - `f` (function) - an axis detector. Axis detectors are functions that return a number between -1 and 1. 131 | 132 | #### `Control:addButton(f)` 133 | Adds a button detector to the control. 134 | - `f` (function) - a button detector. Button detectors are functions that return `true` or `false`. 135 | 136 | #### `Control:addButtonPair(negative, positive)` 137 | Adds a button pair detector to the control, which is generated from two button detectors. The negative button detector will be mapped to -1, and the positive button detector will be mapped to 1. 138 | - `negative` (function) - the negative button detector. 139 | - `positive` (function) - the positive button detector. 140 | 141 | #### `Control:getValue()` 142 | Returns the current axis value of the control. As a shortcut, you can simply call `Control()`, which returns `Control:getValue()`. 143 | 144 | #### ```Control:isDown(dir)``` 145 | Returns whether the control is down or not. The control is considered to be down if its absolute value is greater than the deadzone. 146 | - `dir` (optional) - set this to -1 or 1 to check if the control is down in a certain direction. 147 | 148 | #### ```Control:pressed(dir)``` 149 | Returns whether the control was pressed this frame. 150 | - `dir` (optional) - the direction to check. 151 | 152 | #### ```Control:released(dir)``` 153 | Returns whether the control was released this frame. 154 | - `dir` (optional) - the direction to check. 155 | 156 | #### ```Control:update()``` 157 | Updates the state of the control. Call this on all of your controls each frame. Sorry you have to do this. :( 158 | 159 | ### Detectors 160 | Since detectors are just functions, you could write your own (and in some cases, you might want to). However, Tactile provides built-in detectors that should cover all the common use cases. 161 | 162 | #### `tactile.keys(...)` 163 | Returns a button detector that returns true if any of the specified keys are down. 164 | - `...` (strings) - a list of keys to check for. 165 | 166 | #### `tactile.gamepadButtons(num, ...)` 167 | Returns a button detector that returns true if any of the specified gamepad buttons are held down. 168 | - `num` (number) - the number of the controller to check. 169 | - `...` (strings) - a list of gamepad buttons to check for. 170 | 171 | #### `tactile.gamepadAxis(num, axis)` 172 | Returns an axis detector that returns the value of the specified gamepad axis. 173 | - `num` (number) - the number of the controller to check. 174 | - `axis` (string) - the gamepad axis to check. 175 | 176 | ## License 177 | 178 | Tactile is licensed under the MIT license. 179 | 180 | > Copyright (c) 2016 Andrew Minnich 181 | > 182 | > Permission is hereby granted, free of charge, to any person obtaining a copy 183 | > of this software and associated documentation files (the "Software"), to deal 184 | > in the Software without restriction, including without limitation the rights 185 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 186 | > copies of the Software, and to permit persons to whom the Software is 187 | > furnished to do so, subject to the following conditions: 188 | > 189 | > The above copyright notice and this permission notice shall be included in all 190 | > copies or substantial portions of the Software. 191 | > 192 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 193 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 194 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 195 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 196 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 197 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 198 | > SOFTWARE. 199 | -------------------------------------------------------------------------------- /tactile.lua: -------------------------------------------------------------------------------- 1 | local tactile = { 2 | _VERSION = 'Tactile v2.0.1', 3 | _DESCRIPTION = 'A happy and friendly input library for LÖVE.', 4 | _URL = 'https://github.com/tesselode/tactile', 5 | _LICENSE = [[ 6 | Copyright (c) 2016 Andrew Minnich 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | ]] 26 | } 27 | 28 | local function sign(x) 29 | return x < 0 and -1 or x > 0 and 1 or 0 30 | end 31 | 32 | local function verify(identity, argnum, value, expected, expectedstring) 33 | if type(value) ~= expected then 34 | error(string.format("%s: argument %d should be a %s, got %s", identity, 35 | argnum, expectedstring or expected, type(value))) 36 | end 37 | end 38 | 39 | local Control = {} 40 | 41 | function Control:addAxis(f) 42 | table.insert(self._detectors, f) 43 | return self 44 | end 45 | 46 | function Control:addButton(f) 47 | table.insert(self._detectors, function() 48 | return f() and 1 or 0 49 | end) 50 | return self 51 | end 52 | 53 | function Control:addButtonPair(negative, positive) 54 | table.insert(self._detectors, function() 55 | local n, p = negative(), positive() 56 | return n and p and 0 57 | or n and -1 58 | or p and 1 59 | or 0 60 | end) 61 | return self 62 | end 63 | 64 | function Control:_calculateValue() 65 | for i = #self._detectors, 1, -1 do 66 | local value = self._detectors[i]() 67 | if math.abs(value) > self.deadzone then 68 | return value 69 | end 70 | end 71 | return 0 72 | end 73 | 74 | function Control:getValue() 75 | return self._currentValue 76 | end 77 | 78 | function Control:isDown(dir) 79 | if dir then 80 | return sign(self._currentValue) == sign(dir) 81 | end 82 | return self._currentValue ~= 0 83 | end 84 | 85 | function Control:pressed(dir) 86 | if dir then 87 | dir = sign(dir) 88 | return sign(self._currentValue) == dir 89 | and sign(self._previousValue) ~= dir 90 | end 91 | return self._currentValue ~= 0 92 | and self._previousValue == 0 93 | end 94 | 95 | function Control:released(dir) 96 | if dir then 97 | dir = sign(dir) 98 | return sign(self._currentValue) ~= dir 99 | and sign(self._previousValue) == dir 100 | end 101 | return self._currentValue == 0 102 | and self._previousValue ~= 0 103 | end 104 | 105 | function Control:update() 106 | self._previousValue = self._currentValue 107 | self._currentValue = self:_calculateValue() 108 | end 109 | 110 | function tactile.newControl() 111 | local control = { 112 | deadzone = .5, 113 | _detectors = {}, 114 | _currentValue = 0, 115 | _previousValue = 0, 116 | } 117 | 118 | setmetatable(control, { 119 | __index = Control, 120 | __call = function(t) 121 | return t:getValue() 122 | end 123 | }) 124 | 125 | return control 126 | end 127 | 128 | function tactile.keys(...) 129 | local keys = {...} 130 | for i, key in ipairs(keys) do 131 | verify('tactile.keys()', i, key, 'string', 'KeyConstant (string)') 132 | end 133 | return function() 134 | return love.keyboard.isDown(unpack(keys)) 135 | end 136 | end 137 | 138 | function tactile.gamepadButtons(num, ...) 139 | local buttons = {...} 140 | for i, button in ipairs(buttons) do 141 | verify('tactile.gamepadButtons()', i, button, 'string', 142 | 'GamepadButton (string)') 143 | end 144 | return function() 145 | local joystick = love.joystick.getJoysticks()[num] 146 | return joystick ~= nil and joystick:isGamepadDown(unpack(buttons)) 147 | end 148 | end 149 | 150 | function tactile.gamepadAxis(num, axis) 151 | verify('tactile.gamepadAxis()', 1, num, 'number') 152 | verify('tactile.gamepadAxis()', 2, axis, 'string', 'GamepadAxis (string)') 153 | return function() 154 | local joystick = love.joystick.getJoysticks()[num] 155 | return joystick ~= nil and joystick:getGamepadAxis(axis) or 0 156 | end 157 | end 158 | 159 | return tactile 160 | --------------------------------------------------------------------------------