mirror of
https://github.com/Reuh/ubiquitousse.git
synced 2025-10-27 09:09:30 +00:00
input overhaul
Now event based! Should result in no skipped inputs.
This commit is contained in:
parent
77ece0b9a6
commit
21679dde5c
7 changed files with 599 additions and 1139 deletions
162
input/axis.lua
162
input/axis.lua
|
|
@ -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
|
||||
156
input/button.lua
156
input/button.lua
|
|
@ -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
|
||||
16
input/default.lua
Normal file
16
input/default.lua
Normal file
|
|
@ -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" },
|
||||
}
|
||||
103
input/event.lua
Normal file
103
input/event.lua
Normal file
|
|
@ -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
|
||||
676
input/input.lua
676
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
|
||||
|
|
|
|||
379
input/love.lua
379
input/love.lua
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue