Ga naar inhoud

Collision

Doelstelling

Wij hadden een mooie manier nodig om botsingen te detecteren, want de huidige code daarvoor is niet net en moeilijk om mee te werken.

Toegepaste oplossing

Collision Handler

Hier is de collision handler, het behandelt alles met collisions. Het checkt elke frame voor collisions en geeft voor elke een event, maar ook maakt het mogelijk om de queryen naar colliders in een bepaald gebied.

public class CollisionHandler : BaseSystem
{
    protected EventBus EventBus { get; private set; }
    protected StateManager StateManager { get; private set; }

    protected List<IHasCollider> colliders = new();

    public static IEnumerable<IHasCollider> GetCollidersIn(GameObjectList parent)
        => parent
            .GetDescendants()
            .Where(descendant => descendant is IHasCollider)
            .Select(descendant => (IHasCollider)descendant);

    public override void Initialize(App app, SystemHandler handler)
    {
        base.Initialize(app, handler);

        EventBus = handler.GetSystem<EventBus>();
        StateManager = handler.GetSystem<StateManager>();
    }

    public override void Update(GameTime gameTime)
    {
        if (StateManager.currentState is not BaseState state)
        {
            colliders = new();
            return;
        }

Hier word gekeken naar collisions tussen alle colliders. En collisions tussen dezelfde collider worden natuurlijk overgeslagen.

        colliders = GetCollidersIn(state).ToList();

        var i = 0;
        foreach (var colliderA in colliders)
        {
            foreach (var colliderB in colliders.Skip(i + 1))
            {
                if (CheckCollision(colliderA, colliderB))
                    EventBus.Publish(
                        new CollidersTouchedEvent((GameObject)colliderA, colliderA, (GameObject)colliderB, colliderB)
                    );
            }

            i++;
        }
    }

En hier zijn verschillende query methods voor elke vorm. Zelf moet je meegeven waarin gekeken moet worden voor colliders, zodat de gebruiker ervan bijvoorbeeld kan kiezen om alleen binnen een bepaald object te queryen.

    public static IEnumerable<IHasCollider> QueryPoint(Vector2 point, GameObjectList parent)
        => GetCollidersIn(parent)
            .Where(c => CheckPointCollision(point, c));

    public static IEnumerable<IHasCollider> QueryRect(Rectangle rect, GameObjectList parent)
        => GetCollidersIn(parent)
            .Where(c => CheckRectCollision(rect, c));

    public static IEnumerable<IHasCollider> QueryCircle(Circle circle, GameObjectList parent)
        => GetCollidersIn(parent)
            .Where(c => CheckCircleCollision(circle, c));

    public static IEnumerable<IHasCollider> QueryLine(Line line, GameObjectList parent)
        => GetCollidersIn(parent)
            .Where(c => CheckLineCollision(line, c));

Vanaf hier begint het echt checken van collisions, behalve dat de echte berekeningen in de CollisionHelper zitten. Het meeste wat hier gebeurt is colliders filteren om de juiste method te vinden die de specifieke vormen met elkaar vergelijkt.

Samen met een paar extra checks voor performance, zoals de AABB check aan het begin hieronder.

    public static bool CheckCollision(IHasCollider colliderA, IHasCollider colliderB, bool ignoreWithoutCollider = true)
    {
        if (!CollisionHelper.RectRect(colliderA.GlobalBoundingBox, colliderB.GlobalBoundingBox))
            return false;

        switch (colliderA)
        {
            case IHasRectCollider r:
                if (colliderB is IHasRectCollider)
                    return true;

                return CheckRectCollision(r.GlobalBoundingBox, colliderB);

            case IHasCircleCollider c:
                return CheckCircleCollision(c.GlobalBoundingCircle, colliderB);

            case IHasLineCollider l:
                return CheckLineCollision(l.GlobalBoundingLine, colliderB);

            default:
                if (ignoreWithoutCollider)
                    return false;

                throw new NotImplementedException($"{colliderA} does not implement a known collider interface");
        }
    }

    public static bool CheckPointCollision(Vector2 a, IHasCollider b, bool ignoreWithoutCollider = true)
        => b switch
        {
            IHasRectCollider r => CollisionHelper.PointRect(a, r.GlobalBoundingBox),
            IHasCircleCollider c => CollisionHelper.PointCircle(a, c.GlobalBoundingCircle),
            IHasLineCollider l => CollisionHelper.PointLine(a, l.GlobalBoundingLine),

            _ => ignoreWithoutCollider
                ? false
                : throw new NotImplementedException($"collider of {b} is not yet supported with line collision")
        };

    public static bool CheckRectCollision(Rectangle a, IHasCollider b, bool ignoreWithoutCollider = true)
        => b switch
        {
            IHasRectCollider r => CollisionHelper.RectRect(a, r.GlobalBoundingBox),
            IHasCircleCollider c => CollisionHelper.RectCircle(a, c.GlobalBoundingCircle),
            IHasLineCollider l => CollisionHelper.RectLine(a, l.GlobalBoundingLine),

            _ => ignoreWithoutCollider
                ? false
                : throw new NotImplementedException($"collider of {b} is not yet supported with rect collision")
        };

    public static bool CheckCircleCollision(Circle a, IHasCollider b, bool ignoreWithoutCollider = true)
        => b switch
        {
            IHasRectCollider r => CollisionHelper.RectCircle(r.GlobalBoundingBox, a),
            IHasCircleCollider c => CollisionHelper.CircleCircle(a, c.GlobalBoundingCircle),
            IHasLineCollider l => CollisionHelper.CircleLine(a, l.GlobalBoundingLine),

            _ => ignoreWithoutCollider
                ? false
                : throw new NotImplementedException($"collider of {b} is not yet supported with circle collision")
        };

    public static bool CheckLineCollision(Line a, IHasCollider b, bool ignoreWithoutCollider = true)
        => b switch
        {
            IHasRectCollider r => CollisionHelper.RectLine(r.GlobalBoundingBox, a),
            IHasCircleCollider c => CollisionHelper.CircleLine(c.GlobalBoundingCircle, a),
            IHasLineCollider l => CollisionHelper.LineLine(a, l.GlobalBoundingLine),

            _ => ignoreWithoutCollider
                ? false
                : throw new NotImplementedException($"collider of {b} is not yet supported with line collision")
        };
}

Collision Helper

De collision helper is een collectie van statische methods die simpel checken of twee vormen over elkaar heen staan. Deze methods zijn gehaald van één online bron(Thompson, 2015). Om die reden laat ik de implementaties ervan niet hier zien.

Collider Interfaces

Als laatste zijn hier de interfaces die de verschillende colliders identificeert. Ze zijn niet heel interessant, ze vereisen alleen de benodigde informatie over de collider zelf. Eigenlijk alleen de globale versie van de bijbehorende vorm. En natuurlijk alleen de globale variant omdat de relatieve variant niet nodig is voor gebruikers van deze interfaces.

public interface IHasCollider
{
    public Vector2 GlobalPosition { get; }
    public Rectangle GlobalBoundingBox { get; }
}

public interface IHasRectCollider : IHasCollider
{
    public Vector2 Size { get; }
}

public interface IHasCircleCollider : IHasCollider
{
    public float Radius { get; }

    public virtual Circle GlobalBoundingCircle => new(Radius, GlobalPosition);
}

public interface IHasLineCollider : IHasCollider
{
    public Line GlobalBoundingLine { get; }
}

Klassendiagram

classDiagram class BaseSystem { <<abstract>> } CollisionHandler --|> BaseSystem class CollisionHandler { +IEnumerable~IHasCollider~ QueryPoint(Vector2, GameObjectList)$ +IEnumerable~IHasCollider~ QueryRect(Rectangle, GameObjectList)$ +IEnumerable~IHasCollider~ QueryCircle(Circle, GameObjectList)$ +IEnumerable~IHasCollider~ QueryLine(Line, GameObjectList)$ +bool CheckCollision(IHasCollider, IHasCollider)$ +bool CheckPointCollision(Vector2, IHasCollider)$ +bool CheckRectCollision(Rectangle, IHasCollider)$ +bool CheckCircleCollision(Circle, IHasCollider)$ +bool CheckLineCollision(Line, IHasCollider)$ } IHasCollider --o CollisionHandler class IHasCollider { <<interface>> +Vector2 GlobalPosition +Rectangle GlobalBoundingBox } IHasRectCollider ..|> IHasCollider IHasRectCollider --o CollisionHandler class IHasRectCollider { <<interface>> +Vector2 Size } IHasCircleCollider ..|> IHasCollider IHasCircleCollider --o CollisionHandler class IHasCircleCollider { <<interface>> +float Radius +Circle GlobalBoundingCircle } IHasLineCollider ..|> IHasCollider IHasLineCollider --o CollisionHandler class IHasLineCollider { <<interface>> +Line GlobalBoundingLine }

Bronnen


Laatst geüpdatet: June 9, 2024
Gecreëerd: May 9, 2024