diff --git a/Engine.Physics2D/Abstract/IPhysicsEngine2D.cs b/Engine.Physics2D/Abstract/IPhysicsEngine2D.cs
index df5f397..323a31e 100644
--- a/Engine.Physics2D/Abstract/IPhysicsEngine2D.cs
+++ b/Engine.Physics2D/Abstract/IPhysicsEngine2D.cs
@@ -1,3 +1,5 @@
+using System.Collections.Generic;
+
using Syntriax.Engine.Core;
namespace Syntriax.Engine.Physics2D;
@@ -35,6 +37,26 @@ public interface IPhysicsEngine2D
/// The time step.
void StepIndividual(IRigidBody2D rigidBody, float deltaTime);
+ ///
+ /// Casts a into the scene and returns the closest it hits, if any.
+ ///
+ /// The to cast.
+ /// The maximum distance to check for intersections. Defaults to .
+ ///
+ /// A containing information about the hit, or if no hit was detected.
+ ///
+ RaycastResult? Raycast(Ray2D ray, float length = float.MaxValue);
+
+ ///
+ /// Casts a into the scene and stores all hit results in the provided list.
+ ///
+ /// The to cast.
+ ///
+ /// A list to which all s will be added. The list will be populated with zero or more objects.
+ ///
+ /// The maximum distance to check for intersections. Defaults to .
+ void Raycast(Ray2D ray, IList results, float length = float.MaxValue);
+
readonly record struct PhysicsIterationArguments(IPhysicsEngine2D Sender, float IterationDeltaTime);
readonly record struct PhysicsStepArguments(IPhysicsEngine2D Sender, float StepDeltaTime);
}
diff --git a/Engine.Physics2D/Abstract/IRaycastResolver2D.cs b/Engine.Physics2D/Abstract/IRaycastResolver2D.cs
new file mode 100644
index 0000000..28fc459
--- /dev/null
+++ b/Engine.Physics2D/Abstract/IRaycastResolver2D.cs
@@ -0,0 +1,23 @@
+using Syntriax.Engine.Core;
+
+namespace Syntriax.Engine.Physics2D;
+
+///
+/// Represents a 2D raycast resolver.
+///
+public interface IRaycastResolver2D
+{
+ ///
+ /// Casts a against a specific shape and returns the first hit, if any.
+ ///
+ /// The type of the , which must implement .
+ /// The shape to test against.
+ /// The to cast.
+ ///
+ /// The maximum distance to check along the . Defaults to .
+ ///
+ ///
+ /// A containing information about the intersection, or if there was no hit.
+ ///
+ RaycastResult? RaycastAgainst(T shape, Ray2D ray, float length = float.MaxValue) where T : ICollider2D;
+}
diff --git a/Engine.Physics2D/Abstract/RaycastResult.cs b/Engine.Physics2D/Abstract/RaycastResult.cs
new file mode 100644
index 0000000..55a56f2
--- /dev/null
+++ b/Engine.Physics2D/Abstract/RaycastResult.cs
@@ -0,0 +1,14 @@
+using Syntriax.Engine.Core;
+
+namespace Syntriax.Engine.Physics2D;
+
+public readonly struct RaycastResult(Ray2D ray, ICollider2D collider2D, Vector2D position, Vector2D normal)
+{
+ public readonly Ray2D Ray = ray;
+
+ public readonly Vector2D Position = position;
+ public readonly Vector2D Normal = normal;
+
+ public readonly ICollider2D Collider2D = collider2D;
+ public readonly RigidBody2D? RigidBody2D = collider2D.BehaviourController.GetBehaviourInParent();
+}
diff --git a/Engine.Physics2D/PhysicsEngine2D.cs b/Engine.Physics2D/PhysicsEngine2D.cs
index d7ad475..a39b8e1 100644
--- a/Engine.Physics2D/PhysicsEngine2D.cs
+++ b/Engine.Physics2D/PhysicsEngine2D.cs
@@ -15,6 +15,7 @@ public class PhysicsEngine2D : Behaviour, IPreUpdate, IPhysicsEngine2D
protected readonly ICollisionDetector2D collisionDetector = null!;
protected readonly ICollisionResolver2D collisionResolver = null!;
+ protected readonly IRaycastResolver2D raycastResolver = null!;
private static Comparer SortByPriority() => Comparer.Create((x, y) => y.Priority.CompareTo(x.Priority));
protected ActiveBehaviourCollectorSorted physicsPreUpdateCollector = new() { SortBy = SortByPriority() };
@@ -27,6 +28,35 @@ public class PhysicsEngine2D : Behaviour, IPreUpdate, IPhysicsEngine2D
public int IterationPerStep { get => _iterationPerStep; set => _iterationPerStep = value < 1 ? 1 : value; }
public float IterationPeriod { get => _iterationPeriod; set => _iterationPeriod = value.Max(0.0001f); }
+ public RaycastResult? Raycast(Ray2D ray, float length = float.MaxValue)
+ {
+ RaycastResult? closestResult = null;
+ float closestDistance = float.MaxValue;
+
+ for (int i = 0; i < colliderCollector.Count; i++)
+ if (raycastResolver.RaycastAgainst(colliderCollector[i], ray, length) is RaycastResult raycastResult)
+ {
+ float magnitudeSquared = raycastResult.Position.FromTo(ray.Origin).MagnitudeSquared;
+
+ if (magnitudeSquared > closestDistance)
+ continue;
+
+ closestDistance = magnitudeSquared;
+ closestResult = raycastResult;
+ }
+
+ return closestResult;
+ }
+
+ public void Raycast(Ray2D ray, IList results, float length = float.MaxValue)
+ {
+ results.Clear();
+
+ for (int i = 0; i < colliderCollector.Count; i++)
+ if (raycastResolver.RaycastAgainst(colliderCollector[i], ray, length) is RaycastResult raycastResult)
+ results.Add(raycastResult);
+ }
+
public void Step(float deltaTime)
{
float intervalDeltaTime = deltaTime / IterationPerStep;
@@ -216,13 +246,15 @@ public class PhysicsEngine2D : Behaviour, IPreUpdate, IPhysicsEngine2D
{
collisionDetector = new CollisionDetector2D();
collisionResolver = new CollisionResolver2D();
+ raycastResolver = new RaycastResolver2D();
Priority = int.MaxValue;
}
- public PhysicsEngine2D(ICollisionDetector2D collisionDetector, ICollisionResolver2D collisionResolver)
+ public PhysicsEngine2D(ICollisionDetector2D collisionDetector, ICollisionResolver2D collisionResolver, IRaycastResolver2D raycastResolver)
{
this.collisionDetector = collisionDetector;
this.collisionResolver = collisionResolver;
+ this.raycastResolver = raycastResolver;
Priority = int.MaxValue;
}
}
diff --git a/Engine.Physics2D/PhysicsEngine2DStandalone.cs b/Engine.Physics2D/PhysicsEngine2DStandalone.cs
index fffdba7..dc16d4c 100644
--- a/Engine.Physics2D/PhysicsEngine2DStandalone.cs
+++ b/Engine.Physics2D/PhysicsEngine2DStandalone.cs
@@ -19,6 +19,7 @@ public class PhysicsEngine2DStandalone : IPhysicsEngine2D
private readonly ICollisionDetector2D collisionDetector = null!;
private readonly ICollisionResolver2D collisionResolver = null!;
+ private readonly IRaycastResolver2D raycastResolver = null!;
public int IterationPerStep { get => _iterationCount; set => _iterationCount = value < 1 ? 1 : value; }
@@ -43,6 +44,34 @@ public class PhysicsEngine2DStandalone : IPhysicsEngine2D
rigidBody.BehaviourController.OnBehaviourAdded.RemoveListener(delegateOnBehaviourAdded);
rigidBody.BehaviourController.OnBehaviourRemoved.RemoveListener(delegateOnBehaviourRemoved);
}
+ public RaycastResult? Raycast(Ray2D ray, float length = float.MaxValue)
+ {
+ RaycastResult? closestResult = null;
+ float closestDistance = float.MaxValue;
+
+ for (int i = 0; i < colliders.Count; i++)
+ if (raycastResolver.RaycastAgainst(colliders[i], ray, length) is RaycastResult raycastResult)
+ {
+ float magnitudeSquared = raycastResult.Position.FromTo(ray.Origin).MagnitudeSquared;
+
+ if (magnitudeSquared > closestDistance)
+ continue;
+
+ closestDistance = magnitudeSquared;
+ closestResult = raycastResult;
+ }
+
+ return closestResult;
+ }
+
+ public void Raycast(Ray2D ray, IList results, float length = float.MaxValue)
+ {
+ results.Clear();
+
+ for (int i = 0; i < colliders.Count; i++)
+ if (raycastResolver.RaycastAgainst(colliders[i], ray, length) is RaycastResult raycastResult)
+ results.Add(raycastResult);
+ }
public void Step(float deltaTime)
{
@@ -203,15 +232,17 @@ public class PhysicsEngine2DStandalone : IPhysicsEngine2D
{
collisionDetector = new CollisionDetector2D();
collisionResolver = new CollisionResolver2D();
+ raycastResolver = new RaycastResolver2D();
delegateOnBehaviourAdded = OnBehaviourAdded;
delegateOnBehaviourRemoved = OnBehaviourRemoved;
}
- public PhysicsEngine2DStandalone(ICollisionDetector2D collisionDetector, ICollisionResolver2D collisionResolver)
+ public PhysicsEngine2DStandalone(ICollisionDetector2D collisionDetector, ICollisionResolver2D collisionResolver, IRaycastResolver2D raycastResolver)
{
this.collisionDetector = collisionDetector;
this.collisionResolver = collisionResolver;
+ this.raycastResolver = raycastResolver;
delegateOnBehaviourAdded = OnBehaviourAdded;
delegateOnBehaviourRemoved = OnBehaviourRemoved;
diff --git a/Engine.Physics2D/RaycastResolver2D.cs b/Engine.Physics2D/RaycastResolver2D.cs
new file mode 100644
index 0000000..c04dd78
--- /dev/null
+++ b/Engine.Physics2D/RaycastResolver2D.cs
@@ -0,0 +1,102 @@
+using System.Collections.Generic;
+
+using Syntriax.Engine.Core;
+
+namespace Syntriax.Engine.Physics2D;
+
+public class RaycastResolver2D : IRaycastResolver2D
+{
+ private readonly Pool> lineCacheQueue = new(() => new List(4));
+
+ RaycastResult? IRaycastResolver2D.RaycastAgainst(T shape, Ray2D ray, float length)
+ {
+ if (shape is IShapeCollider2D shapeCollider)
+ return RaycastAgainstShape(shapeCollider, ray, length);
+ else if (shape is ICircleCollider2D circleCollider)
+ return RaycastAgainstCircle(circleCollider, ray, length);
+
+ throw new System.NotSupportedException($"{shape.GetType().FullName} is not supported by {GetType().FullName}. Please implement a {nameof(IRaycastResolver2D)} and use it as the raycast resolver.");
+ }
+
+ public RaycastResult? RaycastAgainstShape(IShapeCollider2D shapeCollider, Ray2D ray, float length)
+ {
+ List line2Ds = lineCacheQueue.Get();
+ line2Ds.Clear();
+
+ RaycastResult? raycastResult = null;
+ float closestRaycastResultSquared = float.MaxValue;
+
+ shapeCollider.ShapeWorld.ToLines(line2Ds);
+
+ Line2D rayLine = ray.ToLine(length);
+
+ foreach (Line2D line in line2Ds)
+ {
+ if (line.Intersects(rayLine))
+ {
+ Vector2D hitPosition = line.IntersectionPoint(rayLine);
+
+ float hitDistanceSquared = ray.Origin.FromTo(hitPosition).MagnitudeSquared;
+
+ if (closestRaycastResultSquared < hitDistanceSquared)
+ continue;
+
+ closestRaycastResultSquared = hitDistanceSquared;
+
+ Vector2D lineDirection = line.Direction;
+
+ Vector2D normal1 = lineDirection.Perpendicular();
+ Vector2D normal2 = -lineDirection.Perpendicular();
+
+ float normalDot1 = rayLine.Direction.Dot(normal1);
+ float normalDot2 = rayLine.Direction.Dot(normal2);
+
+ Vector2D hitNormal = normalDot1 < normalDot2 ? normal1 : normal2;
+
+ if (shapeCollider.ShapeWorld.Overlaps(ray.Origin))
+ hitNormal = hitNormal.Reversed;
+
+ raycastResult = new(ray, shapeCollider, hitPosition, hitNormal);
+ }
+ }
+
+ line2Ds.Clear();
+ lineCacheQueue.Return(line2Ds);
+
+ return raycastResult;
+ }
+
+ public RaycastResult? RaycastAgainstCircle(ICircleCollider2D circleCollider, Ray2D ray, float length)
+ {
+ Circle circle = circleCollider.CircleWorld;
+
+ if (circle.Overlaps(ray.Origin, out _, out float depth))
+ {
+ Vector2D insideNormal = circle.Center.FromTo(ray.Origin).Normalized;
+ Vector2D insidePosition = circle.Center + insideNormal * circle.Radius;
+
+ return new(ray, circleCollider, insidePosition, insideNormal);
+ }
+
+ Vector2D originToCircle = ray.Origin.FromTo(circle.Center);
+ float distanceToClosest = ray.Direction.Dot(originToCircle);
+
+ Vector2D closestPoint = ray.Origin + ray.Direction * distanceToClosest;
+ Vector2D closestPointToCircle = closestPoint.FromTo(circle.Center);
+
+ float closestToCircleDistanceSquared = closestPointToCircle.MagnitudeSquared;
+
+ if (closestToCircleDistanceSquared > circle.RadiusSquared)
+ return null;
+
+ float distanceToHit = distanceToClosest - (circle.Radius - closestToCircleDistanceSquared.Sqrt());
+
+ if (distanceToHit > length || distanceToHit < 0f)
+ return null;
+
+ Vector2D hitPosition = ray.Evaluate(distanceToHit);
+ Vector2D hitNormal = circle.Center.FromTo(hitPosition).Normalized;
+
+ return new(ray, circleCollider, hitPosition, hitNormal);
+ }
+}