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

Remove backend system and ctruLua support

Since I only use the LÖVE backend anyway, this simplifies the code.
Tidied some code.
This commit is contained in:
Étienne Fildadut 2021-07-18 19:30:43 +02:00
parent 9f4c03a136
commit 4b75f21e52
17 changed files with 663 additions and 1067 deletions

View file

@ -16,7 +16,6 @@ local asset_mt = {
-- @tparam assetName string the asset's full name
-- @tparam ... number/string other arguments for the asset loader
-- @return the asset
-- @impl ubiquitousse
__call = function(self, assetName, ...)
local cache = self.cache
local hash = table.concat({assetName, ...}, ".")
@ -37,7 +36,6 @@ local asset_mt = {
end,
--- Preload a list of assets.
-- @impl ubiquitousse
load = function(self, list)
for _, asset in ipairs(list) do
self(asset)
@ -46,7 +44,6 @@ local asset_mt = {
--- Allow loaded assets to be garbage collected.
-- Only useful if the caching mode is set to "manual" duritng creation.
-- @impl ubiquitousse
clear = function(self)
self.cache = {}
end
@ -61,7 +58,6 @@ local asset = {
-- @tparam directory string the directory in which the assets will be loaded
-- @tparam loaders table loaders table: {prefix = function, ...}
-- @tparam mode string[opt="auto"] caching mode
-- @impl ubiquitousse
new = function(dir, loaders, mode)
local cache = {}
if mode == nil or mode == "auto" then
@ -69,15 +65,12 @@ local asset = {
end
return setmetatable({
--- A prefix for asset names
-- @impl ubiquitousse
prefix = dir..".",
--- The asset cache. Each cached asset is indexed with a string key "type.assetName".
-- @impl ubiquitousse
cache = cache,
--- The loaders table.
-- @impl ubiquitousse
loaders = loaders
}, asset_mt)
end

View file

@ -1,31 +0,0 @@
local uqt = require((...):match("^(.-ubiquitousse)%."))
local ctr = require("ctr")
local gfx = require("ctr.gfx")
local madeForCtr = "v1.0"
local madeForUqt = "0.0.1"
-- Check versions
local txt = ""
if ctr.version ~= madeForCtr then
txt = txt .. ("Ubiquitousse ctrµLua backend was made for ctrµLua %s but %s is used!\n")
:format(madeForCtr, uqt.version)
end
if uqt.version ~= madeForUqt then
txt = txt .. ("Ubiquitousse ctrµLua backend was made for Ubiquitousse %s but %s is used!\n")
:format(madeForUqt, uqt.version)
end
-- Show warnings
if txt ~= "" then
txt = txt .. "Things may not work as expected.\n"
print(txt)
for _=0,300 do
gfx.start(gfx.TOP)
gfx.wrappedText(0, 0, txt, gfx.TOP_WIDTH)
gfx.stop()
gfx.render()
end
end

View file

@ -1,30 +0,0 @@
local uqt = require((...):match("^(.-ubiquitousse)%."))
local madeForLove = { 11, "x", "x" }
local madeForUqt = "0.0.1"
-- Check versions
local txt = ""
local actualLove = { love.getVersion() }
for i, v in ipairs(madeForLove) do
if v ~= "x" then
if actualLove[i] ~= v then
txt = txt .. ("Ubiquitousse Löve backend was made for LÖVE %s.%s.%s but %s.%s.%s is used!\n")
:format(madeForLove[1], madeForLove[2], madeForLove[3], actualLove[1], actualLove[2], actualLove[3])
break
end
end
end
if uqt.version ~= madeForUqt then
txt = txt .. ("Ubiquitousse Löve backend was made for Ubiquitousse %s but %s is used!\n")
:format(madeForUqt, uqt.version)
end
-- Show warnings
if txt ~= "" then
txt = txt .. "Things may not work as expected.\n"
print(txt)
love.window.showMessageBox("Compatibility warning", txt, "warning")
end

View file

@ -422,7 +422,6 @@ end
ecs = {
--- Create and returns a world system based on a list of systems.
-- The systems will be instancied for this world.
-- @impl ubiquitousse
world = (...)
let world = setmetatable({
filter = ecs.all(),
@ -436,7 +435,6 @@ ecs = {
end,
--- Returns a filter that returns true if, for every argument, a field with the same name exists in the entity.
-- @impl ubiquitousse
all = (...)
if ... then
let l = {...}
@ -454,7 +452,6 @@ ecs = {
end,
--- Returns a filter that returns true if one of the arguments if the name of a field in the entity.
-- @impl ubiquitousse
any = (...)
if ... then
let l = {...}
@ -472,7 +469,6 @@ ecs = {
end,
--- If uqt.scene is available, returns a new scene that will consist of a ECS world with the specified systems and entities.
-- @impl ubiquitousse
scene = (name, systems={}, entities={})
assert(scene, "ubiquitousse.scene unavailable")
let s = scene.new(name)

View file

@ -9,13 +9,8 @@
-- However, some modules may provide more feature when other modules are available.
-- These dependencies are written at the top of every main module file.
--
-- Ubiquitousse's goal is to run everywhere with the least porting effort possible, so Ubiquitousse tries to only use features that
-- are almost sure to be available everywhere.
--
-- Some Ubiquitousse modules require functions that are not in the Lua standard library, and must therefore be implemented in a backend,
-- such as ubiquitousse.love. When required, modules will try to autodetect the engine it is running on, and load a correct backend.
--
-- Most Ubiquitousse module backends require a few things to be fully implemented:
-- Ubiquitousse's goal is to run everywhere with the least porting effort possible, so while the current version mainly focus LÖVE, it
-- should be easily modifiable to work with something else. Ubiquitousse should only require:
-- * The backend needs to have access to some kind of main loop, or at least a function called very often (may or may not be the
-- same as the redraw screen callback).
-- * Some way of measuring time (preferably with millisecond-precision).
@ -23,9 +18,12 @@
-- * Lua 5.1, 5.2, 5.3 or LuaJit.
-- * Other requirement for specific modules should be described in the module's documentation.
--
-- Functions that depends on LÖVE or anything that's not in the Lua standard libraries (and therefore the one you may want to port to
-- another framework) are indicated by a "-- @impl love" annotation.
--
-- Units used in the API documentation:
-- * All distances are expressed in pixels (px)
-- * All durations are expressed in milliseconds (ms)
-- * All durations are expressed in seconds (ms)
-- These units are only used to make writing documentation easier; you can use other units if you want, as long as you're consistent.
--
-- Style:
@ -35,18 +33,6 @@
-- * CamelCase for class names.
-- * lowerCamelCase is expected for everything else.
--
-- Implementation levels:
-- * backend: nothing defined in Ubiquitousse, must be implemented in backend
-- * mixed: partly implemented in Ubiquitousse but must be complemeted in backend.
-- * ubiquitousse: fully-working version in Ubiquitousse, may or may not be redefined in backend
-- The implementation level is indicated using the "@impl level" annotation.
--
-- For backend writers:
-- If a function defined here already contains some code, this means this code is mandatory and you must put/call
-- it in your implementation (except if the backend provides a more efficient implementation).
-- Also, a backend file shouldn't redefine the ubiquitousse table itself but only redefine the backend-dependant fields.
-- Lua 5.3: The API doesn't make the difference between numbers and integers, so convert to integers when needed.
--
-- For game writer:
-- Ubiquitousse works with Lua 5.1 to 5.3, including LuaJit, but doesn't provide any version checking or compatibility layer
-- between the different versions, so it's up to you to handle that in your game (or ignore the problem and sticks to your
@ -63,10 +49,23 @@ local ubiquitousse
ubiquitousse = {
--- Ubiquitousse version.
-- @impl ubiquitousse
version = "0.0.1"
version = "0.1.0"
}
-- Check LÖVE version
local madeForLove = { 11, "x", "x" }
local actualLove = { love.getVersion() }
for i, v in ipairs(madeForLove) do
if v ~= "x" and actualLove[i] ~= v then
local txt = ("Ubiquitousse was made for LÖVE %s.%s.%s but %s.%s.%s is used!\nThings may not work as expected.")
:format(madeForLove[1], madeForLove[2], madeForLove[3], actualLove[1], actualLove[2], actualLove[3])
print(txt)
love.window.showMessageBox("Compatibility warning", txt, "warning")
break
end
end
-- We're going to require modules requiring Ubiquitousse, so to avoid stack overflows we already register the ubiquitousse package
package.loaded[p] = ubiquitousse
@ -80,13 +79,4 @@ for _, m in ipairs{"signal", "asset", "ecs", "input", "scene", "timer", "util"}
end
end
-- Backend engine autodetect and load
if love then
require(p..".backend.love")
elseif package.loaded["ctr"] then
require(p..".backend.ctrulua")
elseif package.loaded["libretro"] then
error("NYI")
end
return ubiquitousse

162
input/axis.lua Normal file
View file

@ -0,0 +1,162 @@
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

View file

@ -1,277 +0,0 @@
local input = require((...):match("^(.-%.)backend").."input")
local loaded, signal = pcall(require, (...):match("^(.-)input").."signal")
if not loaded then signal = nil end
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")
--- Register signals
if signal then
signal.event:replace("update", oUpdate, input.update)
end
return input

156
input/button.lua Normal file
View file

@ -0,0 +1,156 @@
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

View file

@ -1,14 +1 @@
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
return require((...)..".input")

View file

@ -10,495 +10,24 @@ if not loaded then signal = nil end
-- 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
-- FIXME https://love2d.org/forums/viewtopic.php?p=241434#p241434
--- 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=#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.
-- @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)
: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.
-- @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,
--- 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
--- 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,
--- 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.
-- @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.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 * 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
local button_mt
local axis_mt
local 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).
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 = {},
dt = 0,
---------------------------------
--- Detectors (input sources) ---
---------------------------------
@ -518,12 +47,11 @@ input = {
-- 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
-- @impl love
basicButtonDetector = function(str) end,
--- Make a new button detector from a detector function, string, or list of buttons.
-- @tparam string, function button identifier
-- @impl ubiquitousse
buttonDetector = function(obj)
if type(obj) == "function" then
return obj
@ -557,12 +85,11 @@ input = {
-- 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
-- @impl 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
-- @impl ubiquitousse
axisDetector = function(obj)
if type(obj) == "function" then
return obj
@ -581,91 +108,6 @@ input = {
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({
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,
-- 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({
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,
-- 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({
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,
------------------------------
--- Input detection helpers --
------------------------------
@ -675,13 +117,13 @@ input = {
-- 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
-- @impl backend
-- @impl 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
-- @impl backend
-- @impl love
axisUsed = function(threshold) end,
--- Returns a nice name for the button identifier.
@ -689,7 +131,7 @@ input = {
-- May returns the raw identifier if you're lazy.
-- @tparam string... button identifier string(s)
-- @treturn string... the displayable names
-- @impl backend
-- @impl love
buttonName = function(...) end,
--- Returns a nice name for the axis identifier.
@ -697,7 +139,7 @@ input = {
-- May returns the raw identifier if you're lazy.
-- @tparam string... axis identifier string(s)
-- @treturn string... the displayable names
-- @impl backend
-- @impl love
axisName = function(...) end,
-------------------
@ -710,7 +152,7 @@ input = {
-- 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
-- @impl 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.
@ -719,7 +161,7 @@ input = {
--- Get draw area dimensions.
-- Used for pointers.
-- @impl backend
-- @impl love
getDrawWidth = function() return 1 end,
getDrawHeight = function() return 1 end,
@ -727,10 +169,9 @@ input = {
-- 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
-- @impl ubiquitousse
update = function(newDt)
dt = newDt
updated = {}
input.dt = newDt
input.updated = {}
end
--- If you use LÖVE, note that in order to provide every feature (especially key detection), several callbacks functions will
@ -739,6 +180,16 @@ input = {
-- 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()
@ -749,4 +200,6 @@ if signal then
signal.event:bind("update", input.update)
end
require((...):gsub("input$", "love"))
return input

View file

@ -1,4 +1,4 @@
local input = require((...):match("^(.-%.)backend").."input")
local input = require((...):gsub("love$", "input"))
local loaded, signal = pcall(require, (...):match("^(.-)input").."signal")
if not loaded then signal = nil end

246
input/pointer.lua Normal file
View file

@ -0,0 +1,246 @@
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

View file

@ -22,28 +22,22 @@ if not loaded then timer = nil end
local scene
scene = {
--- The current scene table.
-- @impl ubiquitousse
current = nil,
--- Shortcut for scene.current.timer.
-- @impl ubiquitousse
timer = nil,
--- Shortcut for scene.current.signal.
-- @impl ubiquitousse
signal = nil,
--- The scene stack: list of scene, from the farest one to the nearest.
-- @impl ubiquitousse
stack = {},
--- A prefix for scene modules names.
-- Will search in the "scene" directory by default. Redefine it to fit your own ridiculous filesystem.
-- @impl ubiquitousse
prefix = "scene.",
--- Creates and returns a new Scene object.
-- @tparam[opt="unamed"] string name the new scene name
-- @impl ubiquitousse
new = function(name)
return {
name = name or "unamed", -- The scene name.
@ -68,7 +62,6 @@ scene = {
-- Then the stack is changed to replace the old scene with the new one.
-- @tparam string/table scenePath the new scene module name, or the scene table directly
-- @param ... arguments to pass to the scene's enter function
-- @impl ubiquitousse
switch = function(scenePath, ...)
local previous = scene.current
scene.current = type(scenePath) == "string" and require(scene.prefix..scenePath) or scenePath
@ -89,7 +82,6 @@ scene = {
-- will be reused.
-- @tparam string/table scenePath the new scene module name, or the scene table directly
-- @param ... arguments to pass to the scene's enter function
-- @impl ubiquitousse
push = function(scenePath, ...)
local previous = scene.current
scene.current = type(scenePath) == "string" and require(scene.prefix..scenePath) or scenePath
@ -104,7 +96,6 @@ scene = {
--- Pop the current scene from the scene stack.
-- The previous scene will be set as the current scene, then the current scene exit function will be called,
-- then the previous scene resume function will be called, and then the current scene will be removed from the stack.
-- @impl ubiquitousse
pop = function()
local previous = scene.current
scene.current = scene.stack[#scene.stack-1]
@ -119,7 +110,6 @@ scene = {
end,
--- Pop all scenes.
-- @impl ubiquitousse
popAll = function()
while scene.current do
scene.pop()
@ -130,7 +120,6 @@ scene = {
-- Should be called at every game update. If ubiquitousse.signal is available, will be bound to the "update" signal in signal.event.
-- @tparam number dt the delta-time (milisecond)
-- @param ... arguments to pass to the scene's update function after dt
-- @impl ubiquitousse
update = function(dt, ...)
if scene.current then
if timer then scene.current.timer:update(dt) end
@ -141,7 +130,6 @@ scene = {
--- Draw the current scene.
-- Should be called every time the game is draw. If ubiquitousse.signal is available, will be bound to the "draw" signal in signal.event.
-- @param ... arguments to pass to the scene's draw function
-- @impl ubiquitousse
draw = function(...)
if scene.current then scene.current:draw(...) end
end

View file

@ -1,43 +0,0 @@
local signal = require((...):match("^(.-%.)backend").."signal")
function signal.registerEvents()
local callbacks = { -- everything except run, errorhandler, threaderror
"displayrotated", "draw", "load", "lowmemory", "quit", "update",
"directorydropped", "filedropped", "focus", "mousefocus", "resize", "visible",
"keypressed", "keyreleased", "textedited", "textinput",
"mousemoved", "mousepressed", "mousereleased", "wheelmoved",
"gamepadaxis", "gamepadpressed", "gamepadreleased",
"joystickadded", "joystickaxis", "joystickhat", "joystickpressed", "joystickreleased", "joystickremoved",
"touchmoved", "touchpressed", "touchreleased"
}
local event = signal.event
for _, callback in ipairs(callbacks) do
if callback == "update" then
if love[callback] then
local old = love[callback]
love[callback] = function(dt)
old(dt)
event:emit(callback, dt)
end
else
love[callback] = function(dt)
event:emit(callback, dt)
end
end
else
if love[callback] then
local old = love[callback]
love[callback] = function(...)
old(...)
event:emit(callback, ...)
end
else
love[callback] = function(...)
event:emit(callback, ...)
end
end
end
end
end
return signal

View file

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

View file

@ -2,11 +2,9 @@
let registry_mt = {
--- Map of signals to list of listeners.
-- @impl ubiquitousse
signals = {},
--- Bind one or several functions to a signal name.
-- @impl ubiquitousse
bind = :(name, fn, ...)
if not @signals[name] then
@signals[name] = {}
@ -18,7 +16,6 @@ let registry_mt = {
end,
--- Unbind one or several functions to a signal name.
-- @impl ubiquitousse
unbind = :(name, fn, ...)
if not @signals[name] then
return
@ -34,13 +31,11 @@ let registry_mt = {
end,
--- Remove every bound function to a signal name.
-- @impl ubiquitousse
unbindAll = :(name)
@signals[name] = nil
end,
--- Replace a bound function with another function.
-- @impl ubiquitousse
replace = :(name, sourceFn, destFn)
if not @signals[name] then
@signals[name] = {}
@ -54,13 +49,11 @@ let registry_mt = {
end,
--- Remove every bound function to every signal.
-- @impl ubiquitousse
clear = :()
@signals = {}
end,
--- Emit a signal, i.e. call every function bound to it, with the given arguments.
-- @impl ubiquitousse
emit = :(name, ...)
if @signals[name] then
for _, fn in ipairs(@signals[name]) do
@ -74,13 +67,11 @@ registry_mt.__index = registry_mt
let signal = {
--- Creates and return a new SignalRegistry.
-- A SignalRegistry is a separate ubiquitousse.signal instance: its signals will be independant from other registries.
-- @impl ubiquitousse
new = ()
return setmetatable({ signals = {} }, registry_mt)
end,
--- Global SignalRegistry.
-- @impl ubiquitousse
signals = {},
bind = (...)
return registry_mt.bind(signal, ...)
@ -102,13 +93,50 @@ let signal = {
-- * update(dt), should be called on every game update
-- * draw, should be called on every game draw
-- * for LÖVE, there are callbacks for every LÖVE callback function that need to be called on their corresponding LÖVE callback
-- @impl mixed
event = nil,
--- Call this function to hook signal.event signals to the current backend.
-- For LÖVE, this means overriding every existing LÖVE callback. If a callback is already defined, the new one will call the old function along with the signal:emit.
-- @impl backend
registerEvents = () end
-- @impl love
registerEvents = ()
local callbacks = { -- everything except run, errorhandler, threaderror
"displayrotated", "draw", "load", "lowmemory", "quit", "update",
"directorydropped", "filedropped", "focus", "mousefocus", "resize", "visible",
"keypressed", "keyreleased", "textedited", "textinput",
"mousemoved", "mousepressed", "mousereleased", "wheelmoved",
"gamepadaxis", "gamepadpressed", "gamepadreleased",
"joystickadded", "joystickaxis", "joystickhat", "joystickpressed", "joystickreleased", "joystickremoved",
"touchmoved", "touchpressed", "touchreleased"
}
local event = signal.event
for _, callback in ipairs(callbacks) do
if callback == "update" then
if love[callback] then
local old = love[callback]
love[callback] = function(dt)
old(dt)
event:emit(callback, dt)
end
else
love[callback] = function(dt)
event:emit(callback, dt)
end
end
else
if love[callback] then
local old = love[callback]
love[callback] = function(...)
old(...)
event:emit(callback, ...)
end
else
love[callback] = function(...)
event:emit(callback, ...)
end
end
end
end
end
}
signal.event = signal.new()

View file

@ -133,7 +133,6 @@ local timer_mt = {
--- Update the timer.
-- Should be called at every game update.
-- @tparam number dt the delta-time (time spent since last time the function was called) (miliseconds)
-- @impl ubiquitousse
update = function(self, dt)
local t = self.t
if not t.dead then
@ -199,7 +198,6 @@ local timer_mt = {
-- You shouldn't need to worry about this if your timer belongs to a registry.
-- If you don't use registries, you probably should purge dead timers to free up some memory (dead timers don't do anything otherwise).
-- @treturn bool true if the timer can be discarded, false if it's still active.
-- @impl ubiquitousse
dead = function(self)
return self.t.dead
end
@ -211,7 +209,6 @@ local registry_mt = {
--- Update all the timers in the registry.
-- Should be called at every game update; called by ubiquitousse.update.
-- @tparam number dt the delta-time (time spent since last time the function was called) (miliseconds)
-- @impl ubiquitousse
update = function(self, dt)
-- process timers
for _, timer in ipairs(self.timers) do
@ -228,7 +225,6 @@ local registry_mt = {
--- Create a new timer and add it to the registry.
-- Same as timer_module.run, but add it to the registry.
-- @impl ubiquitousse
run = function(self, func)
local r = timer_module.run(func)
table.insert(self.timers, r)
@ -237,7 +233,6 @@ local registry_mt = {
--- Create a new tween timer and add it to the registry.
-- Same as timer_module.tween, but add it to the registry.
-- @impl ubiquitousse
tween = function(self, duration, tbl, to, method)
local r = timer_module.tween(duration, tbl, to, method)
table.insert(self.timers, r)
@ -245,7 +240,6 @@ local registry_mt = {
end,
--- Cancels all the running timers in this registry.
-- @impl ubiquitousse
clear = function(self)
self.timers = {}
end
@ -258,7 +252,6 @@ timer_module = {
-- A timer registry provides an easy way to handle your timers; it will keep track of them,
-- updating and removing them as needed. If you use the scene system, a scene-specific
-- timer registry is available at ubiquitousse.scene.current.timer.
-- @impl ubiquitousse
new = function()
return setmetatable({
--- Used to store all the functions delayed with ubiquitousse.time.delay
@ -277,7 +270,6 @@ timer_module = {
-- don't want to handle your timers manually.
-- @tparam[opt] function func the function to schedule
-- @treturn timer the object
-- @impl ubiquitousse
run = function(func)
local r = setmetatable({
t = {
@ -316,7 +308,6 @@ timer_module = {
-- @tparam table to the new values
-- @tparam[opt="linear"] string/function method tweening method (string name or the actual function(time, start, change, duration))
-- @treturn timer the object. A duration is already defined, and the :chain methods takes the same arguments as tween (and creates a tween).
-- @impl ubiquitousse
tween = function(duration, tbl, to, method)
method = method or "linear"
method = type(method) == "string" and ease[method] or method