Maneuver with the recoil of your blunderbuss through dangerous crypts

Raycast character controller

Before working on Krypta the projects had been using simple radius comparisons or axis aligned box collision for its player movement. However, for Krypta, we needed something more reliable which could recognize directional collision and identify what is colliding.

I found great reference from Sebastian Lauge's Unity series: 2D Platformer Controller.


The fundemental concept is to do a number of raycasts spread across the area of a moving actor in the direction of its movement. Whenever a ray collides, the distance of the hit is used to limit the movement.


What's great about this solution is that we can now identify in which direction the player collides and what its colliding against. A controller class is compositioned to our player which saves the CollisionInfo used to calculate player states.


The rays are cast against every culled game object in the level, class name "Tile", and checked against all its collider's faces.
































The compositioned controller is a RaycastController, it attaches to a collider and calculates the spacing of the rays which the controller will use; Evenly spacing the raycasts among its border with the skin width offset to avoid annoying edge collision.















The result is accurate, non-frame dependent, collision where the objects collided against are identified and the direction is known.


The concept extends beyond 2 dimensions and the rays can be cast from the surface of any primitive gemotery in its normal.


Of course, using ray collision comes at a performance cost when used in excess, but with proper configuration and culling it was of no issue in Krypta.


    for (int i = 0; i < myRayCount; i++) 
        RaycastHit2D outHit;
        Tile* tile = CheckIntersects(rayOrigin, direction, rayLength, outHit);
        if (tile != nullptr)
            float distance = sqrt(outHit.distanceSqr);
            moveAmount = (distance - mySkinWidth) * direction;
            rayLength = distance;

struct CollisionInfo 
    bool above, below;
    bool left, right;

class PlayerController : public RaycastController
    PlayerController(Player& aPlayer);
    PlayerController() = delete;
    ~PlayerController() = default;

    void Move(VECTOR2F aMoveAmount, bool aStandingOnPlatform);

    void VerticalCollisions(VECTOR2F & aMoveAmount);
    void HorizontalCollisions(VECTOR2F & aMoveAmount);
    bool Intersect(VECTOR2F aFrom, VECTOR2F aTo, const Tile& aTile, RaycastHit2D& aOutIntersectionPoint);


    CollisionInfo myCollisionInfo;
    CollisionInfo myCollisionInfoPrevious;

    Player& myPlayer;



class RaycastController
    RaycastController(AABB2D<float>& aCollider);
    RaycastController() = delete;
    ~RaycastController() = default;

    bool Intersects(const VECTOR2F& a1, const VECTOR2F& a2, const VECTOR2F& b1, const VECTOR2F& b2, RaycastHit2D & aOutIntersectionPoint);
    void UpdateRayOrigins(const VECTOR2F& aPosition);

    void CalculateRaySpacing();
    VECTOR2I myRayCount;
    VECTOR2F myRaySpacing;

    RaycastOrigins myRaycastOrigins;
    AABB2D<float>& myCollider;

    const float myDistanceBetweenRays;
    const float mySkinWidth;
    bool myFirstFlag;


void RaycastController::UpdateRayOrigins(const VECTOR2F& aPosition)

    AABB2D<float> bounds = myCollider;
    bounds.Expand(mySkinWidth * -2);

    myRaycastOrigins.botLeft =    VECTOR2F(aPosition.x + bounds.GetMin().x, aPosition.y + bounds.GetMax().y);
    myRaycastOrigins.botRight =    VECTOR2F(aPosition.x + bounds.GetMax().x, aPosition.y + bounds.GetMax().y);
    myRaycastOrigins.topLeft =    VECTOR2F(aPosition.x + bounds.GetMin().x, aPosition.y + bounds.GetMin().y);
    myRaycastOrigins.topRight =    VECTOR2F(aPosition.x + bounds.GetMax().x, aPosition.y + bounds.GetMin().y);

void RaycastController::CalculateRaySpacing()
    AABB2D<float> bounds = myCollider;
    bounds.Expand(mySkinWidth * -2);

    VECTOR2F size = VECTOR2F(bounds.GetMax().y - bounds.GetMin().y, bounds.GetMax().x - bounds.GetMin().x);
    myRayCount.x = static_cast<int>(size.x / myDistanceBetweenRays * GetWindowRatioInversed());
    myRayCount.y = static_cast<int>(size.y / myDistanceBetweenRays * GetWindowRatio());

    myRaySpacing.x = size.x / static_cast<float>(myRayCount.x - 1);
    myRaySpacing.y = size.y / static_cast<float>(myRayCount.y - 1);


bool RaycastController::Intersects(const VECTOR2F& a1, const VECTOR2F& a2, const VECTOR2F& b1, const VECTOR2F& b2, RaycastHit2D & aOutIntersectionPoint)
    VECTOR2F b = a2 - a1;
    VECTOR2F d = b2 - b1;
    float bDotDPerp = b.x * d.y - b.y * d.x;
    if (bDotDPerp == 0) return false;

    VECTOR2F c = b1 - a1;
    float t = (c.x * d.y - c.y * d.x) / bDotDPerp;
    if (t < 0 || t > 1) return false;

    float u = (c.x * b.y - c.y * b.x) / bDotDPerp;
    if (u < 0 || u > 1) return false;

    VECTOR2F rayDir = b.Normal();
    VECTOR2F faceNormal = (b2 - b1).Normalize();
    if (rayDir.Dot(faceNormal) <= 0) return false;

    aOutIntersectionPoint.normal = faceNormal;
    aOutIntersectionPoint.point = a1 + (b * t);
    aOutIntersectionPoint.distanceSqr = (aOutIntersectionPoint.point - a1).LengthSqr();
    return true;


bool PlayerController::Intersect(VECTOR2F aFrom, VECTOR2F aTo, const Tile& aTile, RaycastHit2D & aOutIntersectionPoint)
    const VECTOR2F position = aTile.GetPosition();
    const VECTOR2F min = aTile.GetCollider().GetMin();
    const VECTOR2F max = aTile.GetCollider().GetMax();
    const VECTOR2F topLeft    (position.x + min.x, position.y + min.y);
    const VECTOR2F topRight    (position.x + max.x, position.y + min.y);
    const VECTOR2F botRight    (position.x + max.x, position.y + max.y);
    const VECTOR2F botLeft    (position.x + min.x, position.y + max.y);

    return    Intersects(aFrom, aTo, topLeft, topRight, aOutIntersectionPoint)    ||
            Intersects(aFrom, aTo, topRight, botRight, aOutIntersectionPoint)    ||
            Intersects(aFrom, aTo, botRight, botLeft, aOutIntersectionPoint)    ||
            Intersects(aFrom, aTo, botLeft, topLeft, aOutIntersectionPoint);