-1

I have been learning c# by making a basic ball bouncing simulation, kind of like the windows screensaver with the bubbles.
I have two balls bouncing around the screen, but when they collide they disappear, and I am not sure why.

I have done debugging using 'Console.WriteLine(value)', and found most values are equating to infinity after the collision.

I ended up scrapping that code, but need a better solution for ball collisions.

** NOTE ** This won't always be just two balls bouncing around a screen, this is just me trying to learn collisions ** NOTE **

Anyone with knowledge on Verlet Integration would be greatly appreciated, because I am very confused.

Here is some of my code & the version of C# I am using:

Screenshot from replit showing c# version

//+++ = I don't know what this is, a yt tutoriaol told me to use it
using System; 
using System.Collections.Generic; //+++
using System.ComponentModel; //+++
using System.Data; //+++
using System.Drawing;
using System.Linq; //+++
using System.Text; //+++
using System.Threading.Tasks; //+++
using System.Windows.Forms; // This doesn't work in standard c#, only in mono for some reason.

public class Form1 : Form
{
    float screenWidth;
    float screenHeight;
    float xpa = 0;
    float ypa = 0;
    float xva = 2;
    float yva = 2;
    float xpb; //later this is set to the width of the form minus the width of the ellipse this is marking the position of
    float ypb; //later this is set to the height of the form, minus the height of the ellipse this is marking the position of
    float xvb = -2;
    float yvb = -2;
//...Unimportant code here...\\
        var refreshTimer = new Timer();
        refreshTimer.Interval = 1;
        refreshTimer.Tick += new EventHandler(refreshTimer_Tick);
        refreshTimer.Start();
    }
//...Unimportant code here...\\

    private void refreshTimer_Tick(object sender, EventArgs e)
    {
        this.Invalidate();
    }

    private void Form1_Paint(object sender, PaintEventArgs e)
    {
        Graphics g = e.Graphics;
//...Unimportant code here...\\     
//Both ellipses bounce when in contact with the wall
//Ellipse A is located at coords (xpa, ypa) with width and height 50
//Ellipse A is located at coords (xpb, ypb) with width and height 50
        
        //Collisions between A & B
        
        float dx = (xpb + 25) - (xpa + 25);
        float dy = (ypb + 25) - (ypa + 25);
        float distance = (float)Math.Sqrt(dx * dx + dy * dy);
        
        if (distance <= 50) 
        {
            float angle = (float)Math.Atan2(dy, dx);
            float sin = (float)Math.Sin(angle);
            float cos = (float)Math.Cos(angle);
        
        }
    }
//...Rest of Code...\\

Does anyone know about Verlet Integration or any other techniques that could help me?

  • In the method `elasticCollision` you're dividing by `va[0] + vb[0]` if this is zero you'll get a divide by zero exception, if it's very small then the resulting velocity will be very large. Try debugging this to see what values you get. – phuzi Mar 09 '23 at 13:06
  • Console.WriteLine(vFinalA[0]); //Infinity Console.WriteLine(vFinalB[0]); //Infinity – Schrödingers Capybara Mar 09 '23 at 13:12
  • FYI - Consider ditching the `Timer` and [implement a game loop](https://gamedev.stackexchange.com/a/67652/6913) using `Application.Idle`. You will get much higher frame rates which makes the animations that much smoother. – John Alexiou Mar 13 '23 at 17:49
  • I am using the timer like I am so I can control the framerate, as I don't want it moving super fast. – Schrödingers Capybara Mar 13 '23 at 17:58
  • Note that verlett integration has nothing to do with the collision logic. – John Alexiou Mar 13 '23 at 18:15
  • Oh, Thanks for that, I didn't know that. All i knew was it related to physics handeling. @JohnAlexiou – Schrödingers Capybara Mar 13 '23 at 20:14
  • @SchrödingersCapybara - Verlette integration (as well as Euler integraton and Runge-Kutta integration) advance the time of the simulation by updating new positions and velocities from the current values and any applied forces. Contact handling happens **at a single instant** in time and its is a modification of the velocities of objects such that they won't go into each other at the next time step. – John Alexiou Mar 13 '23 at 20:25

2 Answers2

2

I simplified the code a lot by adding a Ball class and using the Vector2 Struct from the System.Numerics Namespace (I have included a minimal implementation for mono below). Vector2 contains useful methods and operators for vector math. E.g., you can add two vectors with Vector2 result = v1 + v2.

The Ball class wraps all the state of a ball and some methods like CollideWithWall. The advantage is that we have to write this code only once for all the balls. Ball now stores the center coordinates of the ball, not the top left position. This makes it easier to reason about it. It also stores the radius, allowing us to have balls of different radii.

For the collision I found a working solution from the user mmcdole. I adapted it to C# and your simulation. But the core of your simulation, the integration of the speeds to get the motion, remains the same.

public class Ball
{
    public Brush Brush { get; set; }
    public Vector2 Center { get; set; }
    public Vector2 Velocity { get; set; }
    public float Radius { get; set; }

    // Make mass proportional to the area of the circle
    public float Mass => Radius * Radius;

    public void Move()
    {
        Center += Velocity;
    }

    public void CollideWithWall(Rectangle wall)
    {
        // Only reverse velocity if moving towards the walls

        if (Center.X + Radius >= wall.Right && Velocity.X > 0 || Center.X - Radius < 0 && Velocity.X < 0) {
            Velocity = new Vector2(-Velocity.X, Velocity.Y);
        }
        if (Center.Y + Radius >= wall.Bottom && Velocity.Y > 0 || Center.Y - Radius < 0 && Velocity.Y < 0) {
            Velocity = new Vector2(Velocity.X, -Velocity.Y);
        }
    }

    public void CollideWith(Ball other)
    {
        // From: https://stackoverflow.com/q/345838/880990, author: mmcdole
        Vector2 delta = Center - other.Center;
        float d = delta.Length();
        if (d <= Radius + other.Radius && d > 1e-5) {
            // Minimum translation distance to push balls apart after intersecting
            Vector2 mtd = delta * ((Radius + other.Radius - d) / d);

            // Resolve intersection - inverse mass quantities
            float im1 = 1 / Mass;
            float im2 = 1 / other.Mass;

            // Push-pull them apart based off their mass
            Center += mtd * (im1 / (im1 + im2));
            other.Center -= mtd * (im2 / (im1 + im2));

            // Impact speed
            Vector2 v = Velocity - other.Velocity;
            Vector2 mtdNormalized = Vector2.Normalize(mtd);
            float vn = Vector2.Dot(v, mtdNormalized);

            // Sphere intersecting but moving away from each other already
            if (vn > 0.0f) return;

            // Collision impulse
            const float Restitution = 1.0f; //  perfectly elastic collision

            float i = -(1.0f + Restitution) * vn / (im1 + im2);
            Vector2 impulse = mtdNormalized * i;

            // Change in momentum
            Velocity += impulse * im1;
            other.Velocity -= impulse * im2;
        }
    }

    public void Draw(Graphics g)
    {
        g.FillEllipse(Brush, Center.X - Radius, Center.Y - Radius, 2 * Radius, 2 * Radius);
    }
}

then we can initialize the form with (in Form1)

Ball a = new Ball() {
    Brush = Brushes.Red,
    Center = new Vector2(),
    Velocity = new Vector2(2, 2),
    Radius = 25
};
Ball b = new Ball() {
    Brush = Brushes.Blue,
    Center = new Vector2(),
    Velocity = new Vector2(-2, -2),
    Radius = 40
};

public Form1()
{
    InitializeComponent();
    DoubleBuffered = true;
    Load += Form1_Load; ;
    Paint += Form1_Paint;

    var refreshTimer = new System.Windows.Forms.Timer {
        Interval = 1
    };
    refreshTimer.Tick += RefreshTimer_Tick;
    refreshTimer.Start();
}

void Form1_Load(object sender, EventArgs e)
{
    WindowState = FormWindowState.Normal;
    System.Diagnostics.Debug.WriteLine(Width);
    b.Center = new Vector2(Width - 60, Height - 60);
}

private void RefreshTimer_Tick(object sender, EventArgs e)
{
    Invalidate();
}

Our Paint method now looks like this:

private void Form1_Paint(object sender, PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.LightBlue, ClientRectangle);

    a.Draw(g);
    b.Draw(g);

    a.Move();
    b.Move();

    a.CollideWithWall(ClientRectangle);
    b.CollideWithWall(ClientRectangle);

    a.CollideWith(b);
}

If you want to change the form properties in the forms designer, then you must also call InitializeComponent(); in the form's constructor.


EDIT:

Since you are using mono that does not have the Vextor2 struct, here is a minimal version of it, implementing only the required stuff for the code above:

public struct Vector2
{
    public float X;
    public float Y;

    public Vector2(float x, float y)
    {
        X = x;
        Y = y;
    }

    public static Vector2 operator +(Vector2 left, Vector2 right)
    {
        return new Vector2(left.X + right.X, left.Y + right.Y);
    }

    public static Vector2 operator -(Vector2 left, Vector2 right)
    {
        return new Vector2(left.X - right.X, left.Y - right.Y);
    }

    public static Vector2 operator *(Vector2 left, Vector2 right)
    {
        return new Vector2(left.X * right.X, left.Y * right.Y);
    }

    public static Vector2 operator *(float left, Vector2 right)
    {
        return new Vector2(left * right.X, left * right.Y);
    }

    public static Vector2 operator *(Vector2 left, float right)
    {
        return new Vector2(left.X * right, left.Y * right);
    }

    public static float Dot(Vector2 value1, Vector2 value2)
    {
        return value1.X * value2.X + value1.Y * value2.Y;
    }

    public static Vector2 Normalize(Vector2 value)
    {
        float d = MathF.Sqrt(value.X * value.X + value.Y * value.Y);
        if (d < 1e-10) {
            return value;
        }
        float invNorm = 1.0f / d;
        return new Vector2(value.X * invNorm, value.Y * invNorm);
    }

    public float Length()
    {
        return MathF.Sqrt(X * X + Y * Y);
    }
}

Explanation

I am not going to explain the collision itself. Follow the link to mmcdole's code for this.

You are using a lot of variables like xpa, ypa, xva, yva, xpb, ypb, xvb, yvb. Most of the changes I've made are to reduce the number of variables and avoid code duplication.

For instance we have float xpa and float ypa storing the position of object a. The Vector2 type stores both coordinates in its X and Y fields and requires only one variable. It also contains methods and operator overloads that allow performing arithmetic operations on them.

Example:

// Old code with individual floats
float xpa = 0;
float ypa = 0;
float xva = 2;
float yva = 2;
...
xpa += xva;
ypa += yva;
// New code with Vector2
Vector2 pa = new Vector2(0, 0);
Vector2 va = new Vector2(2, 2);
...
pa += va;

Another problem is that a lot of code is duplicated because it must be applied to the <whatever>a variables and the <whatever>b variables. Especially in the Form1_Paint method.

The idea is to wrap all the variables belonging to a ball in a Ball object (declared as class). Inside this object the variables (or properties with { get; set; }) have the same name, no matter whether the object represents the a ball or the b ball.

Methods inside this Ball class now work with the object's properties.

Example:

public void Draw(Graphics g)
{
    g.FillEllipse(Brush, Center.X - Radius, Center.Y - Radius, 2 * Radius, 2 * Radius);
}

It uses the Brush, Center and Radius properties of the object. I decided to store the color of the ball as Brush, since FillEllipse requires a brush.

From outside, if we have two balls called a and b, we can draw them with the calls:

a.Draw(g);
b.Draw(g);

One code duplication eliminated! The same applies to Move, CollideWithWall and CollideWith (colliding with another ball).

This code works like yours, except for the ball-ball collision.

See also:

Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • The wall collisions is working fine, I just need to get ball-ball collisions working. But thanks for this code, I will use this. The issue is that when the two balls touch, something breaks and they reflect with a velocity of infinity. – Schrödingers Capybara Mar 09 '23 at 13:59
  • This won't work, because in Mono System.Numerics doesn't work. – Schrödingers Capybara Mar 10 '23 at 17:32
  • You can see the [source of `Vector2` here](https://referencesource.microsoft.com/#System.Numerics/System/Numerics/Vector2.cs) and copy the relevant parts or even create your own vector type. This is better than using `float[]` because it is more readable you can add it some logic. Btw. `System.Windows.Forms` should work in .NET 7.0. I am running the example above in .NET 7.0 / Windows 11 – Olivier Jacot-Descombes Mar 10 '23 at 17:45
  • I am not sure that it is .NET, I am using replit's C#. I will post an image in the question if possible. – Schrödingers Capybara Mar 10 '23 at 19:00
  • Okay, since you are using mono (I added the `mono` tag to your question), I downgraded my solution to C# 6.0 / .NET 4.5.2 and also added the `MathF.Sqrt` function not included here, but mono has it. This should work on mono now. – Olivier Jacot-Descombes Mar 11 '23 at 13:45
  • After some research, I am using .NET. I found this out by looking in the console at run time and when I press run it posts "dotnet run" in the console. Also, your code is very confusing (to me, I am a beginner and am just learning c#). Would it be possible for you to break it down to the basics, as this project is a learning experience. – Schrödingers Capybara Mar 13 '23 at 15:47
  • I added an explanation section. You will find much more information on OOP, classes and structs online. – Olivier Jacot-Descombes Mar 13 '23 at 17:25
  • I can't yet up-vote, but your answer deserves it. Thank you for being so helpful and walking me all the way through it. – Schrödingers Capybara Mar 13 '23 at 17:54
  • Where in the code would I handle changing of velocities due to external forces such as gravity, friction, drag, wind resistance, etc.? – Schrödingers Capybara Mar 13 '23 at 20:17
  • I found an error in your code, when `Ball a = new Ball() { Brush = Brushes.Blue, Center = new Vector2(100, 135), Velocity = new Vector2(2, 0), Radius = 40 };` meets `Ball c = new Ball() { Brush = Brushes.Green, Center = new Vector2(300, 135), Velocity = new Vector2(-1, 0), Radius = 10 };` Both balls a and c vanish. It seems to happen any time the y-vel of both is 0, and the x-vel of both dont have the same absolute value (2 and -2 work, but 2 and -1 dont) – Schrödingers Capybara Mar 13 '23 at 21:16
  • Gravity is an acceleration and changes the velocity. So you would have to add a vector (0, g) to the velocity. Wind resistance is proportional to the relative speed squared and would be added as vector to the velocity as well. Friction is a force which independent of speed and acts tangentially to the contact surface and slows down the motion. See also: [Adding air drag to a golf ball trajectory equation](https://gamedev.stackexchange.com/questions/11119/adding-air-drag-to-a-golf-ball-trajectory-equation). – Olivier Jacot-Descombes Mar 14 '23 at 13:17
  • The error is in Vector2.Normalize. We might get a division by 0. Change it to: `public static Vector2 Normalize(Vector2 value) { float d = MathF.Sqrt(value.X * value.X + value.Y * value.Y); if (d < 1e-10) { return value; } float invNorm = 1.0f / d; return new Vector2(value.X * invNorm, value.Y * invNorm); }` – Olivier Jacot-Descombes Mar 14 '23 at 13:32
1

Here is a generalized solution which adds some missing things to my first answer.

It adds two new methods to Vector2:

public float LengthSquared()
{
    return X * X + Y * Y;
}

public override string ToString() => $"Vector2({X:n3}, {Y:n3})";

LengthSquared will be used to calculate air drag. Overriding ToString simplifies debugging.

I replaced the two distinct fields for the balls by one array of balls, allowing us to easily add more balls to the game.

private readonly Ball[] _balls = new Ball[]{
    new Ball() {
        Brush = Brushes.Red,
        Center = new Vector2(100, 135),
        Velocity = new Vector2(10, 0),
        Radius = 40
    },
    new Ball() {
        Brush = Brushes.Blue,
        Center = new Vector2(300, 135.1f),
        Velocity = new Vector2(-4, 0),
        Radius = 15
    },
    new Ball() {
        Brush = Brushes.Green,
        Center = new Vector2(30, 20),
        Velocity = new Vector2(5, 3),
        Radius = 20
    }
};

Form1_Paint can now handle all the balls in a loop. This is a simplification over the first solution where we have to handle each ball individually. We also add friction on the ground, air drag and gravity. For this we declare additional fields and constants in the form:

private const float C = 0.1f; // Air drag coefficient (0 = no drag)
private const float Friction = 0.95f; // Friction coefficient (1 = no friction)

private readonly Vector2 _gravity = new Vector2(0, 0.1f);

The paint method:

private void Form1_Paint(object sender, PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.Clear(Color.LightBlue);

    for (int i = 0; i < _balls.Length; i++) {
        Ball ball = _balls[i];
        ball.Draw(g);

        if (ball.Center.Y + ball.Radius >= ClientRectangle.Bottom) {
            // On ground. Friction with ground
            ball.Velocity *= Friction;
        } else {
            // Accelerate downwards only when not on ground.
            ball.Velocity += _gravity;
        }

        // Air Drag
        float v2 = ball.Velocity.LengthSquared();
        float vAbs = MathF.Sqrt(v2);
        float fDrag = C * v2;
        Vector2 f = fDrag / vAbs * ball.Velocity;
        ball.Velocity -= 1 / ball.Mass * f;

        ball.CollideWithWall(ClientRectangle);

        // Start at i + 1 to handle collision of each pair of balls only once.
        for (int j = i + 1; j < _balls.Length; j++) {
            ball.CollideWith(_balls[j]);
        }

        ball.Move();
    }
}

I also added a bit of damping when colliding with walls (in class Ball):

public void CollideWithWall(Rectangle wall)
{
    const float damping = 0.99f; // 1 = no damping

    // Only reverse velocity if moving towards the walls

    if (Center.X + Radius >= wall.Right && Velocity.X > 0 || Center.X - Radius < 0 && Velocity.X < 0) {
        Velocity = new Vector2(-damping * Velocity.X, Velocity.Y);
    }
    if (Center.Y + Radius >= wall.Bottom && Velocity.Y > 0 || Center.Y - Radius < 0 && Velocity.Y < 0) {
        Velocity = new Vector2(Velocity.X, -damping * Velocity.Y);
    }
}

Now, because of air drag, friction with the ground and damping when bouncing off a wall, the balls will come to a still after a while.

Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188