Colisões
A gente precisa bater nas paredes

O que se move, colide
Agora que existem entities que se movem, surgiu um ‘problema’. Elas atravessam umas às outras como um fantasma 👻.
Há várias formas de fazer isso, e talvez eu tenha feito uma escolha errada aqui, que foi a ‘velocidade’. Cada cálculo custa tempo e você quer mantê-lo o mais baixo possível. A primeira decisão que precisei tomar foi entre cálculos com polígonos ou com simples retângulos (rect).
Atualização 2025: com a experiência que tenho agora, eu usaria só polígonos.
Escolhi rect, já que só tenho formas simples. Quando tenho uma imagem transparente de um personagem, ela tem uma altura e uma largura máximas, e esse é o retângulo inteiro que não pode ser atravessado.
A engine3 foi ampliada com as seguintes funções básicas
---Check if a point is inside the rect.
-- This function will include the border
---@param x integer
---@param y integer
---@param rect Rect The you want to use for the collision check
local function pointInRect(x, y, rect)
return x >= rect.x1 and x <= rect.x2 and y >= rect.y1 and y <= rect.y2
end
---@param x integer
---@param y integer
---@param rects table<Rect> The (collidable)Rects you want to use for the collision check
local function pointInRects(x, y, rects)
for _, rect in pairs(rects) do
if pointInRect(x, y, rect) then
return true
end
end
return false
end
--- Check if a point is inside the rect
-- https://2dengine.com/?p=intersections
---@param x integer X point coordinate
---@param y integer Y point coordinate
---@param l integer X left corner coordinate
---@param t integer Y top corner coordinate
---@param r integer X right corner coordinate
---@param b integer Y bottom corner coordinate
local function pointInAABB(x, y, l, t, r, b)
if x < l or x > r then
return false
end
if y < t or y > b then
return false
end
return true
end
---Check if two boxes overlap
-- https://love2d.org/wiki/BoundingBox.lua
---@param x1 integer X left corner coordinate box 1
---@param y1 integer Y top corner coordinate box 1
---@param w1 integer Width box 1
---@param h1 integer Height box 1
---@param x2 integer X left corner coordinate box 2
---@param y2 integer Y top corner coordinate box 2
---@param w2 integer Width box 2
---@param h2 integer Height box 2
local function rectsOverlap(x1, y1, w1, h1, x2, y2, w2, h2)
return x1 < x2 + w2 and x2 < x1 + w1 and y1 < y2 + h2 and y2 < y1 + h1
end
---Check if two boxes overlap using AABB
---@param x1a integer X left corner coordinate box 1
---@param y1a integer Y top corner coordinate box 1
---@param x2a integer X right corner coordinate box 1
---@param y2a integer Y bottom corner coordinate box 1
---@param x1b integer X left corner coordinate box 2
---@param y1b integer Y top corner coordinate box 2
---@param x2b integer X right corner coordinate box 2
---@param y2b integer Y bottom corner coordinate box 2
local function AABBVsAABB(x1a, y1a, x2a, y2a, x1b, y1b, x2b, y2b)
if x1a > x2b or x1b > x2a then
return false
end
if y1a > y2b or y1b > y2a then
return false
end
return true
end
---Check if two boxes overlap using AABB using width and height
---@param o1x integer
---@param o1y integer
---@param o1w integer
---@param o1h integer
---@param o2x integer
---@param o2y integer
---@param o2w integer
---@param o2h integer
local function AABBWHVsAABBWH(o1x, o1y, o1w, o1h, o2x, o2y, o2w, o2h)
return not ((o2x > o1x + o1w) or (o2x + o2w < o1x) or (o2y > o1y + o1h) or (o2y + o2h < o1y))
end
Depois, uma entity recebe as informações de como lidar com outras entities.
O engraçado é que logo percebi que isso é mesmo muito rápido, mas também limitado demais. Afinal, você também pode ter um círculo ou outras formas ’estranhas’. Se você passar o cursor do mouse sobre um objeto assim, por exemplo, ele já vai dizer o que é fora do círculo (em volta do qual fica o rect invisível).
Por isso agora uma entity usa um polígono para a colisão com o cursor e um rect para o movimento.
local Collision = require "lib.engine3.src.framework.helper.math.collision"
---@field collidableA? table {1[opt](left),2([opt]right),3([opt]top),4([opt]bottom)}
---@field collidableB? table {1[opt](left),2([opt]right),3([opt]top),4([opt]bottom)}
---@field collidableRects? table Collidable rects [{x1,y1,x2,y2}]
function Frameset:new(data)
Siga o caminho
Agora que todas as entities têm os dados necessários, podemos usar o aStar para definir por onde a entity pode andar.
function PathFinding:isPointInCollision(collisionRectsTable, x, y)
for _, collisionRectList in pairs(collisionRectsTable) do
for _, rect in pairs(collisionRectList) do
if Collision.pointInRect(x, y, rect) then
return true
end
end
end
return false
end
Quando você clica em algum lugar da tela, o mapa é consultado para ver por onde o jogador pode andar. O mapa usa a função acima e fica em parte em cache.
local path = aStar()
if path then
path = self:optimizePath(path)
path = self:smoothPath(path, map)
return {targetX = targetX, targetY = targetY, path = path}
end
Como não quero todas as coordenadas x,y, mas só os momentos em que a entity precisa mudar de posição, também uso optimizePath e smoothPath, como você pode ver abaixo.
path {
x = 1, y = 1
x = 2, y = 1
x = 3, y = 1
x = 4, y = 1
x = 5, y = 1
x = 6, y = 1
}
path {
x = 1, y = 1
x = 6, y = 1
}
Para terminar
Um erro que cometi foi querer tratar isso no Love2d com uma thread em segundo plano. Minha ideia era que assim ficaria mais rápido. O resultado foi que ficou mais rápido, sim, mas não batia mais com o que você via na tela, e você acabava vendo justamente um ‘atraso’.
Atualização 2025 Mesmo que pareça simples acima, este sistema vai passar por várias refatorações e vou migrar em parte para um walkmap. É uma camada simples no Photoshop em preto e branco, onde o preto é caminhável e o branco não.