1
0
Fork 0
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:
Étienne Fildadut 2019-12-24 19:05:50 +01:00
parent 523c5d36c0
commit 16e533d176
28 changed files with 2544 additions and 2107 deletions

View file

@ -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
View 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
View file

@ -0,0 +1 @@
return require((...)..".asset")

View file

@ -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
}

View file

@ -1,334 +1,19 @@
--- 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)
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.")
:format(stuffName, expectedVersion, actualVersion)
print(txt)
for _=0,300 do
gfx.start(gfx.TOP)
gfx.wrappedText(0, 0, txt, gfx.TOP_WIDTH)
gfx.stop()
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
local function checkCompat(stuffName, expectedVersion, actualVersion)
if actualVersion ~= expectedVersion then
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
gfx.start(gfx.TOP)
gfx.wrappedText(0, 0, txt, gfx.TOP_WIDTH)
gfx.stop()
gfx.render()
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)

View file

@ -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)
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.")
: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
local function checkCompat(stuffName, expectedVersion, actualVersion)
if actualVersion ~= expectedVersion then
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)
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
View file

@ -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
View 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
View file

@ -0,0 +1 @@
return require((...)..".ecs")

View file

@ -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
View file

@ -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
View file

@ -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
View file

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

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

@ -0,0 +1,319 @@
local input = require((...):match("^(.-%.)backend").."input")
-- Config --
-- Use ScanCodes (layout independant input) instead of KeyConstants (layout dependant) for keyboard input
local useScancodes = true
-- If using ScanCodes, sets this to true so the backend returns the layout-dependant KeyConstant
-- instead of the raw ScanCode when getting the display name. If set to false and using ScanCodes,
-- the user will see keys that don't match what's actually written on his keyboard, which is confusing.
local displayKeyConstant = true
-- Setup
love.mouse.setVisible(false)
-- Button detection
-- FIXME love callbacks do something cleaner
local buttonsInUse = {}
local axesInUse = {}
function love.keypressed(key, scancode, isrepeat)
if useScancodes then key = scancode end
buttonsInUse["keyboard."..key] = true
end
function love.keyreleased(key, scancode)
if useScancodes then key = scancode end
buttonsInUse["keyboard."..key] = nil
end
function love.mousepressed(x, y, button, istouch)
buttonsInUse["mouse."..button] = true
end
function love.mousereleased(x, y, button, istouch)
buttonsInUse["mouse."..button] = nil
end
function love.wheelmoved(x, y)
if y > 0 then
buttonsInUse["mouse.wheel.up"] = true
elseif y < 0 then
buttonsInUse["mouse.wheel.down"] = true
end
if x > 0 then
buttonsInUse["mouse.wheel.right"] = true
elseif x < 0 then
buttonsInUse["mouse.wheel.left"] = true
end
end
function love.mousemoved(x, y, dx, dy)
if dx ~= 0 then axesInUse["mouse.move.x"] = dx/love.graphics.getWidth() end
if dy ~= 0 then axesInUse["mouse.move.y"] = dy/love.graphics.getHeight() end
end
function love.gamepadpressed(joystick, button)
buttonsInUse["gamepad.button."..joystick:getID().."."..button] = true
end
function love.gamepadreleased(joystick, button)
buttonsInUse["gamepad.button."..joystick:getID().."."..button] = nil
end
function love.gamepadaxis(joystick, axis, value)
if value ~= 0 then
axesInUse["gamepad.axis."..joystick:getID().."."..axis] = value
else
axesInUse["gamepad.axis."..joystick:getID().."."..axis] = nil
end
end
-- Windows size
input.drawWidth, input.drawHeight = love.graphics.getWidth(), love.graphics.getHeight()
function love.resize(width, height)
input.drawWidth = width
input.drawHeight = height
end
-- Update
local oUpdate = input.update
input.update = function(dt)
-- love.wheelmoved doesn't trigger when the wheel stop moving, so we need to clear up our stuff at each update
buttonsInUse["mouse.wheel.up"] = nil
buttonsInUse["mouse.wheel.down"] = nil
buttonsInUse["mouse.wheel.right"] = nil
buttonsInUse["mouse.wheel.left"] = nil
-- Same for mouse axis
axesInUse["mouse.move.x"] = nil
axesInUse["mouse.move.y"] = nil
oUpdate(dt)
end
input.basicButtonDetector = function(id)
-- Keyboard
if id:match("^keyboard%.") then
local key = id:match("^keyboard%.(.+)$")
return function()
return useScancodes and love.keyboard.isScancodeDown(key) or love.keyboard.isDown(key)
end
-- Mouse wheel
elseif id:match("^mouse%.wheel%.") then
local key = id:match("^mouse%.wheel%.(.+)$")
return function()
return buttonsInUse["mouse.wheel."..key]
end
-- Mouse
elseif id:match("^mouse%.") then
local key = id:match("^mouse%.(.+)$")
return function()
return love.mouse.isDown(key)
end
-- Gamepad button
elseif id:match("^gamepad%.button%.") then
local gid, key = id:match("^gamepad%.button%.(.+)%.(.+)$")
gid = tonumber(gid)
return function()
local gamepad
for _,j in ipairs(love.joystick.getJoysticks()) do
if j:getID() == gid then gamepad = j end
end
return gamepad and gamepad:isGamepadDown(key)
end
-- Gamepad axis
elseif id:match("^gamepad%.axis%.") then
local gid, axis, threshold = id:match("^gamepad%.axis%.(.+)%.(.+)%%(.+)$")
if not gid then gid, axis = id:match("^gamepad%.axis%.(.+)%.(.+)$") end -- no threshold (=0)
gid = tonumber(gid)
threshold = tonumber(threshold) or 0.1
return function()
local gamepad
for _,j in ipairs(love.joystick.getJoysticks()) do
if j:getID() == gid then gamepad = j end
end
if not gamepad then
return false
else
local val = gamepad:getGamepadAxis(axis)
return (math.abs(val) > math.abs(threshold)) and ((val < 0) == (threshold < 0))
end
end
else
error("Unknown button identifier: "..id)
end
end
input.basicAxisDetector = function(id)
-- Mouse movement
if id:match("^mouse%.move%.") then
local axis, threshold = id:match("^mouse%.move%.(.+)%%(.+)$")
if not axis then axis = id:match("^mouse%.move%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0
return function()
local val, raw, max = axesInUse["mouse.move."..axis] or 0, 0, 1
if axis == "x" then
raw, max = val * love.graphics.getWidth(), love.graphics.getWidth()
elseif axis == "y" then
raw, max = val * love.graphics.getHeight(), love.graphics.getHeight()
end
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
end
-- Mouse position
elseif id:match("^mouse%.position%.") then
local axis, threshold = id:match("^mouse%.position%.(.+)%%(.+)$")
if not axis then axis = id:match("^mouse%.position%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0
return function()
local val, raw, max = 0, 0, 1
if axis == "x" then
max = love.graphics.getWidth() / 2 -- /2 because x=0,y=0 is the center of the screen (an axis value is in [-1,1])
raw = love.mouse.getX() - max
elseif axis == "y" then
max = love.graphics.getHeight() / 2
raw = love.mouse.getY() - max
end
val = raw / max
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
end
-- Gamepad axis
elseif id:match("^gamepad%.axis%.") then
local gid, axis, threshold = id:match("^gamepad%.axis%.(.+)%.(.+)%%(.+)$")
if not gid then gid, axis = id:match("^gamepad%.axis%.(.+)%.(.+)$") end -- no threshold (=0)
gid = tonumber(gid)
threshold = tonumber(threshold) or 0.1
return function()
local gamepad
for _,j in ipairs(love.joystick.getJoysticks()) do
if j:getID() == gid then gamepad = j end
end
if not gamepad then
return 0
else
local val = gamepad:getGamepadAxis(axis)
return math.abs(val) > math.abs(threshold) and val or 0
end
end
else
error("Unknown axis identifier: "..id)
end
end
input.buttonsInUse = function(threshold)
local r = {}
threshold = threshold or 0.5
for b in pairs(buttonsInUse) do
table.insert(r, b)
end
for b,v in pairs(axesInUse) do
if math.abs(v) > threshold then
table.insert(r, b.."%"..(v < 0 and -threshold or threshold))
end
end
return r
end
input.axesInUse = function(threshold)
local r = {}
threshold = threshold or 0.5
for b,v in pairs(axesInUse) do
if math.abs(v) > threshold then
table.insert(r, b.."%"..threshold)
end
end
return r
end
input.buttonName = function(...)
local ret = {}
for _,id in ipairs({...}) do
-- Keyboard
if id:match("^keyboard%.") then
local key = id:match("^keyboard%.(.+)$")
if useScancodes and displayKeyConstant then key = love.keyboard.getKeyFromScancode(key) end
table.insert(ret, key:sub(1,1):upper()..key:sub(2).." key")
-- Mouse wheel
elseif id:match("^mouse%.wheel%.") then
local key = id:match("^mouse%.wheel%.(.+)$")
table.insert(ret, "Mouse wheel "..key)
-- Mouse
elseif id:match("^mouse%.") then
local key = id:match("^mouse%.(.+)$")
table.insert(ret, "Mouse "..key)
-- Gamepad button
elseif id:match("^gamepad%.button%.") then
local gid, key = id:match("^gamepad%.button%.(.+)%.(.+)$")
table.insert(ret, "Gamepad "..gid.." button "..key)
-- Gamepad axis
elseif id:match("^gamepad%.axis%.") then
local gid, axis, threshold = id:match("^gamepad%.axis%.(.+)%.(.+)%%(.+)$")
if not gid then gid, axis = id:match("^gamepad%.axis%.(.+)%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0.1
if axis == "rightx" then
table.insert(ret, ("Gamepad %s right stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "right" or "left", math.abs(threshold*100)))
elseif axis == "righty" then
table.insert(ret, ("Gamepad %s right stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "down" or "up", math.abs(threshold*100)))
elseif axis == "leftx" then
table.insert(ret, ("Gamepad %s left stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "right" or "left", math.abs(threshold*100)))
elseif axis == "lefty" then
table.insert(ret, ("Gamepad %s left stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "down" or "up", math.abs(threshold*100)))
else
table.insert(ret, ("Gamepad %s axis %s (deadzone %s%%)"):format(gid, axis, math.abs(threshold*100)))
end
else
table.insert(ret, id)
end
end
return unpack(ret)
end
input.axisName = function(...)
local ret = {}
for _,id in ipairs({...}) do
-- Binary axis
if id:match(".+%,.+") then
local b1, b2 = input.buttonName(id:match("^(.+)%,(.+)$"))
table.insert(ret, b1.." / "..b2)
-- Mouse movement
elseif id:match("^mouse%.move%.") then
local axis, threshold = id:match("^mouse%.move%.(.+)%%(.+)$")
if not axis then axis = id:match("^mouse%.move%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0
table.insert(ret, ("Mouse %s movement (threshold %s%%)"):format(axis, math.abs(threshold*100)))
-- Mouse position
elseif id:match("^mouse%.position%.") then
local axis, threshold = id:match("^mouse%.position%.(.+)%%(.+)$")
if not axis then axis = id:match("^mouse%.position%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0
table.insert(ret, ("Mouse %s position (threshold %s%%)"):format(axis, math.abs(threshold*100)))
-- Gamepad axis
elseif id:match("^gamepad%.axis%.") then
local gid, axis, threshold = id:match("^gamepad%.axis%.(.+)%.(.+)%%(.+)$")
if not gid then gid, axis = id:match("^gamepad%.axis%.(.+)%.(.+)$") end -- no threshold (=0)
threshold = tonumber(threshold) or 0.1
if axis == "rightx" then
table.insert(ret, ("Gamepad %s right stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "right" or "left", math.abs(threshold*100)))
elseif axis == "righty" then
table.insert(ret, ("Gamepad %s right stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "down" or "up", math.abs(threshold*100)))
elseif axis == "leftx" then
table.insert(ret, ("Gamepad %s left stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "right" or "left", math.abs(threshold*100)))
elseif axis == "lefty" then
table.insert(ret, ("Gamepad %s left stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "down" or "up", math.abs(threshold*100)))
else
table.insert(ret, ("Gamepad %s axis %s (deadzone %s%%)"):format(gid, axis, math.abs(threshold*100)))
end
else
table.insert(ret, id)
end
end
return unpack(ret)
end
-- Default inputs.
input.default.pointer:bind(
{ "absolute", { "keyboard.left", "keyboard.right" }, { "keyboard.up", "keyboard.down" } },
{ "absolute", { "keyboard.a", "keyboard.d" }, { "keyboard.w", "keyboard.s" } },
{ "absolute", "gamepad.axis.1.leftx", "gamepad.axis.1.lefty" },
{ "absolute", { "gamepad.button.1.dpleft", "gamepad.button.1.dpright" }, { "gamepad.button.1.dpup", "gamepad.button.1.dpdown" } }
)
input.default.confirm:bind(
"keyboard.return", "keyboard.space", "keyboard.lshift", "keyboard.e",
"gamepad.button.1.a"
)
input.default.cancel:bind(
"keyboard.escape", "keyboard.backspace",
"gamepad.button.1.b"
)
return input

14
input/init.lua Normal file
View file

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

711
input/input.lua Normal file
View file

@ -0,0 +1,711 @@
--- ubiquitousse.input
-- Depends on a backend.
-- TODO: some key selection helper? Will be backend-implemented, to account for all the possible input methods.
-- TODO: some way to list all possible input / outputs, or make the *inUse make some separation between inputs indiscutitably in use and those who are incertain.
-- TODO: outputs! (rumble, lights, I don't know)
-- TODO: other, optional, default/generic inputs, and a way to know if they are binded.
-- TODO: multiplayer input helpers? something like getting the same input for different players, or default inputs for different players
local input
local sqrt = math.sqrt
local unpack = table.unpack or unpack
local dt = 0
--- Used to store inputs which were updated this frame
-- { Input: true, ... }
-- This table is for internal use and shouldn't be used from an external script.
local updated = {}
--- ButtonInput methods
-- @impl ubiquitousse
local button_mt = {
--- Returns a new ButtonInput with the same properties.
-- @treturn ButtonInput the cloned object
clone = function(self)
return input.button(unpack(self.detectors))
end,
--- Bind new ButtonDetector(s) to this input.
-- @tparam ButtonDetectors ... buttons detectors or buttons identifiers to add
-- @treturn ButtonInput this ButtonInput object
bind = function(self, ...)
for _, d in ipairs({...}) do
table.insert(self.detectors, input.buttonDetector(d))
end
return self
end,
--- Unbind ButtonDetector(s).
-- @tparam ButtonDetectors ... buttons detectors or buttons identifiers to remove
-- @treturn ButtonInput this ButtonInput object
unbind = function(self, ...)
for _, d in ipairs({...}) do
for i, detector in ipairs(self.detectors) do
if detector == d then
table.remove(self.detectors, i)
break
end
end
end
return self
end,
--- Unbind all ButtonDetector(s).
-- @treturn ButtonInput this ButtonInput object
clear = function(self)
self.detectors = {}
return self
end,
--- Grabs the input.
-- This function returns a new input object which mirrors the current object, except it will grab every new input.
-- This means any new button press/down/release will only be visible to the new object; the button will always appear unpressed for the initial object.
-- This is useful for contextual input, for example if you want to display a menu without pausing the game: the menu
-- can grab relevant inputs while it is open, so they don't trigger any action in the rest of the game.
-- An input can be grabbed several times; the one which grabbed it last will be the active one.
-- @treturn ButtonInput the new input object which is grabbing the input
grab = function(self)
local grabbed = setmetatable({}, { __index = self, __newindex = self })
table.insert(self.grabStack, grabbed)
self.grabbing = grabbed
return grabbed
end,
--- Release the input that was grabbed by this object.
-- Input will be given back to the previous object.
-- @treturn ButtonInput this ButtonInput object
release = function(self)
local grabStack = self.grabStack
for i, v in ipairs(grabStack) do
if v == self then
table.remove(grabStack, i)
self.grabbing = grabStack[#grabStack]
return self
end
end
error("This object is currently not grabbing this input")
end,
--- Returns true if the input was just pressed.
-- @treturn boolean true if the input was pressed, false otherwise
pressed = function(self)
if self.grabbing == self then
self:update()
return self.state == "pressed"
else
return false
end
end,
--- Returns true if the input was just released.
-- @treturn boolean true if the input was released, false otherwise
released = function(self)
if self.grabbing == self then
self:update()
return self.state == "released"
else
return false
end
end,
--- Returns true if the input is down.
-- @treturn boolean true if the input is currently down, false otherwise
down = function(self)
if self.grabbing == self then
self:update()
local state = self.state
return state == "down" or state == "pressed"
else
return false
end
end,
--- Returns true if the input is up.
-- @treturn boolean true if the input is currently up, false otherwise
up = function(self)
return not self:down()
end,
--- Update button state.
-- Automatically called, don't call unless you know what you're doing.
-- @impl ubiquitousse
update = function(self)
if not updated[self] then
local down = false
for _, d in ipairs(self.detectors) do
if d() then
down = true
break
end
end
local state = self.state
if down then
if state == "none" or state == "released" then
self.state = "pressed"
else
self.state = "down"
end
else
if state == "down" or state == "pressed" then
self.state = "released"
else
self.state = "none"
end
end
updated[self] = true
end
end
}
button_mt.__index = button_mt
--- AxisInput methods
-- @impl ubiquitousse
local axis_mt = {
--- Returns a new AxisInput with the same properties.
-- @treturn AxisInput the cloned object
clone = function(self)
return input.axis(unpack(self.detectors))
:threshold(self.threshold)
end,
--- Bind new AxisDetector(s) to this input.
-- @tparam AxisDetectors ... axis detectors or axis identifiers to add
-- @treturn AxisInput this AxisInput object
bind = function(self, ...)
for _,d in ipairs({...}) do
table.insert(self.detectors, input.axisDetector(d))
end
return self
end,
--- Unbind AxisDetector(s).
-- @tparam AxisDetectors ... axis detectors or axis identifiers to remove
-- @treturn AxisInput this AxisInput object
unbind = button_mt.unbind,
--- Unbind all AxisDetector(s).
-- @treturn AxisInput this AxisInput object
clear = button_mt.clear,
--- Grabs the input.
-- This function returns a new input object which mirrors the current object, except it will grab every new input.
-- This means any value change will only be visible to the new object; the axis will always appear to be at 0 for the initial object.
-- An input can be grabbed several times; the one which grabbed it last will be the active one.
-- @treturn AxisInput the new input object which is grabbing the input
grab = function(self)
local grabbed
grabbed = setmetatable({
positive = input.button(function() return grabbed:value() > 0 end),
negative = input.button(function() return grabbed:value() < 0 end)
}, { __index = self, __newindex = self })
table.insert(self.grabStack, grabbed)
self.grabbing = grabbed
return grabbed
end,
--- Release the input that was grabbed by this object.
-- Input will be given back to the previous object.
-- @treturn AxisInput this AxisInput object
release = button_mt.release,
--- Sets the default detection threshold (deadzone).
-- @tparam number new the new detection threshold
-- @treturn AxisInput this AxisInput object
threshold = function(self, new)
self.threshold = tonumber(new)
return self
end,
--- Returns the value of the input (between -1 and 1).
-- @tparam[opt=default threshold] number threshold value to use
-- @treturn number the input value
value = function(self, curThreshold)
if self.grabbing == self then
self:update()
local val = self.val
return math.abs(val) > math.abs(curThreshold or self.threshold) and val or 0
else
return 0
end
end,
--- Returns the change in value of the input since last update (between -2 and 2).
-- @treturn number the value delta
delta = function(self)
if self.grabbing == self then
self:update()
return self.dval
else
return 0
end
end,
--- Returns the raw value of the input (between -max and +max).
-- @tparam[opt=default threshold*max] number raw threshold value to use
-- @treturn number the input raw value
raw = function(self, rawThreshold)
if self.grabbing == self then
self:update()
local raw = self.raw
return math.abs(raw) > math.abs(rawThreshold or self.threshold*self.max) and raw or 0
else
return 0
end
end,
--- Return the raw max of the input.
-- @treturn number the input raw max
max = function(self)
self:update()
return self.max
end,
--- The associated button pressed when the axis reaches a positive value.
positive = nil,
--- The associated button pressed when the axis reaches a negative value.
negative = nil,
--- Update axis state.
-- Automatically called, don't call unless you know what you're doing.
-- @impl ubiquitousse
update = function(self)
if not updated[self] then
local val, raw, max = 0, 0, 1
for _, d in ipairs(self.detectors) do
local v, r, m = d() -- v[-1,1], r[-m,+m]
if math.abs(v) > math.abs(val) then
val, raw, max = v, r or v, m or 1
end
end
self.dval = val - self.val
self.val, self.raw, self.max = val, raw, max
updated[self] = true
end
end
}
axis_mt.__index = axis_mt
--- PointerInput methods
-- @impl ubiquitousse
local pointer_mt = {
--- Returns a new PointerInput with the same properties.
-- @treturn PointerInput the cloned object
clone = function(self)
return input.pointer(unpack(self.detectors))
:dimensions(self.width, self.height)
:offset(self.offsetX, self.offsetY)
:speed(self.xSpeed, self.ySpeed)
end,
--- Bind new axis couples to this input.
-- @tparam table{mode,XAxis,YAxis} ... couples of axis detectors, axis identifiers or axis input to add and in which mode
-- @treturn PointerInput this PointerInput object
bind = function(self, ...)
for _, p in ipairs({...}) do
if type(p) == "table" then
local h, v = p[2], p[3]
if getmetatable(h) ~= axis_mt then
h = input.axis(h)
end
if getmetatable(v) ~= axis_mt then
v = input.axis(v)
end
table.insert(self.detectors, { p[1], h, v })
else
error("Pointer detector must be a table")
end
end
return self
end,
--- Unbind axis couple(s).
-- @tparam table{mode,XAxis,YAxis} ... couples of axis detectors, axis identifiers or axis input to remove
-- @treturn PointerInput this PointerInput object
unbind = button_mt.unbind,
--- Unbind all axis couple(s).
-- @treturn PointerInput this PointerInput object
clear = button_mt.clear,
--- Grabs the input.
-- This function returns a new input object which mirrors the current object, except it will grab every new input.
-- This means any value change will only be visible to the new object; the pointer will always appear to be at offsetX,offsetY for the initial object.
-- An input can be grabbed several times; the one which grabbed it last will be the active one.
-- @treturn PointerInput the new input object which is grabbing the input
grab = function(self)
local grabbed
grabbed = {
horizontal = input.axis(function()
local h = grabbed:x()
local width = grabbed.width
return h/width, h, width
end),
vertical = input.axis(function()
local v = grabbed:y()
local height = grabbed.height
return v/height, v, height
end)
}
grabbed.right, grabbed.left = grabbed.horizontal.positive, grabbed.horizontal.negative
grabbed.up, grabbed.down = grabbed.vertical.negative, grabbed.vertical.positive
setmetatable(grabbed, { __index = self, __newindex = self })
table.insert(self.grabStack, grabbed)
self.grabbing = grabbed
return grabbed
end,
--- Release the input that was grabbed by this object.
-- Input will be given back to the previous object.
-- @treturn PointerInput this PointerInput object
release = button_mt.release,
--- Set the moving area half-dimensions.
-- Call without argument to use half the window dimensions.
-- It's the half dimensions because axes values goes from -1 to 1, so theses dimensions only
-- covers values from x=0,y=0 to x=1,y=1. The full moving area will be 4*newWidth*newHeight.
-- @tparam number newWidth new width
-- @tparam number newHeight new height
-- @treturn PointerInput this PointerInput object
dimensions = function(self, newWidth, newHeight)
self.width, self.height = newWidth, newHeight
return self
end,
--- Set the moving area coordinates offset.
-- The offset is a value automatically added to the x and y values when using the x() and y() methods.
-- Call without argument to automatically offset so 0,0 <= x(),y() <= width,height, i.e. offset to width,height.
-- @tparam number newOffX new X offset
-- @tparam number newOffY new Y offset
-- @treturn PointerInput this PointerInput object
offset = function(self, newOffX, newOffY)
self.offsetX, self.offsetY = newOffX, newOffY
return self
end,
--- Set maximal speed (pixels-per-milisecond)
-- Only used in relative mode.
-- Calls without argument to use the raw data and don't apply a speed modifier.
-- @tparam number newXSpeed new X speed
-- @tparam number newYSpeed new Y speed
-- @treturn PointerInput this PointerInput object
speed = function(self, newXSpeed, newYSpeed)
self.xSpeed, self.ySpeed = newXSpeed, newYSpeed or newXSpeed
return self
end,
--- Returns the current X value of the pointer.
-- @treturn number X value
x = function(self)
if self.grabbing == self then
self:update()
return self.valX + (self.offsetX or self.width or input.drawWidth/2)
else
return self.offsetX or self.width or input.drawWidth/2
end
end,
--- Returns the current Y value of the pointer.
-- @treturn number Y value
y = function(self)
if self.grabbing == self then
self:update()
return self.valY + (self.offsetY or self.height or input.drawHeight/2)
else
return self.offsetY or self.height or input.drawHeight/2
end
end,
--- Returns the X and Y value of the pointer, clamped.
-- They are clamped to stay in the ellipse touching all 4 sides of the dimension rectangle, i.e. the
-- (x,y) vector's magnitude reached its maximum either in (0,height) or (width,0).
-- Typically, this is used with square dimensions for player movements: when moving diagonally, the magnitude
-- will be the same as when moving horiontally or vertically, thus avoiding faster diagonal movement, A.K.A. "straferunning".
-- If you're not conviced by my overly complicated explanation: just use this to retrieve x and y for movement and everything
-- will be fine.
-- @treturn number X value
-- @treturn number Y value
clamped = function(self)
local width, height = self.width, self.height
if self.grabbing == self then
self:update()
local x, y = self.valX, self.valY
local cx, cy = x, y
local normalizedMagnitude = (x*x)/(width*width) + (y*y)/(height*height) -- go back to a unit circle
if normalizedMagnitude > 1 then
local magnitude = sqrt(x*x + y*y)
cx, cy = cx / magnitude * width, cy / magnitude * height
end
return cx + (self.offsetX or width or input.drawWidth/2), cy + (self.offsetY or height or input.drawHeight/2)
else
return self.offsetX or width or input.drawWidth/2, self.offsetY or height or input.drawHeight/2
end
end,
--- The associated horizontal axis.
horizontal = nil,
--- The associated vertical axis.
vertical = nil,
--- The associated button pressed when the pointer goes to the right.
right = nil,
--- The associated button pressed when the pointer goes to the left.
left = nil,
--- The associated button pressed when the pointer points up.
up = nil,
--- The associated button pressed when the pointer points down.
down = nil,
--- Update pointer state.
-- Automatically called, don't call unless you know what you're doing.
-- @impl ubiquitousse
update = function(self)
if not updated[self] then
local x, y = self.valX, self.valY
local xSpeed, ySpeed = self.xSpeed, self.ySpeed
local width, height = self.width or input.drawWidth/2, self.height or input.drawHeight/2
local newX, newY = x, y
local maxMovX, maxMovY = 0, 0 -- the maxium axis movement in a direction (used to determine which axes have the priority) (absolute value)
for _, pointer in ipairs(self.detectors) do
local mode, xAxis, yAxis = unpack(pointer)
if mode == "relative" then
local movX, movY = math.abs(xAxis:value()), math.abs(yAxis:value())
if movX > maxMovX then
newX = x + (xSpeed and (xAxis:value() * xSpeed * dt) or xAxis:raw())
maxMovX = movX
end
if movY > maxMovY then
newY = y + (ySpeed and (yAxis:value() * ySpeed * dt) or yAxis:raw())
maxMovY = movY
end
elseif mode == "absolute" then
local movX, movY = math.abs(xAxis:delta()), math.abs(yAxis:delta())
if movX > maxMovX then
newX = xAxis:value() * width
maxMovX = movX
end
if movY > maxMovY then
newY = yAxis:value() * height
maxMovY = movY
end
end
end
self.valX, self.valY = math.min(math.abs(newX), width) * (newX < 0 and -1 or 1), math.min(math.abs(newY), height) * (newY < 0 and -1 or 1)
updated[self] = true
end
end
}
pointer_mt.__index = pointer_mt
--- Input stuff
-- Inspired by Tactile by Andrew Minnich (https://github.com/tesselode/tactile), under the MIT license.
-- Ubiquitousse considers two basic input methods, called buttons (binary input) and axes (analog input).
input = {
---------------------------------
--- Detectors (input sources) ---
---------------------------------
-- Buttons detectors --
-- A button detector is a function which returns true (pressed) or false (unpressed).
-- All buttons are identified using an identifier string, which depends on the backend. The presence of eg., a mouse or keyboard is not assumed.
-- Some identifier strings conventions: (not used internally by Ubiquitousse, but it's nice to have some consistency between backends)
-- They should be in the format "source1.source2.[...].button", for example "keyboard.up" or "gamepad.button.1.a" for the A-button of the first gamepad.
-- If the button is actually an axis (ie, the button is pressed if the axis value passes a certain threshold), the threshold should be in the end of the
-- identifier, preceded by a % : for example "gamepad.axis.1.leftx%-0.5" should return true when the left-stick of the first gamepad is moved to the right
-- by more of 50%. The negative threshold value means that the button will be pressed only when the axis has a negative value (in the example, it won't be
-- pressed when the axis is moved to the right).
--- Makes a new button detector from a identifier string.
-- The function may error if the identifier is incorrect.
-- @tparam string button identifier, depends on the platform Ubiquitousse is running on
-- @treturn the new button detector
-- @impl backend
basicButtonDetector = function(str) end,
--- Make a new button detector from a detector function of string.
-- @tparam string, function button identifier
-- @impl ubiquitousse
buttonDetector = function(obj)
if type(obj) == "function" then
return obj
elseif type(obj) == "string" then
return input.basicButtonDetector(obj)
end
error(("Not a valid button detector: %s"):format(obj))
end,
-- Axis detectors --
-- Similar to buttons detectors, but returns a number between -1 and 1.
-- Threshold value can be used similarly with %.
-- Axis detectors can also be defined by two buttons: if the 1rst button is pressed, value will be -1, if the 2nd is pressed it will be 1
-- and if none or the both are pressed, the value will be 0. This kind of axis identifier is a table {"button1", "button2"}.
-- Axis detectors may also optionally return after the number between -1 and 1 the raw value and max value. The raw value is between -max and +max.
--- Makes a new axis detector from a identifier string.
-- The function may error if the identifier is incorrect.
-- @tparam string axis identifier, depends on the platform Ubiquitousse is running on
-- @treturn the new axis detector
-- @impl backend
basicAxisDetector = function(str) end,
--- Make a new axis detector from a detector function, string, or a couple of buttons.
-- @tparam string, function or table axis identifier
-- @impl ubiquitousse
axisDetector = function(obj)
if type(obj) == "function" then
return obj
elseif type(obj) == "string" then
return input.basicAxisDetector(obj)
elseif type(obj) == "table" then
local b1, b2 = input.buttonDetector(obj[1]), input.buttonDetector(obj[2])
return function()
local d1, d2 = b1(), b2()
if d1 and d2 then return 0
elseif d1 then return -1
elseif d2 then return 1
else return 0 end
end
end
error(("Not a valid axis detector: %s"):format(obj))
end,
------------------------------------------
--- Inputs (the thing you want to use) ---
------------------------------------------
-- Buttons inputs --
-- Button input is a container for buttons detector. A button will be pressed when one of its detectors returns true.
-- Inputs also knows if the button was just pressed or released.
-- @tparam ButtonDetectors ... all the buttons detectors or buttons identifiers
-- @tretrun ButtonInput the object
-- @impl ubiquitousse
button = function(...)
local r = setmetatable({
grabStack = {}, -- grabbers stack, last element is the object currently grabbing this input
grabbing = nil, -- object currently grabbing this input
detectors = {}, -- detectors list
state = "none" -- current state (none, pressed, down, released)
}, button_mt)
table.insert(r.grabStack, r)
r.grabbing = r
r:bind(...)
return r
end,
-- Axis inputs --
-- Axis input is a container for axes detector. An axis input will return the value of the axis detector the most far away from their center (0).
-- Axis input provide a threshold setting ; every axis which has a distance to the center below the threshold (none by default) will be ignored.
-- @tparam AxisDetectors ... all the axis detectors or axis identifiers
-- @tretrun AxisInput the object
-- @impl ubiquitousse
axis = function(...)
local r = setmetatable({
grabStack = {}, -- grabbers stack, last element is the object currently grabbing this input
grabbing = nil, -- object currently grabbing this input
detectors = {}, -- detectors list
val = 0, -- current value between -1 and 1
dval = 0, -- change between -2 and 2
raw = 0, -- raw value between -max and +max
max = 1, -- maximum for raw values
threshold = 0 -- ie., the deadzone
}, axis_mt)
table.insert(r.grabStack, r)
r.grabbing = r
r:bind(...)
r.positive = input.button(function() return r:value() > 0 end)
r.negative = input.button(function() return r:value() < 0 end)
return r
end,
-- Pointer inputs --
-- Pointer inputs are container for two axes input, in order to represent a two-dimensionnal pointing device, e.g. a mouse or a stick.
-- Each pointer detector is a table with 3 fields: mode(string), XAxis(axis), YAxis(axis). mode can either be "relative" or "absolute".
-- In relative mode, the pointer will return the movement since last update (for example to move a mouse pointer with a stick).
-- In absolute mode, the pointer will return the pointer position directly deduced of the current axes position.
-- @tparam table{mode,XAxis,YAxis} ... couples of axis detectors, axis identifiers or axis input to add and in which mode
-- @tretrun PointerInput the object
-- @impl ubiquitousse
pointer = function(...)
local r = setmetatable({
grabStack = {}, -- grabbers stack, first element is the object currently grabbing this input
grabbing = nil, -- object currently grabbing this input
detectors = {}, -- pointers list (composite detectors)
valX = 0, valY = 0, -- pointer position
width = 1, height = 1, -- half-dimensions of the movement area
offsetX = 0, offsetY = 0, -- offsets
xSpeed = 1, ySpeed = 1, -- speed (pixels/milisecond); for relative mode
}, pointer_mt)
table.insert(r.grabStack, r)
r.grabbing = r
r:bind(...)
r.horizontal = input.axis(function()
local h = r:x()
local width = r.width
return h/width, h, width
end)
r.vertical = input.axis(function()
local v = r:y()
local height = r.height
return v/height, v, height
end)
r.right, r.left = r.horizontal.positive, r.horizontal.negative
r.up, r.down = r.vertical.negative, r.vertical.positive
return r
end,
------------------------------
--- Input detection helpers --
------------------------------
-- TODO: make this better
--- Returns a list of the buttons currently in use, identified by their string button identifier.
-- This may also returns "axis threshold" buttons if an axis passes the threshold.
-- @treturn table<string> buttons identifiers list
-- @treturn[opt=0.5] number threshold the threshold to detect axes as button
-- @impl backend
buttonsInUse = function(threshold) end,
--- Returns a list of the axes currently in use, identified by their string axis identifier
-- @treturn table<string> axes identifiers list
-- @treturn[opt=0.5] number threshold the threshold to detect axes
-- @impl backend
axesInUse = function(threshold) end,
--- Returns a nice name for the button identifier.
-- Can be locale-depedant and stuff, it's only for display.
-- May returns the raw identifier if you're lazy.
-- @tparam string... button identifier string(s)
-- @treturn string... the displayable names
-- @impl backend
buttonName = function(...) end,
--- Returns a nice name for the axis identifier.
-- Can be locale-depedant and stuff, it's only for display.
-- May returns the raw identifier if you're lazy.
-- @tparam string... axis identifier string(s)
-- @treturn string... the displayable names
-- @impl backend
axisName = function(...) end,
-------------------
--- Other stuff ---
-------------------
--- Some default inputs.
-- The backend should bind detectors to thoses inputs (don't recreate them).
-- These are used to provide some common input default detectors to allow to start a game quickly on
-- any platform without having to configure the keys.
-- If some key function in your game match one of theses defaults, using it instead of creating a new
-- input would be a good idea.
-- @impl mixed
default = {
pointer = nil, -- Pointer: used to move and select. Example binds: arrow keys, WASD, stick.
confirm = nil, -- Button: used to confirm something. Example binds: Enter, A button.
cancel = nil -- Button: used to cancel something. Example binds: Escape, B button.
},
--- Draw area dimensions.
-- Used for pointers.
-- @impl backend
drawWidth = 1,
drawHeight = 1,
--- Update all the Inputs.
-- Should be called at every game update; called by ubiquitousse.update.
-- The backend can hook into this function to to its input-related updates.
-- @tparam numder dt the delta-time
-- @impl ubiquitousse
update = function(newDt)
dt = newDt
updated = {}
end
}
-- Create default inputs
input.default.pointer = input.pointer()
input.default.confirm = input.button()
input.default.cancel = input.button()
return input

1
scene/init.lua Normal file
View file

@ -0,0 +1 @@
return require((...)..".scene")

View file

@ -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
View file

@ -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
View 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
View 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
View 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
View 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

View file

@ -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.

View file

@ -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
View file

@ -0,0 +1 @@
return require((...)..".util")

336
util/util.lua Normal file
View 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