1
0
Fork 0
mirror of https://github.com/Reuh/ubiquitousse.git synced 2025-10-27 17:19:31 +00:00
ubiquitousse/input/input.lua
Étienne Reuh Fildadut 21679dde5c input overhaul
Now event based! Should result in no skipped inputs.
2022-09-16 20:06:00 +09:00

489 lines
16 KiB
Lua

--- Input management facilities.
--
-- The module returns a single function, `input`.
--
-- **Requires** ubiquitousse.signal.
-- @module input
-- @usage
-- TODO
local signal = require((...):gsub("input%.input$", "signal"))
local event = require((...):gsub("input$", "event"))
local abs, sqrt, floor, ceil, min, max = math.abs, math.sqrt, math.floor, math.ceil, math.min, math.max
-- TODO:
-- friendly name for sources
-- write doc, incl how to define your own source and source expressions, default inputs
-- Always returns 0.
local function zero() return 0 end
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
return fn
end
-- 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
-- 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