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); + } +}