衝突判定

壁にぶつかる必要がある

衝突は避けられない

動くものはぶつかる

動く entity ができたことで、ひとつ「問題」が生まれました。幽霊のように 👻 お互いをすり抜けてしまうのです。

これにはいろいろなやり方がありますが、ここで私は判断を誤ったかもしれません。それは「速さ」です。 計算にはどれも時間がかかるので、できるだけ少なくしたいものです。最初に決めなければならなかったのは、ポリゴンで計算するか、シンプルな四角形(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 にはほかの entity とどう関わるかの情報を持たせます。

面白いことに、これは確かにとても速いけれど、制限が多すぎることにすぐ気づきました。円形や、ほかの「変わった」形もあるからです。たとえばそういうオブジェクトの上にマウスカーソルを乗せると、円の外(見えない 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)

経路をたどる

すべての entity が必要なデータを持ったので、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年の追記 上ではシンプルに見えますが、このシステムは何度もリファクタリングすることになり、一部はウォークマップに切り替えます。これは Photoshop の白黒のシンプルなレイヤーで、黒は歩けて、白は歩けません。