圆圈粘在游戏引擎中的东西上

Circles stick to things in game engine

在物理学中 (written following this tutorial) part of my game engine (on github),有时 AABB 与圆之间或圆与圆之间的碰撞会粘在一起,就像这样(对 gif 伪影表示歉意):

我已经确认即使我不调用 applyFriction 也会发生这种情况,所以不是那样。我还让 positionalCorrection 算法对 percent 使用 1.05f,但即使这样也没有解决任何问题,所以我很困惑。

public final class Collisions {
    private Collisions() {
        // cant instantiate this class
    }

    /**
     * Returns whether two GameObjects are colliding
     *
     * @param a
     *            must be a RectObject or CircleObject
     * @param b
     *            must be a RectObject or CircleObject
     * @return
     */
    public static boolean isColliding(final GameEntity a, final GameEntity b) {
        final CShape as = a.shape;
        final CShape bs = b.shape;

        if (as instanceof RectShape && bs instanceof RectShape) {
            return isColliding((RectShape) as, (RectShape) bs);
        }
        if (as instanceof CircleShape && bs instanceof CircleShape) {
            return isColliding((CircleShape) as, (CircleShape) bs);
        }
        if (as instanceof RectShape && bs instanceof CircleShape) {
            return isColliding((RectShape) as, (CircleShape) bs);
        }
        if (as instanceof CircleShape && bs instanceof RectShape) {
            return isColliding((RectShape) bs, (CircleShape) as);
        }

        throw new UnsupportedOperationException();
    }

    private static boolean isColliding(final RectShape a, final RectShape b) {
        return collisionNormal(a, b) != null;
    }

    private static boolean isColliding(final CircleShape o1, final CircleShape o2) {
        final float c = o1.radius + o2.radius;
        final float b = o1.center.x - o2.center.x;
        final float a = o1.center.y - o2.center.y;

        return c * c > b * b + a * a;
    }

    private static boolean isColliding(final RectShape a, final CircleShape b) {
        final float circleDistance_x = Math.abs(b.center().x - (a.min.x + a.width() / 2));
        final float circleDistance_y = Math.abs(b.center().y - (a.min.y + a.height() / 2));

        if (circleDistance_x > a.width() / 2 + b.radius) {
            return false;
        }
        if (circleDistance_y > a.height() / 2 + b.radius) {
            return false;
        }

        if (circleDistance_x <= a.width() / 2) {
            return true;
        }
        if (circleDistance_y <= a.height() / 2) {
            return true;
        }

        final int cornerDistance_sq = (int) Math.pow(circleDistance_x - a.width() / 2, 2) + (int) Math.pow(circleDistance_y - a.height() / 2, 2);

        return cornerDistance_sq <= (int) Math.pow(b.radius, 2);

    }

    /**
     * Returns the face normal of a collision between a and b
     *
     * @param a
     * @param b
     * @return null if no collision
     */
    private static Vec2D collisionNormal(final RectShape a, final RectShape b) {
        final float w = 0.5f * (a.width() + b.width());
        final float h = 0.5f * (a.height() + b.height());
        final float dx = a.center().x - b.center().x;
        final float dy = a.center().y - b.center().y;

        if (Math.abs(dx) <= w && Math.abs(dy) <= h) {
            /* collision! */
            final float wy = w * dy;
            final float hx = h * dx;

            if (wy > hx) {
                if (wy > -hx) {
                    /* collision at the top */
                    return new Vec2D(0, -1);
                } else {
                    /* on the left */
                    return new Vec2D(1, 0);
                }
            } else {
                if (wy > -hx) {
                    /* on the right */
                    return new Vec2D(-1, 0);
                } else {
                    /* at the bottom */
                    return new Vec2D(0, 1);
                }
            }
        }
        return null;
    }

    public static void fixCollision(final GameEntity a, final GameEntity b) {
        fixCollision(generateManifold(a, b), true);
    }

    /**
     * Fixes a collision between two objects by correcting their positions and applying impulses.
     *
     */
    public static void fixCollision(final CManifold m, final boolean applyFriction) {
        final GameEntity a = m.a;
        final GameEntity b = m.b;

        // Calculate relative velocity
        final Vec2D rv = b.velocity.minus(a.velocity);

        // Calculate relative velocity in terms of the normal direction
        final float velAlongNormal = rv.dotProduct(m.normal);

        // Calculate restitution
        final float e = Math.min(a.getRestitution(), b.getRestitution());

        // Calculate impulse scalar
        float j = -(1 + e) * velAlongNormal;
        j /= a.getInvMass() + b.getInvMass();

        // Apply impulse
        final Vec2D impulse = m.normal.multiply(j);

        a.velocity = a.velocity.minus(impulse.multiply(a.getInvMass()));
        b.velocity = b.velocity.plus(impulse.multiply(b.getInvMass()));

        if (applyFriction) {
            applyFriction(m, j);
        }

        positionalCorrection(m);
    }

    private static void applyFriction(final CManifold m, final float normalMagnitude) {
        final GameEntity a = m.a;
        final GameEntity b = m.b;

        // relative velocity
        final Vec2D rv = b.velocity.minus(a.velocity);
        // normalized tangent force
        final Vec2D tangent = rv.minus(m.normal.multiply(m.normal.dotProduct(rv))).unitVector();
        // friction magnitude
        final float jt = -rv.dotProduct(tangent) / (a.getInvMass() + b.getInvMass());

        // friction coefficient
        final float mu = (a.getStaticFriction() + b.getStaticFriction()) / 2;
        final float dynamicFriction = (a.getDynamicFriction() + b.getDynamicFriction()) / 2;

        // Coulomb's law: force of friction <= force along normal * mu
        final Vec2D frictionImpulse = Math.abs(jt) < normalMagnitude * mu ? tangent.multiply(jt) : tangent.multiply(-normalMagnitude
                * dynamicFriction);

        // apply friction
        a.velocity = a.velocity.minus(frictionImpulse.multiply(a.getInvMass()));
        b.velocity = b.velocity.plus(frictionImpulse.multiply(b.getInvMass()));
    }

    /**
     * Generates a collision manifold from two colliding objects.
     *
     * @param a
     * @param b
     * @return
     */
    private static CManifold generateManifold(final GameEntity a, final GameEntity b) {
        final CManifold m = new CManifold();
        m.a = a;
        m.b = b;
        final CShape as = a.shape;
        final CShape bs = b.shape;

        if (as instanceof RectShape && bs instanceof RectShape) {
            return generateManifold((RectShape) as, (RectShape) bs, m);
        } else if (as instanceof CircleShape && bs instanceof CircleShape) {
            return generateManifold((CircleShape) as, (CircleShape) bs, m);
        } else if (as instanceof RectShape && bs instanceof CircleShape) {
            return generateManifold((RectShape) as, (CircleShape) bs, m);
        } else if (as instanceof CircleShape && bs instanceof RectShape) {
            m.b = a;
            m.a = b;
            return generateManifold((RectShape) bs, (CircleShape) as, m);
        } else {
            throw new UnsupportedOperationException();
        }
    }

    private static CManifold generateManifold(final RectShape a, final RectShape b, final CManifold m) {

        final Rectangle2D r = a.getRect().createIntersection(b.getRect());

        m.normal = collisionNormal(a, b);
        // penetration is the min resolving distance
        m.penetration = (float) Math.min(r.getWidth(), r.getHeight());
        return m;
    }

    private static CManifold generateManifold(final CircleShape a, final CircleShape b, final CManifold m) {
        // A to B
        final Vec2D n = b.center.minus(a.center);
        final float dist = n.length();

        if (dist == 0) {
            // circles are on the same position, choose random but consistent values
            m.normal = new Vec2D(0, 1);
            m.penetration = Math.min(a.radius, b.radius);
            return m;
        }
        // don't recalculate dist to normalize
        m.normal = n.divide(dist);
        m.penetration = b.radius + a.radius - dist;
        return m;
    }

    private static CManifold generateManifold(final RectShape a, final CircleShape b, final CManifold m) {
        // Vector from A to B
        final Vec2D n = b.center.minus(a.center());

        // Closest point on A to center of B
        Vec2D closest = n;

        // Calculate half extents along each axis
        final float x_extent = a.width() / 2;
        final float y_extent = a.height() / 2;

        // Clamp point to edges of the AABB
        closest = new Vec2D(clamp(closest.x, -x_extent, x_extent), clamp(closest.y, -y_extent, y_extent));

        boolean inside = false;

        // Circle is inside the AABB, so we need to clamp the circle's center
        // to the closest edge
        if (n.equals(closest)) {
            inside = true;
            // Find closest axis
            if (Math.abs(closest.x) > Math.abs(closest.y)) {
                // Clamp to closest extent
                closest = new Vec2D(closest.x > 0 ? x_extent : -x_extent, closest.y);
            }
            // y axis is shorter
            else {
                // Clamp to closest extent
                closest = new Vec2D(closest.x, closest.y > 0 ? y_extent : -y_extent);
            }
        }
        // closest point to center of the circle
        final Vec2D normal = n.minus(closest);
        final float d = normal.length();
        final float r = b.radius;
        // Collision normal needs to be flipped to point outside if circle was
        // inside the AABB
        m.normal = inside ? normal.unitVector().multiply(-1) : normal.unitVector();
        m.penetration = r - d;
        return m;
    }

    private static float clamp(final float n, final float lower, final float upper) {
        return Math.max(lower, Math.min(n, upper));
    }

    /**
     * Corrects positions between two colliding objects to avoid "sinking."
     *
     * @param m
     */
    private static void positionalCorrection(final CManifold m) {
        final GameEntity a = m.a;
        final GameEntity b = m.b;

        // the amount to correct by
        final float percent = 1f; // usually .2 to .8
        // the amount in which we don't really care, this avoids vibrating objects.
        final float slop = 0.05f; // usually 0.01 to 0.1

        final float correctionMag = m.penetration + (m.penetration > 0 ? -slop : slop);

        final Vec2D correction = m.normal.multiply(correctionMag / (a.getInvMass() + b.getInvMass()) * percent);
        a.moveRelative(correction.multiply(-1 * a.getInvMass()));
        b.moveRelative(correction.multiply(b.getInvMass()));
    }

}

更新:

我已经解决了这个问题,这样圆圈就不会再粘在其他圆圈上了,这样我每个刻度只在两个对象之间检查和修复一次(不可能两次),但圆圈有时仍然会粘在一起到矩形,仍然可以骑在它们下面。

我解决了这个问题。我只需要在 fixCollision 方法中稍微更改一下计算即可。

This is a link to the commit that fixed the problem, if you're curious.