Colisiones

Tenemos que chocar contra las paredes

Una colisión es inevitable

Lo que se mueve, choca

Ahora que hay entities que se mueven ha surgido un ‘problema’. Se atraviesan unas a otras como un fantasma 👻.

Hay varias formas de hacer esto, y quizá aquí tomé una decisión equivocada: la ‘velocidad’. Cada cálculo cuesta tiempo y quieres que sea lo mínimo posible. La primera decisión que tuve que tomar fue si usar cálculos con polígonos o con simples rectángulos (rect).

Actualización 2025: con la experiencia que tengo ahora, solo usaría polígonos.

Elegí rect, porque solo tengo figuras sencillas. Cuando tengo una imagen transparente de un personaje, tiene un alto y un ancho máximos, y ese es el rectángulo completo que no se puede atravesar.

Engine3 se ha ampliado con estas funciones 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

Después, una entity recibe la información de cómo tratar a otras entities.

Lo curioso es que enseguida me di cuenta de que esto es muy rápido, pero también demasiado limitado. Porque también puedes tener una figura circular u otras figuras ‘raras’. Si por ejemplo pasas el cursor del ratón por encima de un objeto así, ya te dirá lo que es fuera del círculo (alrededor del cual está el rect invisible).

Por eso ahora una entity usa un polígono para la colisión con el cursor y un rect para el movimiento.


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)

Sigue el camino

Ahora que todas las entities tienen los datos necesarios, podemos decidir con aStar por dónde puede caminar la entity.

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

Cuando haces clic en algún punto de la pantalla, se mira en el mapa por dónde puede caminar el jugador. El mapa usa la función de arriba y está en parte en caché.

local path = aStar()
if path then
    path = self:optimizePath(path)
    path = self:smoothPath(path, map)
    return {targetX = targetX, targetY = targetY, path = path}
end

Como no quiero todas las coordenadas x,y, sino solo los momentos en los que la entity tiene que cambiar de posición, también uso optimizePath y smoothPath, como ves aquí abajo.

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

Un error que cometí fue querer gestionar esto en Love2d con un hilo en segundo plano. Mi idea era que así iría más rápido. El resultado fue que sí era más rápido, pero ya no coincidía con lo que veías en pantalla, y justo veías un ‘retraso’.

Actualización 2025 Aunque arriba parezca sencillo, este sistema va a pasar por varias refactorizaciones y en parte voy a pasar a un walkmap. Es una capa sencilla en Photoshop en blanco y negro, donde el negro es transitable y el blanco no.