1.10 動態地圖
使用預計算得到的SDF地圖較難實現動態更新,因為重新計算SDF比較耗時。那么如何能實現動態地圖的需求呢?對于特殊游戲類型,如地牢游戲(Rouge-like)中的地圖,本身就是由均勻網格所組成的,我們可以為其輸入數據,將每一個網格都看作一個矩形,可以用上文中提到的矩形SDF公式來表示單個矩形。
在均勻網格地圖上,當角色在一幀內行走的距離不會超過單個網格的大小時,可以通過檢測每一幀與玩家所在網格相鄰的8個網格的碰撞來實現規避障礙物的功能。如圖1.16所示,當玩家行走之后位于網格4時,圖中最右邊的圓圈代表玩家,此時我們只需要檢測網格6、0、5與玩家的最近距離來進行碰撞規避。

圖1.16 角色在均勻網格地圖上移動的例子
整個過程的偽代碼如下:
float EvalSDF(Vector2 p) { int x = posToGridX(p); // 坐標離散成網格 int y = posToGridY(p); float dist = cellSize; int center = grid[y * width + x]; if (center == WALL) // WALL格子不可行走 dist = min(dist, sdBox(centerPos - vecTopLeft, cellExtents)); int topleft = grid[(y -1) * width + (x -1)]; if (topleft == WALL) dist = min(dist, sdBox(centerPos - vecTop, cellExtents)); int top = grid[(y -1) * width + x]; // ... return dist; } Vector2 EvalGradient(Vector2 p) { /*... */ } void Update() { // 新目標位置 Vector2 nextPlayerPos = playerPos + moveDir * moveSpeed; // 目標位置的最近距離 float d = EvalSDF(nextPlayerPos); // 距離小于玩家半徑,有穿插 if (d < playerRadius) { // 計算最近表面的法線 Vector2 n = EvalGradient(nextPlayerPos); // 將玩家推出障礙區域 nextPlayerPos = nextPlayerPos + n * (playerRadius - d); } playerPos = nextPlayerPos; }
SDF數據是通過讀取網格地圖中的可通過標記來決定這個網格是否參與計算的,因此就可以實現動態修改均勻網格地圖,可以在運行時標記某個網格的通過性。
可以通過取距離場的梯度得到朝向向量,對于簡單的幾何圖形,可以通過幾何方法求出,比如圓形:
Vector2 GradSphere(Vector2 p) { return p.Normalized(); }
對于矩形,假設坐標系原點在矩形中心,矩形的四個象限是相互鏡像的,則可將此問題退化為在一個象限內求解:
Vector2 GradBox(Vector2 p) { // 退化為在+x, +y象限內求解 Vector2 d = Vector2.Abs(p) - halfSize; // 記錄符號,用來還原原始象限 Vector2 sign = Sign(p); // 假設以halfSize為中心,p落在右上區域中,p-halfSize即為所求 if (d.x > 0 && d.y > 0) return (d * sign).Normalized(); // 以halfSize為中心,檢測距離x軸和y軸哪個更近 float max = Max(d.x, d.y); // 距離x軸近,法線為y軸;反之,相反 Vector2 grad = new Vector2(max == d.x ? 1 : 0, max == d.y ? 1 : 0 ); return (grad * sign).Normalized(); }
剛才的方法是不考慮地圖中會出現如圖1.17所示的障礙物卡住玩家的情況的。

圖1.17 障礙物卡住玩家的情況
要解決此問題,只需進行多次迭代求出最終修正的位置即可。而當玩家移動步幅較大時,如閃現等,需要進行連續碰撞檢測。與基于預計算的離散SDF數據有所不同的是,均勻網格的SDF數據都是以函數計算的高精度連續的值,因此計算方法與前文稍有不同。
bool DiskCast(Vector2 origin, Vector2 dir, float r, float maxDist, out float t) { t = 0; while (true) { // 根據當前t求出當前采樣點p Vector2 p = origin + dir * t; // 采樣出最近距離 float d = EvalSDF(p); // 若距離<0,則p點在障礙區域中,結束迭代 if (d < 0) return false; // 當距離與角色半徑的差距大于閾值時,繼續迭代 if (d > radius + 0.001f) t += d - radius; else // 當距離與角色半徑的差距小于閾值時,結束迭代 return false; // 當t大于最大迭代距離時,結束迭代 if (t >= maxDist) { t = maxDist; return true; } } }
而場景中的其他障礙物,如較大的汽車、其他玩家等,則可通過矩形、圓形的SDF函數來表示,并將結果與網格地圖取出的SDF做交集操作。