Столкновения
Мы должны натыкаться на стены

То, что движется, сталкивается
Теперь, когда есть движущиеся entities, появилась ‘проблема’. Они проходят друг сквозь друга, как призрак 👻.
Решить это можно по-разному, и, возможно, здесь я сделал неверный выбор в пользу ‘скорости’. Каждое вычисление стоит времени, и его хочется сводить к минимуму. Первое решение, которое нужно было принять: использовать вычисления с полигонами или с простыми прямоугольниками (rect).
Обновление 2025: с опытом, который у меня есть сейчас, я бы использовал только полигоны.
Я выбрал rect, потому что у меня только простые фигуры. Если у меня есть прозрачное изображение персонажа, у него есть максимальная высота и ширина, и это весь прямоугольник, сквозь который проходить нельзя.
engine3 дополнился следующими базовыми функциями
---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
Затем entity получает информацию о том, как ей вести себя с другими entities.
Забавно, что я быстро понял: это действительно очень быстро, но и слишком ограничено. Ведь бывает круглая фигура или другие ‘странные’ формы. Если, например, навести курсор мыши на такой объект, он уже за пределами круга (вокруг которого находится невидимый rect) покажет, что это.
Поэтому теперь entity использует полигон для столкновения с курсором и rect для движения.
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)
Следуй по пути
Теперь, когда у всех entities есть нужные данные, с помощью aStar можно определить, где 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
Когда вы щёлкаете где-нибудь на экране, по карте проверяется, куда может идти игрок. Карта использует функцию выше и частично хранится в кеше.
local path = aStar()
if path then
path = self:optimizePath(path)
path = self:smoothPath(path, map)
return {targetX = targetX, targetY = targetY, path = path}
end
Поскольку мне нужны не все координаты x,y, а только моменты, когда entity должна сменить позицию, я также использую optimizePath и smoothPath, как видно ниже.
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
}
Напоследок
Одна ошибка, которую я сделал, заключалась в том, что я хотел обрабатывать это в Love2d через фоновый поток. Я думал, что так будет быстрее. В итоге действительно стало быстрее, но это уже не совпадало с тем, что было видно на экране, и появлялась как раз ‘задержка’.
Обновление 2025 Хотя выше всё выглядит просто, эта система пройдёт ещё несколько рефакторингов, и я частично перейду на walkmap. Это простой слой в Photoshop в чёрно-белых цветах, где по чёрному можно ходить, а по белому нет.