github

Capsule Based Collision Detection

So, I've working on a melee weapon framework mod for Binding of Isaac. I've been using a lot of capsule based collision detection for this mod and I thought I'd write a post about it, share what I learned. The math is surprisingly simple and the results are quite good. If you are working on a 2D game and need a simple collision detection system, this might be the way to go.

Capsules!

A capsule is essentially a line with a radius. In 2D, it can be represented as two circles connected by a line. In 3D, it can be represented as two spheres connected by a cylinder. This definition is important because it helps us understand how to detect collisions with a capsule.

Since this is a programming blog, I am going to give you code snippets for a 2D implementation of capsule based collision detection. All the code snippets are in Typescript, and use IsaacScript's data models.

So here is a simple definition of a capsule class

class Capsule {
    private readonly v: Vector;
    private readonly w: Vector;
    private readonly radius: number;
}

Collision Detection

Remember what I said about a capsule being two circles connected by a line? Well, that's what the v and w vectors are. They are the two points that define the line. The radius is the radius of the circles. Okay this is cool and all but how do we detect collisions with this capsule?

    function collidesWithPoint(offset: Vector, rotation: number, point: Vector): boolean {
        return distToSegment(point, v, w) < this.radius;
    }

    function distToSegment(point: Vector, v: Vector, w: Vector) {
        return Math.sqrt(distToSegmentSquared(point, v, w));
    }

    function distToSegmentSquared(point: Vector, v: Vector, w: Vector) {
        const l2 = dist2(v, w);
        if (l2 === 0) return dist2(point, v);
        const t = ((point.X - v.X) * (w.X - v.X) + (point.Y - v.Y) * (w.Y - v.Y)) / l2;
        if (t < 0) return dist2(point, v);
        if (t > 1) return dist2(point, w);
        return dist2(point, Vector(v.X + t * (w.X - v.X), v.Y + t * (w.Y - v.Y)));
    }

    function dist2(v1: Vector, v2: Vector) {
        return sqr(v1.X - v2.X) + sqr(v1.Y - v2.Y);
    }

    function sqr(x: number) {
        return x * x;
    }

Yeah, that's a lot of math. But it's not that complicated. The 'distToSegment' function calculates the distance between a point and a line segment. If this distance is less than the radius of the capsule, then the point is colliding with the capsule.

Rotation and Offset

Now that we have the collision detection, we need to take into account the rotation and offset of the capsule. This is important because the capsule might not be aligned with the x and y axes. We need to take the rotation and offset into account when calculating the position of the capsule. In turn this will affect the collision detection.

export function RotateVectorAroundCenter(v: Vector, center: Vector, angle: number): Vector {
    const x = center.X + (v.X - center.X) * math.cos(angle) - (v.Y - center.Y) * math.sin(angle);
    const y = center.Y + (v.X - center.X) * math.sin(angle) + (v.Y - center.Y) * math.cos(angle);
    return Vector(x, y);
}

This one is easy, it rotates a vector around a center point by a given angle.

    function getVWithRotation(rotation: number): Vector {
        return RotateVectorAroundCenter(this.v, VectorZero, rotation);
    }

    function getWWithRotation(rotation: number): Vector {
        return RotateVectorAroundCenter(this.w, VectorZero, rotation);
    }

These two are also pretty simple. They are methods of the capsule class that return the v and w vectors rotated by the given angle.

    function getVWithRotationAndOffset(offset: Vector, rotation: number): Vector {
        return this.getVWithRotation(rotation).add(offset);
    }

    function getWWithRotationAndOffset(offset: Vector, rotation: number): Vector {
        return this.getWWithRotation(rotation).add(offset);
    }

These two methods are similar to the previous ones, but they also take an offset into account. This is important because the capsule might not be centered at the origin. For my case I use the center of the player as the center of the capsule.

Right, now let's combine these together to calculate collision when the capsule has a rotation and an offset.

    function collidesWithPointWithRotation(offset: Vector, rotation: number, point: Vector): boolean {
        return distToSegment(point, this.getVWithRotationAndOffset(offset, rotation), this.getWWithRotationAndOffset(offset, rotation)) < this.radius;
    }

And that's it! You now have a simple capsule based collision detection system for your 2D game. You can use this to detect collisions between a capsule and a point.

Optimizations

Realistically, you would only need to calculate a handful of predetermined rotations for your capsule especially if you are working on a 2D game. You can store these rotations in a map and use them when needed. This will save you a lot of computation time.

Here is what the code looks like with the rotation cache.

export class WeaponCapsule {
    ...
    private readonly rotationCacheV: Map<number, Vector>;
    private readonly rotationCacheW: Map<number, Vector>;

    ...

    getVWithRotation(rotation: number): Vector {
        return this.rotationCacheV.get(rotation) || this.rotationCacheV.set(rotation, RotateVectorAroundCenter(this.v, VectorZero, rotation)).get(rotation) as Vector;
    }

    getWWithRotation(rotation: number): Vector {
        return this.rotationCacheW.get(rotation) || this.rotationCacheW.set(rotation, RotateVectorAroundCenter(this.w, VectorZero, rotation)).get(rotation) as Vector;
    }

    ...
}

Utility Functions

Here is a collection of utility functions that I use in my capsule class. They are pretty self explanatory.

    function getCapsuleLength(): number {
        return this.v.Distance(this.w) + this.radius * 2
    }

    function collidesWithCircle(offset: Vector,center: Vector, radius: number): boolean {
        return distToSegment(center, this.v.add(offset), this.w.add(offset)) < this.radius + radius;
    }

    function collidesWithEllipse(center: Vector, radiusX: number, radiusY: number): boolean {
        return distToSegment(center, this.v, this.w) < this.radius + Math.max(radiusX, radiusY);
    }

    function collidesWithCapsule(offset: Vector, rotation: number, capsule: WeaponCapsule): boolean {
        return distToSegment(this.v.add(offset), capsule.v, capsule.w) < this.radius + capsule.radius;
    }

Capsule Data, A Use Case

In my mod I use capsules to define the hitboxes of the melee weapons. Every animation frame has one or more capsules that define the hitboxes for that frame. Every capsule holds a data model that defines game specific data such as damage, knockback amount, number of enemies hit per frame etc.

class WeaponCapsule {
    ...
    private readonly weaponCapsuleData: WeaponCapsuleData
    ...


class WeaponCapsuleData {
    private readonly damage: number;
    private readonly knockback: Vector;
    private readonly maxHitEnemies: number;
    private readonly knockbackIgnoresHitCount: boolean;

    constructor(
        damage: number,
        knockback: Vector,
        maxHitEnemies: number,
        knockbackIgnoresHitCount: boolean
    ) {
        this.damage = damage;
        this.knockback = knockback;
        this.maxHitEnemies = maxHitEnemies;
        this.knockbackIgnoresHitCount = knockbackIgnoresHitCount
    }

    public getDamage(): number {
        return this.damage;
    }

    public getKnockback(): Vector {
        return this.knockback;
    }

    public getMaxHitEnemies(): number {
        return this.maxHitEnemies;
    }

    public getKnockbackIgnoresHitCount(): boolean {
        return this.knockbackIgnoresHitCount;
    }
}

This is a very simple example of how you can use capsules in your game. You can use them for hitboxes, collision detection, or any other game mechanic that requires a slightly more complex shape than a circle or a rectangle. You can achieve a lot by combining multiple capsules together.

And finally, here is the capsules in action: Isaac hitting some enemies with a 2 x 4.

isaac_plank.gif

One of the other example weapons I am making for the mod is a scythe, it has a capsule for the blade and a capsule for the handle. The blade capsule has high damage, high knockback and applies a bleed effect, while the handle capsule has low damage and low knockback. This allows me to create a weapon that has different effects depending on where it hits the enemy. The possibilities are endless!