腾讯游戏开发精粹
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.4 SDF的碰撞检测与碰撞响应

前面提到φ(x)≤0表示点x在障碍物内,那么碰撞检测只需要得到点的φ值,然后与碰撞半径r比较即可,φ(x)≤r表示角色与障碍物发生了碰撞。由于栅格地图的SDF数据是离散存储的,但角色移动是连续的,不能把角色在一个栅格内任意位置的φ值等同于栅格顶点,否则会在栅格边界产生巨变。因此,在移动是连续的情况下,无法直接查表获取角色所在位置的φ值,如图1.4所示圆的圆心位置,需要根据周边栅格顶点的φ值采样获取。

图1.4 如何计算圆心的φ

因为距离本身是线性的,可以采用双线性过滤(Bilinear Filtering)采样角色位置的φ值,根据角色所处栅格的四个顶点线性插值可得到场景任意点的φ值,如图1.5所示。

图1.5 双线性过滤示意图

由此完成SDF的碰撞检测,只需要查表和乘法计算,时间复杂度为O(1)。以下为插值获得场景任意点的SD值的代码。

    // 计算位置pos的SD值
    // 每个栅格的实际尺寸为grid,横向栅格数量为width
    public float Sample(Vector2 pos) {
        pos = pos / grid;
        int fx = Mathf.FloorToInt(pos.x);
        int fy = Mathf.FloorToInt(pos.y);
        float rx = pos.x - fx;
        float ry = pos.y - fy;
        int i = fy * width + fx;
        return
          (sdf[i        ] * (1- rx) + sdf[i         + 1] * rx) * (1- ry) +
          (sdf[i + width] * (1- rx) + sdf[i + width + 1] * rx) * ry;
    }

当前几乎所有的MOBA手游在摇杆移动过程中,碰到障碍物之后均是绕着障碍物滑行的,而不是直接停止,因为停止的体验实在很糟糕。那么SDF在发生碰撞后如何处理绕障碍物滑行的问题呢?

如图1.6所示,v表示摇杆方向(角色原始移动方向),当与障碍物发生碰撞后需要沿着v′方向滑行,v′和v的关系是

图1.6 滑行

上式中,n为碰撞法线,如何获取呢?

SDF为纯量场,纯量场中某一点上的梯度(Gradient)指向纯量场增长最快的方向,因此可以利用SDF的梯度作为碰撞法线:

同时,φ(x)几乎随处可导,可以使用有限差分法(Finite Difference)求出x处的梯度:

从而得到碰撞法线n,求出在滑行方向实现碰撞后绕障碍物滑行。以下为求梯度方向的代码。

    // 求位置pos的梯度方向
    public Vector2 Gradient(Vector2 pos) {
        float delta = 1f;
        return 0.5f * new Vector2(
            Sample(new Vector2(pos.x + delta, pos.y)) -
            Sample(new Vector2(pos.x - delta, pos.y)),
            Sample(new Vector2(pos.x, pos.y + delta)) -
            Sample(new Vector2(pos.x, pos.y - delta)));
    }

至此,得到当角色按摇杆方向移动时的实际移动方向代码。

    // 获取在移动过程中使用SDF得到的最佳位置
    public Vector2 GetVaildPositionBySDF(Vector2 pos, Vector2 dir, float speed) {
        Vector2 newPos = pos + dir * speed;
        float SD = Sample(newPos);

        // 不可行走
        if (SD < playerRadius) {
            Vector2 gradient = Gradient(newPos);
            Vector2 adjustDir = dir - gradient * Vector2.Dot(gradient, dir);
            newPos = pos + adjustDir.normalized * speed;

            // 多次迭代
            for (int i = 0; i < 3; i++) {
                SD = Sample(newPos);
                if (SD >= playerRadius) break;
                newPos += Gradient(newPos) * (playerRadius - SD);
            }

            // 避免往返
            if (Vector2.Dot(newPos - pos, dir) < 0)
                newPos = pos;
        }
        return newPos;
    }