Collision twitching when player is on more than 1 block - c#

I'm coding collision in my 2D tile-based game, coded with Monogame. I've ran into a problem where if my player is standing on 2 tiles at once, it starts to twitch because the collision resolving is running 2 times, I want this to only run once. How would I do this? Here is my code:
public void HandleCollisions()
{
Rectangle plyRect = ply.GetBounds(); //get player rectangle
Vector2 currentPos = new Vector2(plyRect.X, plyRect.Y); //get player current position
Vector2 xy = new Vector2( (float)Math.Floor((ply.pos.X + ply.tex.Width) / world.tileSize),
(float)Math.Floor((ply.pos.Y + ply.tex.Height) / world.tileSize)); //get tiles position based on player position
for (int x = (int)xy.X - 4; x <= (int)xy.X + 4; x++) //run through tiles near the player
{
for (int y = (int)xy.Y - 4; y <= (int)xy.Y + 4; y++)
{
if (x >= 0 && y >= 0 && x < world.GetWorldSize().X && y < world.GetWorldSize().Y) //check if tiles are within map
{
if (world.tiles[x, y] != null)
{
if (world.tiles[x, y].collision == Tile.Collision.SOLID) //check if tile is solid
{
Rectangle tileRect = world.tiles[x, y].GetRect(); //get the tiles rectangle
if (plyRect.Intersects(tileRect)) //check if intersecting
{
Vector2 depth = RectangleExtension.GetIntersectionDepth(plyRect, tileRect); //get intersecting depth
if (depth != Vector2.Zero)
{
float absDepthX = Math.Abs(depth.X);
float absDepthY = Math.Abs(depth.Y);
if (absDepthY < absDepthX)
{
currentPos = new Vector2(currentPos.X, currentPos.Y + depth.Y); //resolve Y collision first
}
else
{
currentPos = new Vector2(currentPos.X + depth.X, currentPos.Y); //then resolve X collision
}
}
}
}
}
}
}
}
ply.pos = currentPos; //set player position after the checking is done
}

Once one collision is detected break out of the for loop using a break; statement

Related

Calculate bounce velocity on a line

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.

transform.position not returning the good position

I made a code that tell me how many positions can the player see on a 2D map.
When I test the code by moving all around the map and testing differents position, it's working. The next step was to create a heatmap of the numbers of position the player can see for every positions. But when I try to run the code, it return false positions.
I've tried different local and global transform methods, and it didn't work
void HtPnt (Vector2 g)
{
int pnt = 0;
foreach (Vector2 f in Q)
{
bool compt = false;
for (int i = 0; i < criticalPointDebug.Length - 1; i++)
{
if (IsInsideTriangle(f, g, criticalPointDebug[i].location, criticalPointDebug[i + 1].location))
compt = true;
}
if (compt == true)
pnt += 1;
}
}
// criticalPointDebug is the list of the visibility polygon, and here's my test in update() :
if (Input.GetKeyDown(KeyCode.A))
{
for (int x=0;x<5;x++)
{
Debug.Log(position);
transform.position = new Vector2(5 + .5f * x, 4);
HtPnt(position);
Debug.Log(position);
}
}
if (Input.GetKeyDown(KeyCode.Z))
{
Debug.Log(position);
transform.position = new Vector2(5,4);
position = new Vector2(5, 4);
HtPnt(position);
Debug.Log(position);
}
first test is returning the correct value of the position on the player (before runing the test) 5 times and the second return nonsense result. Thanks in advance for enlighting me on my problem !

Enemies in a list: issue with colliding the enemies with the other enemies

I'm currently working on a game in which a list of enemies is spawned into the level randomly on the x-axis using a list up to the point of the maximum number to spawn (currently set at 10) I have a large portion of the game working however I currently have the problem that the enemies can overlap/stack on top of each other.
What I want is if the collision box of the enemy is intersecting another enemy in the list, for it to no overlap. (if that makes sense) this is the code I have for it so far.
foreach (EnemyClass enemy1 in enemies)
{
if (enemy1.position.X < myPlayer.position.X - 10 && !enemy1.stopMoving)
{
enemy1.position.X += enemy1.amountToMove;
}
if (enemy1.position.X > myPlayer.position.X - 50 && !enemy1.stopMoving)
{
enemy1.position.X -= enemy1.amountToMove;
}
foreach (EnemyClass enemy2 in enemies)
{
if (enemy1 != enemy2 && enemy1.collisionBox.Intersects(enemy2.collisionBox))
{
System.Console.WriteLine("Collision Detected");
}
}
enemy1.collisionX = (int)enemy1.position.X;
enemy1.Update(graphics.GraphicsDevice);
}
Any help would be greatly appreciated.
EDIT: What I missed off was that they technically do collide, however they collide when they are 1000 pixels apart, when the sprite is actually 50 pixels wide.
EDIT: This is the changed collision code..
foreach (EnemyClass enemy in enemies)
{
enemy.collisionX = (int)enemy.position.X;
if (enemy.position.X < myPlayer.position.X - 10 && !enemy.stopMoving)
{
enemy.position.X += enemy.amountToMove;
}
if (enemy.position.X > myPlayer.position.X - 50 && !enemy.stopMoving)
{
enemy.position.X -= enemy.amountToMove;
}
for (int i = 0; i < enemies.Count; i++)
{
for (int j = i + 1; j < enemies.Count; j++)
{
if (enemies[i].collisionBox.Intersects(enemies[i].collisionBox))
{
System.Console.WriteLine("Collision Detected");
}
}
}
enemy.Update(graphics.GraphicsDevice);
}
EDIT: Here is the EnemyClass, which is where the collisionBox is contained:
public EnemyClass(Texture2D newTexture, Vector2 newPosition)
{
texture = newTexture;
position = newPosition;
collisionBox = new Rectangle((int)position.X, (int)position.Y, texture.Width, texture.Height);
randX = random.Next(1, 3);
speed = new Vector2(randX, 0);
}
public void Update(GraphicsDevice graphics)
{
collisionBox = new Rectangle((int)position.X, (int)position.Y, texture.Width, texture.Height);
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(texture, position, Color.White);
}
It seems that the collision is being detected before all of the collision boxes have been updated.
My suggestions for how to order things:
Define the collision box in terms of position.X and position.Y so
that it always accurately reflects the correct position.
Update all the enemies x- and y-positions.
Execute the snippet below after all the enemy positions have been updated.
foreach (EnemyClass enemy in enemies)
{
if (enemy.position.X < myPlayer.position.X - 10 && !enemy.stopMoving)
{
enemy.position.X += enemy.amountToMove;
}
if (enemy.position.X > myPlayer.position.X - 50 && !enemy.stopMoving)
{
enemy.position.X -= enemy.amountToMove;
}
enemy.Update(graphics.GraphicsDevice);
}
for (int i = 0; i < enemies.Count; i++)
{
for (int j = i + 1; j < enemies.Count; j++)
{
if (enemies[i].collisionBox.Intersects(enemies[j].collisionBox))
{
System.Console.WriteLine("Collision Detected");
}
}
}
The above code also has an advantage in not detecting the same collision twice (i.e. enemy1/enemy2 and enemy2/enemy1).

Space Invaders stick to wall

So I have to develop an XNA game for school and I thought Space Invaders was a nice thing to make. But now I'm stuck with a problem. I have a List filled with 'Aliens' from another class. And every second it moves 20 px to the right and I want it to descend when it touches the right side of the screen (not the problem). But I want it to move the opposite direction (to the left) as soon as it touches the wall. Here lies the problem.
As soon as it touches the wall it still moves 20px once, changes direction, moves 20px back and changes direction again. This keeps repeating.
So where lies the problem. I think it has to check earlier if one of the aliens touches the screen but don't know how to accomplish that and thought maybe one of you could help me out because it is very frustrating!
I included the update method
if (xPos % step == 0)
{
if (!isDescending)
{
for (int k = 0; k < sprites.Count; k++)
{
Sprite sprite = sprites[k];
if (touchedRight) sprite.position.X += step;
else sprite.position.X -= step;
}
for (int k = 0; k < sprites.Count; k++)
{
Sprite sprite = sprites[k];
bool hitLeft = sprite.position.X == 0;
bool hitRight = sprite.rect.Right == screenWidth;
if ((hitLeft) || (hitRight))
{
touchedRight = !touchedRight;
isDescending = true;
}
}
}
else
{
isDescending = false;
for (int k = 0; k < sprites.Count; k++)
{
sprites[k].position.Y += sprites[k].rect.Height;
}
}
}
// CheckCollision(alienBounds, k-1);
// sprite.rect = new Rectangle((int)sprite.position.X, (int)sprite.position.Y, 20, 20);
// alienBounds = sprite.rect;
xPos += 1;
Well, this is wrong:
if ((hitLeft) || (hitRight))
When moving right, you only care about hitRight. And when moving left, you care only about hitLeft.
So try
if (touchedRight? hitLeft: hitRight)
This will also fix the issue where multiple aliens can hit the wall at the same time.
Im not sure but I bellieve in Space invaders, if you kill all columns but the most left, it doesnt move completely to the right side of the screen before switching its direction.
So what I suggest is you create a derivate of a Sprite called Invader:
class Invader : Sprite
{
readonly float distanceFromCenterHorizontal;
public Invader(float distanceFromCenterHorizontal)
{
this.distanceFromCenterHorizontal= distanceFromCenterHorizontal;
}
public void UpdatePosition(Point center)
{
this.Position.X = center.X + distanceFromCenterHorizontal;
this.Position.Y = center.Y;
}
}
// global variables:
Point center = ....
bool moveLeft = false;
void createInvaders()
{
int invaderWidth = 20; // whatever your value is
float columnCenter = ((float) columnMax) / 2 * invaderWidth;
for(var column = 0; column < columnMax ; column++)
{
float invaderX = (column * invaderWidth);
float distanceFromCenterHorizontal = invaderX - columnCenter;
for(var row = 0; row < rowMax; row ++)
{
Invader invader = new Invader(distanceFromCenterHorizontal);
invader.Position.Y = row * invaderHeight // Needs adjustment here!
}
}
}
void update()
{
if(center.X == screenWidth) moveLeft = true;
else if(center.X <= 0) moveLeft = false;
center.X += moveLeft? -step : step;
foreach(var invader in invaders) invader.UpdatePosition(center);
}
You want to move the "centerPoint" and all Invaders update According to the new Center.
Im not sure if XNA uses double or float values, so youmight need to adjust this.
You can then easily expand this to create some up / down wobbling.
So i made a new rectangle called 'bounds' which is as wide as the aliens and moves the same as the aliens do. And got it working! (half of it). It moves to the right until it touches the right side, then it goes to the left but keeps going to the left and never goes back to the right. So where am i going wrong this time?
if (xPos % step == 0)
{
if (!isDescending)
{
for (int k = 0; k < sprites.Count; k++)
{
Sprite sprite = sprites[k];
sprite.position.X += step;
}
if (bounds.Right == screenWidth || bounds.Left == 0)
{
touchedRight = !touchedRight;
step *= -1;
}
if (!touchedRight) bounds.X += step;
if (touchedRight) bounds.X -= step;
// for (int k = 0; k < sprites.Count; k++)
// {
// Sprite sprite = sprites[k];
// bool hitLeft = sprite.position.X == 0;
// bool hitRight = sprite.rect.Right == screenWidth;
// if ((hitLeft) || (hitRight))
// {
// touchedRight = !touchedRight;
// isDescending = true;
// }
// }
}

Platform jumping problems with AABB collisions

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.

Categories

Resources