mirror of
https://github.com/Reuh/ubiquitousse.git
synced 2025-10-27 09:09:30 +00:00
Code reorganization, added uqt.ecs, removed LÖVE duplicates (uqt.audio, uqt.draw, uqt.filesystem)
This commit is contained in:
parent
523c5d36c0
commit
16e533d176
28 changed files with 2544 additions and 2107 deletions
91
asset.lua
91
asset.lua
|
|
@ -1,91 +0,0 @@
|
|||
-- ubiquitousse.asset
|
||||
|
||||
-- The asset cache. Each cached asset is indexed with a string key "type.assetName".
|
||||
local cache = setmetatable({}, { __mode = "v" }) -- weak values
|
||||
|
||||
--- Asset manager. Loads asset and cache them.
|
||||
-- This file has no dependicy to either ubiquitousse or a ubiquitousse backend.
|
||||
-- This only provides a streamlined way to handle asset, and doesn't handle the actual file loading/object creation itself; you are expected to provide your own asset loaders.
|
||||
-- See asset.load for more details. Hopefully this will allow you to use asset which are more game-specific than "image" or "audio".
|
||||
local asset
|
||||
asset = setmetatable({
|
||||
--- A prefix for asset names
|
||||
-- @impl ubiquitousse
|
||||
prefix = "",
|
||||
|
||||
--- Load (and cache) an asset.
|
||||
-- Asset name are similar to Lua module names (directory separator is the dot . and no extention should be specified).
|
||||
-- To load an asset, ubiquitousse will, in this order:
|
||||
-- * try to load the directory loader: a file named loader.lua in the same directory as the asset we are trying to load
|
||||
-- * try to load the asset-specific loader: a file in the same directory and with the same name (except with the .lua extension) as the asset we are trying to load
|
||||
-- Loaders should return either:
|
||||
-- * the new asset
|
||||
-- * nil, message if there was an error loading the asset
|
||||
-- These loaders have acces to the following variables:
|
||||
-- * directory: the asset directory (including prefix)
|
||||
-- * name: the asset name (directory information removed)
|
||||
-- * asset: the asset data. May be nil if this is the first loader to run.
|
||||
-- @tparam assetName string the asset's full name
|
||||
-- @return the asset
|
||||
-- @impl ubiquitousse
|
||||
load = function(assetName)
|
||||
if not cache[assetName] then
|
||||
-- Get directory and name
|
||||
local path, name = assetName:match("^([^.]+)%.(.+)$")
|
||||
if not path then
|
||||
path, name = "", assetName
|
||||
end
|
||||
local dir = (asset.prefix..path):gsub("%.", "/")
|
||||
|
||||
-- Setup env
|
||||
local oName, oAsset, oDirectory = _G.name, _G.asset, _G.directory
|
||||
_G.name, _G.asset, _G.directory = name, nil, dir
|
||||
|
||||
-- Load
|
||||
local err = ("couldn't load asset %q:"):format(assetName)
|
||||
|
||||
-- Asset directory loader
|
||||
local f = io.open(dir.."/loader.lua")
|
||||
if f then
|
||||
f:close()
|
||||
local r, msg = dofile(dir.."/loader.lua")
|
||||
if r ~= nil then
|
||||
_G.asset = r
|
||||
else
|
||||
err = err .. ("\n\t* directory loader %q failed to load the asset: %s"):format(dir.."/loader.lua", msg)
|
||||
end
|
||||
else
|
||||
err = err .. ("\n\t* no directory loader %q found"):format(dir.."/loader.lua")
|
||||
end
|
||||
|
||||
-- Asset specific loader
|
||||
local f = io.open(dir.."/"..name..".lua")
|
||||
if f then
|
||||
f:close()
|
||||
local r, msg = dofile(dir.."/"..name..".lua")
|
||||
if r ~= nil then
|
||||
_G.asset = r
|
||||
else
|
||||
err = err .. ("\n\t* asset specific loader %q failed to load the asset: %s"):format(dir.."/"..name..".lua", msg)
|
||||
end
|
||||
else
|
||||
err = err .. ("\n\t* no asset specific loader %q found"):format(dir.."/"..name..".lua")
|
||||
end
|
||||
|
||||
-- Done
|
||||
cache[assetName] = assert(_G.asset, err)
|
||||
|
||||
-- Restore env
|
||||
_G.name, _G.asset, _G.directory = oName, oAsset, oDirectory
|
||||
end
|
||||
|
||||
return cache[assetName]
|
||||
end,
|
||||
}, {
|
||||
--- asset(...) is a shortcut for asset.load(...)
|
||||
__call = function(self, ...)
|
||||
return asset.load(...)
|
||||
end
|
||||
})
|
||||
|
||||
return asset
|
||||
86
asset/asset.lua
Normal file
86
asset/asset.lua
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
--- ubiquitousse.asset
|
||||
-- No dependencies.
|
||||
|
||||
--- Asset manager. Loads asset and cache them.
|
||||
-- This file has no dependency to either ubiquitousse or a ubiquitousse backend.
|
||||
-- This only provides a streamlined way to handle asset, and doesn't handle the actual file loading/object creation itself; you are expected to provide your own asset loaders.
|
||||
-- See the __call method for more details on how assets are loaded. Hopefully this will allow you to use asset which are more game-specific than "image" or "audio".
|
||||
local asset_mt = {
|
||||
--- Load (and cache) an asset.
|
||||
-- Asset name are similar to Lua module names (directory separator is the dot . and no extention should be specified).
|
||||
-- To load an asset, ubiquitousse will try every loaders in the loader list with a name that prefix the asset name.
|
||||
-- The first value returned will be used as the asset value.
|
||||
-- Loaders are called with the arguments:
|
||||
-- * path: the asset full path, except extension
|
||||
-- * ...: other arguments given after the asset name. Can only be number and strings.
|
||||
-- @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, ...}, ".")
|
||||
|
||||
if not cache[hash] then
|
||||
for prefix, fn in pairs(self.loaders) do
|
||||
if assetName:match("^"..prefix) then
|
||||
cache[hash] = fn((self.prefix..assetName):gsub("%.", "/"), ...)
|
||||
if cache[hash] then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
assert(cache[hash], ("couldn't load asset %q"):format(assetName))
|
||||
end
|
||||
|
||||
return cache[hash]
|
||||
end,
|
||||
|
||||
--- Preload a list of assets.
|
||||
-- @impl ubiquitousse
|
||||
load = function(self, list)
|
||||
for _, asset in ipairs(list) do
|
||||
self(asset)
|
||||
end
|
||||
end,
|
||||
|
||||
--- 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
|
||||
}
|
||||
asset_mt.__index = asset_mt
|
||||
|
||||
local asset = {
|
||||
--- Create a new asset manager.
|
||||
-- If the caching "mode" is set to auto (default), the asset manager will allow assets to be automaticaly garbage collected by Lua.
|
||||
-- If set to "manual", the assets will not be garbage collected unless the clear method is called.
|
||||
-- "manual" mode is useful if you have assets that are particularly slow to load and you want full control on when they are loaded and unloaded (typically a loading screen).
|
||||
-- @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
|
||||
setmetatable(cache, { __mode = "v" })
|
||||
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
|
||||
}
|
||||
|
||||
return asset
|
||||
1
asset/init.lua
Normal file
1
asset/init.lua
Normal file
|
|
@ -0,0 +1 @@
|
|||
return require((...)..".asset")
|
||||
12
audio.lua
12
audio.lua
|
|
@ -1,12 +0,0 @@
|
|||
-- ubiquitousse.audio
|
||||
|
||||
--- Audio functions.
|
||||
return {
|
||||
--- Loads an audio file and returns the corresponding audio object.
|
||||
-- TODO: audio object doc & API
|
||||
-- @impl backend
|
||||
load = function(filepath) end,
|
||||
|
||||
-- TODO: doc
|
||||
available = false
|
||||
}
|
||||
|
|
@ -1,22 +1,10 @@
|
|||
--- ctrµLua backend 0.0.1 for Ubiquitousse.
|
||||
-- Provides a partial Ubiquitousse API. Still a lot to implement.
|
||||
-- Made for some ctrµLua version and Ubiquitousse 0.0.1.
|
||||
-- See `ubiquitousse` for Ubiquitousse API.
|
||||
|
||||
-- General
|
||||
local version = "0.0.1"
|
||||
|
||||
-- Require stuff
|
||||
local uqt = require((...):match("^(.-ubiquitousse)%."))
|
||||
local ctr = require("ctr")
|
||||
local gfx = require("ctr.gfx")
|
||||
local hid = require("ctr.hid")
|
||||
|
||||
-- Version compatibility warning
|
||||
do
|
||||
local function checkCompat(stuffName, expectedVersion, actualVersion)
|
||||
local function checkCompat(stuffName, expectedVersion, actualVersion)
|
||||
if actualVersion ~= expectedVersion then
|
||||
local txt = ("Ubiquitousse ctrµLua backend version "..version.." was made for %s %s but %s is used!\nThings may not work as expected.")
|
||||
local txt = ("Ubiquitousse ctrµLua backend was made for %s %s but %s is used!\nThings may not work as expected.")
|
||||
:format(stuffName, expectedVersion, actualVersion)
|
||||
print(txt)
|
||||
for _=0,300 do
|
||||
|
|
@ -26,309 +14,6 @@ do
|
|||
gfx.render()
|
||||
end
|
||||
end
|
||||
end
|
||||
checkCompat("ctrµLua", "v1.0", ctr.version) -- not really a version, just get the latest build
|
||||
checkCompat("Ubiquitousse", "0.0.1", uqt.version)
|
||||
end
|
||||
|
||||
-- Redefine all functions in tbl which also are in toAdd, so when used they call the old function (in tbl) and then the new (in toAdd).
|
||||
-- Functions with names prefixed by a exclamation mark will overwrite the old function.
|
||||
local function add(tbl, toAdd)
|
||||
for k,v in pairs(toAdd) do
|
||||
local old = tbl[k]
|
||||
if k:sub(1,1) == "!" then
|
||||
tbl[k] = v
|
||||
else
|
||||
tbl[k] = function(...)
|
||||
old(...)
|
||||
return v(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- uqt
|
||||
uqt.backend = "ctrulua"
|
||||
|
||||
-- uqt.event: TODO
|
||||
if uqt.event then
|
||||
error("uqt.event: NYI")
|
||||
end
|
||||
|
||||
-- uqt.draw: TODO
|
||||
if uqt.draw then
|
||||
error("uqt.draw: NYI")
|
||||
end
|
||||
|
||||
-- uqt.audio: TODO
|
||||
if uqt.audio then
|
||||
error("uqt.audio: NYI")
|
||||
end
|
||||
|
||||
-- uqt.time
|
||||
if uqt.time then
|
||||
add(uqt.time, {
|
||||
get = ctr.time
|
||||
})
|
||||
end
|
||||
|
||||
-- uqt.input
|
||||
if uqt.input then
|
||||
local keys = {}
|
||||
local touchX, touchY, dTouchX, dTouchY
|
||||
add(uqt.input, {
|
||||
update = function()
|
||||
hid.read()
|
||||
|
||||
keys = hid.keys()
|
||||
|
||||
local nTouchX, nTouchY = hid.touch()
|
||||
dTouchX, dTouchY = nTouchX - touchX, nTouchY - touchY
|
||||
touchX, touchY = nTouchX, nTouchY
|
||||
end,
|
||||
|
||||
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,
|
||||
|
||||
axisDetector = function(...)
|
||||
local ret = {}
|
||||
for _,id in ipairs({...}) do
|
||||
-- Binary axis
|
||||
if id:match(".+%,.+") then
|
||||
local d1, d2 = uqt.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,
|
||||
|
||||
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,
|
||||
|
||||
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,
|
||||
|
||||
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,
|
||||
|
||||
axisName = function(...)
|
||||
local ret = {}
|
||||
for _,id in ipairs({...}) do
|
||||
-- Binary axis
|
||||
if id:match(".+%,.+") then
|
||||
local b1, b2 = uqt.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
|
||||
})
|
||||
|
||||
-- Defaults
|
||||
uqt.input.default.pointer:bind(
|
||||
{ "absolute", "key.left,key.right", "key.up,key.down" },
|
||||
{ "absolute", "circle.x", "circle.y" }
|
||||
)
|
||||
uqt.input.default.confirm:bind("key.a")
|
||||
uqt.input.default.cancel:bind("key.b")
|
||||
end
|
||||
checkCompat("ctrµLua", "v1.0", ctr.version) -- not really a version, just get the latest build
|
||||
checkCompat("Ubiquitousse", "0.0.1", uqt.version)
|
||||
|
|
|
|||
515
backend/love.lua
515
backend/love.lua
|
|
@ -1,518 +1,11 @@
|
|||
--- Löve backend 0.0.1 for Ubiquitousse.
|
||||
-- Provides all the Ubiquitousse API on a Löve environment.
|
||||
-- Made for Löve 11.1.0 and Ubiquitousse 0.0.1.
|
||||
-- See `ubiquitousse` for Ubiquitousse API.
|
||||
|
||||
-- Config
|
||||
local useScancodes = true -- Use ScanCodes (layout independant input) instead of KeyConstants (layout dependant) for keyboard input
|
||||
local displayKeyConstant = 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.
|
||||
|
||||
-- General
|
||||
local version = "0.0.1"
|
||||
|
||||
-- Require stuff
|
||||
local uqt = require((...):match("^(.-ubiquitousse)%."))
|
||||
local m = uqt.module
|
||||
|
||||
-- Version compatibility warning
|
||||
do
|
||||
local function checkCompat(stuffName, expectedVersion, actualVersion)
|
||||
local function checkCompat(stuffName, expectedVersion, actualVersion)
|
||||
if actualVersion ~= expectedVersion then
|
||||
local txt = ("Ubiquitousse Löve backend version "..version.." was made for %s %s but %s is used!\nThings may not work as expected.")
|
||||
local txt = ("Ubiquitousse Löve backend was made for %s %s but %s is used!\nThings may not work as expected.")
|
||||
:format(stuffName, expectedVersion, actualVersion)
|
||||
print(txt)
|
||||
love.window.showMessageBox("Warning", txt, "warning")
|
||||
end
|
||||
end
|
||||
checkCompat("Löve", "11.1.0", ("%s.%s.%s"):format(love.getVersion()))
|
||||
checkCompat("Ubiquitousse", "0.0.1", uqt.version)
|
||||
end
|
||||
|
||||
-- Redefine all functions in tbl which also are in toAdd, so when used they call the old function (in tbl) and then the new (in toAdd).
|
||||
-- Functions with names prefixed by a exclamation mark will overwrite the old function.
|
||||
local function add(tbl, toAdd)
|
||||
for k,v in pairs(toAdd) do
|
||||
local old = tbl[k]
|
||||
if k:sub(1,1) == "!" then
|
||||
tbl[k] = v
|
||||
else
|
||||
tbl[k] = function(...)
|
||||
old(...)
|
||||
return v(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- uqt
|
||||
uqt.backend = "love"
|
||||
|
||||
-- uqt.event
|
||||
if m.event then
|
||||
local updateDefault = uqt.event.update
|
||||
uqt.event.update = function() end
|
||||
function love.update(dt)
|
||||
-- Stuff defined in ubiquitousse.lua
|
||||
updateDefault(dt*1000)
|
||||
|
||||
-- Callback
|
||||
uqt.event.update(dt)
|
||||
end
|
||||
|
||||
local drawDefault = uqt.event.draw
|
||||
uqt.event.draw = function() end
|
||||
function love.draw()
|
||||
if m.draw then
|
||||
love.graphics.push()
|
||||
|
||||
-- Resize type
|
||||
local winW, winH = love.graphics.getWidth(), love.graphics.getHeight()
|
||||
local gameW, gameH = uqt.draw.params.width, uqt.draw.params.height
|
||||
if uqt.draw.params.resizeType == "auto" then
|
||||
love.graphics.scale(winW/gameW, winH/gameH)
|
||||
elseif uqt.draw.params.resizeType == "center" then
|
||||
love.graphics.translate(math.floor(winW/2-gameW/2), math.floor(winH/2-gameH/2))
|
||||
end
|
||||
end
|
||||
|
||||
-- Stuff defined in ubiquitousse.lua
|
||||
drawDefault()
|
||||
|
||||
-- Callback
|
||||
uqt.event.draw()
|
||||
|
||||
if m.draw then
|
||||
love.graphics.pop()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- uqt.draw
|
||||
if m.draw then
|
||||
local defaultFont = love.graphics.getFont()
|
||||
add(uqt.draw, {
|
||||
init = function(params)
|
||||
local p = uqt.draw.params
|
||||
love.window.setTitle(p.title)
|
||||
love.window.setMode(p.width, p.height, {
|
||||
resizable = p.resizable
|
||||
})
|
||||
end,
|
||||
fps = function()
|
||||
return love.timer.getFPS()
|
||||
end,
|
||||
color = function(r, g, b, a)
|
||||
love.graphics.setColor(r, g, b, a)
|
||||
end,
|
||||
text = function(x, y, text)
|
||||
love.graphics.setFont(defaultFont)
|
||||
love.graphics.print(text, x, y)
|
||||
end,
|
||||
point = function(x, y, ...)
|
||||
love.graphics.points(x, y, ...)
|
||||
end,
|
||||
lineWidth = function(width)
|
||||
love.graphics.setLineWidth(width)
|
||||
end,
|
||||
line = function(x1, y1, x2, y2, ...)
|
||||
love.graphics.line(x1, y1, x2, y2, ...)
|
||||
end,
|
||||
polygon = function(...)
|
||||
love.graphics.polygon("fill", ...)
|
||||
end,
|
||||
linedPolygon = function(...)
|
||||
love.graphics.polygon("line", ...)
|
||||
end,
|
||||
["!rectangle"] = function(x, y, width, height)
|
||||
love.graphics.rectangle("fill", x, y, width, height)
|
||||
end,
|
||||
["!linedRectangle"] = function(x, y, width, height)
|
||||
love.graphics.rectangle("line", x, y, width, height)
|
||||
end,
|
||||
circle = function(x, y, radius)
|
||||
love.graphics.circle("fill", x, y, radius)
|
||||
end,
|
||||
linedCircle = function(x, y, radius)
|
||||
love.graphics.circle("line", x, y, radius)
|
||||
end,
|
||||
scissor = function(x, y, width, height)
|
||||
love.graphics.setScissor(x, y, width, height)
|
||||
end,
|
||||
-- TODO: cf draw.lua
|
||||
image = function(filename)
|
||||
local img = love.graphics.newImage(filename)
|
||||
return {
|
||||
width = img:getWidth(),
|
||||
height = img:getHeight(),
|
||||
draw = function(self, x, y, r, sx, sy, ox, oy)
|
||||
love.graphics.draw(img, x, y, r, sx, sy, ox, oy)
|
||||
end
|
||||
}
|
||||
end,
|
||||
font = function(filename, size)
|
||||
local fnt = love.graphics.newFont(filename, size)
|
||||
return {
|
||||
width = function(self, text)
|
||||
return fnt:getWidth(text)
|
||||
end,
|
||||
draw = function(self, text, x, y, r, sx, sy, ox, oy)
|
||||
love.graphics.setFont(fnt)
|
||||
love.graphics.print(text, x, y, r, sx, sy, ox, oy)
|
||||
end
|
||||
}
|
||||
end,
|
||||
})
|
||||
function love.resize(width, height)
|
||||
if uqt.draw.params.resizeType == "none" then
|
||||
uqt.draw.width = width
|
||||
uqt.draw.height = height
|
||||
end
|
||||
end
|
||||
elseif m.input then -- fields required by uqt.input
|
||||
uqt.draw = {
|
||||
width = love.graphics.getWidth(),
|
||||
height = love.graphics.getHeight()
|
||||
}
|
||||
end
|
||||
|
||||
-- uqt.audio
|
||||
if m.audio then
|
||||
add(uqt.audio, {
|
||||
-- TODO: cf audio.lua
|
||||
load = function(filepath)
|
||||
local audio = love.audio.newSource(filepath)
|
||||
return {
|
||||
play = function(self)
|
||||
audio:play()
|
||||
end
|
||||
}
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
-- uqt.time
|
||||
if m.time then
|
||||
add(uqt.time, {
|
||||
get = function()
|
||||
return love.timer.getTime() * 1000
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
-- uqt.input
|
||||
if m.input then
|
||||
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
|
||||
|
||||
love.mouse.setVisible(false)
|
||||
|
||||
add(uqt.input, {
|
||||
-- love.wheelmoved doesn't trigger when the wheel stop moving, so we need to clear up our stuff at each update
|
||||
update = function()
|
||||
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
|
||||
end,
|
||||
|
||||
buttonDetector = function(...)
|
||||
local ret = {}
|
||||
for _,id in ipairs({...}) do
|
||||
-- Keyboard
|
||||
if id:match("^keyboard%.") then
|
||||
local key = id:match("^keyboard%.(.+)$")
|
||||
table.insert(ret, 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%.(.+)$")
|
||||
table.insert(ret, function()
|
||||
return buttonsInUse["mouse.wheel."..key]
|
||||
end)
|
||||
-- Mouse
|
||||
elseif id:match("^mouse%.") then
|
||||
local key = id:match("^mouse%.(.+)$")
|
||||
table.insert(ret, 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)
|
||||
table.insert(ret, 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
|
||||
table.insert(ret, 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
|
||||
return unpack(ret)
|
||||
end,
|
||||
|
||||
axisDetector = function(...)
|
||||
local ret = {}
|
||||
for _,id in ipairs({...}) do
|
||||
-- Binary axis
|
||||
if id:match(".+%,.+") then
|
||||
local d1, d2 = uqt.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)
|
||||
-- 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, 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
|
||||
table.insert(ret, 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
|
||||
table.insert(ret, 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
|
||||
return unpack(ret)
|
||||
end,
|
||||
|
||||
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,
|
||||
|
||||
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,
|
||||
|
||||
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
|
||||
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,
|
||||
|
||||
axisName = function(...)
|
||||
local ret = {}
|
||||
for _,id in ipairs({...}) do
|
||||
-- Binary axis
|
||||
if id:match(".+%,.+") then
|
||||
local b1, b2 = uqt.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
|
||||
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
|
||||
})
|
||||
|
||||
-- Defaults
|
||||
uqt.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"}
|
||||
)
|
||||
uqt.input.default.confirm:bind(
|
||||
"keyboard.enter", "keyboard.space", "keyboard.lshift", "keyboard.e",
|
||||
"gamepad.button.1.a"
|
||||
)
|
||||
uqt.input.default.cancel:bind(
|
||||
"keyboard.escape", "keyboard.backspace",
|
||||
"gamepad.button.1.b"
|
||||
)
|
||||
end
|
||||
checkCompat("Löve", "11.3.0", ("%s.%s.%s"):format(love.getVersion()))
|
||||
checkCompat("Ubiquitousse", "0.0.1", uqt.version)
|
||||
|
|
|
|||
169
draw.lua
169
draw.lua
|
|
@ -1,169 +0,0 @@
|
|||
-- ubiquitousse.draw
|
||||
|
||||
--- The drawing functions: everything that affect the display/window.
|
||||
-- The coordinate system used is:
|
||||
--
|
||||
-- (0,0) +---> (x)
|
||||
-- |
|
||||
-- |
|
||||
-- v (y)
|
||||
--
|
||||
-- x and y values can be float, so make sure to perform math.floor if your engine only support
|
||||
-- integer coordinates.
|
||||
--
|
||||
-- Mostly plagiarized from the Löve API, with some parts from ctrµLua.
|
||||
local draw
|
||||
draw = {
|
||||
--- Initial game view paramters (some defaults).
|
||||
-- @impl ubiquitousse
|
||||
params = {
|
||||
title = "Ubiquitousse Game",
|
||||
width = 800,
|
||||
height = 600,
|
||||
resizable = false,
|
||||
resizeType = "auto"
|
||||
},
|
||||
|
||||
--- Setup the intial game view parameters.
|
||||
-- If a parmeter is not set, a default value will be used.
|
||||
-- This function is expected to be only called once, before doing any drawing operation.
|
||||
-- @tparam table params the game parameters
|
||||
-- @usage -- Default values:
|
||||
-- ubiquitousse.init {
|
||||
-- title = "Ubiquitousse Game", -- usually window title
|
||||
-- width = 800, -- in px
|
||||
-- height = 600, -- in px
|
||||
-- resizable = false, -- can the game be resized?
|
||||
-- resizeType = "auto" -- how to act on resize: "none" to do nothing (0,0 will be top-left)
|
||||
-- "center" to autocenter (0,0 will be at windowWidth/2-gameWidth/2,windowHeight/2-gameHeight/2)
|
||||
-- "auto" to automatically resize to the window size (coordinate system won't change)
|
||||
-- }
|
||||
-- @impl mixed
|
||||
init = function(params)
|
||||
for k, v in pairs(params) do
|
||||
draw.params[k] = v
|
||||
end
|
||||
draw.width = params.width
|
||||
draw.height = params.height
|
||||
end,
|
||||
|
||||
--- Return the number of frames per second.
|
||||
-- @treturn number the current FPS
|
||||
-- @impl backend
|
||||
fps = function() end,
|
||||
|
||||
--- Sets the drawing color
|
||||
-- @tparam number r the red component (0-1)
|
||||
-- @tparam number g the green component (0-1)
|
||||
-- @tparam number b the blue component (0-1)
|
||||
-- @tparam[opt=1] number a the alpha (opacity) component (0-1)
|
||||
-- @impl backend
|
||||
color = function(r, g, b, a) end,
|
||||
|
||||
--- Draws some text.
|
||||
-- @tparam number x x top-left coordinate of the text
|
||||
-- @tparam number y y top-left coordinate of the text
|
||||
-- @tparam string text the text to draw. UTF-8 format, convert if needed.
|
||||
-- @impl backend
|
||||
text = function(x, y, text) end,
|
||||
|
||||
--- Draws a point.
|
||||
-- @tparam number x point x coordinate
|
||||
-- @tparam number y point y coordinate
|
||||
-- @tparam number ... other vertices to draw other points
|
||||
-- @impl backend
|
||||
point = function(x, y, ...) end,
|
||||
|
||||
--- Sets the width.
|
||||
-- @tparam number width the line width
|
||||
-- @impl backend
|
||||
lineWidth = function(width) end,
|
||||
|
||||
--- Draws a line.
|
||||
-- @tparam number x1 line start x coordinate
|
||||
-- @tparam number y1 line start y coordinate
|
||||
-- @tparam number x2 line end x coordinate
|
||||
-- @tparam number y2 line end y coordinate
|
||||
-- @tparam number ... other vertices to continue drawing a polyline
|
||||
-- @impl backend
|
||||
line = function(x1, y1, x2, y2, ...) end,
|
||||
|
||||
--- Draws a filled polygon.
|
||||
-- @tparam number x1,y1,x2,y2... the vertices of the polygon
|
||||
-- @impl backend
|
||||
polygon = function(...) end,
|
||||
|
||||
--- Draws a polygon outline.
|
||||
-- @tparam number x1,y1,x2,y2... the vertices of the polygon
|
||||
-- @impl backend
|
||||
linedPolygon = function(...) end,
|
||||
|
||||
--- Draws a filled rectangle.
|
||||
-- @tparam number x rectangle top-left x coordinate
|
||||
-- @tparam number y rectangle top-left x coordinate
|
||||
-- @tparam number width rectangle width
|
||||
-- @tparam number height rectangle height
|
||||
-- @impl ubiquitousse
|
||||
rectangle = function(x, y, width, height)
|
||||
draw.polygon(x, y, x + width, y, x + width, y + height, x, y + height)
|
||||
end,
|
||||
|
||||
--- Draws a rectangle outline.
|
||||
-- @tparam number x rectangle top-left x coordinate
|
||||
-- @tparam number y rectangle top-left x coordinate
|
||||
-- @tparam number width rectangle width
|
||||
-- @tparam number height rectangle height
|
||||
-- @impl ubiquitousse
|
||||
linedRectangle = function(x, y, width, height)
|
||||
draw.linedPolygon(x, y, x + width, y, x + width, y + height, x, y + height)
|
||||
end,
|
||||
|
||||
--- Draws a filled circle.
|
||||
-- @tparam number x center x coordinate
|
||||
-- @tparam number y center x coordinate
|
||||
-- @tparam number radius circle radius
|
||||
-- @impl backend
|
||||
circle = function(x, y, radius) end,
|
||||
|
||||
--- Draws a circle outline.
|
||||
-- @tparam number x center x coordinate
|
||||
-- @tparam number y center x coordinate
|
||||
-- @tparam number radius circle radius
|
||||
-- @impl backend
|
||||
linedCircle = function(x, y, radius) end,
|
||||
|
||||
--- Enables the scissor test.
|
||||
-- When enabled, every pixel drawn outside of the scissor rectangle is discarded.
|
||||
-- When called withou arguments, it disables the scissor test.
|
||||
-- @tparam number x rectangle top-left x coordinate
|
||||
-- @tparam number y rectangle top-left x coordinate
|
||||
-- @tparam number width rectangle width
|
||||
-- @tparam number height rectangle height
|
||||
-- @impl backend
|
||||
scissor = function(x, y, width, height) end,
|
||||
|
||||
--- The drawing area width, in pixels.
|
||||
-- @requiredby input
|
||||
-- @impl backend
|
||||
width = 800,
|
||||
|
||||
--- The drawing area height, in pixels.
|
||||
-- @requiredby input
|
||||
-- @impl backend
|
||||
height = 600,
|
||||
|
||||
-- TODO: doc & api
|
||||
push = function() end,
|
||||
pop = function() end,
|
||||
translate = function(x, y) end,
|
||||
rotate = function(angle) end,
|
||||
scale = function(sx, sy) end,
|
||||
font = function(filename) end,
|
||||
image = function(filename) end,
|
||||
}
|
||||
|
||||
-- TODO: canvas stuff ; also make everything here actually be shortcut to draw to the game's framebuffer.
|
||||
-- TODO: add software implementations of everything.
|
||||
-- TODO: add function to draw a message (used eg for the error message when there is a version mismatch)
|
||||
|
||||
return draw
|
||||
308
ecs/ecs.can
Normal file
308
ecs/ecs.can
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
--- ubiquitousse.ecs
|
||||
-- No dependency.
|
||||
|
||||
--- Entity Component System library, inspired by the excellent tiny-ecs. Main differences include:
|
||||
-- * ability to nest systems;
|
||||
-- * instanciation of systems for each world;
|
||||
-- * adding and removing entities is done instantaneously.
|
||||
|
||||
-- TODO: Implement a skip list for faster search.
|
||||
|
||||
--- Recursively remove subsystems from a system.
|
||||
let recDestroySystems = (system)
|
||||
for i=#system.systems, 1, -1 do
|
||||
let s = system.systems[i]
|
||||
recDestroySystems(s)
|
||||
system.systems[i] = nil
|
||||
if s.name then
|
||||
system.world.s[s.name] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
--- Recursively call :clear and :onRemoveFromWorld to a list of systems in a world.
|
||||
let recCallOnRemoveFromWorld = (world, systems)
|
||||
for _, s in ipairs(systems) do
|
||||
s:clear()
|
||||
recCallOnRemoveFromWorld(world, s.systems)
|
||||
s:onRemoveFromWorld(world)
|
||||
end
|
||||
end
|
||||
|
||||
--- Iterate through the next entity, based on state s: { previousLinkedListItem }
|
||||
let nextEntity = (s)
|
||||
if s[1] then
|
||||
let var = s[1][1]
|
||||
s[1] = s[1][2]
|
||||
return var
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
--- System fields and methods.
|
||||
-- When they are added to a world, a new, per-world self table is created and used for every method call (which we call "instancied system").
|
||||
-- Instancied systems can be retrieved in system.s or system.systems.
|
||||
-- Oh, the "world" is just the top-level system.
|
||||
let system_mt = {
|
||||
--- Read-only after creation system options ---
|
||||
-- I mean, you can try to change them afterwards. But, heh.
|
||||
|
||||
--- Name of the system (optional).
|
||||
-- Used to create a field with the system's name in world.system.
|
||||
name = nil,
|
||||
|
||||
--- List of subsystems.
|
||||
-- On a instancied system, this is a list of the same subsystems, but instancied for this world.
|
||||
systems = nil,
|
||||
|
||||
--- Returns true if the entity should be added to this system (and therefore its subsystems).
|
||||
-- By default, rejects everything.
|
||||
filter = :(e) return false end,
|
||||
--- Returns true if e1 <= e2.
|
||||
compare = :(e1, e2) return true end,
|
||||
|
||||
--- Modifiable system options ---
|
||||
|
||||
--- Called when adding an entity to the system.
|
||||
onAdd = :(e) end,
|
||||
--- Called when removing an entity from the system.
|
||||
onRemove = :(e) end,
|
||||
--- Called when the system is added to a world.
|
||||
onAddToWorld = :(world) end,
|
||||
--- Called when the system is removed from a world (i.e., the world is destroyed).
|
||||
onRemoveFromWorld = :(world) end,
|
||||
--- Called when updating the system.
|
||||
onUpdate = :(dt) end,
|
||||
--- Called when drawing the system.
|
||||
onDraw = :() end,
|
||||
--- Called when updating the system, for every entity the system contains.
|
||||
process = :(e, dt) end,
|
||||
--- Called when drawing the system, for every entity the system contains.
|
||||
render = :(e) end,
|
||||
|
||||
--- If set, the system will only update every interval seconds.
|
||||
interval = nil,
|
||||
--- The system and its susbsystems will only update if this is true.
|
||||
active = true,
|
||||
--- The system and its subsystems will only draw if this is true.
|
||||
visible = true,
|
||||
|
||||
--- Read-only system options ---
|
||||
|
||||
--- The world the system belongs to.
|
||||
world = nil,
|
||||
--- Number of entities in the system.
|
||||
entityCount = 0,
|
||||
--- Map of named systems in the world (not only subsystems).
|
||||
s = nil,
|
||||
|
||||
--- Private fields ---
|
||||
|
||||
--- First element of the linked list of entities.
|
||||
_first = nil,
|
||||
--- Amount of time waited since last update (if interval is set).
|
||||
_waited = 0,
|
||||
|
||||
--- Methods ---
|
||||
|
||||
--- Add entities to the system and its subsystems.
|
||||
-- If this is called on a subsystem instead of the world, be warned that this will bypass all the parent's systems filters.
|
||||
-- Since :remove will not search for entities in systems where they should have been filtered out, the added entities will not be removed
|
||||
-- when calling :remove on a parent system or the world. The entity can only be removed by calling :remove on the system :add was called on.
|
||||
add = :(...)
|
||||
for _, e in ipairs({...}) do
|
||||
if @filter(e) then
|
||||
if @_first == nil then
|
||||
@_first = { e, nil }
|
||||
elseif @compare(e, @_first[1]) then
|
||||
@_first = { e, @_first }
|
||||
else
|
||||
let entity = @_first
|
||||
while entity[2] ~= nil do
|
||||
if @compare(e, entity[2][1]) then
|
||||
entity[2] = { e, entity[2] }
|
||||
break
|
||||
end
|
||||
entity = entity[2]
|
||||
end
|
||||
if entity[2] == nil then
|
||||
entity[2] = { e, nil }
|
||||
end
|
||||
end
|
||||
for _, s in ipairs(@systems) do
|
||||
s:add(e)
|
||||
end
|
||||
@entityCount += 1
|
||||
@onAdd(e)
|
||||
end
|
||||
end
|
||||
return ...
|
||||
end,
|
||||
--- Remove entities to the system and its subsystems.
|
||||
-- If you intend to call this on a subsystem instead of the world, please read the warning in :add.
|
||||
remove = :(...)
|
||||
for _, e in ipairs({...}) do
|
||||
if @filter(e) then
|
||||
let found = false
|
||||
if @_first == nil then
|
||||
return
|
||||
elseif @_first[1] == e then
|
||||
@_first = @_first[2]
|
||||
found = true
|
||||
else
|
||||
let entity = @_first
|
||||
while entity[2] ~= nil do
|
||||
if entity[2][1] == e then
|
||||
entity[2] = entity[2][2]
|
||||
found = true
|
||||
break
|
||||
end
|
||||
entity = entity[2]
|
||||
end
|
||||
end
|
||||
if found then
|
||||
for _, s in ipairs(@systems) do
|
||||
s:remove(e)
|
||||
end
|
||||
@entityCount -= 1
|
||||
@onRemove(e)
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
--- Returns an iterator that iterate through the entties in this system.
|
||||
iter = :()
|
||||
return nextEntity, { @_first }
|
||||
end,
|
||||
--- Remove every entity from the system and its subsystems.
|
||||
clear = :()
|
||||
for e in @iter() do
|
||||
@remove(e)
|
||||
end
|
||||
for _, s in ipairs(@systems) do
|
||||
s:clear()
|
||||
end
|
||||
end,
|
||||
--- Try to update the system and its subsystems. Should be called on every game update.
|
||||
update = :(dt)
|
||||
if @active then
|
||||
if @interval then
|
||||
@_waited += dt
|
||||
if @_waited < @interval then
|
||||
return
|
||||
end
|
||||
end
|
||||
for _, s in ipairs(@systems) do
|
||||
s:update(dt)
|
||||
end
|
||||
if @process ~= system_mt.process then
|
||||
for e in @iter() do
|
||||
@process(e, dt)
|
||||
end
|
||||
end
|
||||
@onUpdate(dt)
|
||||
if @interval then
|
||||
@_waited = 0
|
||||
end
|
||||
end
|
||||
end,
|
||||
--- Try to draw the system and its subsystems. Should be called on every game draw.
|
||||
draw = :()
|
||||
if @visible then
|
||||
for _, s in ipairs(@systems) do
|
||||
s:draw()
|
||||
end
|
||||
if @render ~= system_mt.render then
|
||||
for e in @iter() do
|
||||
@render(e)
|
||||
end
|
||||
end
|
||||
@onDraw()
|
||||
end
|
||||
end,
|
||||
--- Remove all the entities and subsystems in this system.
|
||||
destroy = :()
|
||||
recCallOnRemoveFromWorld(@world, { @ })
|
||||
recDestroySystems({ systems = { @ } })
|
||||
end
|
||||
}
|
||||
|
||||
--- Recursively instanciate a list of systems for a world:
|
||||
-- * create their self table with instance fields set
|
||||
-- * create a field with their name in world.s (if name defined)
|
||||
let recInstanciateSystems = (world, systems)
|
||||
let t = {}
|
||||
for _, s in ipairs(systems) do
|
||||
table.insert(t, setmetatable({
|
||||
systems = recInstanciateSystems(world, s.systems or {}),
|
||||
world = world,
|
||||
s = world.s
|
||||
}, {
|
||||
__index = :(k)
|
||||
if s[k] ~= nil then
|
||||
return s[k]
|
||||
else
|
||||
return system_mt[k]
|
||||
end
|
||||
end
|
||||
}))
|
||||
let system = t[#t]
|
||||
if s.name then
|
||||
world.s[s.name] = system
|
||||
end
|
||||
end
|
||||
return t
|
||||
end
|
||||
--- Recursively call :onAddToWorld to a list of systems in a world.
|
||||
let recCallOnAddToWorld = (world, systems)
|
||||
for _, s in ipairs(systems) do
|
||||
recCallOnAddToWorld(world, s.systems)
|
||||
s:onAddToWorld(world)
|
||||
end
|
||||
end
|
||||
|
||||
--- Create and returns a world system based on a list of systems.
|
||||
-- The systems will be instancied for this world.
|
||||
let world = (...)
|
||||
let world = setmetatable({
|
||||
filter = (e) return true end,
|
||||
s = {}
|
||||
}, { __index = system_mt })
|
||||
world.world = world
|
||||
world.systems = recInstanciateSystems(world, {...})
|
||||
recCallOnAddToWorld(world, world.systems)
|
||||
return world
|
||||
end
|
||||
|
||||
--- Returns a filter that returns true if, for every argument, a field with the same name exists in the entity.
|
||||
let all = (...)
|
||||
let l = {...}
|
||||
return function(s, e)
|
||||
for _, k in ipairs(l) do
|
||||
if e[k] == nil then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
--- Returns a filter that returns true if one of the arguments if the name of a field in the entity.
|
||||
let any = (...)
|
||||
let l = {...}
|
||||
return function(s, e)
|
||||
for _, k in ipairs(l) do
|
||||
if e[k] ~= nil then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
--- ECS module.
|
||||
return {
|
||||
world = world,
|
||||
all = all,
|
||||
any = any
|
||||
}
|
||||
1
ecs/init.lua
Normal file
1
ecs/init.lua
Normal file
|
|
@ -0,0 +1 @@
|
|||
return require((...)..".ecs")
|
||||
29
event.lua
29
event.lua
|
|
@ -1,29 +0,0 @@
|
|||
-- ubiquitousse.event
|
||||
local uqt = require((...):match("^(.-ubiquitousse)%."))
|
||||
local m = uqt.module
|
||||
|
||||
--- The events: callback functions that will be called when something interesting occurs.
|
||||
-- Theses are expected to be redefined in the game.
|
||||
-- For backend writers: if they already contain code, then this code has to be called on each call, even
|
||||
-- if the user manually redefines them.
|
||||
-- @usage -- in the game's code
|
||||
-- ubiquitousse.event.draw = function()
|
||||
-- ubiquitousse.draw.text(5, 5, "Hello world")
|
||||
-- end
|
||||
return {
|
||||
--- Called each time the game loop is ran. Don't draw here.
|
||||
-- @tparam number dt time since last call, in miliseconds
|
||||
-- @impl mixed
|
||||
update = function(dt)
|
||||
if m.input then uqt.input.update(dt) end
|
||||
if m.time then uqt.time.update(dt) end
|
||||
if m.scene then uqt.scene.update(dt) end
|
||||
end,
|
||||
|
||||
--- Called each time the game expect a new frame to be drawn.
|
||||
-- The screen is expected to be cleared since last frame.
|
||||
-- @impl backend
|
||||
draw = function()
|
||||
if m.scene then uqt.scene.draw() end
|
||||
end
|
||||
}
|
||||
123
init.lua
123
init.lua
|
|
@ -1,13 +1,45 @@
|
|||
-- ubiquitousse
|
||||
|
||||
--- Ubiquitousse Game Engine.
|
||||
-- Main module, containing the main things.
|
||||
-- The API exposed here is the Ubiquitousse API.
|
||||
-- It is as the name does not imply anymore abstract, and must be implemented in a backend, such as ubiquitousse.love.
|
||||
-- When required, this file will try to autodetect the engine it is running on, and load a correct backend.
|
||||
--- Ubiquitousse Game Framework.
|
||||
-- Main module, which will try to load every other Ubiquitousse module when required and provide a few convenience functions.
|
||||
--
|
||||
-- Ubiquitousse may or may not be used as a full game engine. You can delete the modules files you don't need and Ubiquitousse
|
||||
-- should adapt accordingly.
|
||||
-- Ubiquitousse may or may not be used in its totality. You can delete the modules directories you don't need and Ubiquitousse
|
||||
-- should adapt accordingly. You can also simply copy the modules directories you need and use them directly, without using this
|
||||
-- file at all.
|
||||
-- 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:
|
||||
-- * 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).
|
||||
-- * Some kind of filesystem.
|
||||
-- * Lua 5.1, 5.2, 5.3 or LuaJit.
|
||||
-- * Other requirement for specific modules should be described in the module's documentation.
|
||||
--
|
||||
-- Units used in the API documentation:
|
||||
-- * All distances are expressed in pixels (px)
|
||||
-- * All durations are expressed in milliseconds (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:
|
||||
-- * tabs for indentation, spaces for esthetic whitespace (notably in comments)
|
||||
-- * no globals
|
||||
-- * UPPERCASE for constants (or maybe not).
|
||||
-- * 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
|
||||
|
|
@ -20,50 +52,8 @@
|
|||
-- between the different versions, so it's up to you to handle that in your game (or ignore the problem and sticks to your
|
||||
-- main's backend Lua version).
|
||||
--
|
||||
-- Ubiquitousse's goal is to run everywhere with the least porting effort possible.
|
||||
-- To achieve this, the engine needs to stay simple, and only provide features that are almost sure to be
|
||||
-- available everywhere, so writing a backend should be straighforward.
|
||||
--
|
||||
-- However, a full Ubiquitousse backend still have a few requirement about the destination platform:
|
||||
-- * 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).
|
||||
-- * A 2D matrix graphic output with 32bit RGB color depth.
|
||||
-- * Inputs which match ubiquitousse.input.default (a pointing/4 direction input, a confirm button, and a cancel button).
|
||||
-- * Some way of measuring time with millisecond-precision.
|
||||
-- * Some kind of filesystem.
|
||||
-- * An available audio output would be preferable but optional.
|
||||
-- * Lua 5.1, 5.2, 5.3 or LuaJit.
|
||||
--
|
||||
-- Regarding data formats, Ubiquitousse implementations expect and recommend:
|
||||
-- * For images, PNG support is expected.
|
||||
-- * For audio files, OGG Vorbis support is expected.
|
||||
-- * For fonts, TTF support is expected.
|
||||
-- Theses formats are respected for the reference implementations, but Ubiquitousse may provide a script to
|
||||
-- automatically convert data formats from a project at some point.
|
||||
--
|
||||
-- Units used in the API:
|
||||
-- * All distances are expressed in pixels (px)
|
||||
-- * All durations are expressed in milliseconds (ms)
|
||||
--
|
||||
-- Style:
|
||||
-- * tabs for indentation, spaces for esthetic whitespace (notably in comments)
|
||||
-- * no globals
|
||||
-- * UPPERCASE for constants (or maybe not).
|
||||
-- * 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.
|
||||
--
|
||||
-- Some Ubiquitousse modules require parts of other modules to work. Because every module should work when all the others are
|
||||
-- disabled, the backend may need to provide defaults values for a few fields in disabled modules required by an enabled one.
|
||||
-- Thoses fields are indicated with "@requiredby module" annotations.
|
||||
--
|
||||
-- Regarding the documentation: Ubiquitousse used LDoc/LuaDoc styled-comments, but since LDoc hates me and my code, the
|
||||
-- generated result is complete garbage, so please read the documentation directly in the comments here.
|
||||
-- generated result is complete garbage, so please read the documentation directly in the comments here until fix this.
|
||||
-- Stuff you're interested in starts with triple - (e.g., "--- This functions saves the world").
|
||||
--
|
||||
-- @usage local ubiquitousse = require("ubiquitousse")
|
||||
|
|
@ -76,34 +66,31 @@ ubiquitousse = {
|
|||
-- @impl ubiquitousse
|
||||
version = "0.0.1",
|
||||
|
||||
--- Table of enabled modules.
|
||||
-- @impl ubiquitousse
|
||||
module = {
|
||||
time = false,
|
||||
draw = false,
|
||||
audio = false,
|
||||
input = false,
|
||||
scene = false,
|
||||
event = false,
|
||||
asset = false,
|
||||
util = false
|
||||
},
|
||||
--- Should be called each time the game loop is ran; will update every loaded Ubiquitousse module that needs it.
|
||||
-- @tparam number dt time since last call, in miliseconds
|
||||
-- @impl mixed
|
||||
update = function(dt)
|
||||
if ubiquitousse.time then ubiquitousse.time.update(dt) end
|
||||
if ubiquitousse.scene then ubiquitousse.scene.update(dt) end
|
||||
if ubiquitousse.input then ubiquitousse.input.update(dt) end
|
||||
end,
|
||||
|
||||
--- Backend name.
|
||||
-- For consistency, only use lowercase letters [a-z] (no special char)
|
||||
-- @impl backend
|
||||
backend = "unknown"
|
||||
--- Should be called each time the game expect a new frame to be drawn; will draw every loaded Ubiquitousse module that needs it
|
||||
-- The screen is expected to be cleared since last frame.
|
||||
-- @impl mixed
|
||||
draw = function()
|
||||
if ubiquitousse.scene then ubiquitousse.scene.draw() 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
|
||||
|
||||
-- Require external submodules
|
||||
for m in pairs(ubiquitousse.module) do
|
||||
for _, m in ipairs{"asset", "ecs", "input", "scene", "time", "util"} do
|
||||
local s, t = pcall(require, p.."."..m)
|
||||
if s then
|
||||
ubiquitousse[m] = t
|
||||
ubiquitousse.module[m] = true
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
489
input.lua
489
input.lua
|
|
@ -1,489 +0,0 @@
|
|||
-- ubiquitousse.input
|
||||
local uqt = require((...):match("^(.-ubiquitousse)%."))
|
||||
|
||||
--- 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 = {}
|
||||
|
||||
--- Input stuff
|
||||
-- Inspired by Tactile by Andrew Minnich (https://github.com/tesselode/tactile).
|
||||
-- I don't think I need to include the license since it's just inspiration, but for the sake of information, Tactile is under the MIT license.
|
||||
-- Ubiquitousse considers two input methods, called buttons (binary input) and axes (analog input).
|
||||
local input
|
||||
input = {
|
||||
---------------------------------
|
||||
--- Detectors (input sources) ---
|
||||
---------------------------------
|
||||
|
||||
-- Buttons detectors --
|
||||
-- A button detector is a function which returns true (pressed) or false (unpressed).
|
||||
-- Any fuction which returns a boolean can be used as a button detector, but you will probably want to get the data from an HID.
|
||||
-- Ubiquitousse being platform-agnostic, it doesn't suppose there is a keyboard and mouse available for example, and all HID buttons are identified using
|
||||
-- an identifier string, which depends of the backend. Identifier strings should be of 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(s) from the identifier(s) string.
|
||||
-- The function may error if the identifier is incorrect.
|
||||
-- @tparam string button identifier, depends on the platform Ubiquitousse is running on (multiple parameters)
|
||||
-- @treturn each button detector (multiple-returns)
|
||||
-- @impl backend
|
||||
buttonDetector = function(...) end,
|
||||
|
||||
-- Axis detectors --
|
||||
-- Similar to buttons detectors, but returns a number between -1 and 1.
|
||||
-- Threshold value can be used similarly with %.
|
||||
-- Axis detectors should support "binary axis", ie an axis 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 should have an identifier like "button1,button2" (comma-separated).
|
||||
-- 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(s) from the identifier(s) string.
|
||||
-- @tparam string axis identifier, depends on the platform Ubiquitousse is running on (multiple parameters)
|
||||
-- @treturn each axis detector (multiple-returns)
|
||||
-- @impl backend
|
||||
axisDetector = function(...) 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 -- object
|
||||
local detectors = {} -- detectors list
|
||||
local state = "none" -- current state (none, pressed, down, released)
|
||||
local function update() -- update button state
|
||||
if not updated[r] then
|
||||
local down = false
|
||||
for _,d in pairs(detectors) do
|
||||
if d() then
|
||||
down = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if down then
|
||||
if state == "none" or state == "released" then
|
||||
state = "pressed"
|
||||
else
|
||||
state = "down"
|
||||
end
|
||||
else
|
||||
if state == "down" or state == "pressed" then
|
||||
state = "released"
|
||||
else
|
||||
state = "none"
|
||||
end
|
||||
end
|
||||
updated[r] = true
|
||||
end
|
||||
end
|
||||
-- Object
|
||||
r = {
|
||||
--- Returns a new ButtonInput with the same properties.
|
||||
-- @treturn ButtonInput the cloned object
|
||||
clone = function(self)
|
||||
local clone = input.button()
|
||||
for name, detector in pairs(detectors) do
|
||||
if type(name) == "string" then
|
||||
clone:bind(name)
|
||||
else
|
||||
clone:bind(detector)
|
||||
end
|
||||
end
|
||||
return clone
|
||||
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
|
||||
if type(d) == "string" then
|
||||
detectors[d] = input.buttonDetector(d)
|
||||
elseif type(d) == "function" then
|
||||
detectors[d] = d
|
||||
else
|
||||
error("Not a valid button detector")
|
||||
end
|
||||
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
|
||||
detectors[d] = nil
|
||||
end
|
||||
return self
|
||||
end,
|
||||
|
||||
--- Returns true if the input was just pressed.
|
||||
-- @treturn boolean true if the input was pressed, false otherwise
|
||||
pressed = function(self)
|
||||
update()
|
||||
return state == "pressed"
|
||||
end,
|
||||
--- Returns true if the input is down.
|
||||
-- @treturn boolean true if the input is currently down, false otherwise
|
||||
down = function(self)
|
||||
update()
|
||||
return state == "down" or state == "pressed"
|
||||
end,
|
||||
--- Returns true if the input was just released.
|
||||
-- @treturn boolean true if the input was released, false otherwise
|
||||
released = function(self)
|
||||
update()
|
||||
return state == "released"
|
||||
end,
|
||||
}
|
||||
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 th threshold will be ignored.
|
||||
-- @tparam AxisDetectors ... all the axis detectors or axis identifiers
|
||||
-- @tretrun AxisInput the object
|
||||
-- @impl ubiquitousse
|
||||
axis = function(...)
|
||||
local r -- object
|
||||
local detectors = {} -- detectors list
|
||||
local value, raw, max = 0, 0, 1 -- current value between -1 and 1, raw value between -max and +max and maximum for raw values
|
||||
local threshold = 0.5 -- ie., the deadzone
|
||||
local function update() -- update axis state
|
||||
if not updated[r] then
|
||||
value = 0
|
||||
for _,d in pairs(detectors) do
|
||||
local v, r, m = d() -- v[-1,1], r[-m,+m]
|
||||
if math.abs(v) > math.abs(value) then
|
||||
value, raw, max = v, r or v, m or 1
|
||||
end
|
||||
end
|
||||
updated[r] = true
|
||||
end
|
||||
end
|
||||
-- Object
|
||||
r = {
|
||||
--- Returns a new AxisInput with the same properties.
|
||||
-- @treturn AxisInput the cloned object
|
||||
clone = function(self)
|
||||
local clone = input.axis()
|
||||
for name, detector in pairs(detectors) do
|
||||
if type(name) == "string" then
|
||||
clone:bind(name)
|
||||
else
|
||||
clone:bind(detector)
|
||||
end
|
||||
end
|
||||
clone:threshold(threshold)
|
||||
return clone
|
||||
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
|
||||
if type(d) == "string" then
|
||||
detectors[d] = input.axisDetector(d)
|
||||
elseif type(d) == "function" then
|
||||
detectors[d] = d
|
||||
else
|
||||
error("Not a valid axis detector")
|
||||
end
|
||||
end
|
||||
return self
|
||||
end,
|
||||
--- Unbind AxisDetector(s).
|
||||
-- @tparam AxisDetectors ... axis detectors or axis identifiers to remove
|
||||
-- @treturn AxisInput this AxisInput object
|
||||
unbind = function(self, ...)
|
||||
for _,d in ipairs({...}) do
|
||||
detectors[d] = nil
|
||||
end
|
||||
return self
|
||||
end,
|
||||
|
||||
--- Sets the default detection threshold (deadzone).
|
||||
-- @tparam number new the new detection threshold
|
||||
-- @treturn AxisInput this AxisInput object
|
||||
threshold = function(self, new)
|
||||
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)
|
||||
update()
|
||||
return math.abs(value) > math.abs(curThreshold or threshold) and value or 0
|
||||
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)
|
||||
update()
|
||||
return math.abs(raw) > math.abs(rawThreshold or threshold*max) and raw or 0
|
||||
end,
|
||||
--- Return the raw max of the input.
|
||||
-- @treturn number the input raw max
|
||||
max = function(self)
|
||||
update()
|
||||
return max
|
||||
end,
|
||||
|
||||
--- The associated button pressed when the axis reaches a positive value.
|
||||
positive = input.button(function() return r:value() > 0 end),
|
||||
--- The associated button pressed when the axis reaches a negative value.
|
||||
negative = input.button(function() return r:value() < 0 end)
|
||||
}
|
||||
r:bind(...)
|
||||
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 draw = uqt.draw -- requires width and height
|
||||
local pointers = {} -- pointers list
|
||||
local x, y = 0, 0 -- pointer position
|
||||
local width, height = 1, 1 -- half-dimensions of the movement area
|
||||
local offsetX, offsetY = 0, 0 -- offsets
|
||||
local xSpeed, ySpeed = 1, 1 -- speed (pixels/milisecond); for relative mode
|
||||
local r -- object
|
||||
local function update()
|
||||
if not updated[r] then
|
||||
local width, height = width or draw.width/2, height or draw.height/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(pointers) 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 * uqt.time.dt) or xAxis:raw())
|
||||
maxMovX = movX
|
||||
end
|
||||
if movY > maxMovY then
|
||||
newY = y + (ySpeed and (yAxis:value() * ySpeed * uqt.time.dt) or yAxis:raw())
|
||||
maxMovY = movY
|
||||
end
|
||||
elseif mode == "absolute" then
|
||||
if not pointer.previous then pointer.previous = { x = xAxis:value(), y = yAxis:value() } end -- last frame position (to calculate movement/delta)
|
||||
local movX, movY = math.abs(xAxis:value() - pointer.previous.x), math.abs(yAxis:value() - pointer.previous.y)
|
||||
pointer.previous = { x = xAxis:value(), y = yAxis:value() }
|
||||
if movX > maxMovX then
|
||||
newX = xAxis:value() * width
|
||||
maxMovX = movX
|
||||
end
|
||||
if movY > maxMovY then
|
||||
newY = yAxis:value() * height
|
||||
maxMovY = movY
|
||||
end
|
||||
end
|
||||
end
|
||||
x, y = 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[r] = true
|
||||
end
|
||||
end
|
||||
r = {
|
||||
--- Returns a new PointerInput with the same properties.
|
||||
-- @treturn PointerInput the cloned object
|
||||
clone = function(self)
|
||||
return input.pointer(unpack(pointers))
|
||||
:dimensions(width, height)
|
||||
:offset(offsetX, offsetY)
|
||||
:speed(xSpeed, 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
|
||||
if type(p[2]) == "string" then p[2] = input.axis(input.axisDetector(p[2])) end
|
||||
if type(p[3]) == "string" then p[3] = input.axis(input.axisDetector(p[3])) end
|
||||
table.insert(pointers, p)
|
||||
else
|
||||
error("Pointer must be a table")
|
||||
end
|
||||
end
|
||||
return self
|
||||
end,
|
||||
--- Unbind axis couples.
|
||||
-- @tparam table{mode,XAxis,YAxis} ... couples of axis detectors, axis identifiers or axis input to remove
|
||||
-- @treturn PointerInput this PointerInput object
|
||||
unbind = function(self, ...)
|
||||
for _,p in ipairs({...}) do
|
||||
for i,pointer in ipairs(pointers) do
|
||||
if pointer == p then
|
||||
table.remove(pointers, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return self
|
||||
end,
|
||||
|
||||
--- 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)
|
||||
width, 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)
|
||||
offsetX, 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)
|
||||
xSpeed, ySpeed = newXSpeed, newYSpeed or newXSpeed
|
||||
return self
|
||||
end,
|
||||
|
||||
--- Returns the current X value of the pointer.
|
||||
-- @treturn number X value
|
||||
x = function()
|
||||
update()
|
||||
return x + (offsetX or width or draw.width/2)
|
||||
end,
|
||||
--- Returns the current Y value of the pointer.
|
||||
-- @treturn number Y value
|
||||
y = function()
|
||||
update()
|
||||
return y + (offsetY or height or draw.height/2)
|
||||
end,
|
||||
|
||||
--- The associated horizontal axis.
|
||||
horizontal = input.axis(function()
|
||||
local h = r:x()
|
||||
return h/width, h, width
|
||||
end),
|
||||
--- The associated vertical axis.
|
||||
vertical = input.axis(function()
|
||||
local v = r:y()
|
||||
return v/height, v, height
|
||||
end),
|
||||
|
||||
--- 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
|
||||
}
|
||||
r.right, r.left = r.horizontal.positive, r.horizontal.negative
|
||||
r.up, r.down = r.vertical.negative, r.vertical.positive
|
||||
r:bind(...)
|
||||
return r
|
||||
end,
|
||||
|
||||
------------------------------
|
||||
--- Input detection helpers --
|
||||
------------------------------
|
||||
|
||||
--- 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.
|
||||
-- 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.
|
||||
},
|
||||
|
||||
--- Update all the Inputs.
|
||||
-- Supposed to be called in ubiquitousse.event.update.
|
||||
-- The backend can hook into this function to to its input-related updates.
|
||||
-- @tparam numder dt the delta-time
|
||||
-- @impl ubiquitousse
|
||||
update = function(dt)
|
||||
updated = {}
|
||||
end
|
||||
}
|
||||
|
||||
-- Create default inputs
|
||||
input.default.pointer = input.pointer()
|
||||
input.default.confirm = input.button()
|
||||
input.default.cancel = input.button()
|
||||
|
||||
-- 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
|
||||
|
||||
return input
|
||||
269
input/backend/ctrulua.lua
Normal file
269
input/backend/ctrulua.lua
Normal 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
319
input/backend/love.lua
Normal 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
14
input/init.lua
Normal 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
711
input/input.lua
Normal 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
|
||||
1
scene/init.lua
Normal file
1
scene/init.lua
Normal file
|
|
@ -0,0 +1 @@
|
|||
return require((...)..".scene")
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
-- ubiquitousse.scene
|
||||
local uqt = require((...):match("^(.-ubiquitousse)%."))
|
||||
local m = uqt.module
|
||||
--- ubiquitousse.scene
|
||||
-- Optional dependencies: ubiquitousse.time (to provide each scene a time registry)
|
||||
local loaded, time = pcall(require, (...):match("^.-ubiquitousse%.").."time")
|
||||
if not loaded then time = nil end
|
||||
|
||||
--- Scene management.
|
||||
-- You can use use scenes to seperate the different states of your game: for example, a menu scene and a game scene.
|
||||
|
|
@ -14,6 +15,7 @@ local m = uqt.module
|
|||
-- of this is that you can load assets, libraries, etc. outside of the enter callback, so they can be cached and not reloaded each time
|
||||
-- the scene is entered, but all the other scene initialization should be done in the enter callback, since it won't be executed on
|
||||
-- each enter otherwise.
|
||||
-- FIXME: actually more useful to never cache?
|
||||
-- The expected code-organisation is:
|
||||
-- * each scene is in a file, identified by its module name (same identifier used by Lua's require)
|
||||
-- * each scene file create a new scene table using ubiquitousse.scene.new and returns it at the end of the file
|
||||
|
|
@ -26,26 +28,42 @@ scene = setmetatable({
|
|||
-- @impl ubiquitousse
|
||||
current = nil,
|
||||
|
||||
--- Shortcut for scene.current.time.
|
||||
-- @impl ubiquitousse
|
||||
time = nil,
|
||||
|
||||
--- The scene stack: list of scene, from the farest one to the nearest.
|
||||
-- @impl ubiquitousse
|
||||
stack = {},
|
||||
|
||||
--- A prefix for scene modules names
|
||||
--- 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 = "",
|
||||
prefix = "scene.",
|
||||
|
||||
--- Function which load a scene file
|
||||
--- Function which load a scene file.
|
||||
-- @impl ubiquitousse
|
||||
load = function(scenePath)
|
||||
load = function(sceneModule)
|
||||
local scenePath = sceneModule:gsub("%.", "/")
|
||||
for path in package.path:gmatch("[^;]+") do
|
||||
path = path:gsub("%?", (scenePath:gsub("%.", "/")))
|
||||
path = path:gsub("%?", scenePath)
|
||||
local f = io.open(path)
|
||||
if f then
|
||||
f:close()
|
||||
return dofile(path)
|
||||
end
|
||||
end
|
||||
error("can't find scene "..tostring(scenePath))
|
||||
if package.loaded["candran"] then -- Candran support
|
||||
for path in package.path:gsub("%.lua", ".can"):gmatch("[^;]+") do
|
||||
path = path:gsub("%?", scenePath)
|
||||
local f = io.open(path)
|
||||
if f then
|
||||
f:close()
|
||||
return package.loaded["candran"].dofile(path)
|
||||
end
|
||||
end
|
||||
end
|
||||
error("can't find scene "..tostring(sceneModule))
|
||||
end,
|
||||
|
||||
--- Creates and returns a new Scene object.
|
||||
|
|
@ -55,7 +73,7 @@ scene = setmetatable({
|
|||
return {
|
||||
name = name or "unamed", -- The scene name.
|
||||
|
||||
time = m.time and uqt.time.new(), -- Scene-specific TimerRegistry, if uqt.time is enabled.
|
||||
time = time and time.new(), -- Scene-specific TimerRegistry, if uqt.time is available.
|
||||
|
||||
enter = function(self, ...) end, -- Called when entering a scene.
|
||||
exit = function(self) end, -- Called when exiting a scene, and not expecting to come back (scene may be unloaded).
|
||||
|
|
@ -63,22 +81,24 @@ scene = setmetatable({
|
|||
suspend = function(self) end, -- Called when suspending a scene, and expecting to come back (scene won't be unloaded).
|
||||
resume = function(self) end, -- Called when resuming a suspended scene (after calling suspend).
|
||||
|
||||
update = function(self, dt, ...) end, -- Called on each ubiquitousse.event.update on the current scene.
|
||||
draw = function(self, ...) end -- Called on each ubiquitousse.event.draw on the current scene.
|
||||
update = function(self, dt, ...) end, -- Called on each update on the current scene.
|
||||
draw = function(self, ...) end -- Called on each draw on the current scene.
|
||||
}
|
||||
end,
|
||||
-- TODO: handle love.quit / exit all scenes in stack
|
||||
|
||||
--- Switch to a new scene.
|
||||
-- The new scene will be loaded and the current scene will be replaced by the new one,
|
||||
-- then the previous scene exit function will be called, then the enter callback is called on the new scence.
|
||||
-- Then the stack is changed to replace the old scene with the new one.
|
||||
-- @tparam string scenePath the new scene module name
|
||||
-- @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 = scene.load(scene.prefix..scenePath)
|
||||
scene.current.name = scene.current.name or scenePath
|
||||
scene.current = type(scenePath) == "string" and scene.load(scene.prefix..scenePath) or scenePath
|
||||
scene.time = scene.current.time
|
||||
scene.current.name = scene.current.name or tostring(scenePath)
|
||||
if previous then previous:exit() end
|
||||
scene.current:enter(...)
|
||||
scene.stack[math.max(#scene.stack, 1)] = scene.current
|
||||
|
|
@ -88,13 +108,14 @@ scene = setmetatable({
|
|||
-- Similar to ubiquitousse.scene.switch, except suspend is called on the current scene instead of exit,
|
||||
-- and the current scene is not replaced: when the new scene call ubiquitousse.scene.pop, the old scene
|
||||
-- will be reused.
|
||||
-- @tparam string scenePath the new scene module name
|
||||
-- @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 = scene.load(scene.prefix..scenePath)
|
||||
scene.current.name = scene.current.name or scenePath
|
||||
scene.current = type(scenePath) == "string" and scene.load(scene.prefix..scenePath) or scenePath
|
||||
scene.time = scene.current.time
|
||||
scene.current.name = scene.current.name or tostring(scenePath)
|
||||
if previous then previous:suspend() end
|
||||
scene.current:enter(...)
|
||||
table.insert(scene.stack, scene.current)
|
||||
|
|
@ -107,25 +128,26 @@ scene = setmetatable({
|
|||
pop = function()
|
||||
local previous = scene.current
|
||||
scene.current = scene.stack[#scene.stack-1]
|
||||
scene.time = scene.current.time
|
||||
if previous then previous:exit() end
|
||||
if scene.current then scene.current:resume() end
|
||||
table.remove(scene.stack)
|
||||
end,
|
||||
|
||||
--- Update the current scene.
|
||||
-- Should be called in ubiquitousse.event.update.
|
||||
-- Should be called at every game update; called by ubiquitousse.update.
|
||||
-- @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 m.time then scene.current.time.update(dt) end
|
||||
if time then scene.current.time:update(dt) end
|
||||
scene.current:update(dt, ...)
|
||||
end
|
||||
end,
|
||||
|
||||
--- Draw the current scene.
|
||||
-- Should be called in ubiquitousse.event.draw.
|
||||
-- Should be called every time the game is draw; called by ubiquitousse.draw.
|
||||
-- @param ... arguments to pass to the scene's draw function
|
||||
-- @impl ubiquitousse
|
||||
draw = function(...)
|
||||
333
time.lua
333
time.lua
|
|
@ -1,333 +0,0 @@
|
|||
-- ubiquitousse.time
|
||||
local ease = require((...):match("^(.-ubiquitousse)%.")..".lib.easing")
|
||||
|
||||
--- Returns true if all the values in the list are true ; functions in the list will be called and the test will be performed on their return value.
|
||||
-- Returns default if the list is empty.
|
||||
local function all(list, default)
|
||||
if #list == 0 then
|
||||
return default
|
||||
else
|
||||
local r = true
|
||||
for _,v in ipairs(list) do
|
||||
if type(v) == "function" then
|
||||
r = r and v()
|
||||
else
|
||||
r = r and v
|
||||
end
|
||||
end
|
||||
return r
|
||||
end
|
||||
end
|
||||
|
||||
--- Time related functions
|
||||
local function newTimerRegistry()
|
||||
--- Used to store all the functions delayed with ubiquitousse.time.delay
|
||||
-- The default implementation use the structure {<key: function> = <value: data table>, ...}
|
||||
-- This table is for internal use and shouldn't be used from an external script.
|
||||
local delayed = {}
|
||||
|
||||
-- Used to calculate the deltatime
|
||||
local lastTime
|
||||
|
||||
local registry
|
||||
registry = {
|
||||
--- Creates and return a new TimerRegistry.
|
||||
-- A TimerRegistry is a separate ubiquitousse.time instance: its TimedFunctions will be independant
|
||||
-- from the one registered using ubiquitousse.time.run (the global TimerRegistry). If you use the scene
|
||||
-- system, a scene-specific TimerRegistry is available at ubiquitousse.scene.current.time.
|
||||
-- @impl ubiquitousse
|
||||
new = function()
|
||||
local new = newTimerRegistry()
|
||||
new.get = registry.get
|
||||
return new
|
||||
end,
|
||||
|
||||
--- Returns the number of miliseconds elapsed since some point in time.
|
||||
-- This point is fixed but undetermined, so this function should only be used to calculate durations.
|
||||
-- Should at least have millisecond-precision, but can be more precise if available.
|
||||
-- @impl backend
|
||||
get = function() end,
|
||||
|
||||
--- Update all the TimedFunctions calls.
|
||||
-- Supposed to be called in ubiquitousse.event.update.
|
||||
-- @tparam[opt=calculate here] number dt the delta-time (time spent since last time the function was called) (miliseconds)
|
||||
-- @impl ubiquitousse
|
||||
update = function(dt)
|
||||
if dt then
|
||||
registry.dt = dt
|
||||
else
|
||||
if lastTime then
|
||||
local newTime = registry.get()
|
||||
registry.dt = newTime - lastTime
|
||||
lastTime = newTime
|
||||
else
|
||||
lastTime = registry.get()
|
||||
end
|
||||
end
|
||||
|
||||
local done = {} -- functions done running
|
||||
|
||||
local d = delayed
|
||||
for func, t in pairs(d) do
|
||||
if t and all(t.initWhen, true) then
|
||||
t.initWhen = {}
|
||||
local co = t.coroutine
|
||||
t.after = t.after - dt
|
||||
if t.forceStart or (t.after <= 0 and all(t.startWhen, true)) then
|
||||
t.startWhen = {}
|
||||
d[func] = false -- niling here cause the next pair iteration to error
|
||||
table.insert(done, func)
|
||||
if not co then
|
||||
co = coroutine.create(func)
|
||||
t.coroutine = co
|
||||
t.started = registry.get()
|
||||
if t.times > 0 then t.times = t.times - 1 end
|
||||
for _, f in ipairs(t.onStart) do f(t.object) end
|
||||
end
|
||||
assert(coroutine.resume(co, function(delay)
|
||||
t.after = delay or 0
|
||||
d[func] = t
|
||||
coroutine.yield()
|
||||
end, dt))
|
||||
for _, f in ipairs(t.onUpdate) do f(t.object) end
|
||||
if all(t.stopWhen, false) then t.forceStop = true end
|
||||
if t.forceStop or coroutine.status(co) == "dead" then
|
||||
if t.forceStop
|
||||
or (t.during >= 0 and t.started + t.during < registry.get())
|
||||
or (t.times == 0)
|
||||
or (not all(t.repeatWhile, true))
|
||||
or (t.every == -1 and t.times == -1 and t.during == -1 and #t.repeatWhile == 0) -- no repeat
|
||||
then
|
||||
for _, f in ipairs(t.onEnd) do f(t.object) end
|
||||
else
|
||||
if t.times > 0 then t.times = t.times - 1 end
|
||||
t.after = t.every
|
||||
t.coroutine = coroutine.create(func)
|
||||
d[func] = t
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, func in ipairs(done) do
|
||||
if not d[func] then
|
||||
d[func] = nil
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
--- Schedule a function to run.
|
||||
-- The function will receive as first parameter the wait(time) function, which will pause the function execution for time miliseconds.
|
||||
-- @tparam[opt] function func the function to schedule
|
||||
-- @treturn TimedFunction the object
|
||||
-- @impl ubiquitousse
|
||||
run = function(func)
|
||||
-- Creates empty function (the TimedFunction may be used for time measure or stuff like that which doesn't need a specific function)
|
||||
func = func or function() end
|
||||
|
||||
-- Since delayed functions can end in any order, it doesn't really make sense to use a integer-keyed list.
|
||||
-- Using the function as the key works and it's unique.
|
||||
delayed[func] = {
|
||||
object = nil,
|
||||
coroutine = nil,
|
||||
started = 0,
|
||||
|
||||
after = -1,
|
||||
every = -1,
|
||||
times = -1,
|
||||
during = -1,
|
||||
|
||||
initWhen = {},
|
||||
startWhen = {},
|
||||
repeatWhile = {},
|
||||
stopWhen = {},
|
||||
|
||||
forceStart = false,
|
||||
forceStop = false,
|
||||
|
||||
onStart = {},
|
||||
onUpdate = {},
|
||||
onEnd = {}
|
||||
}
|
||||
|
||||
local t = delayed[func] -- internal data
|
||||
local r -- external interface
|
||||
r = {
|
||||
--- Timed conditions ---
|
||||
--- Wait time milliseconds before running the function.
|
||||
after = function(_, time)
|
||||
t.after = time
|
||||
return r
|
||||
end,
|
||||
--- Run the function every time millisecond.
|
||||
every = function(_, time)
|
||||
t.every = time
|
||||
return r
|
||||
end,
|
||||
--- The function will not execute more than count times.
|
||||
times = function(_, count)
|
||||
t.times = count
|
||||
return r
|
||||
end,
|
||||
--- The TimedFunction will be active for a time duration.
|
||||
during = function(_, time)
|
||||
t.during = time
|
||||
return r
|
||||
end,
|
||||
|
||||
--- Function conditions ---
|
||||
--- Starts the function execution when func() returns true. Checked before the "after" condition,
|
||||
-- meaning the "after" countdown starts when func() returns true.
|
||||
-- If multiple init functions are added, init will trigger only when all of them returns true.
|
||||
initWhen = function(_, func)
|
||||
table.insert(t.initWhen, func)
|
||||
return r
|
||||
end,
|
||||
--- Starts the function execution when func() returns true. Checked after the "after" condition.
|
||||
-- If multiple start functions are added, start will trigger only when all of them returns true.
|
||||
startWhen = function(_, func)
|
||||
table.insert(t.startWhen, func)
|
||||
return r
|
||||
end,
|
||||
--- When the functions ends, the execution won't stop and will repeat as long as func() returns true.
|
||||
-- Will cancel timed repeat conditions if false but needs other timed repeat conditions to be true to create a new repeat.
|
||||
-- If multiple repeat functions are added, a repeat will trigger only when all of them returns true.
|
||||
repeatWhile = function(_, func)
|
||||
table.insert(t.repeatWhile, func)
|
||||
return r
|
||||
end,
|
||||
--- Stops the function execution when func() returns true. Checked before all timed conditions.
|
||||
-- If multiple stop functions are added, stop will trigger only when all of them returns true.
|
||||
stopWhen = function(_, func)
|
||||
table.insert(t.stopWhen, func)
|
||||
return r
|
||||
end,
|
||||
|
||||
--- Conditions override ---
|
||||
--- Force the function to start its execution.
|
||||
start = function(_)
|
||||
t.forceStart = true
|
||||
return r
|
||||
end,
|
||||
--- Force the function to stop its execution.
|
||||
stop = function(_)
|
||||
t.forceStop = true
|
||||
return r
|
||||
end,
|
||||
|
||||
--- Callbacks functions ---
|
||||
--- Will execute func(self) when the function execution start.
|
||||
onStart = function(_, func)
|
||||
table.insert(t.onStart, func)
|
||||
return r
|
||||
end,
|
||||
--- Will execute func(self) each frame the main function is run..
|
||||
onUpdate = function(_, func)
|
||||
table.insert(t.onUpdate, func)
|
||||
return r
|
||||
end,
|
||||
--- Will execute func(self) when the function execution end.
|
||||
onEnd = function(_, func)
|
||||
table.insert(t.onEnd, func)
|
||||
return r
|
||||
end,
|
||||
|
||||
--- Chaining ---
|
||||
--- Creates another TimedFunction which will be initialized when the current one ends.
|
||||
-- Returns the new TimedFunction.
|
||||
chain = function(_, func)
|
||||
local done = false
|
||||
r:onEnd(function() done = true end)
|
||||
return registry.run(func)
|
||||
:initWhen(function() return done end)
|
||||
end
|
||||
}
|
||||
t.object = r
|
||||
return r
|
||||
end,
|
||||
|
||||
--- Tween some numeric values.
|
||||
-- @tparam number duration tween duration (miliseconds)
|
||||
-- @tparam table tbl the table containing the values to tween
|
||||
-- @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 TimedFunction 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
|
||||
|
||||
local time = 0 -- tweening time elapsed
|
||||
local from = {} -- initial state
|
||||
|
||||
local function update(tbl_, from_, to_) -- apply the method to tbl_ recursively (doesn't handle cycles)
|
||||
for k, v in pairs(to_) do
|
||||
if type(v) == "table" then
|
||||
update(tbl_[k], from_[k], to_[k])
|
||||
else
|
||||
if time < duration then
|
||||
tbl_[k] = method(time, from_[k], v - from_[k], duration)
|
||||
else
|
||||
tbl_[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local r = registry.run(function(wait, dt)
|
||||
time = time + dt
|
||||
update(tbl, from, to)
|
||||
end):during(duration)
|
||||
:onStart(function()
|
||||
local function copy(stencil, source, dest) -- copy initial state recursively
|
||||
for k, v in pairs(stencil) do
|
||||
if type(v) == "table" then
|
||||
if not dest[k] then dest[k] = {} end
|
||||
copy(stencil[k], source[k], dest[k])
|
||||
else
|
||||
dest[k] = source[k]
|
||||
end
|
||||
end
|
||||
end
|
||||
copy(to, tbl, from)
|
||||
end)
|
||||
|
||||
--- Creates another tween which will be initialized when the current one ends.
|
||||
-- If tbl_ and/or method_ are not specified, the values from the current tween will be used.
|
||||
-- Returns the new tween.
|
||||
r.chain = function(_, duration_, tbl_, to_, method_)
|
||||
if not method_ and to_ then
|
||||
if type(to_) == "string" then
|
||||
tbl_, to_, method_ = tbl, tbl_, to_
|
||||
else
|
||||
method_ = method
|
||||
end
|
||||
elseif not method_ and not to_ then
|
||||
tbl_, to_, method_ = tbl, tbl_, method
|
||||
end
|
||||
|
||||
local done = false
|
||||
r:onEnd(function() done = true end)
|
||||
return registry.tween(duration_, tbl_, to_, method_)
|
||||
:initWhen(function() return done end)
|
||||
end
|
||||
|
||||
return r
|
||||
end,
|
||||
|
||||
--- Cancels all the running TimedFunctions.
|
||||
-- @impl ubiquitousse
|
||||
clear = function()
|
||||
delayed = {}
|
||||
end,
|
||||
|
||||
--- Time since last update (miliseconds).
|
||||
-- @impl ubiquitousse
|
||||
dt = 0
|
||||
}
|
||||
|
||||
return registry
|
||||
end
|
||||
|
||||
return newTimerRegistry()
|
||||
6
time/backend/ctrulua.lua
Normal file
6
time/backend/ctrulua.lua
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
local time = require((...):match("^(.-%.)backend").."time")
|
||||
local ctr = require("ctr")
|
||||
|
||||
time.get = ctr.time
|
||||
|
||||
return time
|
||||
7
time/backend/love.lua
Normal file
7
time/backend/love.lua
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
local time = require((...):match("^(.-%.)backend").."time")
|
||||
|
||||
time.get = function()
|
||||
return love.timer.getTime() * 1000
|
||||
end
|
||||
|
||||
return time
|
||||
14
time/init.lua
Normal file
14
time/init.lua
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
local time
|
||||
|
||||
local p = ...
|
||||
if love then
|
||||
time = require(p..".backend.love")
|
||||
elseif package.loaded["ctr"] then
|
||||
time = require(p..".backend.ctrulua")
|
||||
elseif package.loaded["libretro"] then
|
||||
error("NYI")
|
||||
else
|
||||
error("no backend for ubiquitousse.time")
|
||||
end
|
||||
|
||||
return time
|
||||
350
time/time.lua
Normal file
350
time/time.lua
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
--- ubiquitousse.time
|
||||
-- Depends on a backend.
|
||||
|
||||
local ease = require((...):match("^.-time")..".easing")
|
||||
local time
|
||||
|
||||
--- Returns true if all the values in the list are true ; functions in the list will be called and the test will be performed on their return value.
|
||||
-- Returns default if the list is empty.
|
||||
local function all(list, default)
|
||||
if #list == 0 then
|
||||
return default
|
||||
else
|
||||
local r = true
|
||||
for _,v in ipairs(list) do
|
||||
if type(v) == "function" then
|
||||
r = r and v()
|
||||
else
|
||||
r = r and v
|
||||
end
|
||||
end
|
||||
return r
|
||||
end
|
||||
end
|
||||
|
||||
--- Registry methods.
|
||||
local registry_mt = {
|
||||
--- Update all the TimedFunctions calls.
|
||||
-- Should be called at every game update; called by ubiquitousse.update.
|
||||
-- @tparam[opt=calculate here] number dt the delta-time (time spent since last time the function was called) (miliseconds)
|
||||
-- @impl ubiquitousse
|
||||
update = function(self, dt)
|
||||
local currentTime = time.get()
|
||||
|
||||
if not dt then
|
||||
dt = currentTime - self.lastTime
|
||||
self.lastTime = currentTime
|
||||
end
|
||||
self.dt = dt
|
||||
|
||||
local done = {} -- functions done running
|
||||
|
||||
local d = self.delayed
|
||||
for func, t in pairs(d) do
|
||||
if t and all(t.initWhen, true) then
|
||||
t.initWhen = {}
|
||||
local co = t.coroutine
|
||||
t.after = t.after - dt
|
||||
if t.forceStart or (t.after <= 0 and all(t.startWhen, true)) then
|
||||
t.startWhen = {}
|
||||
d[func] = false -- niling here cause the next pair iteration to error
|
||||
table.insert(done, func)
|
||||
if not co then
|
||||
co = coroutine.create(func)
|
||||
t.coroutine = co
|
||||
t.started = currentTime
|
||||
if t.times > 0 then t.times = t.times - 1 end
|
||||
for _, f in ipairs(t.onStart) do f(t.object) end
|
||||
end
|
||||
assert(coroutine.resume(co, function(delay)
|
||||
t.after = delay or 0
|
||||
d[func] = t
|
||||
coroutine.yield()
|
||||
end, dt))
|
||||
for _, f in ipairs(t.onUpdate) do f(t.object) end
|
||||
if all(t.stopWhen, false) then t.forceStop = true end
|
||||
if t.forceStop or coroutine.status(co) == "dead" then
|
||||
if t.forceStop
|
||||
or (t.during >= 0 and t.started + t.during < currentTime)
|
||||
or (t.times == 0)
|
||||
or (not all(t.repeatWhile, true))
|
||||
or (t.every == -1 and t.times == -1 and t.during == -1 and #t.repeatWhile == 0) -- no repeat
|
||||
then
|
||||
for _, f in ipairs(t.onEnd) do f(t.object) end
|
||||
else
|
||||
if t.times > 0 then t.times = t.times - 1 end
|
||||
t.after = t.every
|
||||
t.coroutine = coroutine.create(func)
|
||||
d[func] = t
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, func in ipairs(done) do
|
||||
if not d[func] then
|
||||
d[func] = nil
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
--- Schedule a function to run.
|
||||
-- The function will receive as first parameter the wait(time) function, which will pause the function execution for time miliseconds.
|
||||
-- As a second parameter, the function will receive the delta time (dt).
|
||||
-- @tparam[opt] function func the function to schedule
|
||||
-- @treturn TimedFunction the object
|
||||
-- @impl ubiquitousse
|
||||
run = function(self, func)
|
||||
-- Creates empty function (the TimedFunction may be used for time measure or stuff like that which doesn't need a specific function)
|
||||
func = func or function() end
|
||||
|
||||
-- Since delayed functions can end in any order, it doesn't really make sense to use a integer-keyed list.
|
||||
-- Using the function as the key works and it's unique.
|
||||
self.delayed[func] = {
|
||||
object = nil,
|
||||
coroutine = nil,
|
||||
started = 0,
|
||||
|
||||
after = -1,
|
||||
every = -1,
|
||||
times = -1,
|
||||
during = -1,
|
||||
|
||||
initWhen = {},
|
||||
startWhen = {},
|
||||
repeatWhile = {},
|
||||
stopWhen = {},
|
||||
|
||||
forceStart = false,
|
||||
forceStop = false,
|
||||
|
||||
onStart = {},
|
||||
onUpdate = {},
|
||||
onEnd = {}
|
||||
}
|
||||
|
||||
local t = self.delayed[func] -- internal data
|
||||
local r -- external interface
|
||||
r = {
|
||||
--- Timed conditions ---
|
||||
--- Wait time milliseconds before running the function.
|
||||
after = function(_, time)
|
||||
t.after = time
|
||||
return r
|
||||
end,
|
||||
--- Run the function every time millisecond.
|
||||
every = function(_, time)
|
||||
t.every = time
|
||||
return r
|
||||
end,
|
||||
--- The function will not execute more than count times.
|
||||
times = function(_, count)
|
||||
t.times = count
|
||||
return r
|
||||
end,
|
||||
--- The TimedFunction will be active for a time duration.
|
||||
during = function(_, time)
|
||||
t.during = time
|
||||
return r
|
||||
end,
|
||||
|
||||
--- Function conditions ---
|
||||
--- Starts the function execution when func() returns true. Checked before the "after" condition,
|
||||
-- meaning the "after" countdown starts when func() returns true.
|
||||
-- If multiple init functions are added, init will trigger only when all of them returns true.
|
||||
initWhen = function(_, func)
|
||||
table.insert(t.initWhen, func)
|
||||
return r
|
||||
end,
|
||||
--- Starts the function execution when func() returns true. Checked after the "after" condition.
|
||||
-- If multiple start functions are added, start will trigger only when all of them returns true.
|
||||
startWhen = function(_, func)
|
||||
table.insert(t.startWhen, func)
|
||||
return r
|
||||
end,
|
||||
--- When the functions ends, the execution won't stop and will repeat as long as func() returns true.
|
||||
-- Will cancel timed repeat conditions if false but needs other timed repeat conditions to be true to create a new repeat.
|
||||
-- If multiple repeat functions are added, a repeat will trigger only when all of them returns true.
|
||||
repeatWhile = function(_, func)
|
||||
table.insert(t.repeatWhile, func)
|
||||
return r
|
||||
end,
|
||||
--- Stops the function execution when func() returns true. Checked before all timed conditions.
|
||||
-- If multiple stop functions are added, stop will trigger only when all of them returns true.
|
||||
stopWhen = function(_, func)
|
||||
table.insert(t.stopWhen, func)
|
||||
return r
|
||||
end,
|
||||
|
||||
--- Conditions override ---
|
||||
--- Force the function to start its execution.
|
||||
start = function(_)
|
||||
t.forceStart = true
|
||||
return r
|
||||
end,
|
||||
--- Force the function to stop its execution.
|
||||
stop = function(_)
|
||||
t.forceStop = true
|
||||
return r
|
||||
end,
|
||||
|
||||
--- Callbacks functions ---
|
||||
--- Will execute func(self) when the function execution start.
|
||||
onStart = function(_, func)
|
||||
table.insert(t.onStart, func)
|
||||
return r
|
||||
end,
|
||||
--- Will execute func(self) each frame the main function is run.
|
||||
onUpdate = function(_, func)
|
||||
table.insert(t.onUpdate, func)
|
||||
return r
|
||||
end,
|
||||
--- Will execute func(self) when the function execution end.
|
||||
onEnd = function(_, func)
|
||||
table.insert(t.onEnd, func)
|
||||
return r
|
||||
end,
|
||||
|
||||
--- Chaining ---
|
||||
--- Creates another TimedFunction which will be initialized when the current one ends.
|
||||
-- Returns the new TimedFunction.
|
||||
chain = function(_, func)
|
||||
local done = false
|
||||
r:onEnd(function() done = true end)
|
||||
return self:run(func)
|
||||
:initWhen(function() return done end)
|
||||
end
|
||||
}
|
||||
t.object = r
|
||||
return r
|
||||
end,
|
||||
|
||||
--- Tween some numeric values.
|
||||
-- @tparam number duration tween duration (miliseconds)
|
||||
-- @tparam table tbl the table containing the values to tween
|
||||
-- @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 TimedFunction 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(self, duration, tbl, to, method)
|
||||
method = method or "linear"
|
||||
method = type(method) == "string" and ease[method] or method
|
||||
|
||||
local time = 0 -- tweening time elapsed
|
||||
local from = {} -- initial state
|
||||
|
||||
local function update(tbl_, from_, to_) -- apply the method to tbl_ recursively (doesn't handle cycles)
|
||||
for k, v in pairs(to_) do
|
||||
if type(v) == "table" then
|
||||
update(tbl_[k], from_[k], to_[k])
|
||||
else
|
||||
if time < duration then
|
||||
tbl_[k] = method(time, from_[k], v - from_[k], duration)
|
||||
else
|
||||
tbl_[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local r = self:run(function(wait, dt)
|
||||
time = time + dt
|
||||
update(tbl, from, to)
|
||||
end):during(duration)
|
||||
:onStart(function()
|
||||
local function copy(stencil, source, dest) -- copy initial state recursively
|
||||
for k, v in pairs(stencil) do
|
||||
if type(v) == "table" then
|
||||
if not dest[k] then dest[k] = {} end
|
||||
copy(stencil[k], source[k], dest[k])
|
||||
else
|
||||
dest[k] = source[k]
|
||||
end
|
||||
end
|
||||
end
|
||||
copy(to, tbl, from)
|
||||
end)
|
||||
|
||||
--- Creates another tween which will be initialized when the current one ends.
|
||||
-- If tbl_ and/or method_ are not specified, the values from the current tween will be used.
|
||||
-- Returns the new tween.
|
||||
r.chain = function(_, duration_, tbl_, to_, method_)
|
||||
if not method_ and to_ then
|
||||
if type(to_) == "string" then
|
||||
tbl_, to_, method_ = tbl, tbl_, to_
|
||||
else
|
||||
method_ = method
|
||||
end
|
||||
elseif not method_ and not to_ then
|
||||
tbl_, to_, method_ = tbl, tbl_, method
|
||||
end
|
||||
|
||||
local done = false
|
||||
r:onEnd(function() done = true end)
|
||||
return self:tween(duration_, tbl_, to_, method_)
|
||||
:initWhen(function() return done end)
|
||||
end
|
||||
|
||||
return r
|
||||
end,
|
||||
|
||||
--- Cancels all the running TimedFunctions.
|
||||
-- @impl ubiquitousse
|
||||
clear = function(self)
|
||||
self.delayed = {}
|
||||
end
|
||||
}
|
||||
registry_mt.__index = registry_mt
|
||||
|
||||
--- Time related functions
|
||||
time = {
|
||||
--- Creates and return a new TimerRegistry.
|
||||
-- A TimerRegistry is a separate ubiquitousse.time instance: its TimedFunctions will be independant
|
||||
-- from the one registered using ubiquitousse.time.run (the global TimerRegistry). If you use the scene
|
||||
-- system, a scene-specific TimerRegistry is available at ubiquitousse.scene.current.time.
|
||||
-- @impl ubiquitousse
|
||||
new = function()
|
||||
return setmetatable({
|
||||
--- Used to store all the functions delayed with ubiquitousse.time.delay
|
||||
-- The default implementation use the structure {<key: function> = <value: data table>, ...}
|
||||
-- This table is for internal use and shouldn't be used from an external script.
|
||||
delayed = {},
|
||||
|
||||
-- Used to calculate the deltatime
|
||||
lastTime = time.get(),
|
||||
|
||||
--- Time since last timer update (miliseconds).
|
||||
dt = 0
|
||||
}, registry_mt)
|
||||
end,
|
||||
|
||||
--- Returns the number of miliseconds elapsed since some point in time.
|
||||
-- This point is fixed but undetermined, so this function should only be used to calculate durations.
|
||||
-- Should at least have millisecond-precision, but can be more precise if available.
|
||||
-- @impl backend
|
||||
get = function() end,
|
||||
|
||||
--- Time since last update (miliseconds).
|
||||
-- @impl ubiquitousse
|
||||
dt = 0,
|
||||
|
||||
--- Global TimerRegistry.
|
||||
-- @impl ubiquitousse
|
||||
delayed = {},
|
||||
lastTime = 0,
|
||||
update = function(...)
|
||||
return registry_mt.update(time, ...)
|
||||
end,
|
||||
run = function(...)
|
||||
return registry_mt.run(time, ...)
|
||||
end,
|
||||
tween = function(...)
|
||||
return registry_mt.tween(time, ...)
|
||||
end,
|
||||
clear = function(...)
|
||||
return registry_mt.clear(time, ...)
|
||||
end
|
||||
}
|
||||
|
||||
return time
|
||||
5
todo.txt
5
todo.txt
|
|
@ -1,9 +1,10 @@
|
|||
Ubiquitousse, also known as "The World's Best Video Game Engine Of All Time", despite its perfectness, is still not perfect.
|
||||
Ubiquitousse, also known as "The World's Best Video Game Framework Of All Time", despite its perfectness, is still not perfect.
|
||||
More specifically, what is lacking to officially turn Ubiquitousse into a sacred text, is:
|
||||
- An i18n API. While some languages are clearly superior to others, the general consensus seems to think otherwise. Ubiquitousse
|
||||
should be able to get an ordered list of prefered languages and provide translation helpers. See The Pong.
|
||||
- Some API are still lacking an API and/or implementation. Search "TODO" for more information.
|
||||
- A filesystem API, to access the game's filesystem. May also rewrite Lua's io functions, see the next item.
|
||||
- A filesystem API, to access the game's filesystem. May also rewrite Lua's io functions, see the next item. Though the LÖVE API is fine.
|
||||
- An audio and draw API. There were one but it was simply plagiarized from LÖVE, so, meh. Might just provide LÖVE API compat layers for other backends in the end.
|
||||
- A sandboxing system. Ubiquitousse should be able to run in a Ubiquitousse game safely. Since Ubiquitousse can run itself, it will
|
||||
then seems reasonable to claim Ubiquitousse run the universe. See World Domination for Dummies.
|
||||
- A libretro backend, so Ubiquitousse really become ubiquitous. If you didn't know, it's the goal.
|
||||
|
|
|
|||
52
util.lua
52
util.lua
|
|
@ -1,52 +0,0 @@
|
|||
-- ubiquitousse.util
|
||||
|
||||
--- Various functions useful for game developement.
|
||||
-- No dependicy on either ubiquitousse or a ubiquitousse backend.
|
||||
local util
|
||||
util = {
|
||||
--- AABB collision check.
|
||||
-- @tparam number x1 first rectangle top-left x coordinate
|
||||
-- @tparam number y1 first rectangle top-left y coordinate
|
||||
-- @tparam number w1 first rectangle width
|
||||
-- @tparam number h1 first rectangle height
|
||||
-- @tparam number x2 second rectangle top-left x coordinate
|
||||
-- @tparam number y2 second rectangle top-left y coordinate
|
||||
-- @tparam number w2 second rectangle width
|
||||
-- @tparam number h2 second rectangle height
|
||||
-- @treturn true if the objects collide, false otherwise
|
||||
aabb = function(x1, y1, w1, h1, x2, y2, w2, h2)
|
||||
if w1 < 0 then x1 = x1 + w1; w1 = -w1 end
|
||||
if h1 < 0 then y1 = y1 + h1; h1 = -h1 end
|
||||
if w2 < 0 then x2 = x2 + w2; w2 = -w2 end
|
||||
if h2 < 0 then y2 = y2 + h2; h2 = -h2 end
|
||||
return x1 + w1 >= x2 and x1 <= x2 + w2 and
|
||||
y1 + h1 >= y2 and y1 <= y2 + h2
|
||||
end,
|
||||
|
||||
--- Remove the first occurence of an element in a table.
|
||||
-- @tparam table t the table
|
||||
-- @param x the element to remove
|
||||
-- @return x
|
||||
remove = function(t, x)
|
||||
for i, v in ipairs(t) do
|
||||
if v == x then
|
||||
table.remove(t, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
return x
|
||||
end,
|
||||
|
||||
--- Returns a new table where the keys and values have been inverted.
|
||||
-- @tparam table t the table
|
||||
-- @treturn table the inverted table
|
||||
invert = function(t)
|
||||
local r = {}
|
||||
for k, v in pairs(t) do
|
||||
r[v] = k
|
||||
end
|
||||
return r
|
||||
end
|
||||
}
|
||||
|
||||
return util
|
||||
1
util/init.lua
Normal file
1
util/init.lua
Normal file
|
|
@ -0,0 +1 @@
|
|||
return require((...)..".util")
|
||||
336
util/util.lua
Normal file
336
util/util.lua
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
--- ubiquitousse.util
|
||||
-- No dependency.
|
||||
|
||||
--- Various functions useful for game developement.
|
||||
local util, group_mt
|
||||
util = {
|
||||
-------------------
|
||||
--- Basic maths ---
|
||||
-------------------
|
||||
|
||||
--- AABB collision check.
|
||||
-- @tparam number x1 first rectangle top-left x coordinate
|
||||
-- @tparam number y1 first rectangle top-left y coordinate
|
||||
-- @tparam number w1 first rectangle width
|
||||
-- @tparam number h1 first rectangle height
|
||||
-- @tparam number x2 second rectangle top-left x coordinate
|
||||
-- @tparam number y2 second rectangle top-left y coordinate
|
||||
-- @tparam number w2 second rectangle width
|
||||
-- @tparam number h2 second rectangle height
|
||||
-- @treturn true if the objects collide, false otherwise
|
||||
aabb = function(x1, y1, w1, h1, x2, y2, w2, h2)
|
||||
if w1 < 0 then x1 = x1 + w1; w1 = -w1 end
|
||||
if h1 < 0 then y1 = y1 + h1; h1 = -h1 end
|
||||
if w2 < 0 then x2 = x2 + w2; w2 = -w2 end
|
||||
if h2 < 0 then y2 = y2 + h2; h2 = -h2 end
|
||||
return x1 + w1 >= x2 and x1 <= x2 + w2 and
|
||||
y1 + h1 >= y2 and y1 <= y2 + h2
|
||||
end,
|
||||
|
||||
-----------------------
|
||||
--- List operations ---
|
||||
-----------------------
|
||||
|
||||
--- Remove the first occurence of an element in a list.
|
||||
-- @tparam table t the list
|
||||
-- @param x the element to remove
|
||||
-- @tparam[opt=#t] number n the number of expected elements in the list, including nil values
|
||||
-- @return x
|
||||
remove = function(t, x, n)
|
||||
n = n or #t
|
||||
for i=1, n do
|
||||
if t[i] == x then
|
||||
table.remove(t, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
return x
|
||||
end,
|
||||
|
||||
--- Extract the list of elements with a specific key from a list of tables
|
||||
-- @tparam table t the list of tables
|
||||
-- @param k the chosen key
|
||||
-- @tparam[opt=#t] number n the number of expected elements in the list, including nil values
|
||||
-- @treturn the extracted table
|
||||
extract = function(t, k, n)
|
||||
n = n or #t
|
||||
local r = {}
|
||||
for i=1, n do
|
||||
r[i] = t[i][k]
|
||||
end
|
||||
return r
|
||||
end,
|
||||
|
||||
--- Applies a function to every item in list t.
|
||||
-- The function receive two argument: the value and then the key.
|
||||
-- @tparam table t initial list
|
||||
-- @tparam function fn the function to apply
|
||||
-- @tparam[opt=#t] number n the number of expected elements in the list, including nil values
|
||||
-- @treturn table the initial list
|
||||
each = function(t, fn, n)
|
||||
n = n or #t
|
||||
for i=1, n do
|
||||
fn(t[i], i)
|
||||
end
|
||||
return t
|
||||
end,
|
||||
|
||||
--- Applies a function to every item in list t and returns the associated new list.
|
||||
-- The function receive two argument: the value and then the key.
|
||||
-- @tparam table t initial list
|
||||
-- @tparam function fn the function to apply
|
||||
-- @tparam[opt=#t] number n the number of expected elements in the list, including nil values
|
||||
-- @treturn table the new list
|
||||
map = function(t, fn, n)
|
||||
n = n or #t
|
||||
local r = {}
|
||||
for i=1, n do
|
||||
r[i] = fn(t[i], i)
|
||||
end
|
||||
return r
|
||||
end,
|
||||
|
||||
--- Test if all the values in the list are true. Optionnaly applies a function to get the truthness.
|
||||
-- The function receive two argument: the value and then the key.
|
||||
-- @tparam table t initial list
|
||||
-- @tparam function fn the function to apply
|
||||
-- @tparam[opt=#t] number n the number of expected elements in the list, including nil values
|
||||
-- @treturn boolean result
|
||||
all = function(t, fn, n)
|
||||
n = n or #t
|
||||
fn = fn or function(v) return v end
|
||||
local r = true
|
||||
for i=1, n do
|
||||
r = r and fn(t[i], i)
|
||||
end
|
||||
return r
|
||||
end,
|
||||
|
||||
--- Test if at least one value in the list is true. Optionnaly applies a function to get the truthness.
|
||||
-- The function receive two argument: the value and then the key.
|
||||
-- @tparam table t initial list
|
||||
-- @tparam function fn the function to apply
|
||||
-- @tparam[opt=#t] number n the number of expected elements in the list, including nil values
|
||||
-- @treturn boolean result
|
||||
any = function(t, fn, n)
|
||||
n = n or #t
|
||||
fn = fn or function(v) return v end
|
||||
for i=1, n do
|
||||
if fn(t[i], i) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end,
|
||||
|
||||
-----------------------------
|
||||
--- Dictionary operations ---
|
||||
-----------------------------
|
||||
|
||||
--- Returns a new table where the keys and values have been inverted.
|
||||
-- @tparam table t the table
|
||||
-- @treturn table the inverted table
|
||||
invert = function(t)
|
||||
local r = {}
|
||||
for k, v in pairs(t) do
|
||||
r[v] = k
|
||||
end
|
||||
return r
|
||||
end,
|
||||
|
||||
--- Perform a deep copy of a table.
|
||||
-- The copied table will keep the share the same metatable as the original table.
|
||||
-- Note this uses pairs() to perform the copy, which will honor the __pairs methamethod if present.
|
||||
-- @tparam table t the table
|
||||
-- @treturn table the copied table
|
||||
copy = function(t, cache)
|
||||
if cache == nil then cache = {} end
|
||||
local r = {}
|
||||
cache[t] = r
|
||||
for k, v in pairs(t) do
|
||||
if type(v) == "table" then
|
||||
r[k] = cache[v] or util.copy(v, cache)
|
||||
else
|
||||
r[k] = v
|
||||
end
|
||||
end
|
||||
return setmetatable(r, getmetatable(t))
|
||||
end,
|
||||
|
||||
-----------------------
|
||||
--- Random and UUID ---
|
||||
-----------------------
|
||||
|
||||
--- Generate a UUID v4.
|
||||
-- @treturn string the UUID in its canonical representation
|
||||
uuid4 = function()
|
||||
return ("xxxxxxxx-xxxx-4xxx-Nxxx-xxxxxxxxxxxx") -- version 4
|
||||
:gsub("N", math.random(0x8, 0xb)) -- variant 1
|
||||
:gsub("x", function() return ("%x"):format(math.random(0x0, 0xf)) end) -- random hexadecimal digit
|
||||
end,
|
||||
|
||||
-----------------------
|
||||
--- Object grouping ---
|
||||
-----------------------
|
||||
|
||||
--- Groups objects in a meta-object-proxy-thingy.
|
||||
-- Works great with Lua 5.2+. LuaJit requires to be built with Lua 5.2 compatibility enabled to support group comparaison.
|
||||
-- @tparam table list of objects
|
||||
-- @tparam[opt=#t] number n the number of expected elements in the list, including nil values
|
||||
-- @tparam[opt=nil] table p list of parents. Used to find the first arguments of method calls.
|
||||
-- @treturn group object
|
||||
group = function(t, n, p)
|
||||
n = n or #t
|
||||
return setmetatable({ _n = n, _t = t, _p = p or false }, group_mt)
|
||||
end
|
||||
}
|
||||
|
||||
group_mt = {
|
||||
--- Everything but comparaison: returns a new group
|
||||
__add = function(self, other)
|
||||
if getmetatable(other) == group_mt then
|
||||
if getmetatable(self) == group_mt then
|
||||
return util.group(util.map(self._t, function(v, i) return v + other._t[i] end, self._n), self._n)
|
||||
else
|
||||
return util.group(util.map(other._t, function(v) return self + v end, self._n), self._n)
|
||||
end
|
||||
else
|
||||
return util.group(util.map(self._t, function(v) return v + other end, self._n), self._n)
|
||||
end
|
||||
end,
|
||||
__sub = function(self, other)
|
||||
if getmetatable(other) == group_mt then
|
||||
if getmetatable(self) == group_mt then
|
||||
return util.group(util.map(self._t, function(v, i) return v - other._t[i] end, self._n), self._n)
|
||||
else
|
||||
return util.group(util.map(other._t, function(v) return self - v end, self._n), self._n)
|
||||
end
|
||||
else
|
||||
return util.group(util.map(self._t, function(v) return v - other end, self._n), self._n)
|
||||
end
|
||||
end,
|
||||
__mul = function(self, other)
|
||||
if getmetatable(other) == group_mt then
|
||||
if getmetatable(self) == group_mt then
|
||||
return util.group(util.map(self._t, function(v, i) return v * other._t[i] end, self._n), self._n)
|
||||
else
|
||||
return util.group(util.map(other._t, function(v) return self * v end, self._n), self._n)
|
||||
end
|
||||
else
|
||||
return util.group(util.map(self._t, function(v) return v * other end, self._n), self._n)
|
||||
end
|
||||
end,
|
||||
__div = function(self, other)
|
||||
if getmetatable(other) == group_mt then
|
||||
if getmetatable(self) == group_mt then
|
||||
return util.group(util.map(self._t, function(v, i) return v / other._t[i] end, self._n), self._n)
|
||||
else
|
||||
return util.group(util.map(other._t, function(v) return self / v end, self._n), self._n)
|
||||
end
|
||||
else
|
||||
return util.group(util.map(self._t, function(v) return v / other end, self._n), self._n)
|
||||
end
|
||||
end,
|
||||
__mod = function(self, other)
|
||||
if getmetatable(other) == group_mt then
|
||||
if getmetatable(self) == group_mt then
|
||||
return util.group(util.map(self._t, function(v, i) return v % other._t[i] end, self._n), self._n)
|
||||
else
|
||||
return util.group(util.map(other._t, function(v) return self % v end, self._n), self._n)
|
||||
end
|
||||
else
|
||||
return util.group(util.map(self._t, function(v) return v % other end, self._n), self._n)
|
||||
end
|
||||
end,
|
||||
__pow = function(self, other)
|
||||
if getmetatable(other) == group_mt then
|
||||
if getmetatable(self) == group_mt then
|
||||
return util.group(util.map(self._t, function(v, i) return v ^ other._t[i] end, self._n), self._n)
|
||||
else
|
||||
return util.group(util.map(other._t, function(v) return self ^ v end, self._n), self._n)
|
||||
end
|
||||
else
|
||||
return util.group(util.map(self._t, function(v) return v ^ other end, self._n), self._n)
|
||||
end
|
||||
end,
|
||||
__unm = function(self)
|
||||
return util.group(util.map(self._t, function(v) return -v end, self._n), self._n)
|
||||
end,
|
||||
__concat = function(self, other)
|
||||
if getmetatable(other) == group_mt then
|
||||
if getmetatable(self) == group_mt then
|
||||
return util.group(util.map(self._t, function(v, i) return v .. other._t[i] end, self._n), self._n)
|
||||
else
|
||||
return util.group(util.map(other._t, function(v) return self .. v end, self._n), self._n)
|
||||
end
|
||||
else
|
||||
return util.group(util.map(self._t, function(v) return v .. other end, self._n), self._n)
|
||||
end
|
||||
end,
|
||||
__len = function(self)
|
||||
return util.group(util.map(self._t, function(v) return #v end, self._n), self._n)
|
||||
end,
|
||||
__index = function(self, k)
|
||||
return util.group(util.extract(self._t, k, self._n), self._n, self._t)
|
||||
end,
|
||||
|
||||
--- Comparaison: returns true if true for every object of the group
|
||||
__eq = function(self, other)
|
||||
if getmetatable(other) == group_mt then
|
||||
if getmetatable(self) == group_mt then
|
||||
return util.all(self._t, function(v, i) return v == other._t[i] end, self._n)
|
||||
else
|
||||
return util.all(other._t, function(v) return self == v end, self._n)
|
||||
end
|
||||
else
|
||||
return util.all(self._t, function(v) return v == other end, self._n)
|
||||
end
|
||||
end,
|
||||
__lt = function(self, other)
|
||||
if getmetatable(other) == group_mt then
|
||||
if getmetatable(self) == group_mt then
|
||||
return util.all(self._t, function(v, i) return v < other._t[i] end, self._n)
|
||||
else
|
||||
return util.all(other._t, function(v) return self < v end, self._n)
|
||||
end
|
||||
else
|
||||
return util.all(self._t, function(v) return v < other end, self._n)
|
||||
end
|
||||
end,
|
||||
__le = function(self, other)
|
||||
if getmetatable(other) == group_mt then
|
||||
if getmetatable(self) == group_mt then
|
||||
return util.all(self._t, function(v, i) return v <= other._t[i] end, self._n)
|
||||
else
|
||||
return util.all(other._t, function(v) return self <= v end, self._n)
|
||||
end
|
||||
else
|
||||
return util.all(self._t, function(v) return v <= other end, self._n)
|
||||
end
|
||||
end,
|
||||
|
||||
--- Special cases
|
||||
__newindex = function(self, k, v)
|
||||
if getmetatable(v) == group_mt then -- unpack
|
||||
util.each(self._t, function(t, i) t[k] = v._t[i] end, self._n)
|
||||
else
|
||||
util.each(self._t, function(t) t[k] = v end, self._n)
|
||||
end
|
||||
end,
|
||||
__call = function(self, selfArg, ...)
|
||||
if getmetatable(selfArg) == group_mt and self._p then -- method call
|
||||
local a = {...}
|
||||
return util.group(util.map(self._t, function(v, i) return v(self._p[i], unpack(a)) end, self._n), self._n)
|
||||
else
|
||||
local a = {selfArg, ...}
|
||||
return util.group(util.map(self._t, function(v) return v(unpack(a)) end, self._n), self._n)
|
||||
end
|
||||
end,
|
||||
|
||||
--- Full-blown debugger
|
||||
__tostring = function(self)
|
||||
return ("group{%s}"):format(table.concat(util.map(self._t, tostring, self._n), ", "))
|
||||
end
|
||||
}
|
||||
|
||||
return util
|
||||
Loading…
Add table
Add a link
Reference in a new issue