Reputation: 148
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;
}
}
Upvotes: 0
Views: 211
Reputation:
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.
Upvotes: 1