Balls phasing through each other in physics engine

75 Views Asked by At

I'm trying to write a simple physics engine in C++; it's been going well so far, and all of the functions work as intended except for the physics between the balls themselves. During collisions, the balls will sometimes just phase through eachother.

#include <iostream>
#include <stdio.h>
#include <raylib.h>
#include <raymath.h>
#include <stdlib.h>
#include <time.h>
#include <chrono>
#include <list>

using namespace std;

class Ball {
public:
    Vector2 position;
    Vector2 velocity;
    float radius;
    Color color;
};

void updateBall(Ball &ball, float gravity, float frictionCoefficient, float deltaTime, float screenWidth, float screenHeight, float bounciness, std::list<Ball> &balls) {

    ball.velocity.y -= gravity;

    // Apply friction if the ball is at the top or bottom of the screen
    ball.velocity.x *= (ball.position.y >= screenHeight - ball.radius || ball.position.y <= ball.radius) ? frictionCoefficient : 1;

    // Update the ball's position based on its velocity
    ball.position.x += ball.velocity.x * deltaTime;
    ball.position.y += -ball.velocity.y * deltaTime;

    // Clamp the ball's position within the screen bounds
    ball.position.x = Clamp(ball.position.x, ball.radius, screenWidth - ball.radius);
    ball.position.y = Clamp(ball.position.y, ball.radius, screenHeight - ball.radius);

    // Check for collisions with screen edges and apply bounciness
    ball.velocity.x *= (ball.position.x == ball.radius || ball.position.x == screenWidth - ball.radius) ? -bounciness : 1;
    ball.velocity.y *= (ball.position.y == ball.radius || ball.position.y == screenHeight - ball.radius) ? -bounciness : 1;

    for (auto &otherBall : balls) {
        // Avoid checking the ball against itself
        if (&otherBall == &ball) {
            continue;
        }

        // Calculate the distance between the two balls
        float distance = Vector2Distance(ball.position, otherBall.position);

        // Check if a collision has occurred
        if (distance < (ball.radius + otherBall.radius)) {
            ball.velocity.x = -(ball.velocity.x);
            otherBall.velocity.x = -(otherBall.velocity.x);
            ball.velocity.y = -(ball.velocity.y);
            otherBall.velocity.y = -(otherBall.velocity.y);
        }
    }
}

Ball newBall(Vector2 position, Vector2 velocity, float radius, Color color) {
    Ball tempBall;
    tempBall.position = position;
    tempBall.velocity = velocity;
    tempBall.radius = radius;
    tempBall.color = color;
    return tempBall;
}

int main() {
    SetConfigFlags(FLAG_WINDOW_RESIZABLE);

    int screenWidth = 900;
    int screenHeight = 900;

    InitWindow(screenWidth, screenHeight, "cppPhysics");
    SetTargetFPS(120);

    const float gravity = 9.81f;
    const float frictionCoefficient = 0.981f;
    const float bounciness = 0.7f;

    Ball ballOne = newBall({100, 100}, {400, -450}, 60, WHITE);
    Ball ballTwo = newBall({50, 200}, {400, 450}, 45, BLUE);
    Ball ballThree = newBall({200, 50}, {300, -450}, 20, YELLOW);

    std::list<Ball> balls;
    balls.push_back(ballOne);
    balls.push_back(ballTwo);
    balls.push_back(ballThree);

    while (!WindowShouldClose()) {
        if (IsWindowResized()) {
            screenWidth = GetScreenWidth();
            screenHeight = GetScreenHeight();
        }
        float deltaTime = GetFrameTime();

        for (auto &ball : balls) {
            updateBall(ball, gravity, frictionCoefficient, deltaTime, screenWidth, screenHeight, bounciness, balls);
        }

        if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
            for (auto &ball : balls) {
                ball.velocity.y += 500;
                ball.velocity.x += 500;
            }
        }

        BeginDrawing();
        ClearBackground(BLACK);

        for (auto &ball : balls) {
            DrawCircleV(ball.position, ball.radius, ball.color);
        }

        EndDrawing();
    }

    CloseWindow();
    return 0;
}

Note: The code here is shortened; the actual program i'm making uses randomized values for the mouseclick and color values.

Optimally, they would bounce away from eachother (kind of like how they do off the ground). I've already tried moving the code to the start of the function, and making the function break if a collision is detected.

1

There are 1 best solutions below

2
On

You have quantum-jumping balls:

float deltaTime = GetFrameTime();
updateBall(ball, gravity, frictionCoefficient, deltaTime, screenWidth, screenHeight, bounciness, balls);

ball.position.x += ball.velocity.x * deltaTime;
ball.position.y += -ball.velocity.y * deltaTime;

Before digging any deeper, you should strongly consider switching to fixed-delta physics:

accumulatedDelta += GetFrameTime(); // accumulatedDelta is a member value
while(accumulatedDelta > someConstant)
{
    updateBall(ball, gravity, frictionCoefficient, someConstant, screenWidth, screenHeight, bounciness, balls);  
    accumulatedDelta -= someConstant;
}

That will rid you of most phasing. It will make your physics less frame-duration-dependent and if you ever get to multiplayer, your synchronisation and determinism will be some much easier to deal with.

You'll need to pick someConstant with care. Too small, you'll do too much physics. Too large and you get phasing and/or balls that don't update every frame. I like 1 hundredth of a second, unless my frame rate is very high.

Edit: And there's a second problem. Bigger one :-)

    if (distance < (ball.radius + otherBall.radius)) 
    {
        // flip direction

Once two balls touch, after you flip the direction, they may still touch. Then, at the next frame, you will flip the direction again, making the ball continue. You need to check if ball A intersect ball B and ball A's velocity is toward ball B before flipping ball A's direction.

I don't have the time to run and debug your code, but, approximately, something like:

// Calculate the distance between the two balls
float distance = Vector2Distance(ball.position, otherBall.position);

// Check if a collision has occurred
if (distance < (ball.radius + otherBall.radius)) 
{
    distance2 = Vector2Distance(ball.position - ball.velocity, otherBall.position); // hopefully you overloaded Vector2 operator+ and - ?

    if (distance2 > distance) // if the velocity flipped would make us further
    {

        ball.velocity.x = -(ball.velocity.x);
        otherBall.velocity.x = -(otherBall.velocity.x);
        ball.velocity.y = -(ball.velocity.y);
        otherBall.velocity.y = -(otherBall.velocity.y);
    }
}