Patch up some template holes.

More consistent collision detection, LiveForNFrames system, drawSystem split, more correct LOVE types, inspect.lus library for table printing, and a few improved tiny types.
This commit is contained in:
Sage Vaillancourt 2025-03-19 14:07:19 -04:00
parent 3316f3fc3b
commit 7c69889098
10 changed files with 531 additions and 98 deletions

View File

@ -9,9 +9,8 @@ local SOME_TABLE = {}
---@alias BitMask number ---@alias BitMask number
---@alias ButtonState { receivedInputThisFrame: boolean, aJustPressed: boolean, bJustPressed: boolean, upJustPressed: boolean, downJustPressed: boolean, leftJustPressed: boolean, rightJustPressed: boolean } ---@alias ButtonState { receivedInputThisFrame: boolean, aJustPressed: boolean, bJustPressed: boolean, upJustPressed: boolean, downJustPressed: boolean, leftJustPressed: boolean, rightJustPressed: boolean }
---@alias Collision { collisionBetween: Entity[] } ---@alias Collision { collisionBetween: Entity[] }
---@alias CrankState { crankChange: number, changeInLastHalfSecond: number }
---@alias Entity table ---@alias Entity table
---@alias InRelations Entity[] ---@alias FontData love.FontData
---@alias XyPair { x: number, y: number } ---@alias XyPair { x: number, y: number }
T = { T = {
@ -22,10 +21,6 @@ T = {
marker = SOME_TABLE, marker = SOME_TABLE,
---@type fun(self) ---@type fun(self)
SelfFunction = function() end, SelfFunction = function() end,
---@type pd_image
pd_image = SOME_TABLE,
---@type pd_font
pd_font = SOME_TABLE,
---@type AnyComponent ---@type AnyComponent
AnyComponent = SOME_TABLE, AnyComponent = SOME_TABLE,
@ -39,14 +34,11 @@ T = {
---@type Collision ---@type Collision
Collision = SOME_TABLE, Collision = SOME_TABLE,
---@type CrankState
CrankState = SOME_TABLE,
---@type Entity ---@type Entity
Entity = SOME_TABLE, Entity = SOME_TABLE,
---@type InRelations ---@type FontData
InRelations = SOME_TABLE, FontData = SOME_TABLE,
---@type XyPair ---@type XyPair
XyPair = SOME_TABLE, XyPair = SOME_TABLE,

View File

@ -59,9 +59,8 @@ local SOME_TABLE = {}
XyPair = "{ x: number, y: number }", XyPair = "{ x: number, y: number }",
Collision = "{ collisionBetween: Entity[] }", Collision = "{ collisionBetween: Entity[] }",
BitMask = "number", BitMask = "number",
InRelations = "Entity[]", FontData = "love.FontData",
ButtonState = "{ receivedInputThisFrame: boolean, aJustPressed: boolean, bJustPressed: boolean, upJustPressed: boolean, downJustPressed: boolean, leftJustPressed: boolean, rightJustPressed: boolean }", ButtonState = "{ receivedInputThisFrame: boolean, aJustPressed: boolean, bJustPressed: boolean, upJustPressed: boolean, downJustPressed: boolean, leftJustPressed: boolean, rightJustPressed: boolean }",
CrankState = "{ crankChange: number, changeInLastHalfSecond: number }",
})) }))
T = { T = {
bool = true, bool = true,
@ -70,11 +69,7 @@ T = {
str = "", str = "",
marker = SOME_TABLE, marker = SOME_TABLE,
---@type fun(self) ---@type fun(self)
SelfFunction = function() end, SelfFunction = function() end,!!(dumpTypeObjects())
---@type pd_image
pd_image = SOME_TABLE,
---@type pd_font
pd_font = SOME_TABLE,!!(dumpTypeObjects())
} }
---@generic T ---@generic T

367
lib/inspect.lua Normal file
View File

@ -0,0 +1,367 @@
local _tl_compat
if (tonumber((_VERSION or ""):match("[%d.]*$")) or 0) < 5.3 then
local p, m = pcall(require, "compat53.module")
if p then
_tl_compat = m
end
end
local math = _tl_compat and _tl_compat.math or math
local string = _tl_compat and _tl_compat.string or string
local table = _tl_compat and _tl_compat.table or table
local inspect = { Options = {} }
inspect._VERSION = "inspect.lua 3.1.0"
inspect._URL = "http://github.com/kikito/inspect.lua"
inspect._DESCRIPTION = "human-readable representations of tables"
inspect._LICENSE = [[
MIT LICENSE
Copyright (c) 2022 Enrique García Cota
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
inspect.KEY = setmetatable({}, {
__tostring = function()
return "inspect.KEY"
end,
})
inspect.METATABLE = setmetatable({}, {
__tostring = function()
return "inspect.METATABLE"
end,
})
local tostring = tostring
local rep = string.rep
local match = string.match
local char = string.char
local gsub = string.gsub
local fmt = string.format
local _rawget
if rawget then
_rawget = rawget
else
_rawget = function(t, k)
return t[k]
end
end
local function rawpairs(t)
return next, t, nil
end
local function smartQuote(str)
if match(str, '"') and not match(str, "'") then
return "'" .. str .. "'"
end
return '"' .. gsub(str, '"', '\\"') .. '"'
end
local shortControlCharEscapes = {
["\a"] = "\\a",
["\b"] = "\\b",
["\f"] = "\\f",
["\n"] = "\\n",
["\r"] = "\\r",
["\t"] = "\\t",
["\v"] = "\\v",
["\127"] = "\\127",
}
local longControlCharEscapes = { ["\127"] = "\127" }
for i = 0, 31 do
local ch = char(i)
if not shortControlCharEscapes[ch] then
shortControlCharEscapes[ch] = "\\" .. i
longControlCharEscapes[ch] = fmt("\\%03d", i)
end
end
local function escape(str)
return (gsub(gsub(gsub(str, "\\", "\\\\"), "(%c)%f[0-9]", longControlCharEscapes), "%c", shortControlCharEscapes))
end
local luaKeywords = {
["and"] = true,
["break"] = true,
["do"] = true,
["else"] = true,
["elseif"] = true,
["end"] = true,
["false"] = true,
["for"] = true,
["function"] = true,
["goto"] = true,
["if"] = true,
["in"] = true,
["local"] = true,
["nil"] = true,
["not"] = true,
["or"] = true,
["repeat"] = true,
["return"] = true,
["then"] = true,
["true"] = true,
["until"] = true,
["while"] = true,
}
local function isIdentifier(str)
return type(str) == "string" and not not str:match("^[_%a][_%a%d]*$") and not luaKeywords[str]
end
local flr = math.floor
local function isSequenceKey(k, sequenceLength)
return type(k) == "number" and flr(k) == k and 1 <= k and k <= sequenceLength
end
local defaultTypeOrders = {
["number"] = 1,
["boolean"] = 2,
["string"] = 3,
["table"] = 4,
["function"] = 5,
["userdata"] = 6,
["thread"] = 7,
}
local function sortKeys(a, b)
local ta, tb = type(a), type(b)
if ta == tb and (ta == "string" or ta == "number") then
return a < b
end
local dta = defaultTypeOrders[ta] or 100
local dtb = defaultTypeOrders[tb] or 100
return dta == dtb and ta < tb or dta < dtb
end
local function getKeys(t)
local seqLen = 1
while _rawget(t, seqLen) ~= nil do
seqLen = seqLen + 1
end
seqLen = seqLen - 1
local keys, keysLen = {}, 0
for k in rawpairs(t) do
if not isSequenceKey(k, seqLen) then
keysLen = keysLen + 1
keys[keysLen] = k
end
end
table.sort(keys, sortKeys)
return keys, keysLen, seqLen
end
local function countCycles(x, cycles)
if type(x) == "table" then
if cycles[x] then
cycles[x] = cycles[x] + 1
else
cycles[x] = 1
for k, v in rawpairs(x) do
countCycles(k, cycles)
countCycles(v, cycles)
end
countCycles(getmetatable(x), cycles)
end
end
end
local function makePath(path, a, b)
local newPath = {}
local len = #path
for i = 1, len do
newPath[i] = path[i]
end
newPath[len + 1] = a
newPath[len + 2] = b
return newPath
end
local function processRecursive(process, item, path, visited)
if item == nil then
return nil
end
if visited[item] then
return visited[item]
end
local processed = process(item, path)
if type(processed) == "table" then
local processedCopy = {}
visited[item] = processedCopy
local processedKey
for k, v in rawpairs(processed) do
processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited)
if processedKey ~= nil then
processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited)
end
end
local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited)
if type(mt) ~= "table" then
mt = nil
end
setmetatable(processedCopy, mt)
processed = processedCopy
end
return processed
end
local function puts(buf, str)
buf.n = buf.n + 1
buf[buf.n] = str
end
local Inspector = {}
local Inspector_mt = { __index = Inspector }
local function tabify(inspector)
puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level))
end
function Inspector:getId(v)
local id = self.ids[v]
local ids = self.ids
if not id then
local tv = type(v)
id = (ids[tv] or 0) + 1
ids[v], ids[tv] = id, id
end
return tostring(id)
end
function Inspector:putValue(v)
local buf = self.buf
local tv = type(v)
if tv == "string" then
puts(buf, smartQuote(escape(v)))
elseif tv == "number" or tv == "boolean" or tv == "nil" or tv == "cdata" or tv == "ctype" then
puts(buf, tostring(v))
elseif tv == "table" and not self.ids[v] then
local t = v
if t == inspect.KEY or t == inspect.METATABLE then
puts(buf, tostring(t))
elseif self.level >= self.depth then
puts(buf, "{...}")
else
if self.cycles[t] > 1 then
puts(buf, fmt("<%d>", self:getId(t)))
end
local keys, keysLen, seqLen = getKeys(t)
puts(buf, "{")
self.level = self.level + 1
for i = 1, seqLen + keysLen do
if i > 1 then
puts(buf, ",")
end
if i <= seqLen then
puts(buf, " ")
self:putValue(t[i])
else
local k = keys[i - seqLen]
tabify(self)
if isIdentifier(k) then
puts(buf, k)
else
puts(buf, "[")
self:putValue(k)
puts(buf, "]")
end
puts(buf, " = ")
self:putValue(t[k])
end
end
local mt = getmetatable(t)
if type(mt) == "table" then
if seqLen + keysLen > 0 then
puts(buf, ",")
end
tabify(self)
puts(buf, "<metatable> = ")
self:putValue(mt)
end
self.level = self.level - 1
if keysLen > 0 or type(mt) == "table" then
tabify(self)
elseif seqLen > 0 then
puts(buf, " ")
end
puts(buf, "}")
end
else
puts(buf, fmt("<%s %d>", tv, self:getId(v)))
end
end
function inspect.inspect(root, options)
options = options or {}
local depth = options.depth or math.huge
local newline = options.newline or "\n"
local indent = options.indent or " "
local process = options.process
if process then
root = processRecursive(process, root, {}, {})
end
local cycles = {}
countCycles(root, cycles)
local inspector = setmetatable({
buf = { n = 0 },
ids = {},
cycles = cycles,
depth = depth,
level = 0,
newline = newline,
indent = indent,
}, Inspector_mt)
inspector:putValue(root)
return table.concat(inspector.buf)
end
setmetatable(inspect, {
__call = function(_, root, options)
return inspect.inspect(root, options)
end,
})
_G.Inspect = inspect
return inspect

View File

@ -19,8 +19,6 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]] ]]
---@class World
---@class System ---@class System
---@field world World field points to the World that the System belongs to. Useful for adding and removing Entities from the world dynamically via the System. ---@field world World field points to the World that the System belongs to. Useful for adding and removing Entities from the world dynamically via the System.
---@field active boolean flag for whether or not the System is updated automatically. Inactive Systems should be updated manually or not at all via system:update(dt). Defaults to true. ---@field active boolean flag for whether or not the System is updated automatically. Inactive Systems should be updated manually or not at all via system:update(dt). Defaults to true.
@ -29,12 +27,14 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---@field index number is the System's index in the World. Lower indexed Systems are processed before higher indices. The index is a read only field; to set the index, use tiny.setSystemIndex(world, system). ---@field index number is the System's index in the World. Lower indexed Systems are processed before higher indices. The index is a read only field; to set the index, use tiny.setSystemIndex(world, system).
---@field indices table<any, any> field is a table of Entity keys to their indices in the entities list. Most Systems can ignore this. ---@field indices table<any, any> field is a table of Entity keys to their indices in the entities list. Most Systems can ignore this.
---@field modified boolean indicator for if the System has been modified in the last update. If so, the onModify callback will be called on the System in the next update, if it has one. This is usually managed by tiny-ecs, so users should mostly ignore this, too. ---@field modified boolean indicator for if the System has been modified in the last update. If so, the onModify callback will be called on the System in the next update, if it has one. This is usually managed by tiny-ecs, so users should mostly ignore this, too.
---@field preProcess nil | fun(self, dt: number): nil | table
---@field postProcess nil | fun(self, dt: number)
---@field isDrawSystem nil | boolean
--- @module tiny-ecs --- @module tiny-ecs
-- @author Calvin Rose --- @author Calvin Rose
-- @license MIT --- @license MIT
-- @copyright 2016 --- @copyright 2016
local tiny = {} local tiny = {}
-- Local versions of standard lua functions -- Local versions of standard lua functions
@ -103,14 +103,13 @@ local filterJoin
-- A helper function to filters from string -- A helper function to filters from string
local filterBuildString local filterBuildString
local function filterJoinRaw(invert, joining_op, ...) local function filterJoinRaw(invert, joining_op, ...)
local _args = {...} local _args = { ... }
return function(system, e) return function(system, e)
local acc local acc
local args = _args local args = _args
if joining_op == 'or' then if joining_op == "or" then
acc = false acc = false
for i = 1, #args do for i = 1, #args do
local v = args[i] local v = args[i]
@ -119,7 +118,7 @@ local function filterJoinRaw(invert, joining_op, ...)
elseif type(v) == "function" then elseif type(v) == "function" then
acc = acc or v(system, e) acc = acc or v(system, e)
else else
error 'Filter token must be a string or a filter function.' error("Filter token must be a string or a filter function.")
end end
end end
else else
@ -131,7 +130,7 @@ local function filterJoinRaw(invert, joining_op, ...)
elseif type(v) == "function" then elseif type(v) == "function" then
acc = acc and v(system, e) acc = acc and v(system, e)
else else
error 'Filter token must be a string or a filter function.' error("Filter token must be a string or a filter function.")
end end
end end
end end
@ -146,69 +145,68 @@ local function filterJoinRaw(invert, joining_op, ...)
end end
do do
function filterJoin(...) function filterJoin(...)
local state, value = pcall(filterJoinRaw, ...) local state, value = pcall(filterJoinRaw, ...)
if state then return value else return nil, value end if state then
return value
else
return nil, value
end
end end
local function buildPart(str) local function buildPart(str)
local accum = {} local accum = {}
local subParts = {} local subParts = {}
str = str:gsub('%b()', function(p) str = str:gsub("%b()", function(p)
subParts[#subParts + 1] = buildPart(p:sub(2, -2)) subParts[#subParts + 1] = buildPart(p:sub(2, -2))
return ('\255%d'):format(#subParts) return ("\255%d"):format(#subParts)
end) end)
for invert, part, sep in str:gmatch('(%!?)([^%|%&%!]+)([%|%&]?)') do for invert, part, sep in str:gmatch("(%!?)([^%|%&%!]+)([%|%&]?)") do
if part:match('^\255%d+$') then if part:match("^\255%d+$") then
local partIndex = tonumber(part:match(part:sub(2))) local partIndex = tonumber(part:match(part:sub(2)))
accum[#accum + 1] = ('%s(%s)') accum[#accum + 1] = ("%s(%s)"):format(invert == "" and "" or "not", subParts[partIndex])
:format(invert == '' and '' or 'not', subParts[partIndex])
else else
accum[#accum + 1] = ("(e[%s] %s nil)") accum[#accum + 1] = ("(e[%s] %s nil)"):format(make_safe(part), invert == "" and "~=" or "==")
:format(make_safe(part), invert == '' and '~=' or '==')
end end
if sep ~= '' then if sep ~= "" then
accum[#accum + 1] = (sep == '|' and ' or ' or ' and ') accum[#accum + 1] = (sep == "|" and " or " or " and ")
end end
end end
return table.concat(accum) return table.concat(accum)
end end
function filterBuildString(str) function filterBuildString(str)
local source = ("return function(_, e) return %s end") local source = ("return function(_, e) return %s end"):format(buildPart(str))
:format(buildPart(str))
local loader, err = loadstring(source) local loader, err = loadstring(source)
if err then if err then
error(err) error(err)
end end
return loader() return loader()
end end
end end
--- Makes a Filter that selects Entities with all specified Components and --- Makes a Filter that selects Entities with all specified Components and
-- Filters. -- Filters.
function tiny.requireAll(...) function tiny.requireAll(...)
return filterJoin(false, 'and', ...) return filterJoin(false, "and", ...)
end end
--- Makes a Filter that selects Entities with at least one of the specified --- Makes a Filter that selects Entities with at least one of the specified
-- Components and Filters. -- Components and Filters.
function tiny.requireAny(...) function tiny.requireAny(...)
return filterJoin(false, 'or', ...) return filterJoin(false, "or", ...)
end end
--- Makes a Filter that rejects Entities with all specified Components and --- Makes a Filter that rejects Entities with all specified Components and
-- Filters, and selects all other Entities. -- Filters, and selects all other Entities.
function tiny.rejectAll(...) function tiny.rejectAll(...)
return filterJoin(true, 'and', ...) return filterJoin(true, "and", ...)
end end
--- Makes a Filter that rejects Entities with at least one of the specified --- Makes a Filter that rejects Entities with at least one of the specified
-- Components and Filters, and selects all other Entities. -- Components and Filters, and selects all other Entities.
function tiny.rejectAny(...) function tiny.rejectAny(...)
return filterJoin(true, 'or', ...) return filterJoin(true, "or", ...)
end end
--- Makes a Filter from a string. Syntax of `pattern` is as follows. --- Makes a Filter from a string. Syntax of `pattern` is as follows.
@ -224,7 +222,11 @@ end
-- @param pattern -- @param pattern
function tiny.filter(pattern) function tiny.filter(pattern)
local state, value = pcall(filterBuildString, pattern) local state, value = pcall(filterBuildString, pattern)
if state then return value else return nil, value end if state then
return value
else
return nil, value
end
end end
--- System functions. --- System functions.
@ -463,8 +465,7 @@ function tiny.world(...)
entities = {}, entities = {},
-- List of Systems -- List of Systems
systems = {} systems = {},
}, worldMetaTable) }, worldMetaTable)
tiny_add(ret, ...) tiny_add(ret, ...)
@ -673,7 +674,6 @@ end
-- Adds, removes, and changes Entities that have been marked. -- Adds, removes, and changes Entities that have been marked.
function tiny_manageEntities(world) function tiny_manageEntities(world)
local e2r = world.entitiesToRemove local e2r = world.entitiesToRemove
local e2c = world.entitiesToChange local e2c = world.entitiesToChange
@ -793,7 +793,6 @@ end
-- Systems. If `filter` is not supplied, all Systems are updated. Put this -- Systems. If `filter` is not supplied, all Systems are updated. Put this
-- function in your main loop. -- function in your main loop.
function tiny.update(world, dt, filter) function tiny.update(world, dt, filter)
tiny_manageSystems(world) tiny_manageSystems(world)
tiny_manageEntities(world) tiny_manageEntities(world)
@ -809,8 +808,7 @@ function tiny.update(world, dt, filter)
onModify(system, dt) onModify(system, dt)
end end
local preWrap = system.preWrap local preWrap = system.preWrap
if preWrap and if preWrap and ((not filter) or filter(world, system)) then
((not filter) or filter(world, system)) then
preWrap(system, dt) preWrap(system, dt)
end end
end end
@ -853,12 +851,10 @@ function tiny.update(world, dt, filter)
for i = 1, #systems do for i = 1, #systems do
local system = systems[i] local system = systems[i]
local postWrap = system.postWrap local postWrap = system.postWrap
if postWrap and system.active and if postWrap and system.active and ((not filter) or filter(world, system)) then
((not filter) or filter(world, system)) then
postWrap(system, dt) postWrap(system, dt)
end end
end end
end end
--- Removes all Entities from the World. --- Removes all Entities from the World.
@ -924,11 +920,11 @@ worldMetaTable = {
clearSystems = tiny.clearSystems, clearSystems = tiny.clearSystems,
getEntityCount = tiny.getEntityCount, getEntityCount = tiny.getEntityCount,
getSystemCount = tiny.getSystemCount, getSystemCount = tiny.getSystemCount,
setSystemIndex = tiny.setSystemIndex setSystemIndex = tiny.setSystemIndex,
}, },
__tostring = function() __tostring = function()
return "<tiny-ecs_World>" return "<tiny-ecs_World>"
end end,
} }
_G.tiny = tiny _G.tiny = tiny

View File

@ -27,13 +27,40 @@ local scenarios = {
end, end,
} }
scenarios.textTestScenario() local currentScenario = scenarios.textTestScenario
local freeze = false
local delta
function love.load() function love.load()
currentScenario()
World:setSystemIndex(LiveForNFrames, 1)
love.graphics.setBackgroundColor(1, 1, 1) love.graphics.setBackgroundColor(1, 1, 1)
love.graphics.setFont(EtBt7001Z0xa(32)) love.graphics.setFont(EtBt7001Z0xa(32))
end end
function love.draw() function love.update(dt)
World:update(love.timer.getDelta()) delta = dt
if freeze then
return
end
if love.keyboard.isDown("r") then
World:clearEntities()
currentScenario()
freeze = false
end
if love.keyboard.isDown("f") then
freeze = not freeze
end
World:update(delta, function(_, system)
return not system.isDrawSystem
end)
end
function love.draw()
World:update(delta, function(_, system)
return system.isDrawSystem
end)
end end

View File

@ -1,15 +1,28 @@
collidingEntities = filteredSystem("collidingEntitites", { collidingEntities = filteredSystem("collidingEntitites", {
velocity = T.XyPair,
position = T.XyPair, position = T.XyPair,
size = T.XyPair, size = T.XyPair,
canCollideWith = T.BitMask, canCollideWith = T.BitMask,
isSolid = Maybe(T.bool), isSolid = Maybe(T.bool),
}) })
local function intersects(rect, rectOther)
local left = rect.position.x
local right = rect.position.x + rect.size.x
local top = rect.position.y
local bottom = rect.position.y + rect.size.y
local leftOther = rectOther.position.x
local rightOther = rectOther.position.x + rectOther.size.x
local topOther = rectOther.position.y
local bottomOther = rectOther.position.y + rectOther.size.y
return leftOther < right and left < rightOther and topOther < bottom and top < bottomOther
end
filteredSystem( filteredSystem(
"collisionDetection", "collisionDetection",
{ position = T.XyPair, size = T.XyPair, canBeCollidedBy = T.BitMask, isSolid = Maybe(T.bool) }, { position = T.XyPair, size = T.XyPair, canBeCollidedBy = T.BitMask, isSolid = Maybe(T.bool) },
-- Here, the entity, e, refers to some entity that a moving object may be colliding *into* -- Here, the entity, e, refers to some entity that a moving object may be colliding *into*
function(e, _, system) function(e, _, system)
for _, collider in pairs(collidingEntities.entities) do for _, collider in pairs(collidingEntities.entities) do
if if
@ -18,19 +31,7 @@ filteredSystem(
and e.canBeCollidedBy and e.canBeCollidedBy
and bit.band(collider.canCollideWith, e.canBeCollidedBy) ~= 0 and bit.band(collider.canCollideWith, e.canBeCollidedBy) ~= 0
then then
local colliderTop = collider.position.y if intersects(e, collider) then
local colliderBottom = collider.position.y + collider.size.y
local entityTop = e.position.y
local entityBottom = entityTop + e.size.y
local withinY = (entityTop > colliderTop and entityTop < colliderBottom)
or (entityBottom > colliderTop and entityBottom < colliderBottom)
if
withinY
and collider.position.x < e.position.x + e.size.x
and collider.position.x + collider.size.x > e.position.x
then
system.world:addEntity({ collisionBetween = { e, collider } }) system.world:addEntity({ collisionBetween = { e, collider } })
end end
end end

View File

@ -4,3 +4,11 @@ filteredSystem("decay", { decayAfterSeconds = T.number }, function(e, dt, system
system.world:removeEntity(e) system.world:removeEntity(e)
end end
end) end)
LiveForNFrames = filteredSystem("liveForNFrames", { liveForNFrames = T.number }, function(e, _, system)
e.liveForNFrames = e.liveForNFrames - 1
if e.liveForNFrames <= 0 then
system.world:removeEntity(e)
end
end)

View File

@ -1,27 +1,49 @@
local gfx = love.graphics local gfx = love.graphics
filteredSystem("drawRectangles", { position = T.XyPair, drawAsRectangle = { size = T.XyPair } }, function(e, _, _) ---@generic T
gfx.fillRect(e.position.x, e.position.y, e.drawAsRectangle.size.x, e.drawAsRectangle.size.y) ---@param shape T | fun()
end) ---@param process fun(entity: T, dt: number, system: System) | nil
---@return System | { entities: T[] }
local function drawSystem(name, shape, process)
local system = filteredSystem(name, shape, process, function(_, a, b)
if a.z ~= nil and b.z ~= nil then
return a.z < b.z
end
if a.z ~= nil then
return true
end
return false
end)
system.isDrawSystem = true
return system
end
filteredSystem("drawSprites", { position = T.XyPair, drawAsSprite = T.pd_image }, function(e) local spriteDrawSystem = drawSystem(
if e.position.y < Camera.pan.y - 240 or e.position.y > Camera.pan.y + 480 then "drawSprites",
{ position = T.XyPair, drawAsSprite = T.Drawable, rotation = Maybe(T.number) },
function(e)
if not e.drawAsSprite then
return return
end end
e.drawAsSprite:draw(e.position.x, e.position.y) gfx.draw(e.drawAsSprite, e.position.x, e.position.y)
end) end
)
function spriteDrawSystem:preProcess()
gfx.setColor(1, 1, 1)
end
local margin = 8 local margin = 8
filteredSystem( drawSystem(
"drawText", "drawText",
{ position = T.XyPair, drawAsText = { text = T.str, style = Maybe(T.str), font = Maybe(T.pd_font) } }, { position = T.XyPair, drawAsText = { text = T.str, style = Maybe(T.str), font = Maybe(T.FontData) } },
function(e) function(e)
local font = gfx.getFont() -- e.drawAsText.font or AshevilleSans14Bold local font = e.drawAsText.font or gfx.getFont() -- e.drawAsText.font or AshevilleSans14Bold
local textHeight = font:getHeight() local textHeight = font:getHeight()
local textWidth = font:getWidth(e.drawAsText.text) local textWidth = font:getWidth(e.drawAsText.text)
local bgLeftEdge = e.position.x - margin - textWidth / 2 local bgLeftEdge = e.position.x - margin -- - (textWidth / 2)
local bgTopEdge = e.position.y - 2 local bgTopEdge = e.position.y - 2
local bgWidth, bgHeight = textWidth + (margin * 2), textHeight + 2 local bgWidth, bgHeight = textWidth + (margin * 2), textHeight + 2
@ -34,9 +56,15 @@ filteredSystem(
gfx.rectangle("fill", bgLeftEdge, bgTopEdge, bgWidth, bgHeight) gfx.rectangle("fill", bgLeftEdge, bgTopEdge, bgWidth, bgHeight)
gfx.setColor(0, 0, 0) gfx.setColor(0, 0, 0)
gfx.drawRect("line", bgLeftEdge, bgTopEdge, bgWidth, bgHeight) gfx.rectangle("line", bgLeftEdge, bgTopEdge, bgWidth, bgHeight)
end end
gfx.print(e.drawAsText.text, bgLeftEdge + margin, bgTopEdge + margin) gfx.print(e.drawAsText.text, bgLeftEdge + margin, bgTopEdge + margin)
end end
) )
drawSystem("drawRectangles", { position = T.XyPair, drawAsRectangle = { size = T.XyPair } }, function(e, _, _)
gfx.setColor(1, 1, 1, 0.5)
local mode = e.drawAsRectangle.mode or "line" -- (e.highlighted and "fill" or "line")
gfx.rectangle(mode, e.position.x, e.position.y, e.drawAsRectangle.size.x, e.drawAsRectangle.size.y)
end)

View File

@ -1,13 +1,15 @@
---@generic T ---@generic T
---@param shape T | fun() ---@param shape T | fun()
---@param process fun(entity: T, dt: number, system: System) | nil ---@param process fun(entity: T, dt: number, system: System) | nil
---@param compare nil | fun(system: System, entityA: T, entityB: T): boolean
---@return System | { entities: T[] } ---@return System | { entities: T[] }
function filteredSystem(name, shape, process) function filteredSystem(name, shape, process, compare)
assert(type(name) == "string") assert(type(name) == "string")
assert(type(shape) == "table" or type(shape) == "function") assert(type(shape) == "table" or type(shape) == "function")
assert(process == nil or type(process) == "function") assert(process == nil or type(process) == "function")
local system = tiny.processingSystem() local system = compare and tiny.sortedProcessingSystem() or tiny.processingSystem()
system.compare = compare
system.name = name system.name = name
if type(shape) == "table" then if type(shape) == "table" then
local keys = {} local keys = {}

View File

@ -1,12 +1,18 @@
---@meta ---@meta tiny-ecs
---@class World ---@class World
World = {} World = {}
function World:add(...) end function World:add(...) end
---@generic T : Entity
---@param entity T
---@return T
function World:addEntity(entity) end function World:addEntity(entity) end
---@generic T : System
---@param system T
---@return T
function World:addSystem(system) end function World:addSystem(system) end
function World:remove(...) end function World:remove(...) end
@ -15,17 +21,28 @@ function World:removeEntity(entity) end
function World:removeSystem(system) end function World:removeSystem(system) end
--- Manages Entities and Systems marked for deletion or addition. Call this
--- before modifying Systems and Entities outside of a call to `tiny.update`.
--- Do not call this within a call to `tiny.update`.
function World:refresh() end function World:refresh() end
---@param dt number ---@param dt number
function World:update(dt) end ---@param systemFilter nil | fun(world: World, system: System): boolean
function World:update(dt, systemFilter) end
--- Removes all Entities from the World.
function World:clearEntities() end function World:clearEntities() end
--- Removes all Systems from the World.
function World:clearSystems() end function World:clearSystems() end
--- Gets number of Entities in the World.
function World:getEntityCount() end function World:getEntityCount() end
--- Gets number of Systems in World.
function World:getSystemCount() end function World:getSystemCount() end
function World:setSystemIndex() end --- Sets the index of a System in the World, and returns the old index. Changes
--- the order in which they Systems processed, because lower indexed Systems are
--- processed first. Returns the old system.index.
function World:setSystemIndex(world, system, index) end