I have been working on this simple physics system for a while and i originaly started with euler physics but couldn’t manage to get stable collisions working and after switching to verlet integration i still cant get anything stable. It works completely fine when there is not enough balls to stack on top of each other but as soon as i add more balls and they land ontop of eachother they start flying off and its chaotic. you can see the full code and just the specific collision function below and im using C++ and sfml.
// Collision check
void checkCollisions(std::vector<Ball>& balls, std::vector<std::vector<std::vector<Ball*>>>& grid, int gridWidth, int gridHeight, int cellSize) {
const float damping = 0.8f;
const float epsilon = 0.001f;
const float separationFactor = 1.01f; // Slight increase in separation
const int collisionIterations = 5; // Number of iterations for collision resolution
int cellX = static_cast<int>(position.x) / cellSize;
int cellY = static_cast<int>(position.y) / cellSize;
for (int iteration = 0; iteration < collisionIterations; ++iteration) {
// Check neighboring cells for potential collisions
for (int x = std::max(0, cellX - 1); x <= std::min(gridWidth - 1, cellX + 1); ++x) {
for (int y = std::max(0, cellY - 1); y <= std::min(gridHeight - 1, cellY + 1); ++y) {
for (auto* otherBall : grid[x][y]) {
if (otherBall == this) continue;
float minDist = radius + otherBall->radius;
float dx = position.x - otherBall->position.x;
float dy = position.y - otherBall->position.y;
float distance = std::sqrt(dx * dx + dy * dy);
if (distance < minDist && distance > epsilon) {
float nx = dx / distance;
float ny = dy / distance;
// Calculate relative velocity
float relativeVelocityX = (position.x - oldPosition.x) - (otherBall->position.x - otherBall->oldPosition.x);
float relativeVelocityY = (position.y - oldPosition.y) - (otherBall->position.y - otherBall->oldPosition.y);
float relativeVelocityDotNormal = relativeVelocityX * nx + relativeVelocityY * ny;
// Only proceed with collision resolution if balls are moving towards each other
if (relativeVelocityDotNormal < 0) {
float overlap = minDist - distance;
float moveDistance = overlap / 2.0f * separationFactor; // Slight increase in separation
// Move balls apart along the collision normal
position.x += nx * moveDistance;
position.y += ny * moveDistance;
otherBall->position.x -= nx * moveDistance;
otherBall->position.y -= ny * moveDistance;
// Calculate velocities
float v1x = position.x - oldPosition.x;
float v1y = position.y - oldPosition.y;
float v2x = otherBall->position.x - otherBall->oldPosition.x;
float v2y = otherBall->position.y - otherBall->oldPosition.y;
// Calculate normal velocity
float vn = (v2x - v1x) * nx + (v2y - v1y) * ny;
// Calculate impulse scalar
float impulse = (2.0f * vn) / (1.0f + 1.0f); // Assuming equal mass balls
// Apply impulse to change velocities
oldPosition.x -= impulse * nx * damping;
oldPosition.y -= impulse * ny * damping;
otherBall->oldPosition.x += impulse * nx * damping;
otherBall->oldPosition.y += impulse * ny * damping;
}
}
else if (distance <= epsilon) {
// Randomly perturb positions to prevent sticking
float perturbation = minDist - epsilon;
float angle = static_cast<float>(rand()) / RAND_MAX * 2.0f * M_PI;
position.x += std::cos(angle) * perturbation / 2.0f;
position.y += std::sin(angle) * perturbation / 2.0f;
otherBall->position.x -= std::cos(angle) * perturbation / 2.0f;
otherBall->position.y -= std::sin(angle) * perturbation / 2.0f;
}
}
}
}
}
}
and here is the full code just incase:
#include <cmath>
#include <SFML/Graphics.hpp>
#include <vector>
#include <iostream>
#include <cstdlib> // for rand()
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
// Define the Vector2 class
class Vector2 {
public:
float x;
float y;
// Add a default constructor
Vector2() : x(0), y(0) {}
// Add a constructor that takes x and y as arguments
Vector2(float x, float y) : x(x), y(y) {}
// Define vector operations
Vector2 operator+(const Vector2& other) const {
Vector2 result;
result.x = x + other.x;
result.y = y + other.y;
return result;
}
Vector2 operator-(const Vector2& other) const {
Vector2 result;
result.x = x - other.x;
result.y = y - other.y;
return result;
}
Vector2 operator*(float scalar) const {
Vector2 result;
result.x = x * scalar;
result.y = y * scalar;
return result;
}
Vector2 operator/(float scalar) const {
Vector2 result;
result.x = x / scalar;
result.y = y / scalar;
return result;
}
float length() const {
return std::sqrt(x * x + y * y);
}
Vector2 normalize() const {
float len = length();
if (len > 0) {
return Vector2(x / len, y / len);
}
return *this;
}
float dot(const Vector2& other) const {
return x * other.x + y * other.y;
}
};
// Ball class
class Ball {
private:
sf::CircleShape shape;
Vector2 oldPosition;
Vector2 acceleration;
Vector2 gravity = Vector2(0, 980);
float radius;
public:
Vector2 position;
Ball(float radius, sf::Color color, float x, float y)
: radius(radius), position(x, y), oldPosition(x, y), acceleration(0, 0) {
shape.setRadius(radius);
shape.setFillColor(color);
shape.setPosition(x - radius, y - radius);
}
void updatePhysics(float dt) {
gravity.y = 980;
Vector2 velocity = (position - oldPosition) / dt; // Calculate velocity
oldPosition = position;
position = position + velocity * dt + acceleration * (0.5f * dt * dt);
acceleration = gravity; // Apply gravity
// Threshold values
const float restThreshold = 0.1f; // Lower threshold for resting
const float minMovement = 0.01f; // Minimum movement threshold
// Prevent jittering when at rest at bottom of window
if (std::abs(velocity.y) < restThreshold && position.y + radius >= 600) {
position.y = 600 - radius;
acceleration.y = 0;
gravity.y = 0;
oldPosition.y = position.y;
}
// Bounce off the top of the window
if (position.y - radius < 0) {
position.y = radius;
velocity.y = -velocity.y * 0.9f; // Dampen the bounce and invert the velocity
oldPosition.y = position.y - velocity.y * dt;
}
// Bounce off the bottom of the window
if (position.y + radius > 600) {
position.y = 600 - radius;
velocity.y = -velocity.y * 0.9f; // Dampen the bounce and invert the velocity
oldPosition.y = position.y - velocity.y * dt;
}
// Bounce off the sides of the window
if (position.x - radius < 0) {
position.x = radius;
velocity.x = -velocity.x * 0.7f; // Dampen the bounce and invert the velocity
oldPosition.x = position.x - velocity.x * dt;
}
else if (position.x + radius > 1000) {
position.x = 1000 - radius;
velocity.x = -velocity.x * 0.7f; // Dampen the bounce and invert the velocity
oldPosition.x = position.x - velocity.x * dt;
}
shape.setPosition(position.x - radius, position.y - radius);
}
// Collision check
void checkCollisions(std::vector<Ball>& balls, std::vector<std::vector<std::vector<Ball*>>>& grid, int gridWidth, int gridHeight, int cellSize) {
const float damping = 0.8f;
const float epsilon = 0.001f;
const float separationFactor = 1.01f; // Slight increase in separation
const int collisionIterations = 5; // Number of iterations for collision resolution
int cellX = static_cast<int>(position.x) / cellSize;
int cellY = static_cast<int>(position.y) / cellSize;
for (int iteration = 0; iteration < collisionIterations; ++iteration) {
// Check neighboring cells for potential collisions
for (int x = std::max(0, cellX - 1); x <= std::min(gridWidth - 1, cellX + 1); ++x) {
for (int y = std::max(0, cellY - 1); y <= std::min(gridHeight - 1, cellY + 1); ++y) {
for (auto* otherBall : grid[x][y]) {
if (otherBall == this) continue;
float minDist = radius + otherBall->radius;
float dx = position.x - otherBall->position.x;
float dy = position.y - otherBall->position.y;
float distance = std::sqrt(dx * dx + dy * dy);
if (distance < minDist && distance > epsilon) {
float nx = dx / distance;
float ny = dy / distance;
// Calculate relative velocity
float relativeVelocityX = (position.x - oldPosition.x) - (otherBall->position.x - otherBall->oldPosition.x);
float relativeVelocityY = (position.y - oldPosition.y) - (otherBall->position.y - otherBall->oldPosition.y);
float relativeVelocityDotNormal = relativeVelocityX * nx + relativeVelocityY * ny;
// Only proceed with collision resolution if balls are moving towards each other
if (relativeVelocityDotNormal < 0) {
float overlap = minDist - distance;
float moveDistance = overlap / 2.0f * separationFactor; // Slight increase in separation
// Move balls apart along the collision normal
position.x += nx * moveDistance;
position.y += ny * moveDistance;
otherBall->position.x -= nx * moveDistance;
otherBall->position.y -= ny * moveDistance;
// Calculate velocities
float v1x = position.x - oldPosition.x;
float v1y = position.y - oldPosition.y;
float v2x = otherBall->position.x - otherBall->oldPosition.x;
float v2y = otherBall->position.y - otherBall->oldPosition.y;
// Calculate normal velocity
float vn = (v2x - v1x) * nx + (v2y - v1y) * ny;
// Calculate impulse scalar
float impulse = (2.0f * vn) / (1.0f + 1.0f); // Assuming equal mass balls
// Apply impulse to change velocities
oldPosition.x -= impulse * nx * damping;
oldPosition.y -= impulse * ny * damping;
otherBall->oldPosition.x += impulse * nx * damping;
otherBall->oldPosition.y += impulse * ny * damping;
}
}
else if (distance <= epsilon) {
// Randomly perturb positions to prevent sticking
float perturbation = minDist - epsilon;
float angle = static_cast<float>(rand()) / RAND_MAX * 2.0f * M_PI;
position.x += std::cos(angle) * perturbation / 2.0f;
position.y += std::sin(angle) * perturbation / 2.0f;
otherBall->position.x -= std::cos(angle) * perturbation / 2.0f;
otherBall->position.y -= std::sin(angle) * perturbation / 2.0f;
}
}
}
}
}
}
void draw(sf::RenderWindow& window) {
window.draw(shape);
}
};
int main() {
sf::RenderWindow window(sf::VideoMode(1000, 600), "Verlet Integration");
std::vector<Ball> balls;
const int ballNum = 20;
const int Maxradius = 20;
const int Minradius = 20;
balls.reserve(ballNum);
// Initialize balls
for (int i = 0; i < ballNum; i++) {
balls.emplace_back(rand() % Maxradius + Minradius, sf::Color(rand() % 255, rand() % 255, rand() % 255), rand() % 1000, rand() % 600);
}
// Grid parameters
int gridWidth = 1000 / (Maxradius * 2);
int gridHeight = 600 / (Maxradius * 2);
int cellSize = 2 * Maxradius;
std::vector<std::vector<std::vector<Ball*>>> grid(gridWidth, std::vector<std::vector<Ball*>>(gridHeight));
sf::Clock displayClock;
sf::Clock physicsClock;
float physicsDt = 1.0f / 240.0f;
float physicsAccumulator = 0.0f;
while (window.isOpen()) {
sf::Event event;
while (window.pollEvent(event)) {
if (event.type == sf::Event::Closed)
window.close();
}
float frameTime = displayClock.restart().asSeconds();
physicsAccumulator += frameTime;
// Physics update loop
while (physicsAccumulator >= physicsDt) {
// Clear grid
for (auto& column : grid) {
for (auto& cell : column) {
cell.clear();
}
}
// Update ball positions
for (auto& ball : balls) {
ball.updatePhysics(physicsDt);
int cellX = static_cast<int>(ball.position.x) / cellSize;
int cellY = static_cast<int>(ball.position.y) / cellSize;
if (cellX >= 0 && cellX < gridWidth && cellY >= 0 && cellY < gridHeight) {
grid[cellX][cellY].push_back(&ball);
}
}
// Resolve collisions
for (auto& ball : balls) {
ball.checkCollisions(balls, grid, gridWidth, gridHeight, cellSize);
}
physicsAccumulator -= physicsDt;
}
// Clear window
window.clear(sf::Color::White);
// Draw balls
for (auto& ball : balls) {
ball.draw(window);
}
// Display frame
window.display();
// Ensure consistent frame rate
sf::sleep(sf::seconds(1.0f / 60.0f) - displayClock.getElapsedTime());
}
return 0;
}
I have tried a bunch of tweaking to the dampening and this is what seems to work best but other then that im not sure what else i can do.
1