1
0
Fork 0
mirror of https://github.com/Reuh/ubiquitousse.git synced 2025-10-27 17:19:31 +00:00

Code reorganization, added uqt.ecs, removed LÖVE duplicates (uqt.audio, uqt.draw, uqt.filesystem)

This commit is contained in:
Étienne Fildadut 2019-12-24 19:05:50 +01:00
parent 523c5d36c0
commit 16e533d176
28 changed files with 2544 additions and 2107 deletions

269
input/backend/ctrulua.lua Normal file
View file

@ -0,0 +1,269 @@
local input = require((...):match("^(.-%.)backend").."input")
local gfx = require("ctr.gfx")
local hid = require("ctr.hid")
local keys = {}
local touchX, touchY, dTouchX, dTouchY
local oUpdate = input.update
input.update = function(dt)
hid.read()
keys = hid.keys()
local nTouchX, nTouchY = hid.touch()
dTouchX, dTouchY = nTouchX - touchX, nTouchY - touchY
touchX, touchY = nTouchX, nTouchY
oUpdate(dt)
end
input.buttonDetector = function(...)
local ret = {}
for _,id in ipairs({...}) do
-- Keys
if id:match("^key%.") then
local key = id:match("^key%.(.+)$")
table.insert(ret, function()
return keys.held[key]
end)
else
error("Unknown button identifier: "..id)
end
end
return table.unpack(ret)
end
input.axisDetector = function(...)
local ret = {}
for _,id in ipairs({...}) do
-- Binary axis
if id:match(".+%,.+") then
local d1, d2 = input.buttonDetector(id:match("^(.+)%,(.+)$"))
table.insert(ret, function()
local b1, b2 = d1(), d2()
if b1 and b2 then return 0
elseif b1 then return -1
elseif b2 then return 1
else return 0 end
end)
-- Touch movement
elseif id:match("^touch%.move%.") then
local axis, threshold = id:match("^touch%.move%.(.+)%%(.+)$")
if not axis then axis = id:match("^touch%.move%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0
table.insert(ret, function()
local val, raw, max
if axis == "x" then
raw, max = dTouchX, gfx.BOTTOM_WIDTH
elseif axis == "y" then
raw, max = dTouchY, gfx.BOTTOM_HEIGHT
end
val = raw / max
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
end)
-- Touch position
elseif id:match("^touch%.position%.") then
local axis, threshold = id:match("^touch%.position%.(.+)%%(.+)$")
if not axis then axis = id:match("^touch%.position%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0
table.insert(ret, function()
local val, raw, max
if axis == "x" then
max = gfx.BOTTOM_WIDTH / 2 -- /2 because x=0,y=0 is the center of the screen (an axis value is in [-1,1])
raw = touchX - max
elseif axis == "y" then
max = gfx.BOTTOM_HEIGHT / 2
raw = touchY - max
end
val = raw / max
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
end)
-- Circle pad axis
elseif id:match("^circle%.") then
local axis, threshold = id:match("^circle%.(.+)%%(.+)$")
if not axis then axis = id:match("^circle%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0
table.insert(ret, function()
local x, y = hid.circle()
local val, raw, max = 0, 0, 156
if axis == "x" then raw = x
elseif axis == "y" then raw = y end
val = raw / max
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
end)
-- C-Stick axis
elseif id:match("^cstick%.") then
local axis, threshold = id:match("^cstick%.(.+)%%(.+)$")
if not axis then axis = id:match("^cstick%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0
table.insert(ret, function()
local x, y = hid.cstick()
local val, raw, max = 0, 0, 146
if axis == "x" then raw = x
elseif axis == "y" then raw = y end
val = raw / max
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
end)
-- Accelerometer axis
elseif id:match("^accel%.") then
local axis, threshold = id:match("^accel%.(.+)%%(.+)$")
if not axis then axis = id:match("^accel%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0
table.insert(ret, function()
local x, y, z = hid.accel()
local val, raw, max = 0, 0, 32768 -- no idea actually, but it's a s16
if axis == "x" then raw = x
elseif axis == "y" then raw = y
elseif axis == "z" then raw = z end
val = raw / max
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
end)
-- Gyroscope axis
elseif id:match("^gyro%.") then
local axis, threshold = id:match("^gyro%.(.+)%%(.+)$")
if not axis then axis = id:match("^gyro%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0
table.insert(ret, function()
local roll, pitch, yaw = hid.gyro()
local val, raw, max = 0, 0, 32768 -- no idea actually, but it's a s16
if axis == "roll" then raw = roll
elseif axis == "pitch" then raw = pitch
elseif axis == "yaw" then raw = yaw end
val = raw / max
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
end)
else
error("Unknown axis identifier: "..id)
end
end
return table.unpack(ret)
end
input.buttonsInUse = function(threshold)
local r = {}
for key, held in pairs(keys.held) do
if held then table.insert(r, "key."..key) end
end
return r
end
input.axesInUse = function(threshold)
local r = {}
threshold = threshold or 0.5
if math.abs(touchX) / gfx.BOTTOM_WIDTH > threshold then table.insert(r, "touch.position.x%"..threshold) end
if math.abs(touchY) / gfx.BOTTOM_HEIGHT > threshold then table.insert(r, "touch.position.y%"..threshold) end
if math.abs(dTouchX) / gfx.BOTTOM_WIDTH > threshold then table.insert(r, "touch.move.x%"..threshold) end
if math.abs(dTouchY) / gfx.BOTTOM_HEIGHT > threshold then table.insert(r, "touch.move.y%"..threshold) end
local circleX, circleY = hid.circle()
if math.abs(circleX) / 156 > threshold then table.insert(r, "circle.x%"..threshold) end
if math.abs(circleY) / 156 > threshold then table.insert(r, "circle.y%"..threshold) end
if ctr.apt.isNew3DS() then
local cstickX, cstickY = hid.cstick()
if math.abs(cstickY) / 146 > threshold then table.insert(r, "cstick.y%"..threshold) end
if math.abs(cstickX) / 146 > threshold then table.insert(r, "cstick.x%"..threshold) end
end
local accelX, accelY, accelZ = hid.accel()
if math.abs(accelX) / 32768 > threshold then table.insert(r, "accel.x%"..threshold) end
if math.abs(accelY) / 32768 > threshold then table.insert(r, "accel.y%"..threshold) end
if math.abs(accelZ) / 32768 > threshold then table.insert(r, "accel.z%"..threshold) end
-- no gyro, because it is always in use
return r
end
input.buttonName = function(...)
local ret = {}
for _,id in ipairs({...}) do
-- Key
if id:match("^key%.") then
local key = id:match("^key%.(.+)$")
table.insert(ret, key:sub(1,1):upper()..key:sub(2).." key")
else
table.insert(ret, id)
end
end
return table.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)
-- Touch movement
elseif id:match("^touch%.move%.") then
local axis, threshold = id:match("^touch%.move%.(.+)%%(.+)$")
if not axis then axis = id:match("^touch%.move%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0
table.insert(ret, ("Touch %s movement (threshold %s%%)"):format(axis, math.abs(threshold*100)))
-- Touch position
elseif id:match("^touch%.position%.") then
local axis, threshold = id:match("^touch%.position%.(.+)%%(.+)$")
if not axis then axis = id:match("^touch%.position%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0
table.insert(ret, ("Touch %s position (threshold %s%%)"):format(axis, math.abs(threshold*100)))
-- Circle pad axis
elseif id:match("^circle%.") then
local axis, threshold = id:match("^circle%.(.+)%%(.+)$")
if not axis then axis = id:match("^circle%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0
if axis == "x" then
table.insert(ret, ("Circle pad horizontal axis (deadzone %s%%)"):format(math.abs(threshold*100)))
elseif axis == "y" then
table.insert(ret, ("Circle pad vertical axis (deadzone %s%%)"):format(math.abs(threshold*100)))
else
table.insert(ret, ("Circle pad %s axis (deadzone %s%%)"):format(axis, math.abs(threshold*100)))
end
-- C-Stick axis
elseif id:match("^cstick%.") then
local axis, threshold = id:match("^cstick%.(.+)%%(.+)$")
if not axis then axis = id:match("^cstick%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0
if axis == "x" then
table.insert(ret, ("C-Stick horizontal axis (deadzone %s%%)"):format(math.abs(threshold*100)))
elseif axis == "y" then
table.insert(ret, ("C-Stick vertical axis (deadzone %s%%)"):format(math.abs(threshold*100)))
else
table.insert(ret, ("C-Stick %s axis (deadzone %s%%)"):format(axis, math.abs(threshold*100)))
end
-- Accelerometer axis
elseif id:match("^accel%.") then
local axis, threshold = id:match("^accel%.(.+)%%(.+)$")
if not axis then axis = id:match("^accel%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0
table.insert(ret, ("Accelerometer %s axis (deadzone %s%%)"):format(axis, math.abs(threshold*100)))
-- Gyroscope axis
elseif id:match("^gyro%.") then
local axis, threshold = id:match("^gyro%.(.+)%%(.+)$")
if not axis then axis = id:match("^gyro%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0
table.insert(ret, ("Gyroscope %s axis (deadzone %s%%)"):format(axis, math.abs(threshold*100)))
else
table.insert(ret, id)
end
end
return table.unpack(ret)
end
-- Size
input.screenWidth, input.screenHeight = gfx.TOP_WIDTH, gfx.TOP_HEIGHT
-- Defaults
input.default.pointer:bind(
{ "absolute", "key.left,key.right", "key.up,key.down" },
{ "absolute", "circle.x", "circle.y" }
)
input.default.confirm:bind("key.a")
input.default.cancel:bind("key.b")
return input

319
input/backend/love.lua Normal file
View file

@ -0,0 +1,319 @@
local input = require((...):match("^(.-%.)backend").."input")
-- 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
-- FIXME love callbacks do something cleaner
local buttonsInUse = {}
local axesInUse = {}
function love.keypressed(key, scancode, isrepeat)
if useScancodes then key = scancode end
buttonsInUse["keyboard."..key] = true
end
function love.keyreleased(key, scancode)
if useScancodes then key = scancode end
buttonsInUse["keyboard."..key] = nil
end
function love.mousepressed(x, y, button, istouch)
buttonsInUse["mouse."..button] = true
end
function love.mousereleased(x, y, button, istouch)
buttonsInUse["mouse."..button] = nil
end
function love.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 love.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 love.gamepadpressed(joystick, button)
buttonsInUse["gamepad.button."..joystick:getID().."."..button] = true
end
function love.gamepadreleased(joystick, button)
buttonsInUse["gamepad.button."..joystick:getID().."."..button] = nil
end
function love.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.drawWidth, input.drawHeight = love.graphics.getWidth(), love.graphics.getHeight()
function love.resize(width, height)
input.drawWidth = width
input.drawHeight = height
end
-- 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 gid, key = id:match("^gamepad%.button%.(.+)%.(.+)$")
gid = tonumber(gid)
return function()
local gamepad
for _,j in ipairs(love.joystick.getJoysticks()) do
if j:getID() == gid then gamepad = j end
end
return gamepad and gamepad:isGamepadDown(key)
end
-- Gamepad axis
elseif id:match("^gamepad%.axis%.") then
local gid, axis, threshold = id:match("^gamepad%.axis%.(.+)%.(.+)%%(.+)$")
if not gid then gid, axis = id:match("^gamepad%.axis%.(.+)%.(.+)$") end -- no threshold (=0)
gid = tonumber(gid)
threshold = tonumber(threshold) or 0.1
return function()
local gamepad
for _,j in ipairs(love.joystick.getJoysticks()) do
if j:getID() == gid then gamepad = j end
end
if not gamepad 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 gid, axis, threshold = id:match("^gamepad%.axis%.(.+)%.(.+)%%(.+)$")
if not gid then gid, axis = id:match("^gamepad%.axis%.(.+)%.(.+)$") end -- no threshold (=0)
gid = tonumber(gid)
threshold = tonumber(threshold) or 0.1
return function()
local gamepad
for _,j in ipairs(love.joystick.getJoysticks()) do
if j:getID() == gid then gamepad = j end
end
if not gamepad 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.buttonsInUse = 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 r
end
input.axesInUse = 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 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 gid, key = id:match("^gamepad%.button%.(.+)%.(.+)$")
table.insert(ret, "Gamepad "..gid.." button "..key)
-- Gamepad axis
elseif id:match("^gamepad%.axis%.") then
local gid, axis, threshold = id:match("^gamepad%.axis%.(.+)%.(.+)%%(.+)$")
if not gid then gid, axis = id:match("^gamepad%.axis%.(.+)%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0.1
if axis == "rightx" then
table.insert(ret, ("Gamepad %s right stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "right" or "left", math.abs(threshold*100)))
elseif axis == "righty" then
table.insert(ret, ("Gamepad %s right stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "down" or "up", math.abs(threshold*100)))
elseif axis == "leftx" then
table.insert(ret, ("Gamepad %s left stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "right" or "left", math.abs(threshold*100)))
elseif axis == "lefty" then
table.insert(ret, ("Gamepad %s left stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "down" or "up", math.abs(threshold*100)))
else
table.insert(ret, ("Gamepad %s axis %s (deadzone %s%%)"):format(gid, axis, math.abs(threshold*100)))
end
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 gid, axis, threshold = id:match("^gamepad%.axis%.(.+)%.(.+)%%(.+)$")
if not gid then gid, axis = id:match("^gamepad%.axis%.(.+)%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0.1
if axis == "rightx" then
table.insert(ret, ("Gamepad %s right stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "right" or "left", math.abs(threshold*100)))
elseif axis == "righty" then
table.insert(ret, ("Gamepad %s right stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "down" or "up", math.abs(threshold*100)))
elseif axis == "leftx" then
table.insert(ret, ("Gamepad %s left stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "right" or "left", math.abs(threshold*100)))
elseif axis == "lefty" then
table.insert(ret, ("Gamepad %s left stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "down" or "up", math.abs(threshold*100)))
else
table.insert(ret, ("Gamepad %s axis %s (deadzone %s%%)"):format(gid, axis, math.abs(threshold*100)))
end
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"
)
return input

14
input/init.lua Normal file
View file

@ -0,0 +1,14 @@
local input
local p = ...
if love then
input = require(p..".backend.love")
elseif package.loaded["ctr"] then
input = require(p..".backend.ctrulua")
elseif package.loaded["libretro"] then
error("NYI")
else
error("no backend for ubiquitousse.input")
end
return input

711
input/input.lua Normal file
View file

@ -0,0 +1,711 @@
--- ubiquitousse.input
-- Depends on a backend.
-- 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 input
local sqrt = math.sqrt
local unpack = table.unpack or unpack
local dt = 0
--- 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.
local updated = {}
--- ButtonInput methods
-- @impl ubiquitousse
local button_mt = {
--- 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, detector in ipairs(self.detectors) do
if detector == 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,
--- Grabs the input.
-- This function returns a new input object which mirrors the current object, except it will grab 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 grab relevant inputs while it is open, so they don't trigger any action in the rest of the game.
-- An input can be grabbed several times; the one which grabbed it last will be the active one.
-- @treturn ButtonInput the new input object which is grabbing the input
grab = function(self)
local grabbed = setmetatable({}, { __index = self, __newindex = self })
table.insert(self.grabStack, grabbed)
self.grabbing = grabbed
return grabbed
end,
--- Release the input that was grabbed by this object.
-- Input will be given back to the previous object.
-- @treturn ButtonInput this ButtonInput object
release = function(self)
local grabStack = self.grabStack
for i, v in ipairs(grabStack) do
if v == self then
table.remove(grabStack, i)
self.grabbing = grabStack[#grabStack]
return self
end
end
error("This object is currently not grabbing 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.grabbing == 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.grabbing == 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.grabbing == 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.
-- @impl ubiquitousse
update = function(self)
if not 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
updated[self] = true
end
end
}
button_mt.__index = button_mt
--- AxisInput methods
-- @impl ubiquitousse
local axis_mt = {
--- 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)
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,
--- Grabs the input.
-- This function returns a new input object which mirrors the current object, except it will grab 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 grabbed several times; the one which grabbed it last will be the active one.
-- @treturn AxisInput the new input object which is grabbing the input
grab = function(self)
local grabbed
grabbed = setmetatable({
positive = input.button(function() return grabbed:value() > 0 end),
negative = input.button(function() return grabbed:value() < 0 end)
}, { __index = self, __newindex = self })
table.insert(self.grabStack, grabbed)
self.grabbing = grabbed
return grabbed
end,
--- Release the input that was grabbed by this object.
-- Input will be given back to the previous object.
-- @treturn AxisInput this AxisInput object
release = button_mt.release,
--- Sets the default detection threshold (deadzone).
-- @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.grabbing == 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.grabbing == 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.grabbing == 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,
--- 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.
-- @impl ubiquitousse
update = function(self)
if not 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
updated[self] = true
end
end
}
axis_mt.__index = axis_mt
--- PointerInput methods
-- @impl ubiquitousse
local pointer_mt = {
--- 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,
--- Grabs the input.
-- This function returns a new input object which mirrors the current object, except it will grab 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 grabbed several times; the one which grabbed it last will be the active one.
-- @treturn PointerInput the new input object which is grabbing the input
grab = function(self)
local grabbed
grabbed = {
horizontal = input.axis(function()
local h = grabbed:x()
local width = grabbed.width
return h/width, h, width
end),
vertical = input.axis(function()
local v = grabbed:y()
local height = grabbed.height
return v/height, v, height
end)
}
grabbed.right, grabbed.left = grabbed.horizontal.positive, grabbed.horizontal.negative
grabbed.up, grabbed.down = grabbed.vertical.negative, grabbed.vertical.positive
setmetatable(grabbed, { __index = self, __newindex = self })
table.insert(self.grabStack, grabbed)
self.grabbing = grabbed
return grabbed
end,
--- Release the input that was grabbed by this object.
-- Input will be given back to the previous object.
-- @treturn PointerInput this PointerInput object
release = button_mt.release,
--- 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.grabbing == self then
self:update()
return self.valX + (self.offsetX or self.width or input.drawWidth/2)
else
return self.offsetX or self.width or input.drawWidth/2
end
end,
--- Returns the current Y value of the pointer.
-- @treturn number Y value
y = function(self)
if self.grabbing == self then
self:update()
return self.valY + (self.offsetY or self.height or input.drawHeight/2)
else
return self.offsetY or self.height or input.drawHeight/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.grabbing == 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.drawWidth/2), cy + (self.offsetY or height or input.drawHeight/2)
else
return self.offsetX or width or input.drawWidth/2, self.offsetY or height or input.drawHeight/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.
-- @impl ubiquitousse
update = function(self)
if not updated[self] then
local x, y = self.valX, self.valY
local xSpeed, ySpeed = self.xSpeed, self.ySpeed
local width, height = self.width or input.drawWidth/2, self.height or input.drawHeight/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 * dt) or xAxis:raw())
maxMovX = movX
end
if movY > maxMovY then
newY = y + (ySpeed and (yAxis:value() * ySpeed * 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)
updated[self] = true
end
end
}
pointer_mt.__index = pointer_mt
--- 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).
input = {
---------------------------------
--- 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).
--- 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
-- @impl backend
basicButtonDetector = function(str) end,
--- Make a new button detector from a detector function of string.
-- @tparam string, function button identifier
-- @impl ubiquitousse
buttonDetector = function(obj)
if type(obj) == "function" then
return obj
elseif type(obj) == "string" then
return input.basicButtonDetector(obj)
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
-- @impl backend
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
-- @impl ubiquitousse
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,
------------------------------------------
--- Inputs (the thing you want to use) ---
------------------------------------------
-- 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
-- @impl ubiquitousse
button = function(...)
local r = setmetatable({
grabStack = {}, -- grabbers stack, last element is the object currently grabbing this input
grabbing = nil, -- object currently grabbing this input
detectors = {}, -- detectors list
state = "none" -- current state (none, pressed, down, released)
}, button_mt)
table.insert(r.grabStack, r)
r.grabbing = r
r:bind(...)
return r
end,
-- 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
-- @impl ubiquitousse
axis = function(...)
local r = setmetatable({
grabStack = {}, -- grabbers stack, last element is the object currently grabbing this input
grabbing = nil, -- object currently grabbing 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
}, axis_mt)
table.insert(r.grabStack, r)
r.grabbing = r
r:bind(...)
r.positive = input.button(function() return r:value() > 0 end)
r.negative = input.button(function() return r:value() < 0 end)
return r
end,
-- 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
-- @impl ubiquitousse
pointer = function(...)
local r = setmetatable({
grabStack = {}, -- grabbers stack, first element is the object currently grabbing this input
grabbing = nil, -- object currently grabbing 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.grabStack, r)
r.grabbing = 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,
------------------------------
--- 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.
-- @treturn table<string> buttons identifiers list
-- @treturn[opt=0.5] number threshold the threshold to detect axes as button
-- @impl backend
buttonsInUse = function(threshold) end,
--- Returns a list of the axes currently in use, identified by their string axis identifier
-- @treturn table<string> axes identifiers list
-- @treturn[opt=0.5] number threshold the threshold to detect axes
-- @impl backend
axesInUse = 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
-- @impl backend
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
-- @impl backend
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.
-- @impl mixed
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.
},
--- Draw area dimensions.
-- Used for pointers.
-- @impl backend
drawWidth = 1,
drawHeight = 1,
--- Update all the Inputs.
-- Should be called at every game update; called by ubiquitousse.update.
-- The backend can hook into this function to to its input-related updates.
-- @tparam numder dt the delta-time
-- @impl ubiquitousse
update = function(newDt)
dt = newDt
updated = {}
end
}
-- Create default inputs
input.default.pointer = input.pointer()
input.default.confirm = input.button()
input.default.cancel = input.button()
return input