commit fc40d440df0f02d8b252ce73175b17e56d3f9db5 Author: Reuh Date: Tue Apr 26 14:55:33 2016 +0200 Initial commit diff --git a/audio.lua b/audio.lua new file mode 100644 index 0000000..5bb2146 --- /dev/null +++ b/audio.lua @@ -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 +} diff --git a/backend/ctrulua.lua b/backend/ctrulua.lua new file mode 100644 index 0000000..ac9078a --- /dev/null +++ b/backend/ctrulua.lua @@ -0,0 +1,2 @@ +-- TODO: everything +error("TODO level over 9000") diff --git a/backend/love.lua b/backend/love.lua new file mode 100644 index 0000000..bbfb432 --- /dev/null +++ b/backend/love.lua @@ -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 diff --git a/config.ld b/config.ld new file mode 100644 index 0000000..36d99c8 --- /dev/null +++ b/config.ld @@ -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 diff --git a/draw.lua b/draw.lua new file mode 100644 index 0000000..1745a8d --- /dev/null +++ b/draw.lua @@ -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, +} diff --git a/event.lua b/event.lua new file mode 100644 index 0000000..7265fe4 --- /dev/null +++ b/event.lua @@ -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 +} diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..a9ead65 --- /dev/null +++ b/init.lua @@ -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 diff --git a/input.lua b/input.lua new file mode 100644 index 0000000..4b0c27c --- /dev/null +++ b/input.lua @@ -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 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 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 diff --git a/lib/easing.lua b/lib/easing.lua new file mode 100644 index 0000000..c287982 --- /dev/null +++ b/lib/easing.lua @@ -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, +} diff --git a/scene.lua b/scene.lua new file mode 100644 index 0000000..3e70f1c --- /dev/null +++ b/scene.lua @@ -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 diff --git a/time.lua b/time.lua new file mode 100644 index 0000000..5d2184b --- /dev/null +++ b/time.lua @@ -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 { = , ...} + -- 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