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

Initial commit

This commit is contained in:
Reuh 2016-04-26 14:55:33 +02:00
commit fc40d440df
11 changed files with 1828 additions and 0 deletions

9
audio.lua Normal file
View file

@ -0,0 +1,9 @@
-- abstract.audio
--- Audio functions.
return {
--- Loads an audio file and returns the corresponding audio object.
-- TODO: audio object doc
-- @impl backend
load = function(filepath) end
}

2
backend/ctrulua.lua Normal file
View file

@ -0,0 +1,2 @@
-- TODO: everything
error("TODO level over 9000")

494
backend/love.lua Normal file
View file

@ -0,0 +1,494 @@
--- Löve backend 0.0.1 for Abstract.
-- Provides all the Abstract API on a Löve environment.
-- Made for Löve 0.10.1 and abstract 0.0.1.
-- See `abstract` for Abstract 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 abstract = require((...):match("^(.-abstract)%."))
-- Version compatibility warning
do
local function checkCompat(stuffName, expectedVersion, actualVersion)
if actualVersion ~= expectedVersion then
local txt = ("Abstract 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", "0.10.1", ("%s.%s.%s"):format(love.getVersion()))
checkCompat("abstract", "0.0.1", abstract.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).
local function add(tbl, toAdd)
for k,v in pairs(toAdd) do
local old = tbl[k]
tbl[k] = function(...)
old(...)
return v(...)
end
end
end
-- abstract
abstract.backend = "love"
add(abstract, {
setup = function(params)
local p = abstract.params
love.window.setTitle(p.title)
love.window.setMode(p.width, p.height, {
resizable = p.resizable
})
end
})
-- abstract.event
do
local updateDefault = abstract.event.update
abstract.event.update = function() end
function love.update(dt)
-- Value update
abstract.fps = love.timer.getFPS()
abstract.dt = love.timer.getDelta()
-- Stuff defined in abstract.lua
updateDefault(dt)
-- Callback
abstract.event.update(dt)
end
local drawDefault = abstract.event.draw
abstract.event.draw = function() end
function love.draw()
love.graphics.push()
-- Resize type
local winW, winH = love.graphics.getWidth(), love.graphics.getHeight()
local gameW, gameH = abstract.params.width, abstract.params.height
if abstract.params.resizeType == "auto" then
love.graphics.scale(winW/gameW, winH/gameH)
elseif abstract.params.resizeType == "center" then
love.graphics.translate(math.floor(winW/2-gameW/2), math.floor(winH/2-gameH/2))
end
-- Stuff defined in abstract.lua
drawDefault()
-- Callback
abstract.event.draw()
love.graphics.pop()
end
end
-- abstract.draw
local defaultFont = love.graphics.getFont()
add(abstract.draw, {
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,
line = function(x1, y1, x2, y2)
love.graphics.line(x1, y1, x2, y2)
end,
rectangle = function(x, y, width, height)
love.graphics.rectangle("fill", x, y, width, height)
end,
scissor = function(x, y, width, height)
love.graphics.setScissor(x, y, width, height)
end,
-- TODO: doc
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 abstract.params.resizeType == "none" then
abstract.draw.width = width
abstract.draw.height = height
end
end
-- abstract.audio
add(abstract.audio, {
-- TODO: doc
load = function(filepath)
local audio = love.audio.newSource(filepath)
return {
play = function(self)
audio:play()
end
}
end
})
-- abstract.time
add(abstract.time, {
get = function()
return love.timer.getTime()
end
})
-- abstract.input
do
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
-- love.wheelmoved doesn't trigger when the wheel stop moving, so we need to clear up our stuff after love.update (so in love.draw)
add(love, {
draw = 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
})
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(abstract.input, {
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 = abstract.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 = {}
local 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 = {}
local 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(r, 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 = abstract.input.buttonName(id:match("^(.+)%,(.+)$"))
table.insert(ret, b1.." / "..b2)
-- Mouse move
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 move
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(r, id)
end
end
return unpack(ret)
end
})
-- Defaults
abstract.input.default.pointer:bind(
{ "absolute", "keyboard.left,keyboard.right", "keyboard.up,keyboard.down" },
{ "absolute", "gamepad.axis.1.leftx", "gamepad.axis.1.lefty" }
)
abstract.input.default.up:bind(
"keyboard.up", "keyboard.w",
"gamepad.button.1.dpup", "gamepad.axis.1.lefty%-0.5"
)
abstract.input.default.down:bind(
"keyboard.down", "keyboard.s",
"gamepad.button.1.dpdown", "gamepad.axis.1.lefty%0.5"
)
abstract.input.default.right:bind(
"keyboard.right", "keyboard.d",
"gamepad.button.1.dpright", "gamepad.axis.1.leftx%0.5"
)
abstract.input.default.left:bind(
"keyboard.left", "keyboard.a",
"gamepad.button.1.dpleft", "gamepad.axis.1.leftx%-0.5"
)
abstract.input.default.confirm:bind(
"keyboard.enter", "keyboard.space", "keyboard.lshift", "keyboard.e",
"gamepad.button.1.a"
)
abstract.input.default.cancel:bind(
"keyboard.escape", "keyboard.backspace",
"gamepad.button.1.b"
)
end

12
config.ld Normal file
View file

@ -0,0 +1,12 @@
project = "Abstract"
description = "Abstract Game Engine"
full_description = "A simple Lua game engine, made to run everywhere. See the abstract module for more information."
title = "Abstract Reference"
package = "abstract"
file = "./"
format = "markdown"
style = "!fixed"
not_luadoc = true

72
draw.lua Normal file
View file

@ -0,0 +1,72 @@
-- abstract.draw
local abstract = require((...):match("^(.-abstract)%."))
--- 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.
return {
--- Sets the drawing color
-- @tparam number r the red component (0-255)
-- @tparam number g the green component (0-255)
-- @tparam number b the blue component (0-255)
-- @tparam[opt=255] number a the alpha (opacity) component (0-255)
-- @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 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
-- @impl backend
line = function(x1, y1, x2, y2) 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 backend
rectangle = function(x, y, width, height) 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.
-- @impl backend
width = abstract.params.width,
--- The drawing area height, in pixels.
-- @impl backend
height = abstract.params.height,
-- TODO: doc & api
push = function() end,
pop = function() end,
polygon = function(...) end,
circle = function(x, y, radius) end,
translate = function(x, y) end,
font = function(filename) end,
image = function(filename) end,
}

30
event.lua Normal file
View file

@ -0,0 +1,30 @@
-- abstract.event
local abstract = require((...):match("^(.-abstract)%."))
local input = abstract.input
local time = abstract.time
local scene = abstract.scene
--- 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.
-- @usage -- in the game's code
-- abstract.event.draw = function()
-- abstract.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 seconds
-- @impl mixed
update = function(dt)
input.update(dt)
time.update(dt)
scene.update(dt)
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()
scene.draw()
end
}

118
init.lua Normal file
View file

@ -0,0 +1,118 @@
-- abstract
--- Abstract Engine.
-- Main module, containing the abstract things.
-- The API exposed here is the Abstract API.
-- It is as the name imply abstract, and must be implemented in a backend, such as abstract.love.
-- When required, this file will try to autodetect the engine it is running on, and load a correct backend.
--
-- For backend writers:
-- If a function defined here already contains some code, this means this code is mandatory and you must put/call
-- it in your implementation.
-- Also, a backend file shouldn't redefine the abstract table itself but only redefine the backend-dependant fields.
-- The API doesn't make the difference between numbers and integers, so convert to integers when needed.
--
-- Abstract'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, Abstract still make some small assumptions about the engine:
-- * The engine has 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).
-- * 32bit color depth.
--
-- Regarding data formats, Abstract reference implemtations 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 Abstract may provide a script to
-- automatically convert data formats from a project at some point.
--
-- 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 abstract, must be implemented in backend
-- * mixed: partly implemented in abstract but must be complemeted in backend
-- * abstract: fully-working version in abstract, may or may not be redefined in backend
-- The implementation level is indicated using the "@impl level" annotation.
--
-- @usage local abstract = require("abstract")
local p = ... -- require path
local abstract
abstract = {
--- Abstract version.
-- @impl abstract
version = "0.0.1",
--- Backend name.
-- For consistency, only use lowercase letters [a-z] (no special char)
-- @impl backend
backend = "unknown",
--- General game paramters (some defaults).
-- @impl abstract
params = {
title = "Abstract Engine",
width = 800,
height = 600,
resizable = false,
resizeType = "auto"
},
--- Setup general game 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:
-- abstract.setup {
-- title = "Abstract Engine", -- 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
setup = function(params)
for k, v in pairs(params) do
abstract.params[k] = v
end
abstract.draw.width = params.width
abstract.draw.height = params.height
end,
--- Frames per second (the backend should update this value).
-- @impl backend
fps = 60,
--- Time since last frame (seconds)
-- @impl backend
dt = 0
}
-- We're going to require modules requiring abstract, so to avoid stack overflows we already register the abstract package
package.loaded[p] = abstract
-- External submodules
abstract.time = require(p..".time")
abstract.draw = require(p..".draw")
abstract.audio = require(p..".audio")
abstract.input = require(p..".input")
abstract.scene = require(p..".scene")
abstract.event = require(p..".event")
-- Backend engine autodetect and load
if love then
require(p..".backend.love")
elseif package.loaded["ctr"] then
require(p..".backend.ctrulua")
end
return abstract

385
input.lua Normal file
View file

@ -0,0 +1,385 @@
-- abstract.input
local abstract = require((...):match("^(.-abstract)%."))
local draw = abstract.draw
--- 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.
-- Abstract 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.
-- Abstract being abstract, 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 abstract 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).
--- Makes a new axis detector(s) from the identifier(s) string.
-- @tparam string axis identifier, depends on the platform abstract 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 abstract
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 = {
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 = 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 = function(self, ...)
for _,d in ipairs({...}) do
detectors[d] = nil
end
return self
end,
pressed = function(_)
update()
return state == "pressed"
end,
down = function(_)
update()
return state == "down" or state == "pressed"
end,
released = function(_)
update()
return state == "released"
end,
}
r:bind(...)
return r
end,
-- TODO: doc
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 = {
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 = 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 = function(self, ...)
for _,d in ipairs({...}) do
detectors[d] = nil
end
return self
end,
threshold = function(self, new)
threshold = tonumber(new)
return self
end,
value = function(_, curThreshold)
update()
return math.abs(value) > math.abs(curThreshold or threshold) and value or 0
end,
raw = function(_, rawThreshold)
update()
return math.abs(raw) > math.abs(rawThreshold or threshold*max) and raw or 0
end,
max = function(_)
update()
return max
end
}
r:bind(...)
return r
end,
--- 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,
-- TODO: doc
pointer = function(...)
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/second); 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 * abstract.dt) or xAxis:raw())
maxMovX = movX
end
if movY > maxMovY then
newY = y + (ySpeed and (yAxis:value() * ySpeed * abstract.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 = {
clone = function(self)
return input.pointer(unpack(pointers))
:dimensions(width, height)
:offset(offsetX, offsetY)
:speed(xSpeed, ySpeed)
end,
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 = 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.
-- @impl abstract
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.
-- @impl abstract
offset = function(self, newOffX, newOffY)
offsetX, offsetY = newOffX, newOffY
return self
end,
--- Set maximal speed (pixels-per-second)
-- Only used in relative mode.
-- Calls without argument to use the raw data and don't apply a speed modifier.
-- @impl abstract
speed = function(self, newXSpeed, newYSpeed)
xSpeed, ySpeed = newXSpeed, newYSpeed or newXSpeed
return self
end,
x = function()
update()
return x + (offsetX or width or draw.width/2)
end,
y = function()
update()
return y + (offsetY or height or draw.height/2)
end
}
r:bind(...)
return r
end,
--- Some default inputs.
-- The backend may 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, -- A default Pointer: used to move. Example binds: arrow keys, stick.
up = nil, -- Button: similar to pointer, but only the up button.
down = nil, -- Button: similar to pointer, but only the down button.
right = nil, -- Button: similar to pointer, but only the right button.
left = nil, -- Button: similar to pointer, but only the left button.
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 abstract.event.update.
-- @tparam numder dt the delta-time
-- @impl abstract
update = function(dt)
updated = {}
end
}
-- Create default inputs
input.default.pointer = input.pointer()
input.default.up = input.button()
input.default.down = input.button()
input.default.right = input.button()
input.default.left = input.button()
input.default.confirm = input.button()
input.default.cancel = input.button()
return input

435
lib/easing.lua Normal file
View file

@ -0,0 +1,435 @@
--
-- Adapted from
-- Tweener's easing functions (Penner's Easing Equations)
-- and http://code.google.com/p/tweener/ (jstweener javascript version)
--
--[[
Disclaimer for Robert Penner's Easing Equations license:
TERMS OF USE - EASING EQUATIONS
Open source under the BSD License.
Copyright © 2001 Robert Penner
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of the author nor the names of contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
]]
-- For all easing functions:
-- t = elapsed time
-- b = begin
-- c = change == ending - beginning
-- d = duration (total time)
local pow = math.pow
local sin = math.sin
local cos = math.cos
local pi = math.pi
local sqrt = math.sqrt
local abs = math.abs
local asin = math.asin
local function linear(t, b, c, d)
return c * t / d + b
end
local function inQuad(t, b, c, d)
t = t / d
return c * pow(t, 2) + b
end
local function outQuad(t, b, c, d)
t = t / d
return -c * t * (t - 2) + b
end
local function inOutQuad(t, b, c, d)
t = t / d * 2
if t < 1 then
return c / 2 * pow(t, 2) + b
else
return -c / 2 * ((t - 1) * (t - 3) - 1) + b
end
end
local function outInQuad(t, b, c, d)
if t < d / 2 then
return outQuad (t * 2, b, c / 2, d)
else
return inQuad((t * 2) - d, b + c / 2, c / 2, d)
end
end
local function inCubic (t, b, c, d)
t = t / d
return c * pow(t, 3) + b
end
local function outCubic(t, b, c, d)
t = t / d - 1
return c * (pow(t, 3) + 1) + b
end
local function inOutCubic(t, b, c, d)
t = t / d * 2
if t < 1 then
return c / 2 * t * t * t + b
else
t = t - 2
return c / 2 * (t * t * t + 2) + b
end
end
local function outInCubic(t, b, c, d)
if t < d / 2 then
return outCubic(t * 2, b, c / 2, d)
else
return inCubic((t * 2) - d, b + c / 2, c / 2, d)
end
end
local function inQuart(t, b, c, d)
t = t / d
return c * pow(t, 4) + b
end
local function outQuart(t, b, c, d)
t = t / d - 1
return -c * (pow(t, 4) - 1) + b
end
local function inOutQuart(t, b, c, d)
t = t / d * 2
if t < 1 then
return c / 2 * pow(t, 4) + b
else
t = t - 2
return -c / 2 * (pow(t, 4) - 2) + b
end
end
local function outInQuart(t, b, c, d)
if t < d / 2 then
return outQuart(t * 2, b, c / 2, d)
else
return inQuart((t * 2) - d, b + c / 2, c / 2, d)
end
end
local function inQuint(t, b, c, d)
t = t / d
return c * pow(t, 5) + b
end
local function outQuint(t, b, c, d)
t = t / d - 1
return c * (pow(t, 5) + 1) + b
end
local function inOutQuint(t, b, c, d)
t = t / d * 2
if t < 1 then
return c / 2 * pow(t, 5) + b
else
t = t - 2
return c / 2 * (pow(t, 5) + 2) + b
end
end
local function outInQuint(t, b, c, d)
if t < d / 2 then
return outQuint(t * 2, b, c / 2, d)
else
return inQuint((t * 2) - d, b + c / 2, c / 2, d)
end
end
local function inSine(t, b, c, d)
return -c * cos(t / d * (pi / 2)) + c + b
end
local function outSine(t, b, c, d)
return c * sin(t / d * (pi / 2)) + b
end
local function inOutSine(t, b, c, d)
return -c / 2 * (cos(pi * t / d) - 1) + b
end
local function outInSine(t, b, c, d)
if t < d / 2 then
return outSine(t * 2, b, c / 2, d)
else
return inSine((t * 2) -d, b + c / 2, c / 2, d)
end
end
local function inExpo(t, b, c, d)
if t == 0 then
return b
else
return c * pow(2, 10 * (t / d - 1)) + b - c * 0.001
end
end
local function outExpo(t, b, c, d)
if t == d then
return b + c
else
return c * 1.001 * (-pow(2, -10 * t / d) + 1) + b
end
end
local function inOutExpo(t, b, c, d)
if t == 0 then return b end
if t == d then return b + c end
t = t / d * 2
if t < 1 then
return c / 2 * pow(2, 10 * (t - 1)) + b - c * 0.0005
else
t = t - 1
return c / 2 * 1.0005 * (-pow(2, -10 * t) + 2) + b
end
end
local function outInExpo(t, b, c, d)
if t < d / 2 then
return outExpo(t * 2, b, c / 2, d)
else
return inExpo((t * 2) - d, b + c / 2, c / 2, d)
end
end
local function inCirc(t, b, c, d)
t = t / d
return(-c * (sqrt(1 - pow(t, 2)) - 1) + b)
end
local function outCirc(t, b, c, d)
t = t / d - 1
return(c * sqrt(1 - pow(t, 2)) + b)
end
local function inOutCirc(t, b, c, d)
t = t / d * 2
if t < 1 then
return -c / 2 * (sqrt(1 - t * t) - 1) + b
else
t = t - 2
return c / 2 * (sqrt(1 - t * t) + 1) + b
end
end
local function outInCirc(t, b, c, d)
if t < d / 2 then
return outCirc(t * 2, b, c / 2, d)
else
return inCirc((t * 2) - d, b + c / 2, c / 2, d)
end
end
local function inElastic(t, b, c, d, a, p)
if t == 0 then return b end
t = t / d
if t == 1 then return b + c end
if not p then p = d * 0.3 end
local s
if not a or a < abs(c) then
a = c
s = p / 4
else
s = p / (2 * pi) * asin(c/a)
end
t = t - 1
return -(a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b
end
-- a: amplitud
-- p: period
local function outElastic(t, b, c, d, a, p)
if t == 0 then return b end
t = t / d
if t == 1 then return b + c end
if not p then p = d * 0.3 end
local s
if not a or a < abs(c) then
a = c
s = p / 4
else
s = p / (2 * pi) * asin(c/a)
end
return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p) + c + b
end
-- p = period
-- a = amplitud
local function inOutElastic(t, b, c, d, a, p)
if t == 0 then return b end
t = t / d * 2
if t == 2 then return b + c end
if not p then p = d * (0.3 * 1.5) end
if not a then a = 0 end
local s
if not a or a < abs(c) then
a = c
s = p / 4
else
s = p / (2 * pi) * asin(c / a)
end
if t < 1 then
t = t - 1
return -0.5 * (a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b
else
t = t - 1
return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p ) * 0.5 + c + b
end
end
-- a: amplitud
-- p: period
local function outInElastic(t, b, c, d, a, p)
if t < d / 2 then
return outElastic(t * 2, b, c / 2, d, a, p)
else
return inElastic((t * 2) - d, b + c / 2, c / 2, d, a, p)
end
end
local function inBack(t, b, c, d, s)
if not s then s = 1.70158 end
t = t / d
return c * t * t * ((s + 1) * t - s) + b
end
local function outBack(t, b, c, d, s)
if not s then s = 1.70158 end
t = t / d - 1
return c * (t * t * ((s + 1) * t + s) + 1) + b
end
local function inOutBack(t, b, c, d, s)
if not s then s = 1.70158 end
s = s * 1.525
t = t / d * 2
if t < 1 then
return c / 2 * (t * t * ((s + 1) * t - s)) + b
else
t = t - 2
return c / 2 * (t * t * ((s + 1) * t + s) + 2) + b
end
end
local function outInBack(t, b, c, d, s)
if t < d / 2 then
return outBack(t * 2, b, c / 2, d, s)
else
return inBack((t * 2) - d, b + c / 2, c / 2, d, s)
end
end
local function outBounce(t, b, c, d)
t = t / d
if t < 1 / 2.75 then
return c * (7.5625 * t * t) + b
elseif t < 2 / 2.75 then
t = t - (1.5 / 2.75)
return c * (7.5625 * t * t + 0.75) + b
elseif t < 2.5 / 2.75 then
t = t - (2.25 / 2.75)
return c * (7.5625 * t * t + 0.9375) + b
else
t = t - (2.625 / 2.75)
return c * (7.5625 * t * t + 0.984375) + b
end
end
local function inBounce(t, b, c, d)
return c - outBounce(d - t, 0, c, d) + b
end
local function inOutBounce(t, b, c, d)
if t < d / 2 then
return inBounce(t * 2, 0, c, d) * 0.5 + b
else
return outBounce(t * 2 - d, 0, c, d) * 0.5 + c * .5 + b
end
end
local function outInBounce(t, b, c, d)
if t < d / 2 then
return outBounce(t * 2, b, c / 2, d)
else
return inBounce((t * 2) - d, b + c / 2, c / 2, d)
end
end
return {
linear = linear,
inQuad = inQuad,
outQuad = outQuad,
inOutQuad = inOutQuad,
outInQuad = outInQuad,
inCubic = inCubic ,
outCubic = outCubic,
inOutCubic = inOutCubic,
outInCubic = outInCubic,
inQuart = inQuart,
outQuart = outQuart,
inOutQuart = inOutQuart,
outInQuart = outInQuart,
inQuint = inQuint,
outQuint = outQuint,
inOutQuint = inOutQuint,
outInQuint = outInQuint,
inSine = inSine,
outSine = outSine,
inOutSine = inOutSine,
outInSine = outInSine,
inExpo = inExpo,
outExpo = outExpo,
inOutExpo = inOutExpo,
outInExpo = outInExpo,
inCirc = inCirc,
outCirc = outCirc,
inOutCirc = inOutCirc,
outInCirc = outInCirc,
inElastic = inElastic,
outElastic = outElastic,
inOutElastic = inOutElastic,
outInElastic = outInElastic,
inBack = inBack,
outBack = outBack,
inOutBack = inOutBack,
outInBack = outInBack,
inBounce = inBounce,
outBounce = outBounce,
inOutBounce = inOutBounce,
outInBounce = outInBounce,
}

112
scene.lua Normal file
View file

@ -0,0 +1,112 @@
-- abstract.scene
local abstract = require((...):match("^(.-abstract)%."))
local time = abstract.time
--- Returns the file path of the given module name.
local function getPath(modname)
local filepath = ""
for path in package.path:gmatch("[^;]+") do
local path = path:gsub("%?", (modname:gsub("%.", "/")))
local f = io.open(path)
if f then f:close() filepath = path break end
end
return filepath
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.
-- This module is fully implemented in abstract and is mostly a "recommended way" of organising an abstract-based game.
-- However, you don't have to use this if you don't want to. abstract.scene handles all the differents abstract-states and
-- make them scene-independent, for example by creating a scene-specific TimerRegistry (TimedFunctions that are keept accross
-- states are generally a bad idea). Theses scene-specific states should be created and available in the table returned by
-- abstract.scene.new.
-- 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 abstract.scene.new and returns it at the end of the file
-- Order of callbacks:
-- * all scene exit callbacks are called before changing the stack or the current scene (ie, abstract.scene.current and the
-- last stack element is the scene in which the exit or suspend function was called)
-- * all scene enter callbacks are called before changing the stack or the current scene (ie, abstract.scene.current and the
-- last stack element is the previous scene which was just exited, and not the new scene)
local scene
scene = {
--- The current scene table.
-- @impl abstract
current = nil,
--- The scene stack: list of scene, from the farest one to the nearest.
-- @impl abstract
stack = {},
--- Creates and returns a new Scene object.
-- @impl abstract
new = function()
return {
time = time.new(), -- Scene-specific TimerRegistry.
exit = function() end, -- Called when exiting a scene, and not expecting to come back (scene will be unloaded).
suspend = function() end, -- Called when suspending a scene, and expecting to come back (scene won't be unloaded).
resume = function() end, -- Called when resuming a suspended scene (after calling suspend).
update = function(dt) end, -- Called on each abstract.event.update on the current scene.
draw = function() end -- Called on each abstract.event.draw on the current scene.
}
end,
--- Switch to a new scene.
-- The current scene exit function will be called, the new scene will be loaded, and then
-- the current scene will then be replaced by the new one.
-- @tparam string scenePath the new scene module name
-- @impl abstract
switch = function(scenePath)
if scene.current then scene.current.exit() end
scene.current = dofile(getPath(scenePath))
local i = #scene.stack
scene.stack[math.max(i, 1)] = scene.current
end,
--- Push a new scene to the scene stack.
-- Similar to abstract.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 abstract.scene.pop, the old scene
-- will be reused.
-- @tparam string scenePath the new scene module name
-- @impl abstract
push = function(scenePath)
if scene.current then scene.current.suspend() end
scene.current = dofile(getPath(scenePath))
table.insert(scene.stack, scene.current)
end,
--- Pop the current scene from the scene stack.
-- The current scene exit function will be called, then the previous scene resume function will be called.
-- Then the current scene will be removed from the stack, and the previous scene will be set as the current scene.
-- @impl abstract
pop = function()
if scene.current then scene.current.exit() end
local previous = scene.stack[#scene.stack-1]
if previous then previous.resume() end
table.remove(scene.stack)
scene.current = previous
end,
--- Update the current scene.
-- Should be called in abstract.event.update.
-- @tparam number dt the delta-time
-- @impl abstract
update = function(dt)
if scene.current then
scene.current.time.update(dt)
scene.current.update(dt)
end
end,
--- Draw the current scene.
-- Should be called in abstract.event.draw.
-- @impl abstract
draw = function()
if scene.current then scene.current.draw() end
end
}
return scene

159
time.lua Normal file
View file

@ -0,0 +1,159 @@
-- abstract.time
local abstract = require((...):match("^(.-abstract)%."))
local ease = require((...):match("^(.-abstract)%.")..".lib.easing")
--- Time related functions
local time
local function newTimerRegistry()
--- Used to store all the functions delayed with abstract.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 = {}
return {
--- Creates and return a new TimerRegistry.
-- A TimerRegistry is a separate abstract.time instance: its TimedFunctions will be independant
-- from the one registered using abstract.time.run (the global TimerRegistry). If you use the scene
-- system, a scene-specific TimerRegistry is available at abstract.scene.current.time.
new = function()
return newTimerRegistry()
end,
--- Returns the number of seconds 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. Should be called using abstract.time.get and not in
-- non-global TimerRegistry.
-- @impl backend
get = function() end,
--- Update all the TimedFunctions calls.
-- Supposed to be called in abstract.event.update.
-- @tparam numder dt the delta-time
-- @impl abstract
update = function(dt)
local d = delayed
for func, t in pairs(d) do
local co = t.coroutine
t.after = t.after - dt
if t.after <= 0 then
d[func] = nil
if not co then
co = coroutine.create(func)
t.coroutine = co
t.started = time.get()
if t.times > 0 then t.times = t.times - 1 end
t.onStart()
end
assert(coroutine.resume(co, function(delay)
t.after = delay
d[func] = t
coroutine.yield()
end, dt))
if coroutine.status(co) == "dead" then
if (t.during >= 0 and t.started + t.during < time.get())
or (t.times == 0)
or (t.every == -1 and t.times == -1 and t.during == -1) -- no repeat
then
t.onEnd()
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,
--- Schedule a function to run.
-- The function will receive as first parameter the wait(time) function, which will pause the function execution for time seconds.
-- The second parameter is the delta-time.
-- @tparam[opt=0] number delay delay in seconds before first run
-- @tparam[opt] function func the function to schedule
-- @treturn TimedFunction the object
-- @impl abstract
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)
local 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] = {
coroutine = nil,
started = 0,
after = -1,
every = -1,
times = -1,
during = -1,
onStart = function() end,
onEnd = function() end
}
local t = delayed[func] -- internal data
local r -- external interface
r = {
after = function(_, time)
t.after = time
return r
end,
every = function(_, time)
t.every = time
return r
end,
times = function(_, count)
t.times = count
return r
end,
during = function(_, time)
t.during = time
return r
end,
onStart = function(_, func)
t.onStart = func
return r
end,
onEnd = function(_, func)
t.onEnd = func
return r
end
}
return r
end,
--- Tween some numeric values.
-- @tparam number duration tween duration (seconds)
-- @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
-- @impl abstract
tween = function(duration, tbl, to, method)
local method = method or "linear"
method = type(method) == "string" and ease[method] or method
local time = 0
local from = {}
for k in pairs(to) do from[k] = tbl[k] end
return time.run(function(wait, dt)
time = time + dt
for k, v in pairs(to) do
tbl[k] = method(time, from[k], to[k] - from[k], duration)
end
end):during(duration)
end,
--- Cancels all the running TimedFunctions.
-- @impl abstract
clear = function()
delayed = {}
end
}
end
time = newTimerRegistry()
return time