I'm creating a MonoGame 2D engine framework for a platforming game, and I'm having trouble creating the collision response system. Although I've gotten SAT detection to work, the response travels across the actual direction of the static body's edge rather than its normal. Reversing the axes of the normal has not worked for me and does nothing, it has only created glitches involving the body going off screen.
Since I'm trying to make a platformer, I only want the normals of the static body to be considered as directions to respond. For example, if the static body is a box, I only want the moving body to travel on 90 degree normals.
Here is a video of the problem in action: https://www.youtube.com/watch?v=-wyXfZkxis0
And the source for the "Collision" module, which has all of the relevant geometric calculations inside (translation vector algorithm at the bottom):
using System;
using Microsoft.Xna.Framework;
namespace Crossfrog.Ferrum.Engine.Modules
{
public static class Collision
{
public static bool RectsCollide(Rectangle rect1, Rectangle rect2)
{
return
rect1.X <= rect2.X + rect2.Width &&
rect1.Y <= rect2.Y + rect2.Height &&
rect1.X + rect1.Width >= rect2.X &&
rect1.Y + rect1.Height >= rect2.Y;
}
private static float DotProduct(Vector2 v1, Vector2 v2)
{
return (v1.X * v2.X) + (v1.Y * v2.Y);
}
private static Vector2 NormalBetween(Vector2 v1, Vector2 v2)
{
return new Vector2(-(v1.Y - v2.Y), v1.X - v2.X);
}
private struct ProjectionLine
{
public float Start;
public float End;
}
private static ProjectionLine ProjectLine(Vector2[] points, Vector2 normal)
{
var projectionLine = new ProjectionLine() { Start = float.MaxValue, End = float.MinValue };
foreach (var p in points)
{
var projectionScale = DotProduct(p, normal);
projectionLine.Start = Math.Min(projectionScale, projectionLine.Start);
projectionLine.End = Math.Max(projectionScale, projectionLine.End);
}
return projectionLine;
}
private static bool CheckOverlapSAT(Vector2[] shape1, Vector2[] shape2)
{
for (int i = 0; i < shape1.Length; i++)
{
var vertex = shape1[i];
var nextVertex = shape1[(i + 1) % shape1.Length];
var edgeNormal = NormalBetween(vertex, nextVertex);
var firstProjection = ProjectLine(shape1, edgeNormal);
var secondProjection = ProjectLine(shape2, edgeNormal);
if (!(firstProjection.Start <= secondProjection.End && firstProjection.End >= secondProjection.Start))
return false;
}
return true;
}
public static bool ConvexPolysCollide(Vector2[] shape1, Vector2[] shape2)
{
return CheckOverlapSAT(shape1, shape2) && CheckOverlapSAT(shape2, shape1);
}
private static float? CollisionResponseAcrossLine(ProjectionLine line1, ProjectionLine line2)
{
if (line1.Start <= line2.Start && line1.End > line2.Start)
return line2.Start - line1.End;
else if (line2.Start <= line1.Start && line2.End > line1.Start)
return line2.End - line1.Start;
return null;
}
public static Vector2 MTVBetween(Vector2[] mover, Vector2[] collider)
{
if (!ConvexPolysCollide(mover, collider))
return Vector2.Zero;
float minResponseMagnitude = float.MaxValue;
var responseNormal = Vector2.Zero;
for (int c = 0; c < collider.Length; c++)
{
var cPoint = collider[c];
var cNextPoint = collider[(c + 1) % collider.Length];
var cEdgeNormal = NormalBetween(cPoint, cNextPoint);
var cProjected = ProjectLine(collider, cEdgeNormal);
var mProjected = ProjectLine(mover, cEdgeNormal);
var responseMagnitude = CollisionResponseAcrossLine(cProjected, mProjected);
if (responseMagnitude != null && responseMagnitude < minResponseMagnitude)
{
minResponseMagnitude = (float)responseMagnitude;
responseNormal = cEdgeNormal;
}
}
var normalLength = responseNormal.Length();
responseNormal /= normalLength;
minResponseMagnitude /= normalLength;
var mtv = responseNormal * minResponseMagnitude;
return mtv;
}
}
}
Your code is nearly right, just follow these steps and it should work.
Normalize your normal in NormalBetween(). Without this, your projected values are distorted and shouldn't be compared to get the right axis.
return Vector2.Normalize(new Vector2(-(v1.Y - v2.Y), v1.X - v2.X));
Use the same collision expression in CollisionResponseAcrossLine() as in CheckOverlapSAT(). Or just use one detection method for both.
if (line1.Start <= line2.Start && line1.End >= line2.Start) // use the >= operator
return line2.Start - line1.End;
else if (line2.Start <= line1.Start && line2.End >= line1.Start) // use the >= operator
return line2.End - line1.Start;
return null;
Compare the absolute magnitudes inside MTVBetween(). The calculated magnitudes can be negative, when they are pointing to the other direction of the normal.
if (responseMagnitude != null && Math.Abs(responseMagnitude.Value) < Math.Abs(minResponseMagnitude))
The following code is no longer needed, because we already normalized the vector in 1.
//var normalLength = responseNormal.Length();
//responseNormal /= normalLength;
//minResponseMagnitude /= normalLength;
This should get your example to work. But when you try it with two polygons, that have different seperation axes, it won't work, because in the collision response code, you only check for the axes of the static collider. The axes from the mover should also be checked, like you did in the collision detection method CheckOverlapSAT().
Calling the CheckOverlapSAT() method inside MTVBetween() seems to be redundant, you could also interrupt the MTVBetween() method, when any responseMagnitude is null.
And last but not least, consider replacing your CollisionResponseAcrossLine() code with the following:
private static float? CollisionResponseAcrossLine(ProjectionLine line1, ProjectionLine line2)
{
float distToStartOfLine2 = line2.Start - line1.End;
if (distToStartOfLine2 > 0)
return null;
float distToEndOfLine2 = line2.End - line1.Start;
if (distToEndOfLine2 < 0)
return null;
if (-distToStartOfLine2 < distToEndOfLine2) // negate distToStartOfLine2, cause it's always negative
return distToStartOfLine2;
else
return distToEndOfLine2;
}
This also accounts for the scenario of the player being inside an obstacle. It compares the distances to both sides and chooses the smaller one. Previously the player would always go to the same edge in this scenario.
If you want code that only supports AABBs, then you could go a simpler route without relying on the SAT. But I guess you want to support polygons too.
Related
I'm trying to implement bounce physics on a ball in my game using MonoGame c#. I've googled plenty but I'm unable to understand how to do this.
The circle should be able to hit any of the red lines and bounce realistically (not just invert the velocity).
I'm using this code to detect collision:
public bool IntersectCircle(Vector2 pos, float radius, out Vector2 circleWhenHit)
{
circleWhenHit = default;
// find the closest point on the line segment to the center of the circle
var line = End - Start;
var lineLength = line.Length();
var lineNorm = (1 / lineLength) * line;
var segmentToCircle = pos - Start;
var closestPointOnSegment = Vector2.Dot(segmentToCircle, line) / lineLength;
// Special cases where the closest point happens to be the end points
Vector2 closest;
if (closestPointOnSegment < 0) closest = Start;
else if (closestPointOnSegment > lineLength) closest = End;
else closest = Start + closestPointOnSegment * lineNorm;
// Find that distance. If it is less than the radius, then we
// are within the circle
var distanceFromClosest = pos - closest;
var distanceFromClosestLength = distanceFromClosest.Length();
if (distanceFromClosestLength > radius)
return false;
// So find the distance that places the intersection point right at
// the radius. This is the center of the circle at the time of collision
// and is different than the result from Doswa
var offset = (radius - distanceFromClosestLength) * ((1 / distanceFromClosestLength) * distanceFromClosest);
circleWhenHit = pos - offset;
return true;
}
And this code when the ball wants to change position:
private void GameBall_OnPositionChange(object sender, GameBallPositionChangedEventArgs e)
{
foreach(var boundary in mapBounds)
{
if (boundary.IntersectCircle(e.TargetPosition, gameBall.Radius, out Vector2 colVector))
{
var normalizedVelocity = Vector2.Normalize(e.Velocity);
var velo = e.Velocity.Length();
var surfaceNormal = Vector2.Normalize(colVector - e.CurrentPosition);
e.Velocity = Vector2.Reflect(normalizedVelocity, surfaceNormal) * velo;
e.TargetPosition = e.CurrentPosition;
break;
}
}
}
This code gives a decent result but I'm not using my boundary positions to calculate an angle.
How do I proceed to take those into account?
EDIT:
I've removed the event based update. I've added collision between players and the ball. This is now my map-update method:
foreach (var entity in circleGameEntities)
{
for (int i = 0; i < interpolatePos; i++)
{
entity.UpdatePosition(gameTime, interpolatePos);
var intersectingBoundaries = mapBounds
.Where(b =>
{
var intersects = b.IntersectCircle(entity.Position, entity.Radius, 0f, out _);
if (intersects)
averageNormal += b.Normal;
return intersects;
}).ToList();
if (intersectingBoundaries.Count > 0)
{
averageNormal.Normalize();
var normalizedVelocity = Vector2.Normalize(entity.Velocity); // Normalisera hastigheten
var velo = entity.Velocity.Length();
entity.Velocity = Vector2.Reflect(normalizedVelocity, averageNormal) * velo * entity.Bounciness;
entity.UpdatePosition(gameTime, interpolatePos);
}
foreach (var otherEntity in circleGameEntities.Where(e => e != entity))
{
if (entity.CollidesWithCircle(otherEntity, out Vector2 d))
{
Vector2 CMVelocity = (otherEntity.Mass * otherEntity.Velocity + entity.Mass * entity.Velocity) / (otherEntity.Mass + entity.Mass);
var otherEntityNorm = otherEntity.Position - entity.Position;
otherEntityNorm.Normalize();
var entityNorm = -otherEntityNorm;
var myVelocity = entity.Velocity;
myVelocity -= CMVelocity;
myVelocity = Vector2.Reflect(myVelocity, otherEntityNorm);
myVelocity += CMVelocity;
entity.Velocity = myVelocity;
entity.UpdatePosition(gameTime, interpolatePos);
var otherEntityVelocity = otherEntity.Velocity;
otherEntityVelocity -= CMVelocity;
otherEntityVelocity = Vector2.Reflect(otherEntityVelocity, entityNorm);
otherEntityVelocity += CMVelocity;
otherEntity.Velocity = otherEntityVelocity;
otherEntity.UpdatePosition(gameTime, interpolatePos);
}
}
}
entity.UpdateDrag(gameTime);
entity.Update(gameTime);
}
This code works quite well but sometimes the objects get stuck inside the walls and eachother.
CircleGameEntity class:
class CircleGameEntity : GameEntity
{
internal float Drag { get; set; } = .9999f;
internal float Radius => Scale * (Texture.Width + Texture.Height) / 4;
internal float Bounciness { get; set; } = 1f;
internal float Mass => BaseMass * Scale;
internal float BaseMass { get; set; }
internal Vector2 Velocity { get; set; }
internal float MaxVelocity { get; set; } = 10;
internal void UpdatePosition(GameTime gameTime, int interpolate)
{
var velocity = Velocity;
if (velocity.X < 0 && velocity.X < -MaxVelocity)
velocity.X = -MaxVelocity;
else if (velocity.X > 0 && velocity.X > MaxVelocity)
velocity.X = MaxVelocity;
if (velocity.Y < 0 && velocity.Y < -MaxVelocity)
velocity.Y = -MaxVelocity;
else if (velocity.Y > 0 && velocity.Y > MaxVelocity)
velocity.Y = MaxVelocity;
Velocity = velocity;
Position += Velocity / interpolate;
}
internal void UpdateDrag(GameTime gameTime)
{
Velocity *= Drag;
}
internal bool CollidesWithCircle(CircleGameEntity otherCircle, out Vector2 depth)
{
var a = Position;
var b = otherCircle.Position;
depth = Vector2.Zero;
float distance = Vector2.Distance(a, b);
if (Radius + otherCircle.Radius > distance)
{
float result = (Radius + otherCircle.Radius) - distance;
depth.X = (float)Math.Cos(result);
depth.Y = (float)Math.Sin(result);
}
return depth != Vector2.Zero;
}
}
The surfaceNormal is not the boundary normal, but the angle between the collision point and the center of the circle. This vector takes the roundness of the ball into account and is the negative of the ball's direction(if normalized) as if it hit the surface head-on, which is not needed unless the other surface is curved.
In the Boundary class calculate the angle and one of the normals in the constructor and store them as public readonly:
public readonly Vector2 Angle; // replaces lineNorm for disabiguity
public readonly Vector2 Normal;
public readonly Vector2 Length;
public Boundary(... , bool inside) // inside determines which normal faces the center
{
// ... existing constructor code
var line = End - Start;
Length = line.Length();
Angle = (1 / Length) * line;
Normal = new Vector2(-Angle.Y,Angle.X);
if (inside) Normal *= -1;
}
public bool IntersectCircle(Vector2 pos, float radius)
{
// find the closest point on the line segment to the center of the circle
var segmentToCircle = pos - Start;
var closestPointOnSegment = Vector2.Dot(segmentToCircle, End - Start) / Length;
// Special cases where the closest point happens to be the end points
Vector2 closest;
if (closestPointOnSegment < 0) closest = Start;
else if (closestPointOnSegment > Length) closest = End;
else closest = Start + closestPointOnSegment * Angle;
// Find that distance. If it is less than the radius, then we
// are within the circle
var distanceFromClosest = pos - closest;
return (distanceFromClosest.LengthSquared() > radius * radius); //the multiply is faster than square root
}
The change position code subset:
// ...
var normalizedVelocity = Vector2.Normalize(e.Velocity);
var velo = e.Velocity.Length();
e.Velocity = Vector2.Reflect(normalizedVelocity, boundary.Normal) * velo;
//Depending on timing and movement code, you may need add the next line to resolve the collision during the current step.
e.CurrentPosition += e.Velocity;
//...
This updated code assumes a single-sided non-moving boundary line as prescribed by the inside variable.
I am not a big fan of C# events in games, since it adds layers of delay (both, internally to C# and during proper use the cast of sender.)
I would be remiss not to mention your abuse of the e variable. e should always be treated as a value type: i.e. read-only. The sender variable should be cast(slow) and used for writing purposes.
I need to check if two lines intersect. These are currently wrapped in edge colliders.
In my minimal example i using Collider2D.OverlapsCollider
public class EdgeColliderChecker : MonoBehaviour
{
public EdgeCollider2D e1;
public EdgeCollider2D e2;
void Update () {
Collider2D[] results1 = new Collider2D[1];
e1.OverlapCollider(new ContactFilter2D(), results1);
if (results1[0] != null)
{
Debug.Log(results1[0].name);
}
Collider2D[] results2 = new Collider2D[1];
e1.OverlapCollider(new ContactFilter2D(), results2);
if (results2[0] != null) {
Debug.Log(results2[0].name);
}
}
}
This is how i have set up my scene:
As you can see in the picture above the two lines clearly intersect.
The issue is that nothing is outputed to the console.
I am not 100% sure about how ContactFilter should be configured but looking at the documentation it is used for filtering out results. So leaving it blank should include everything.
I really only need to do the check between two lines. So a function that takes them as arguments and returns a bool indicating intersection would be most convenient. Unfortunetaly I could not find such function in Unity.
It should not be overly complicated to construct the function myself but i would prefer to use the functions unity provide as much as possible. So consider this more of a unity related question than a math related one.
EDIT:
Using Collider2D.IsTouching(Collider2D) does not seem to work either. I use the same setup as before with this code instead:
public class EdgeColliderChecker : MonoBehaviour
{
public EdgeCollider2D e1;
public EdgeCollider2D e2;
void Update () {
if (e1.IsTouching(e2)) {
Debug.Log("INTERSECTION");
}
}
}
Edit 2:
I tried creating my own method for this:
public static class EdgeColliderExtentions {
public static List<Collider2D> GetInterSections(this EdgeCollider2D collider)
{
List<Collider2D> intersections = new List<Collider2D>();
Vector2[] points = collider.points;
for (int i = 0; i < points.Length - 1; i++)
{
Vector2 curr = collider.transform.TransformPoint(points[i]);
Vector2 next = collider.transform.TransformPoint(points[i + 1]);
Vector2 diff = next - curr;
Vector2 dir = diff.normalized;
float distance = diff.magnitude;
RaycastHit2D[] results = new RaycastHit2D[30];
ContactFilter2D filter = new ContactFilter2D();
Debug.DrawLine(curr, curr + dir * distance, Color.red, 1 / 60f);
int hits = Physics2D.Raycast(curr, dir, filter, results, distance);
for (int j = 0; i < hits; i++)
{
Collider2D intersection = results[j].collider;
if (intersection != collider)
{
intersections.Add(intersection);
}
}
}
return intersections;
}
}
EdgeColliderChecker:
public class EdgeColliderChecker : MonoBehaviour
{
public EdgeCollider2D e1;
void Update ()
{
List<Collider2D> hits = e1.GetInterSections();
if (hits.Count > 0) {
Debug.Log(hits.Count);
}
}
}
Still nothing. Even though the points i calculate align perfectly with the collider:
I did the math for it and it seems to work ok, not tested very thorougly. The intersection check is a bit choppy if it is run while the colliders are moving around:
public class Line {
private Vector2 start;
private Vector2 end;
public Line(Vector2 start, Vector2 end)
{
this.start = start;
this.end = end;
}
public static Vector2 GetIntersectionPoint(Line a, Line b)
{
//y = kx + m;
//k = (y2 - y1) / (x2 - x1)
float kA = (a.end.y - a.start.y) / (a.end.x - a.start.x);
float kB = (b.end.y - b.start.y) / (b.end.x - b.start.x);
//m = y - k * x
float mA = a.start.y - kA * a.start.x;
float mB = b.start.y - kB * b.start.x;
float x = (mB - mA) / (kA - kB);
float y = kA * x + mA;
return new Vector2(x,y);
}
public static bool Intersects(Line a, Line b)
{
Vector2 intersect = GetIntersectionPoint(a, b);
if (Vector2.Distance(a.start, intersect) < Vector2.Distance(a.start, a.end) &&
Vector2.Distance(a.end, intersect) < Vector2.Distance(a.start, a.end))
{
return true;
}
return false;
}
}
public static class EdgeColliderExtentions
{
public static bool Intersects(this EdgeCollider2D collider, EdgeCollider2D other)
{
Vector2[] points = collider.points;
Vector2[] otherPoints = other.points;
for (int i = 0; i < points.Length - 1; i++)
{
Vector2 start = collider.transform.TransformPoint(points[i]);
Vector2 end = collider.transform.TransformPoint(points[i + 1]);
Line line = new Line(start, end);
for (int j = 0; j < otherPoints.Length - 1; j++)
{
Vector2 otherStart = other.transform.TransformPoint(otherPoints[i]);
Vector2 otherEnd = other.transform.TransformPoint(otherPoints[i + 1]);
Line otherLine = new Line(otherStart, otherEnd);
if (Line.Intersects(line, otherLine))
{
return true;
}
}
}
return false;
}
}
But I'd really like to use something provided by unity instead.
Use Collider.bounds.Intersects(Collider.bounds) to determine if two bounds are intersecting:
void Update () {
if (e1.bounds.Intersects(e2.bounds)) {
Debug.Log("Bounds intersecting");
}
}
This unfortunately won't let you know if the edges are intersecting. However, if this tests false, you can skip testing the edges.
I realized that i could exhange one of the edge colliders for a polygon collider for my use case.
Using a polygon and an edge collider with Collider2D.OverlapsCollider() works as expected.
I don't know if i should accept this as an answer or not because it does not solve the original question about finding a funciton in unity for line intersection.
You could get a relatively close approximation with the following:
You could create a script that adds several small box colliders evenly along the line, the more the better. And then just do normal collision detection. But the more boxes (higher precision), the more costly computation-wise.
Simple problem - find if a point is inside a convex Polygon. There is algorithm described yet due to be beeng an in Wolfram language and I ve got something wrong. This is what I have:
private static bool PointInside2D(Vector2 point, Vector2 lineStart, Vector2 lineEnd) {
var v1 = lineStart - point;
var edge = lineStart - lineEnd;
return !(edge.x * v1.y - edge.y * v1.x < 0);
}
private static bool PointInsideRect2D(Vector2 point, IList<Vector2> rect) {
var lastPoint = rect.Count - 1;
bool? lastResult = null;
for (var i = 0; i < lastPoint; ++i) {
if (lastResult == null) {
lastResult = PointInside2D(point, rect[i], rect[i + 1]);
}
else {
if (lastResult != PointInside2D(point, rect[i], rect[i + 1])) {
return false;
}
}
}
return lastResult == PointInside2D( point, rect[lastPoint], rect[0] );
}
and it does not work sadly... I looked at some refrence implementations here tried them seems also not to work..
test data I use is for convex:
[(262.8, 669.1); (1623.9, 718.2); (200.4, 895.4); (1817.8, 1540.8)]
and (288, 815) and (1078, 890) as test points.
Can any one explain what I've got wrong in that algorithm/its implementations?
I believe your algorithm works correctly. It tests, if tested point lies on the same side (left or right) of all edges of polygon. But it requires, that all points in polygon declaration are sorted in clockwise or anti-clockwise order, that is not true for [(262.8, 669.1); (1623.9, 718.2); (200.4, 895.4); (1817.8, 1540.8)].
When I changed order of points in polygon, following program seem to return correct results:
public static void Main()
{
Vector2 p1 = new Vector2(288, 815);
Vector2 p2 = new Vector2(1078, 890);
//Please notice order of points is changed to clockwise
IList<Vector2> Polygon = new List<Vector2>(new Vector2[] { new Vector2(262.8f, 669.1f), new Vector2(200.4f, 895.4f), new Vector2(1817.8f, 1540.8f), new Vector2(1623.9f, 718.2f) });
bool p1Result = PointInsideRect2D(p1, Polygon);
bool p2Result = PointInsideRect2D(p2, Polygon);
}
private static bool PointInside2D(Vector2 point, Vector2 lineStart, Vector2 lineEnd)
{
var v1 = lineStart - point;
var edge = lineStart - lineEnd;
return !(edge.X * v1.Y - edge.Y * v1.X < 0);
}
private static bool PointInsideRect2D(Vector2 point, IList<Vector2> rect)
{
var lastPoint = rect.Count - 1;
bool? lastResult = null;
for (var i = 0; i < lastPoint; ++i)
{
if (lastResult == null)
{
lastResult = PointInside2D(point, rect[i], rect[i + 1]);
}
else
{
if (lastResult != PointInside2D(point, rect[i], rect[i + 1]))
{
return false;
}
}
}
return lastResult == PointInside2D(point, rect[lastPoint], rect[0]);
}
Im trying to do a coroutine that sets the character to hurt mode for a persiod. During this period i want it to flash. Im not familiar with modulus but i tried to use it to flip the sprite renderer every 10 or so cycles. But i get really weird values, is it cause its a float? How should i do this in unity?
public IEnumerator Hurt_cr (float sec, Vector2 attackPosition, float force)
{
isHurt = true;
AddForceAtPosition (attackPosition, force);
SpriteRenderer sprite = GetComponent< SpriteRenderer> ();
float t = Time.deltaTime;
while (t < sec) {
if ((t % 10) == 0) {
print ("sprite on : " + sprite.enabled);
sprite.enabled = !sprite.enabled;
}
yield return null;
t += Time.deltaTime;
}
isHurt = false;
}
Don't use a coroutine, it's totally useless. Put this into Update:
sprite.enabled = ((int)(Time.time / 10)) % 2 == 0;
EDIT: added (int) cast for good measure.
EDIT 2: to have it flash a fixed number of times
Add the class propery:
public int flashes = 0;
private bool flashingLastFrame = false;
In Update count the flashes, if any:
bool flashingNow;
if (flashes > 0)
{
flashingNow = ((int)(Time.time / 10)) % 2 == 0;
if (flashingNow != flashingLastFrame)
{
flashingLastFrame = flashingNow;
sprite.enabled = flashingNow;
flashes--; // forgot this to make it stop
}
}
To have it flash 3 times set from the outside:
obj.flashes = 6;
Or add a convenience method:
public void Flash(int times)
{
flashes = times * 2;
}
EDIT 3: For super-fast flashing:
if (flashes > 0)
{
flashingLastFrame = !flashingLastFrame;
sprite.enabled = flashingLastFrame;
flashes--;
}
When my AABB physics engine resolves an intersection, it does so by finding the axis where the penetration is smaller, then "push out" the entity on that axis.
Considering the "jumping moving left" example:
If velocityX is bigger than velocityY, AABB pushes the entity out on the Y axis, effectively stopping the jump (result: the player stops in mid-air).
If velocityX is smaller than velocitY (not shown in diagram), the program works as intended, because AABB pushes the entity out on the X axis.
How can I solve this problem?
Source code:
public void Update()
{
Position += Velocity;
Velocity += World.Gravity;
List<SSSPBody> toCheck = World.SpatialHash.GetNearbyItems(this);
for (int i = 0; i < toCheck.Count; i++)
{
SSSPBody body = toCheck[i];
body.Test.Color = Color.White;
if (body != this && body.Static)
{
float left = (body.CornerMin.X - CornerMax.X);
float right = (body.CornerMax.X - CornerMin.X);
float top = (body.CornerMin.Y - CornerMax.Y);
float bottom = (body.CornerMax.Y - CornerMin.Y);
if (SSSPUtils.AABBIsOverlapping(this, body))
{
body.Test.Color = Color.Yellow;
Vector2 overlapVector = SSSPUtils.AABBGetOverlapVector(left, right, top, bottom);
Position += overlapVector;
}
if (SSSPUtils.AABBIsCollidingTop(this, body))
{
if ((Position.X >= body.CornerMin.X && Position.X <= body.CornerMax.X) &&
(Position.Y + Height/2f == body.Position.Y - body.Height/2f))
{
body.Test.Color = Color.Red;
Velocity = new Vector2(Velocity.X, 0);
}
}
}
}
}
public static bool AABBIsOverlapping(SSSPBody mBody1, SSSPBody mBody2)
{
if(mBody1.CornerMax.X <= mBody2.CornerMin.X || mBody1.CornerMin.X >= mBody2.CornerMax.X)
return false;
if (mBody1.CornerMax.Y <= mBody2.CornerMin.Y || mBody1.CornerMin.Y >= mBody2.CornerMax.Y)
return false;
return true;
}
public static bool AABBIsColliding(SSSPBody mBody1, SSSPBody mBody2)
{
if (mBody1.CornerMax.X < mBody2.CornerMin.X || mBody1.CornerMin.X > mBody2.CornerMax.X)
return false;
if (mBody1.CornerMax.Y < mBody2.CornerMin.Y || mBody1.CornerMin.Y > mBody2.CornerMax.Y)
return false;
return true;
}
public static bool AABBIsCollidingTop(SSSPBody mBody1, SSSPBody mBody2)
{
if (mBody1.CornerMax.X < mBody2.CornerMin.X || mBody1.CornerMin.X > mBody2.CornerMax.X)
return false;
if (mBody1.CornerMax.Y < mBody2.CornerMin.Y || mBody1.CornerMin.Y > mBody2.CornerMax.Y)
return false;
if(mBody1.CornerMax.Y == mBody2.CornerMin.Y)
return true;
return false;
}
public static Vector2 AABBGetOverlapVector(float mLeft, float mRight, float mTop, float mBottom)
{
Vector2 result = new Vector2(0, 0);
if ((mLeft > 0 || mRight < 0) || (mTop > 0 || mBottom < 0))
return result;
if (Math.Abs(mLeft) < mRight)
result.X = mLeft;
else
result.X = mRight;
if (Math.Abs(mTop) < mBottom)
result.Y = mTop;
else
result.Y = mBottom;
if (Math.Abs(result.X) < Math.Abs(result.Y))
result.Y = 0;
else
result.X = 0;
return result;
}
It's hard to read code of other people, but I think this is one possible (purely brainstormed) workaround, although of course I'm not able to test it:
Before the collision detection happens, save the players velocity to some temporary variable.
After you've done your collision response, check if the players X or Y position has been corrected
If X position has been changed, manually reset (as a kind of "safety reset") the players Y velocity to the one that he had before the response.
By the way, what happens with your current code when your players hits the roof while jumping?
I had that same problem. Even the Microsoft platformer starterskit seems to have that bug.
The solution I found so far is to either use multisampling (argh) or to use the movedirection of the object: only move the distance between those 2 objects and no further when a collision is detected (and do that for each axis).
One possible solution I found is sorting the objects before resolving based on the velocity of the player.