Collisioni

Dobbiamo andare a sbattere contro i muri

Una collisione è inevitabile

Ciò che si muove, collide

Ora che ci sono entity in movimento è nato un ‘problema’. Si attraversano a vicenda come un fantasma 👻.

Ci sono diversi modi per risolverlo, e forse qui ho fatto una valutazione sbagliata, cioè la ‘velocità’. Ogni calcolo costa tempo e vuoi tenerlo il più basso possibile. La prima decisione da prendere era se usare calcoli con poligoni o con semplici rettangoli (rect).

Aggiornamento 2025: con l’esperienza che ho adesso userei solo poligoni.

Ho scelto i rect, visto che ho solo figure semplici. Quando ho un’immagine trasparente di un personaggio, questa ha un’altezza e una larghezza massime, e quello è il rettangolo totale che non si può attraversare.

Engine3 è stato esteso con queste funzioni di base

---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

Poi un’entity riceve le informazioni su come comportarsi con le altre entity.

Il bello è che ho scoperto presto che è davvero molto veloce, ma anche troppo limitato. Perché puoi avere anche una figura circolare o altre figure ‘strane’. Se per esempio passi con il cursore del mouse sopra un oggetto del genere, ti dirà cos’è già fuori dal cerchio (attorno a cui c’è il rect invisibile).

Per questo un’entity ora usa un poligono per la collisione con il cursore e un rect per il 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)

Segui il percorso

Ora che tutte le entity hanno i dati necessari, possiamo stabilire con aStar dove l’entity può camminare.

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 clicchi da qualche parte sullo schermo, si controlla sulla mappa dove il giocatore può camminare. La mappa usa la funzione qui sopra ed è in parte in cache.

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

Visto che non voglio tutte le coordinate x,y, ma solo i momenti in cui l’entity deve cambiare posizione, uso anche optimizePath e smoothPath, come vedi qui sotto.

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
}

Per finire

Un errore che avevo fatto era voler gestire tutto questo in Love2d tramite un thread in background. La mia idea era che così sarebbe stato più veloce. Il risultato è che era davvero più veloce, ma non corrispondeva più a quello che vedevi sullo schermo, anzi, vedevi proprio un ‘ritardo’.

Aggiornamento 2025 Anche se qui sopra sembra semplice, questo sistema avrà diversi refactoring e passerò in parte a una walkmap. È un semplice livello in Photoshop in bianco e nero, dove il nero è calpestabile e il bianco no.