From 21679dde5c6b0493d3932008e9c901db19ac5aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Reuh=20Fildadut?= Date: Fri, 16 Sep 2022 20:04:53 +0900 Subject: [PATCH] input overhaul Now event based! Should result in no skipped inputs. --- input/axis.lua | 162 ----------- input/button.lua | 156 ----------- input/default.lua | 16 ++ input/event.lua | 103 +++++++ input/input.lua | 676 ++++++++++++++++++++++++++++++++-------------- input/love.lua | 379 -------------------------- input/pointer.lua | 246 ----------------- 7 files changed, 599 insertions(+), 1139 deletions(-) delete mode 100644 input/axis.lua delete mode 100644 input/button.lua create mode 100644 input/default.lua create mode 100644 input/event.lua delete mode 100644 input/love.lua delete mode 100644 input/pointer.lua diff --git a/input/axis.lua b/input/axis.lua deleted file mode 100644 index da3c2b6..0000000 --- a/input/axis.lua +++ /dev/null @@ -1,162 +0,0 @@ -local input = require((...):gsub("axis$", "input")) -local button_mt = require((...):gsub("axis$", "button")) - ---- AxisInput methods -local axis_mt -axis_mt = { - -- Axis inputs -- - -- Axis input is a container for axes detector. An axis input will return the value of the axis detector the most far away from their center (0). - -- Axis input provide a threshold setting; every axis which has a distance to the center below the threshold (none by default) will be ignored. - -- @tparam AxisDetectors ... all the axis detectors or axis identifiers - -- @tretrun AxisInput the object - _new = function(...) - local r = setmetatable({ - hijackStack = {}, -- hijackers stack, last element is the object currently hijacking this input - hijacking = nil, -- object currently hijacking this input - detectors = {}, -- detectors list - val = 0, -- current value between -1 and 1 - dval = 0, -- change between -2 and 2 - raw = 0, -- raw value between -max and +max - max = 1, -- maximum for raw values - threshold = 0, -- ie., the deadzone - triggeringThreshold = 0.5 -- digital button threshold - }, axis_mt) - table.insert(r.hijackStack, r) - r.hijacking = r - r:bind(...) - r.positive = input.button(function() return r:value() > r.triggeringThreshold end) - r.negative = input.button(function() return r:value() < -r.triggeringThreshold end) - return r - end, - - --- Returns a new AxisInput with the same properties. - -- @treturn AxisInput the cloned object - clone = function(self) - return input.axis(unpack(self.detectors)) - :threshold(self.threshold) - :triggeringThreshold(self.triggeringThreshold) - end, - - --- Bind new AxisDetector(s) to this input. - -- @tparam AxisDetectors ... axis detectors or axis identifiers to add - -- @treturn AxisInput this AxisInput object - bind = function(self, ...) - for _,d in ipairs({...}) do - table.insert(self.detectors, input.axisDetector(d)) - end - return self - end, - --- Unbind AxisDetector(s). - -- @tparam AxisDetectors ... axis detectors or axis identifiers to remove - -- @treturn AxisInput this AxisInput object - unbind = button_mt.unbind, - --- Unbind all AxisDetector(s). - -- @treturn AxisInput this AxisInput object - clear = button_mt.clear, - - --- Hijacks the input. - -- This function returns a new input object which mirrors the current object, except it will hijack every new input. - -- This means any value change will only be visible to the new object; the axis will always appear to be at 0 for the initial object. - -- An input can be hijacked several times; the one which hijacked it last will be the active one. - -- @treturn AxisInput the new input object which is hijacking the input - hijack = function(self) - local hijacked - hijacked = setmetatable({ - positive = input.button(function() return hijacked:value() > self.triggeringThreshold end), - negative = input.button(function() return hijacked:value() < -self.triggeringThreshold end) - }, { __index = self, __newindex = self }) - table.insert(self.hijackStack, hijacked) - self.hijacking = hijacked - return hijacked - end, - --- Release the input that was hijacked by this object. - -- Input will be given back to the previous object. - -- @treturn AxisInput this AxisInput object - free = button_mt.free, - - --- Sets the default detection threshold (deadzone). - -- 0 by default. - -- @tparam number new the new detection threshold - -- @treturn AxisInput this AxisInput object - threshold = function(self, new) - self.threshold = tonumber(new) - return self - end, - - --- Returns the value of the input (between -1 and 1). - -- @tparam[opt=default threshold] number threshold value to use - -- @treturn number the input value - value = function(self, curThreshold) - if self.hijacking == self then - self:update() - local val = self.val - return math.abs(val) > math.abs(curThreshold or self.threshold) and val or 0 - else - return 0 - end - end, - --- Returns the change in value of the input since last update (between -2 and 2). - -- @treturn number the value delta - delta = function(self) - if self.hijacking == self then - self:update() - return self.dval - else - return 0 - end - end, - --- Returns the raw value of the input (between -max and +max). - -- @tparam[opt=default threshold*max] number raw threshold value to use - -- @treturn number the input raw value - raw = function(self, rawThreshold) - if self.hijacking == self then - self:update() - local raw = self.raw - return math.abs(raw) > math.abs(rawThreshold or self.threshold*self.max) and raw or 0 - else - return 0 - end - end, - --- Return the raw max of the input. - -- @treturn number the input raw max - max = function(self) - self:update() - return self.max - end, - - --- Sets the default triggering threshold, i.e. how the minimal axis value for which the associated buttons will be considered down. - -- 0.5 by default. - -- @tparam number new the new triggering threshold - -- @treturn AxisInput this AxisInput object - triggeringThreshold = function(self, new) - self.triggeringThreshold = tonumber(new) - return self - end, - - --- The associated button pressed when the axis reaches a positive value. - positive = nil, - --- The associated button pressed when the axis reaches a negative value. - negative = nil, - - --- Update axis state. - -- Automatically called, don't call unless you know what you're doing. - update = function(self) - if not input.updated[self] then - local val, raw, max = 0, 0, 1 - for _, d in ipairs(self.detectors) do - local v, r, m = d() -- v[-1,1], r[-m,+m] - if math.abs(v) > math.abs(val) then - val, raw, max = v, r or v, m or 1 - end - end - self.dval = val - self.val - self.val, self.raw, self.max = val, raw, max - input.updated[self] = true - end - end, - - --- LÖVE note: other callbacks that are defined in backend/love.lua and need to be called in the associated LÖVE callbacks. -} -axis_mt.__index = axis_mt - -return axis_mt diff --git a/input/button.lua b/input/button.lua deleted file mode 100644 index fc700d9..0000000 --- a/input/button.lua +++ /dev/null @@ -1,156 +0,0 @@ -local input = require((...):gsub("button$", "input")) - ---- ButtonInput methods -local button_mt -button_mt = { - -- Buttons inputs -- - -- Button input is a container for buttons detector. A button will be pressed when one of its detectors returns true. - -- Inputs also knows if the button was just pressed or released. - -- @tparam ButtonDetectors ... all the buttons detectors or buttons identifiers - -- @tretrun ButtonInput the object - _new = function(...) - local r = setmetatable({ - hijackStack = {}, -- hijackers stack, last element is the object currently hijacking this input - hijacking = nil, -- object currently hijacking this input - detectors = {}, -- detectors list - state = "none" -- current state (none, pressed, down, released) - }, button_mt) - table.insert(r.hijackStack, r) - r.hijacking = r - r:bind(...) - return r - end, - - --- Returns a new ButtonInput with the same properties. - -- @treturn ButtonInput the cloned object - clone = function(self) - return input.button(unpack(self.detectors)) - end, - - --- Bind new ButtonDetector(s) to this input. - -- @tparam ButtonDetectors ... buttons detectors or buttons identifiers to add - -- @treturn ButtonInput this ButtonInput object - bind = function(self, ...) - for _, d in ipairs({...}) do - table.insert(self.detectors, input.buttonDetector(d)) - end - return self - end, - --- Unbind ButtonDetector(s). - -- @tparam ButtonDetectors ... buttons detectors or buttons identifiers to remove - -- @treturn ButtonInput this ButtonInput object - unbind = function(self, ...) - for _, d in ipairs({...}) do - for i=#self.detectors, 1, -1 do - if self.detectors[i] == d then - table.remove(self.detectors, i) - break - end - end - end - return self - end, - --- Unbind all ButtonDetector(s). - -- @treturn ButtonInput this ButtonInput object - clear = function(self) - self.detectors = {} - return self - end, - - --- Hijacks the input. - -- This function returns a new input object which mirrors the current object, except it will hijack every new input. - -- This means any new button press/down/release will only be visible to the new object; the button will always appear unpressed for the initial object. - -- This is useful for contextual input, for example if you want to display a menu without pausing the game: the menu - -- can hijack relevant inputs while it is open, so they don't trigger any action in the rest of the game. - -- An input can be hijacked several times; the one which hijacked it last will be the active one. - -- @treturn ButtonInput the new input object which is hijacking the input - hijack = function(self) - local hijacked = setmetatable({}, { __index = self, __newindex = self }) - table.insert(self.hijackStack, hijacked) - self.hijacking = hijacked - return hijacked - end, - --- Release the input that was hijacked by this object. - -- Input will be given back to the previous object. - -- @treturn ButtonInput this ButtonInput object - free = function(self) - local hijackStack = self.hijackStack - for i, v in ipairs(hijackStack) do - if v == self then - table.remove(hijackStack, i) - self.hijacking = hijackStack[#hijackStack] - return self - end - end - error("This object is currently not hijacking this input") - end, - - --- Returns true if the input was just pressed. - -- @treturn boolean true if the input was pressed, false otherwise - pressed = function(self) - if self.hijacking == self then - self:update() - return self.state == "pressed" - else - return false - end - end, - --- Returns true if the input was just released. - -- @treturn boolean true if the input was released, false otherwise - released = function(self) - if self.hijacking == self then - self:update() - return self.state == "released" - else - return false - end - end, - --- Returns true if the input is down. - -- @treturn boolean true if the input is currently down, false otherwise - down = function(self) - if self.hijacking == self then - self:update() - local state = self.state - return state == "down" or state == "pressed" - else - return false - end - end, - --- Returns true if the input is up. - -- @treturn boolean true if the input is currently up, false otherwise - up = function(self) - return not self:down() - end, - - --- Update button state. - -- Automatically called, don't call unless you know what you're doing. - update = function(self) - if not input.updated[self] then - local down = false - for _, d in ipairs(self.detectors) do - if d() then - down = true - break - end - end - local state = self.state - if down then - if state == "none" or state == "released" then - self.state = "pressed" - else - self.state = "down" - end - else - if state == "down" or state == "pressed" then - self.state = "released" - else - self.state = "none" - end - end - input.updated[self] = true - end - end -} -button_mt.__index = button_mt - -return button_mt diff --git a/input/default.lua b/input/default.lua new file mode 100644 index 0000000..c1c68ba --- /dev/null +++ b/input/default.lua @@ -0,0 +1,16 @@ + return { + move = { + horizontal = { + "child.positive - child.negative", + positive = { "scancode.right", "scancode.d", "axis.leftx.p", "button.dpright" }, + negative = { "scancode.left", "scancode.a", "axis.leftx.n", "button.dpleft" }, + }, + vertical = { + "child.positive - child.negative", + positive = { "scancode.down", "scancode.s", "axis.lefty.p", "button.dpdown" }, + negative = { "scancode.up", "scancode.w", "axis.lefty.n", "button.dpup" }, + }, + }, + confirm = { "scancode['return']", "scancode.space", "scancode.e", "button.a" }, + cancel = { "scancode.escape", "scancode.backspace", "button.b" }, +} diff --git a/input/event.lua b/input/event.lua new file mode 100644 index 0000000..69e4c11 --- /dev/null +++ b/input/event.lua @@ -0,0 +1,103 @@ +local signal = require((...):gsub("input%.event$", "signal")) +local max, min = math.max, math.min + +--- This event registry is where every input object will listen for source events. +-- +-- Available events: +-- * `"source.name"`: triggered when source.name (example name) is updated. +-- Will pass the arguments _new value_ (number), _filter_ (optional), _..._ (additional arguments for the filter, optional). +-- `filter` is an optional filter function that will be called by the listening inputs with arguments filter(input object, new value, ...), +-- and should return the (eventually modified) new value. If it returns `nil`, the input will ignore the event (for example if the event concerns +-- a joystick that is not linked with the input). +-- * `"_active"`: triggered when any input is active, used for input detection in `onActiveNextSource`. +-- Will pass arguments _source name_ (string), _new value_, _filter_, _..._ (same arguments as other source updates, with source name added). +local event = signal.new() + +local function update(source, new, filter, ...) + event:emit(source, new, filter, ...) + event:emit("_active", source, new, filter, ...) +end +local function impulse(source, new, filter, ...) -- input without release-like event; immediately release input + event:emit(source, new, filter, ...) + event:emit("_active", source, new, filter, ...) + event:emit(source, 0, filter, ...) +end + +local function joystickFilter(input, new, joystick) + if input._joystick and joystick:getID() ~= input._joystick:getID() then + return nil -- ignore if not from the selected joystick + end + return new +end +local function joystickAxisFilter(input, new, joystick) + if input._joystick and joystick:getID() ~= input._joystick:getID() then + return nil -- ignore if not from the selected joystick + end + local deadzone = input:_deadzone() + if math.abs(new) < deadzone then + return 0 -- apply deadzone on axis value + else + return new + end +end + +-- Binding LÖVE events -- + +signal.event:bind("keypressed", function(key, scancode, isrepeat) + update(("key.%s"):format(key), 1) + update(("scancode.%s"):format(scancode), 1) +end) +signal.event:bind("keyreleased", function(key, scancode) + update(("key.%s"):format(key), 0) + update(("scancode.%s"):format(scancode), 0) +end) + +signal.event:bind("textinput", function(text) + impulse(("text.%s"):format(text), 1) +end) + +signal.event:bind("mousepressed", function(x, y, button, istouch, presses) + update(("mouse.%s"):format(button), 1) +end) +signal.event:bind("mousereleased", function(x, y, button, istouch, presses) + update(("mouse.%s"):format(button), 0) +end) + +signal.event:bind("mousemoved", function(x, y, dx, dy, istouch) + if dx > 0 then impulse("mouse.dx.p", dx) + elseif dx < 0 then impulse("mouse.dx.n", -dx) end + if dy > 0 then impulse("mouse.dy.p", dy) + elseif dy < 0 then impulse("mouse.dy.n", -dy) end + if dx ~= 0 then impulse("mouse.dx", dx) end + if dy ~= 0 then impulse("mouse.dy", dy) end + update("mouse.x", x) + update("mouse.y", y) +end) + +signal.event:bind("wheelmoved", function(x, y) + if x > 0 then impulse("wheel.x.p", x) + elseif x < 0 then impulse("wheel.x.n", -x) end + if y > 0 then impulse("wheel.y.p", y) + elseif y < 0 then impulse("wheel.y.n", -y) end + if x ~= 0 then impulse("wheel.x", x) end + if y ~= 0 then impulse("wheel.y", y) end +end) + +signal.event:bind("gamepadpressed", function(joystick, button) + update(("button.%s"):format(button), 1, joystickFilter, joystick) +end) +signal.event:bind("gamepadreleased", function(joystick, button) + update(("button.%s"):format(button), 0, joystickFilter, joystick) +end) + +signal.event:bind("gamepadaxis", function(joystick, axis, value) + update(("axis.%s.p"):format(axis), max(value,0), joystickAxisFilter, joystick) + update(("axis.%s.n"):format(axis), -min(value,0), joystickAxisFilter, joystick) + update(("axis.%s"):format(axis), value, joystickAxisFilter, joystick) +end) + +signal.event:bind("update", function(dt) + event:emit("dt", dt) -- don't trigger _active event, as frankly that would be kinda stupid +end) + +return event diff --git a/input/input.lua b/input/input.lua index 9cba78e..a55c293 100644 --- a/input/input.lua +++ b/input/input.lua @@ -1,205 +1,489 @@ ---- ubiquitousse.input --- Depends on a backend. --- Optional dependencies: ubiquitousse.signal (to bind to update signal in signal.event) -local loaded, signal = pcall(require, (...):match("^(.-)input").."signal") -if not loaded then signal = nil end +--- Input management facilities. +-- +-- The module returns a single function, `input`. +-- +-- **Requires** ubiquitousse.signal. +-- @module input +-- @usage +-- TODO --- TODO: some key selection helper? Will be backend-implemented, to account for all the possible input methods. --- TODO: some way to list all possible input / outputs, or make the *inUse make some separation between inputs indiscutitably in use and those who are incertain. --- TODO: outputs! (rumble, lights, I don't know) --- TODO: other, optional, default/generic inputs, and a way to know if they are binded. --- TODO: multiplayer input helpers? something like getting the same input for different players, or default inputs for different players +local signal = require((...):gsub("input%.input$", "signal")) +local event = require((...):gsub("input$", "event")) --- FIXME https://love2d.org/forums/viewtopic.php?p=241434#p241434 +local abs, sqrt, floor, ceil, min, max = math.abs, math.sqrt, math.floor, math.ceil, math.min, math.max -local button_mt -local axis_mt -local pointer_mt +-- TODO: +-- friendly name for sources +-- write doc, incl how to define your own source and source expressions, default inputs ---- Input stuff --- Inspired by Tactile by Andrew Minnich (https://github.com/tesselode/tactile), under the MIT license. --- Ubiquitousse considers two basic input methods, called buttons (binary input) and axes (analog input). -local input -input = { - --- Used to store inputs which were updated this frame - -- { Input: true, ... } - -- This table is for internal use and shouldn't be used from an external script. - updated = {}, +-- Always returns 0. +local function zero() return 0 end - dt = 0, - - --------------------------------- - --- Detectors (input sources) --- - --------------------------------- - - -- Buttons detectors -- - -- A button detector is a function which returns true (pressed) or false (unpressed). - -- All buttons are identified using an identifier string, which depends on the backend. The presence of eg., a mouse or keyboard is not assumed. - -- Some identifier strings conventions: (not used internally by Ubiquitousse, but it's nice to have some consistency between backends) - -- They should be in the format "source1.source2.[...].button", for example "keyboard.up" or "gamepad.button.1.a" for the A-button of the first gamepad. - -- If the button is actually an axis (ie, the button is pressed if the axis value passes a certain threshold), the threshold should be in the end of the - -- identifier, preceded by a % : for example "gamepad.axis.1.leftx%-0.5" should return true when the left-stick of the first gamepad is moved to the right - -- by more of 50%. The negative threshold value means that the button will be pressed only when the axis has a negative value (in the example, it won't be - -- pressed when the axis is moved to the right). - -- Buttons can also be defined by a list of buttons (string or functions), in which case the button will be considered down if all the buttons are down. - - --- Makes a new button detector from a identifier string. - -- The function may error if the identifier is incorrect. - -- @tparam string button identifier, depends on the platform Ubiquitousse is running on - -- @treturn the new button detector - -- @require love - basicButtonDetector = function(str) end, - - --- Make a new button detector from a detector function, string, or list of buttons. - -- @tparam string, function button identifier - buttonDetector = function(obj) - if type(obj) == "function" then - return obj - elseif type(obj) == "string" then - return input.basicButtonDetector(obj) - elseif type(obj) == "table" then - local l = {} - for _, b in ipairs(obj) do - table.insert(l, input.buttonDetector(b)) - end - return function() - for _, b in ipairs(l) do - if not b() then - return false - end - end - return true - end - end - error(("Not a valid button detector: %s"):format(obj)) - end, - - -- Axis detectors -- - -- Similar to buttons detectors, but returns a number between -1 and 1. - -- Threshold value can be used similarly with %. - -- Axis detectors can also be defined by two buttons: if the 1rst button is pressed, value will be -1, if the 2nd is pressed it will be 1 - -- and if none or the both are pressed, the value will be 0. This kind of axis identifier is a table {"button1", "button2"}. - -- Axis detectors may also optionally return after the number between -1 and 1 the raw value and max value. The raw value is between -max and +max. - - --- Makes a new axis detector from a identifier string. - -- The function may error if the identifier is incorrect. - -- @tparam string axis identifier, depends on the platform Ubiquitousse is running on - -- @treturn the new axis detector - -- @require love - basicAxisDetector = function(str) end, - - --- Make a new axis detector from a detector function, string, or a couple of buttons. - -- @tparam string, function or table axis identifier - axisDetector = function(obj) - if type(obj) == "function" then - return obj - elseif type(obj) == "string" then - return input.basicAxisDetector(obj) - elseif type(obj) == "table" then - local b1, b2 = input.buttonDetector(obj[1]), input.buttonDetector(obj[2]) - return function() - local d1, d2 = b1(), b2() - if d1 and d2 then return 0 - elseif d1 then return -1 - elseif d2 then return 1 - else return 0 end - end - end - error(("Not a valid axis detector: %s"):format(obj)) - end, - - ------------------------------ - --- Input detection helpers -- - ------------------------------ - -- TODO: make this better - - --- Returns a list of the buttons currently in use, identified by their string button identifier. - -- This may also returns "axis threshold" buttons if an axis passes the threshold. - -- @tparam[opt=0.5] number threshold the threshold to detect axes as button - -- @treturn string,... buttons identifiers list - -- @require love - buttonUsed = function(threshold) end, - - --- Returns a list of the axes currently in use, identified by their string axis identifier - -- @tparam[opt=0.5] number threshold the threshold to detect axes - -- @treturn string,... axes identifiers list - -- @require love - axisUsed = function(threshold) end, - - --- Returns a nice name for the button identifier. - -- Can be locale-depedant and stuff, it's only for display. - -- May returns the raw identifier if you're lazy. - -- @tparam string... button identifier string(s) - -- @treturn string... the displayable names - -- @require love - buttonName = function(...) end, - - --- Returns a nice name for the axis identifier. - -- Can be locale-depedant and stuff, it's only for display. - -- May returns the raw identifier if you're lazy. - -- @tparam string... axis identifier string(s) - -- @treturn string... the displayable names - -- @require love - axisName = function(...) end, - - ------------------- - --- Other stuff --- - ------------------- - - --- Some default inputs. - -- The backend should bind detectors to thoses inputs (don't recreate them). - -- These are used to provide some common input default detectors to allow to start a game quickly on - -- any platform without having to configure the keys. - -- If some key function in your game match one of theses defaults, using it instead of creating a new - -- input would be a good idea. - -- @require love - default = { - pointer = nil, -- Pointer: used to move and select. Example binds: arrow keys, WASD, stick. - confirm = nil, -- Button: used to confirm something. Example binds: Enter, A button. - cancel = nil -- Button: used to cancel something. Example binds: Escape, B button. - }, - - --- Get draw area dimensions. - -- Used for pointers. - -- @require love - getDrawWidth = function() return 1 end, - getDrawHeight = function() return 1 end, - - --- Update all the Inputs. - -- Should be called at every game update. If ubiquitousse.signal is available, will be bound to the "update" signal in signal.event. - -- The backend can hook into this function to to its input-related updates. - -- @tparam numder dt the delta-time - update = function(newDt) - input.dt = newDt - input.updated = {} +local function loadexp(exp, env) + local fn + if loadstring then + fn = assert(loadstring("return "..exp, "input expression")) + setfenv(fn, env) + else + fn = assert(load("return "..exp, "input expression", "t", env)) end - - --- If you use LÖVE, note that in order to provide every feature (especially key detection), several callbacks functions will - -- need to be called on LÖVE events. See backend/love.lua. - -- If ubiquitousse.signal is available, these callbacks will be bound to signals in signal.event (with the same name as the LÖVE - -- callbacks, minux the "love."). -} - -package.loaded[...] = input -button_mt = require((...):gsub("input$", "button")) -axis_mt = require((...):gsub("input$", "axis")) -pointer_mt = require((...):gsub("input$", "pointer")) - --- Constructors -input.button = button_mt._new -input.axis = axis_mt._new -input.pointer = pointer_mt._new - --- Create default inputs -input.default.pointer = input.pointer() -input.default.confirm = input.button() -input.default.cancel = input.button() - --- Bind signals -if signal then - signal.event:bind("update", input.update) + return fn end -require((...):gsub("input$", "love")) +-- Set a value in a table using its path string. +local function setPath(t, path, val) + for part in path:gmatch("(.-)%.") do + assert(t[part]) + t = t[part] + end + t[path:match("[^%.]+$")] = val +end +local function ensurePath(t, path, default) + for part in path:gmatch("(.-)%.") do + if not t[part] then t[part] = {} end + t = t[part] + end + local final = path:match("[^%.]+$") + if not t[final] then + t[final] = default + end +end -return input +-- Functions available in input expressions. +local expressionEnv +expressionEnv = { + floor = floor, + ceil = ceil, + abs = abs, + clamp = function(x, xmin, xmax) + return min(max(x, xmin), xmax) + end, + min = function(x, y, ...) + local m = min(x, y) + if ... then + return expressionEnv.min(m, ...) + else + return m + end + end, + max = function(x, y, ...) + local m = max(x, y) + if ... then + return expressionEnv.max(m, ...) + else + return m + end + end, + deadzone = function(x, deadzone) + if abs(x) < deadzone then + return 0 + end + return x + end +} + +-- List of modifiers that can be applied to a source in an expression +local sourceModifiers = { "passive", "active" } +for _, mod in ipairs(sourceModifiers) do + expressionEnv[mod] = function(...) return ... end +end + +local input_mt + +--- Make a new input object. +-- t: input configuration table (optional) +-- @function input +local function make_input(t) + local self = setmetatable({ + config = t or {}, + children = {}, + event = signal.new(), + _sourceCache = {}, + _event = signal.group(), + _afterFilterEvent = signal.new(), + _boundSourceEvents = {} + }, input_mt) + self:reload() + return self +end + +--- Input methods. +-- @type Input +input_mt = { + --- Input configuration table. + -- It can be used to recreate this input object later (by passing the table as an argument for the input constructor). + -- This table does not contain any userdata and should be easily serializable (e.g. to save custom input binding config). + -- This doesn't include input state, grab state, the event registry and the selected joystick since they may change often during runtime. + -- Can be changed anytime, but you may need to call `reload` to apply changes. + -- @usage + -- player.config = { + -- "key.a", "key.d - key.a", {"key.left + x", x=0.5}, -- list of input sources expressions + -- jump = {...}, -- children input + -- deadzone = 0.05, -- The deadzone for analog inputs (e.g. joystick axes): if the input absolute value is strictly below this, it will be considered as 0. + -- threshold = 0.05 -- The pressed threshold: an input is considered down if above or equal to this value. + -- } + config = {}, + --- List and map of children inputs. + -- {[child1.name]=child1, [child2.name]=child2, child1, child2...} + children = {}, + --- Name of the input. + -- Defined on children inputs only. + name = nil, + + --- False if the input is currently not grabbed, a subinput otherwise. + -- This may be different between each subinput. + grabbed = false, + --- False if the input is not a subinput, the input it grabbed otherwise. + -- This may be different between each subinput. + grabbing = false, + --- Input event registry. + -- The following events are available: + -- + -- * `"moved"`: called when the input value change, with arguments (new value, delta since last event) + -- * `"pressed"`: called when the input is pressed + -- * `"released"`: called when the input is released + -- + -- For pointer inputs (have a "horizontal" and "vertical" children inputs) is also avaible: + -- + -- * `"pointer moved"`: called when the pointer position change, with arguments (new pointer x, new pointer y, delta x since last event, delta y since last event) + -- + -- Each subinput has a different event registry. + event = nil, + + -- Input state, independendant between each grab. Reset by :neutralize(). + _state = "none", -- none, pressed or released + _value = 0, -- input value + _prevValue = 0, -- value last frame + + -- Input state, shared between grabs. + _event = nil, -- Event group for all event binded by this input. + _sourceCache = {}, -- Map of the values currently taken by every source this input use. + _afterFilterEvent = nil, -- Event registry that resend the source events after applying the eventual filter function. + _boundSourceEvents = {}, -- Map of sources events that are binded (and thus will send events to _afterFilterEvent). + _joystick = nil, -- Currently selected joystick for this player. Also shared with children inputs. + + --- Update the input and its children. + -- Should be called every frame, typically _after_ you've done all your input handling + -- (otherwise `pressed` and `released` may never return true and `delta` might be wrong). + -- (Note: this should not be called on subinputs) + update = function(self) + self:_update() + self._prevValue = self._value + for _, i in ipairs(self.children) do + i:update() + end + end, + + --- Create a new input object based on this input `config` data. + clone = function(self) + return make_input(self.config) + end, + + --- Relond the input `config`, and do the same for its children. + -- This will reenable the input if it was disabled using `disable`. + reload = function(self) + -- clear all events we bounded previously + self._event:clear() + self._boundSourceEvents = {} + -- remove removed children + for i=#self.children, 1, -1 do + local c = self.children[i] + if not self.config[c.name] then + c:disable() + table.remove(self.children, i) + end + end + -- reload children + for _, c in ipairs(self.children) do + c:reload() + end + -- add added children + for subname, subt in pairs(self.config) do + if type(subname) == "string" and type(subt) == "table" and not rawget(self, subname) then + local c = make_input(subt) + c.name = subname + table.insert(self.children, c) + self.children[subname] = c + self[subname] = c + end + end + -- rebind source events + for _, exp in ipairs(self.config) do + -- extract args + local args = {} + if type(exp) == "table" then + for k, v in pairs(exp) do + if k ~= 1 then + args[k] = v + end + end + exp = exp[1] + end + -- build env + local env = {} + for k, v in pairs(args) do env[k] = v end + setmetatable(env, { + __index = function(t, key) + if key == "value" then return self:value() end + return self._sourceCache[key] or expressionEnv[key] + end + }) + -- extract sources + local sources = {} + local srcmt + srcmt = { -- metamethods of sources values during the scanning process + __add = zero, __sub = zero, + __mul = zero, __div = zero, + __mod = zero, __pow = zero, + __unm = zero, __idiv = zero, + __index = function(t, key) + local i = rawget(t, 1) + if i then sources[i][1] = sources[i][1] .. "." .. key + else table.insert(sources, { key }) + end + return setmetatable({ i or #sources }, srcmt) + end + } + local scanEnv = setmetatable({ value = 0 }, { __index = srcmt.__index }) -- value is not a source + for k, v in pairs(args) do scanEnv[k] = v end -- add args + for k in pairs(expressionEnv) do scanEnv[k] = zero end -- add functions + for _, mod in ipairs(sourceModifiers) do -- add modifiers functions + scanEnv[mod] = function(source) + assert(getmetatable(source) == srcmt, ("trying to apply %s modifier on a non-source value"):format(mod)) + sources[rawget(source, 1)][mod] = true + return source + end + end + loadexp(exp, scanEnv)() -- scan! + -- set every source to passive if there is a dt source + local hasDt = false + for _, s in ipairs(sources) do + if s[1] == "dt" then hasDt = true break end + end + if hasDt then + for _, s in ipairs(sources) do + if s[1] ~= "dt" and not s.active then + s.passive = true + end + end + end + -- setup function + local fn = loadexp(exp, env) + -- init sources and bind to source events + local boundAfterFilterEvent = {} + local function onAfterFilterEvent(new) self:_update(fn()) end + for _, s in ipairs(sources) do + local sname = s[1] + ensurePath(self._sourceCache, sname, 0) + if not self._boundSourceEvents[sname] then + if sname:match("^child%.") then + local cname = sname:match("^child%.(.*)$") + assert(self.children[cname], ("input expression refer to %s but this input has no child named %s"):format(sname, cname)) + self._event:bind(self.children[cname].event, "moved", function(new) -- child event -> self._afterFilterEvent link + setPath(self._sourceCache, sname, new) + self._afterFilterEvent:emit(sname, new) + end) + else + self._event:bind(event, sname, function(new, filter, ...) -- event source -> self._afterFilterEvent link + if filter then + new = filter(self, new, ...) + if not new then return end -- filtered out + end + setPath(self._sourceCache, sname, new) + self._afterFilterEvent:emit(sname, new) + end) + end + self._boundSourceEvents[sname] = true + end + if not boundAfterFilterEvent[sname] and not s.passive then + self._event:bind(self._afterFilterEvent, sname, onAfterFilterEvent) -- self._afterFilterEvent -> input update link + boundAfterFilterEvent[sname] = true + end + end + end + -- rebind pointer events + if self.config.horizontal and self.config.horizontal then + self._event:bind(self.horizontal.event, "moved", function(new, delta) self.event:emit("pointer moved", new, self.vertical:value(), delta, 0) end) + self._event:bind(self.vertical.event, "moved", function(new, delta) self.event:emit("pointer moved", self.horizontal:value(), new, 0, delta) end) + end + end, + --- Disable the input and its children, preventing further updates and events. + -- The input can be reenabled using `reload`. + disable = function(self) + for _, c in ipairs(self.children) do + c:disable() + end + self._event:clear() + end, + + --- Will call fn(source) on the next activated source (including sources not currently used by this input). + -- Typically used to detect an input in your game input binding settings. + -- @param fn function that will be called on the next activated source matching the filter + -- @param[opt] filter list of string patterns that sources must start with (example `{"button", "key"}` to only get buttons and key sources) + onNextActiveSource = function(self, fn, filter) + local function onevent(source, new, filterfn, ...) + if filter then + local ok = false + for _, f in ipairs(filter) do + if source:match("^"..f) then + ok = true + break + end + end + if not ok then return end + end + if filterfn then + new = filterfn(self, new, ...) + if new == nil then return end + end + if abs(new) >= self:_threshold() then + event:unbind("_active", onevent) + fn(source) + end + end + event:bind("_active", onevent) + end, + + --- Grab the input and its children input and returns the new subinput. + -- + -- A grabbed input will no longer update and instead pass all new update to the subinput. + -- This is typically used for contextual action or pause menus: by grabbing the player input, all the direct use of + -- this input in the game will stop (can't move caracter, ...) and instead you can use the subinput to handle input in the pause menu. + -- To stop grabbing an input, you will need to `:release` the subinput. + -- + -- This will also reset the input to a neutral state. The subinput will share everything with this input, except + -- `grabbed`, `grabbing`, `event` (a new event registry is created), and of course its current state. + grab = function(self) + local g = { + grabbed = false, + grabbing = self, + event = signal.new(), + children = {} + } + for _, c in ipairs(self.children) do + g[c.name] = c:grab() + table.insert(g.children, g[c.name]) + end + self:neutralize() + self.grabbed = setmetatable(g, { __index = self }) + return g + end, + --- Release a subinput and its children. + -- The parent grabbed input will be updated again. This subinput will be reset to a neutral position and won't be updated further. + release = function(self) + assert(self.grabbing, "not a grabbed input") + for _, c in ipairs(self.children) do + c:release() + end + self:neutralize() + self.grabbing.grabbed = false + self.grabbing = false + end, + + --- Set the state of this input to a neutral position (i.e. value = 0). + neutralize = function(self) + self:_update(0) + self._state = "none" + self._value = 0 + self._prevValue = 0 + end, + + --- Set the joystick associated with this input. + -- The input will ignore every other joystick. + -- Set joystick to `nil` to disable and get input from every connected joystick. + -- @param joystick LÖVE jostick object to associate + setJoystick = function(self, joystick) + self._joystick = joystick + for _, i in ipairs(self.children) do + i:setJoystick(joystick) + end + end, + --- Returns the currently selected joystick. + getJoystick = function(self) + return self._joystick + end, + + --- Returns true if the input is currently down. + down = function(self) + return self._state == "down" or self._state == "pressed" + end, + --- Returns true if the input has just been pressed. + pressed = function(self) + return self._state == "pressed" + end, + --- Returns true if the input has just been released. + released = function(self) + return self._state == "released" + end, + --- Returns the current value of the input. + value = function(self) + return self._value + end, + --- Returns the delta value of the input since the last call to `update`. + delta = function(self) + return self._value - self._prevValue + end, + --- If there is a horizontal and vertical children inputs, this returns the horizontal value and the vertical value. + -- Typically used for movement/axes pairs (e.g. to get x,y of a stick or directional pad). + pointer = function(self) + return self.horizontal:value(), self.vertical:value() + end, + --- Same as `pointer`, but normalize the returned vector, i.e. "clamp" the returned x,y coordinates into a circle of radius 1. + -- Typically used to avoid faster movement on diagonals + -- (as if both horizontal and vertical values are 1, the pointer vector has √2 magnitude, higher than the 1 magnitude of a purely vertical or horizontal movement). + clamped = function(self) + local x, y = self:pointer() + local mag = x*x + y*y + if mag > 1 then + local d = sqrt(mag) + return x/d, y/d + else + return x, y + end + end, + + -- Update the state of the input: called at least on every input value change and on :update(). + -- new: new value of the input if it has changed (number, can be anything, but typically in [0-1]) (optional) + _update = function(self, new) + if self.grabbed then + self.grabbed:_update(new) -- pass onto grabber + else + local threshold = self:_threshold() + -- update values + new = new or self._value + local old = self._value + self._value = new + -- update state and emit events + local delta = new - old + if delta ~= 0 then + self.event:emit("moved", new, delta) + end + if abs(new) >= threshold then + if abs(old) < threshold then + self._state = "pressed" + self.event:emit("pressed") + else + self._state = "down" + end + else + if abs(old) >= threshold then + self._state = "released" + self.event:emit("released") + else + self._state = "none" + end + end + end + end, + -- Returns the deadzone of the input. + _deadzone = function(self) + return self.config.deadzone or 0.05 + end, + -- Returns the threshold of the input. + _threshold = function(self) + return self.config.threshold or 0.05 + end, +} +input_mt.__index = input_mt + +return make_input diff --git a/input/love.lua b/input/love.lua deleted file mode 100644 index b6e66d9..0000000 --- a/input/love.lua +++ /dev/null @@ -1,379 +0,0 @@ -local input = require((...):gsub("love$", "input")) - -local loaded, signal = pcall(require, (...):match("^(.-)input").."signal") -if not loaded then signal = nil end - --- Config -- - --- Use ScanCodes (layout independant input) instead of KeyConstants (layout dependant) for keyboard input -local useScancodes = true --- If using ScanCodes, sets this to true so the backend returns the layout-dependant KeyConstant --- instead of the raw ScanCode when getting the display name. If set to false and using ScanCodes, --- the user will see keys that don't match what's actually written on his keyboard, which is confusing. -local displayKeyConstant = true - --- Setup -love.mouse.setVisible(false) - --- Button detection -local buttonsInUse = {} -local axesInUse = {} -function input.keypressed(key, scancode, isrepeat) - if useScancodes then key = scancode end - buttonsInUse["keyboard."..key] = true -end -function input.keyreleased(key, scancode) - if useScancodes then key = scancode end - buttonsInUse["keyboard."..key] = nil -end -function input.mousepressed(x, y, button, istouch) - buttonsInUse["mouse."..button] = true -end -function input.mousereleased(x, y, button, istouch) - buttonsInUse["mouse."..button] = nil -end -function input.wheelmoved(x, y) - if y > 0 then - buttonsInUse["mouse.wheel.up"] = true - elseif y < 0 then - buttonsInUse["mouse.wheel.down"] = true - end - if x > 0 then - buttonsInUse["mouse.wheel.right"] = true - elseif x < 0 then - buttonsInUse["mouse.wheel.left"] = true - end -end -function input.mousemoved(x, y, dx, dy) - if dx ~= 0 then axesInUse["mouse.move.x"] = dx/love.graphics.getWidth() end - if dy ~= 0 then axesInUse["mouse.move.y"] = dy/love.graphics.getHeight() end -end -function input.gamepadpressed(joystick, button) - buttonsInUse["gamepad.button."..joystick:getID().."."..button] = true -end -function input.gamepadreleased(joystick, button) - buttonsInUse["gamepad.button."..joystick:getID().."."..button] = nil -end -function input.gamepadaxis(joystick, axis, value) - if value ~= 0 then - axesInUse["gamepad.axis."..joystick:getID().."."..axis] = value - else - axesInUse["gamepad.axis."..joystick:getID().."."..axis] = nil - end -end - --- Windows size -input.getDrawWidth, input.getDrawHeight = love.graphics.getWidth, love.graphics.getHeight - --- Update -local oUpdate = input.update -input.update = function(dt) - -- love.wheelmoved doesn't trigger when the wheel stop moving, so we need to clear up our stuff at each update - buttonsInUse["mouse.wheel.up"] = nil - buttonsInUse["mouse.wheel.down"] = nil - buttonsInUse["mouse.wheel.right"] = nil - buttonsInUse["mouse.wheel.left"] = nil - -- Same for mouse axis - axesInUse["mouse.move.x"] = nil - axesInUse["mouse.move.y"] = nil - - oUpdate(dt) -end - -input.basicButtonDetector = function(id) - -- Keyboard - if id:match("^keyboard%.") then - local key = id:match("^keyboard%.(.+)$") - return function() - return useScancodes and love.keyboard.isScancodeDown(key) or love.keyboard.isDown(key) - end - -- Mouse wheel - elseif id:match("^mouse%.wheel%.") then - local key = id:match("^mouse%.wheel%.(.+)$") - return function() - return buttonsInUse["mouse.wheel."..key] - end - -- Mouse - elseif id:match("^mouse%.") then - local key = id:match("^mouse%.(.+)$") - return function() - return love.mouse.isDown(key) - end - -- Gamepad button - elseif id:match("^gamepad%.button%.") then - local gidkey = id:match("^gamepad%.button%.(.+)$") - local key = gidkey:match("([^.]+)$") - local gid = tonumber(gidkey:match("^(.+)%..+$")) - local gamepad - return function() - if not gamepad or not gamepad:isConnected() then - for _, j in ipairs(love.joystick.getJoysticks()) do - if (gid and j:getID() == gid) or j:isGamepad() then - gamepad = j - break - end - end - end - return gamepad and gamepad:isGamepadDown(key) - end - -- Gamepad axis - elseif id:match("^gamepad%.axis%.") then - local gidaxis, threshold = id:match("^gamepad%.axis%.(.+)%%(.+)$") - if not gidaxis then gidaxis = id:match("^gamepad%.axis%.(.+)$") end -- no threshold (=0.5) - local axis = gidaxis:match("([^.]+)$") - local gid = tonumber(gidaxis:match("^(.+)%..+$")) - threshold = tonumber(threshold) or 0.5 - local gamepad - return function() - if not gamepad or not gamepad:isConnected() then - for _, j in ipairs(love.joystick.getJoysticks()) do - if (gid and j:getID() == gid) or j:isGamepad() then - gamepad = j - break - end - end - end - if not gamepad or not gamepad:isConnected() then - return false - else - local val = gamepad:getGamepadAxis(axis) - return (math.abs(val) > math.abs(threshold)) and ((val < 0) == (threshold < 0)) - end - end - else - error("Unknown button identifier: "..id) - end -end - -input.basicAxisDetector = function(id) - -- Mouse movement - if id:match("^mouse%.move%.") then - local axis, threshold = id:match("^mouse%.move%.(.+)%%(.+)$") - if not axis then axis = id:match("^mouse%.move%.(.+)$") end -- no threshold (=0) - threshold = tonumber(threshold) or 0 - return function() - local val, raw, max = axesInUse["mouse.move."..axis] or 0, 0, 1 - if axis == "x" then - raw, max = val * love.graphics.getWidth(), love.graphics.getWidth() - elseif axis == "y" then - raw, max = val * love.graphics.getHeight(), love.graphics.getHeight() - end - return math.abs(val) > math.abs(threshold) and val or 0, raw, max - end - -- Mouse position - elseif id:match("^mouse%.position%.") then - local axis, threshold = id:match("^mouse%.position%.(.+)%%(.+)$") - if not axis then axis = id:match("^mouse%.position%.(.+)$") end -- no threshold (=0) - threshold = tonumber(threshold) or 0 - return function() - local val, raw, max = 0, 0, 1 - if axis == "x" then - max = love.graphics.getWidth() / 2 -- /2 because x=0,y=0 is the center of the screen (an axis value is in [-1,1]) - raw = love.mouse.getX() - max - elseif axis == "y" then - max = love.graphics.getHeight() / 2 - raw = love.mouse.getY() - max - end - val = raw / max - return math.abs(val) > math.abs(threshold) and val or 0, raw, max - end - -- Gamepad axis - elseif id:match("^gamepad%.axis%.") then - local gidaxis, threshold = id:match("^gamepad%.axis%.(.+)%%(.+)$") - if not gidaxis then gidaxis = id:match("^gamepad%.axis%.(.+)$") end -- no threshold (=0.1) - local axis = gidaxis:match("([^.]+)$") - local gid = tonumber(gidaxis:match("^(.+)%..+$")) - threshold = tonumber(threshold) or 0.1 - local gamepad - return function() - if not gamepad or not gamepad:isConnected() then - for _, j in ipairs(love.joystick.getJoysticks()) do - if (gid and j:getID() == gid) or j:isGamepad() then - gamepad = j - break - end - end - end - if not gamepad or not gamepad:isConnected() then - return 0 - else - local val = gamepad:getGamepadAxis(axis) - return math.abs(val) > math.abs(threshold) and val or 0 - end - end - else - error("Unknown axis identifier: "..id) - end -end - -input.buttonUsed = function(threshold) - local r = {} - threshold = threshold or 0.5 - for b in pairs(buttonsInUse) do - table.insert(r, b) - end - for b,v in pairs(axesInUse) do - if math.abs(v) > threshold then - table.insert(r, b.."%"..(v < 0 and -threshold or threshold)) - end - end - return unpack(r) -end - -input.axisUsed = function(threshold) - local r = {} - threshold = threshold or 0.5 - for b,v in pairs(axesInUse) do - if math.abs(v) > threshold then - table.insert(r, b.."%"..threshold) - end - end - return unpack(r) -end - -input.buttonName = function(...) - local ret = {} - for _,id in ipairs({...}) do - -- Keyboard - if id:match("^keyboard%.") then - local key = id:match("^keyboard%.(.+)$") - if useScancodes and displayKeyConstant then key = love.keyboard.getKeyFromScancode(key) end - table.insert(ret, key:sub(1,1):upper()..key:sub(2).." key") - -- Mouse wheel - elseif id:match("^mouse%.wheel%.") then - local key = id:match("^mouse%.wheel%.(.+)$") - table.insert(ret, "Mouse wheel "..key) - -- Mouse - elseif id:match("^mouse%.") then - local key = id:match("^mouse%.(.+)$") - table.insert(ret, "Mouse "..key) - -- Gamepad button - elseif id:match("^gamepad%.button%.") then - local gidkey = id:match("^gamepad%.button%.(.+)$") - local key = gidkey:match("([^.]+)$") - local gid = tonumber(gidkey:match("^(.+)%..+$")) - if gid then - table.insert(ret, "Gamepad "..gid.." button "..key) - else - table.insert(ret, "Gamepad button "..key) - end - -- Gamepad axis - elseif id:match("^gamepad%.axis%.") then - local gidaxis, threshold = id:match("^gamepad%.axis%.(.+)%%(.+)$") - if not gidaxis then gidaxis = id:match("^gamepad%.axis%.(.+)$") end -- no threshold (=0.5) - local axis = gidaxis:match("([^.]+)$") - local gid = tonumber(gidaxis:match("^(.+)%..+$")) - threshold = tonumber(threshold) or 0.5 - - local str - if gid then - str = "Gamepad "..gid - else - str = "Gamepad" - end - if axis == "rightx" then - str = str .. (" right stick %s (deadzone %s%%)"):format(threshold >= 0 and "right" or "left") - elseif axis == "righty" then - str = str .. (" right stick %s (deadzone %s%%)"):format(threshold >= 0 and "down" or "up") - elseif axis == "leftx" then - str = str .. (" left stick %s (deadzone %s%%)"):format(threshold >= 0 and "right" or "left") - elseif axis == "lefty" then - str = str .. (" left stick %s (deadzone %s%%)"):format(threshold >= 0 and "down" or "up") - else - str = str .. (" axis %s (deadzone %s%%)"):format(axis, math.abs(threshold*100)) - end - str = str .. (" (deadzone %s%%)"):format(math.abs(threshold*100)) - - table.insert(ret, str) - else - table.insert(ret, id) - end - end - return unpack(ret) -end - -input.axisName = function(...) - local ret = {} - for _,id in ipairs({...}) do - -- Binary axis - if id:match(".+%,.+") then - local b1, b2 = input.buttonName(id:match("^(.+)%,(.+)$")) - table.insert(ret, b1.." / "..b2) - -- Mouse movement - elseif id:match("^mouse%.move%.") then - local axis, threshold = id:match("^mouse%.move%.(.+)%%(.+)$") - if not axis then axis = id:match("^mouse%.move%.(.+)$") end -- no threshold (=0) - threshold = tonumber(threshold) or 0 - table.insert(ret, ("Mouse %s movement (threshold %s%%)"):format(axis, math.abs(threshold*100))) - -- Mouse position - elseif id:match("^mouse%.position%.") then - local axis, threshold = id:match("^mouse%.position%.(.+)%%(.+)$") - if not axis then axis = id:match("^mouse%.position%.(.+)$") end -- no threshold (=0) - threshold = tonumber(threshold) or 0 - table.insert(ret, ("Mouse %s position (threshold %s%%)"):format(axis, math.abs(threshold*100))) - -- Gamepad axis - elseif id:match("^gamepad%.axis%.") then - local gidaxis, threshold = id:match("^gamepad%.axis%.(.+)%%(.+)$") - if not gidaxis then gidaxis = id:match("^gamepad%.axis%.(.+)$") end -- no threshold (=0.1) - local axis = gidaxis:match("([^.]+)$") - local gid = tonumber(gidaxis:match("^(.+)%..+$")) - threshold = tonumber(threshold) or 0.1 - - local str - if gid then - str = "Gamepad "..gid - else - str = "Gamepad" - end - if axis == "rightx" then - str = str .. (" right stick %s (deadzone %s%%)"):format(threshold >= 0 and "right" or "left") - elseif axis == "righty" then - str = str .. (" right stick %s (deadzone %s%%)"):format(threshold >= 0 and "down" or "up") - elseif axis == "leftx" then - str = str .. (" left stick %s (deadzone %s%%)"):format(threshold >= 0 and "right" or "left") - elseif axis == "lefty" then - str = str .. (" left stick %s (deadzone %s%%)"):format(threshold >= 0 and "down" or "up") - else - str = str .. (" axis %s (deadzone %s%%)"):format(axis, math.abs(threshold*100)) - end - str = str .. (" (deadzone %s%%)"):format(math.abs(threshold*100)) - - table.insert(ret, str) - else - table.insert(ret, id) - end - end - return unpack(ret) -end - --- Default inputs. -input.default.pointer:bind( - { "absolute", { "keyboard.left", "keyboard.right" }, { "keyboard.up", "keyboard.down" } }, - { "absolute", { "keyboard.a", "keyboard.d" }, { "keyboard.w", "keyboard.s" } }, - { "absolute", "gamepad.axis.1.leftx", "gamepad.axis.1.lefty" }, - { "absolute", { "gamepad.button.1.dpleft", "gamepad.button.1.dpright" }, { "gamepad.button.1.dpup", "gamepad.button.1.dpdown" } } -) -input.default.confirm:bind( - "keyboard.return", "keyboard.space", "keyboard.lshift", "keyboard.e", - "gamepad.button.1.a" -) -input.default.cancel:bind( - "keyboard.escape", "keyboard.backspace", - "gamepad.button.1.b" -) - ---- Register signals -if signal then - signal.event:bind("keypressed", input.keypressed) - signal.event:bind("keyreleased", input.keyreleased) - signal.event:bind("mousepressed", input.mousepressed) - signal.event:bind("mousereleased", input.mousereleased) - signal.event:bind("wheelmoved", input.wheelmoved) - signal.event:bind("mousemoved", input.mousemoved) - signal.event:bind("gamepadpressed", input.gamepadpressed) - signal.event:bind("gamepadreleased", input.gamepadreleased) - signal.event:bind("gamepadaxis", input.gamepadaxis) - signal.event:replace("update", oUpdate, input.update) -end - -return input diff --git a/input/pointer.lua b/input/pointer.lua deleted file mode 100644 index d5e268a..0000000 --- a/input/pointer.lua +++ /dev/null @@ -1,246 +0,0 @@ -local input = require((...):gsub("pointer$", "input")) -local button_mt = require((...):gsub("pointer$", "button")) -local axis_mt = require((...):gsub("pointer$", "axis")) - -local sqrt = math.sqrt - ---- PointerInput methods -local pointer_mt -pointer_mt = { - -- Pointer inputs -- - -- Pointer inputs are container for two axes input, in order to represent a two-dimensionnal pointing device, e.g. a mouse or a stick. - -- Each pointer detector is a table with 3 fields: mode(string), XAxis(axis), YAxis(axis). mode can either be "relative" or "absolute". - -- In relative mode, the pointer will return the movement since last update (for example to move a mouse pointer with a stick). - -- In absolute mode, the pointer will return the pointer position directly deduced of the current axes position. - -- @tparam table{mode,XAxis,YAxis} ... couples of axis detectors, axis identifiers or axis input to add and in which mode - -- @tretrun PointerInput the object - _new = function(...) - local r = setmetatable({ - hijackStack = {}, -- hijackers stack, first element is the object currently hijacking this input - hijacking = nil, -- object currently hijacking this input - detectors = {}, -- pointers list (composite detectors) - valX = 0, valY = 0, -- pointer position - width = 1, height = 1, -- half-dimensions of the movement area - offsetX = 0, offsetY = 0, -- offsets - xSpeed = 1, ySpeed = 1, -- speed (pixels/milisecond); for relative mode - }, pointer_mt) - table.insert(r.hijackStack, r) - r.hijacking = r - r:bind(...) - r.horizontal = input.axis(function() - local h = r:x() - local width = r.width - return h/width, h, width - end) - r.vertical = input.axis(function() - local v = r:y() - local height = r.height - return v/height, v, height - end) - r.right, r.left = r.horizontal.positive, r.horizontal.negative - r.up, r.down = r.vertical.negative, r.vertical.positive - return r - end, - - --- Returns a new PointerInput with the same properties. - -- @treturn PointerInput the cloned object - clone = function(self) - return input.pointer(unpack(self.detectors)) - :dimensions(self.width, self.height) - :offset(self.offsetX, self.offsetY) - :speed(self.xSpeed, self.ySpeed) - end, - - --- Bind new axis couples to this input. - -- @tparam table{mode,XAxis,YAxis} ... couples of axis detectors, axis identifiers or axis input to add and in which mode - -- @treturn PointerInput this PointerInput object - bind = function(self, ...) - for _, p in ipairs({...}) do - if type(p) == "table" then - local h, v = p[2], p[3] - if getmetatable(h) ~= axis_mt then - h = input.axis(h) - end - if getmetatable(v) ~= axis_mt then - v = input.axis(v) - end - table.insert(self.detectors, { p[1], h, v }) - else - error("Pointer detector must be a table") - end - end - return self - end, - --- Unbind axis couple(s). - -- @tparam table{mode,XAxis,YAxis} ... couples of axis detectors, axis identifiers or axis input to remove - -- @treturn PointerInput this PointerInput object - unbind = button_mt.unbind, - --- Unbind all axis couple(s). - -- @treturn PointerInput this PointerInput object - clear = button_mt.clear, - - --- Hijacks the input. - -- This function returns a new input object which mirrors the current object, except it will hijack every new input. - -- This means any value change will only be visible to the new object; the pointer will always appear to be at offsetX,offsetY for the initial object. - -- An input can be hijacked several times; the one which hijacked it last will be the active one. - -- @treturn PointerInput the new input object which is hijacking the input - hijack = function(self) - local hijacked - hijacked = { - horizontal = input.axis(function() - local h = hijacked:x() - local width = hijacked.width - return h/width, h, width - end), - vertical = input.axis(function() - local v = hijacked:y() - local height = hijacked.height - return v/height, v, height - end) - } - hijacked.right, hijacked.left = hijacked.horizontal.positive, hijacked.horizontal.negative - hijacked.up, hijacked.down = hijacked.vertical.negative, hijacked.vertical.positive - setmetatable(hijacked, { __index = self, __newindex = self }) - table.insert(self.hijackStack, hijacked) - self.hijacking = hijacked - return hijacked - end, - --- Free the input that was hijacked by this object. - -- Input will be given back to the previous object. - -- @treturn PointerInput this PointerInput object - free = button_mt.free, - - --- Set the moving area half-dimensions. - -- Call without argument to use half the window dimensions. - -- It's the half dimensions because axes values goes from -1 to 1, so theses dimensions only - -- covers values from x=0,y=0 to x=1,y=1. The full moving area will be 4*newWidth*newHeight. - -- @tparam number newWidth new width - -- @tparam number newHeight new height - -- @treturn PointerInput this PointerInput object - dimensions = function(self, newWidth, newHeight) - self.width, self.height = newWidth, newHeight - return self - end, - --- Set the moving area coordinates offset. - -- The offset is a value automatically added to the x and y values when using the x() and y() methods. - -- Call without argument to automatically offset so 0,0 <= x(),y() <= width,height, i.e. offset to width,height. - -- @tparam number newOffX new X offset - -- @tparam number newOffY new Y offset - -- @treturn PointerInput this PointerInput object - offset = function(self, newOffX, newOffY) - self.offsetX, self.offsetY = newOffX, newOffY - return self - end, - --- Set maximal speed (pixels-per-milisecond) - -- Only used in relative mode. - -- Calls without argument to use the raw data and don't apply a speed modifier. - -- @tparam number newXSpeed new X speed - -- @tparam number newYSpeed new Y speed - -- @treturn PointerInput this PointerInput object - speed = function(self, newXSpeed, newYSpeed) - self.xSpeed, self.ySpeed = newXSpeed, newYSpeed or newXSpeed - return self - end, - - --- Returns the current X value of the pointer. - -- @treturn number X value - x = function(self) - if self.hijacking == self then - self:update() - return self.valX + (self.offsetX or self.width or input.getDrawWidth()/2) - else - return self.offsetX or self.width or input.getDrawWidth()/2 - end - end, - --- Returns the current Y value of the pointer. - -- @treturn number Y value - y = function(self) - if self.hijacking == self then - self:update() - return self.valY + (self.offsetY or self.height or input.getDrawHeight()/2) - else - return self.offsetY or self.height or input.getDrawHeight()/2 - end - end, - - --- Returns the X and Y value of the pointer, clamped. - -- They are clamped to stay in the ellipse touching all 4 sides of the dimension rectangle, i.e. the - -- (x,y) vector's magnitude reached its maximum either in (0,height) or (width,0). - -- Typically, this is used with square dimensions for player movements: when moving diagonally, the magnitude - -- will be the same as when moving horiontally or vertically, thus avoiding faster diagonal movement, A.K.A. "straferunning". - -- If you're not conviced by my overly complicated explanation: just use this to retrieve x and y for movement and everything - -- will be fine. - -- @treturn number X value - -- @treturn number Y value - clamped = function(self) - local width, height = self.width, self.height - if self.hijacking == self then - self:update() - local x, y = self.valX, self.valY - local cx, cy = x, y - local normalizedMagnitude = (x*x)/(width*width) + (y*y)/(height*height) -- go back to a unit circle - if normalizedMagnitude > 1 then - local magnitude = sqrt(x*x + y*y) - cx, cy = cx / magnitude * width, cy / magnitude * height - end - return cx + (self.offsetX or width or input.getDrawWidth()/2), cy + (self.offsetY or height or input.getDrawHeight()/2) - else - return self.offsetX or width or input.getDrawWidth()/2, self.offsetY or height or input.getDrawHeight()/2 - end - end, - - --- The associated horizontal axis. - horizontal = nil, - --- The associated vertical axis. - vertical = nil, - - --- The associated button pressed when the pointer goes to the right. - right = nil, - --- The associated button pressed when the pointer goes to the left. - left = nil, - --- The associated button pressed when the pointer points up. - up = nil, - --- The associated button pressed when the pointer points down. - down = nil, - - --- Update pointer state. - -- Automatically called, don't call unless you know what you're doing. - update = function(self) - if not input.updated[self] then - local x, y = self.valX, self.valY - local xSpeed, ySpeed = self.xSpeed, self.ySpeed - local width, height = self.width or input.getDrawWidth()/2, self.height or input.getDrawHeight()/2 - local newX, newY = x, y - local maxMovX, maxMovY = 0, 0 -- the maxium axis movement in a direction (used to determine which axes have the priority) (absolute value) - for _, pointer in ipairs(self.detectors) do - local mode, xAxis, yAxis = unpack(pointer) - if mode == "relative" then - local movX, movY = math.abs(xAxis:value()), math.abs(yAxis:value()) - if movX > maxMovX then - newX = x + (xSpeed and (xAxis:value() * xSpeed * input.dt) or xAxis:raw()) - maxMovX = movX - end - if movY > maxMovY then - newY = y + (ySpeed and (yAxis:value() * ySpeed * input.dt) or yAxis:raw()) - maxMovY = movY - end - elseif mode == "absolute" then - local movX, movY = math.abs(xAxis:delta()), math.abs(yAxis:delta()) - if movX > maxMovX then - newX = xAxis:value() * width - maxMovX = movX - end - if movY > maxMovY then - newY = yAxis:value() * height - maxMovY = movY - end - end - end - self.valX, self.valY = math.min(math.abs(newX), width) * (newX < 0 and -1 or 1), math.min(math.abs(newY), height) * (newY < 0 and -1 or 1) - input.updated[self] = true - end - end -} -pointer_mt.__index = pointer_mt - -return pointer_mt