/* OneLoneCoder.com - Programming Balls! #1 Circle Vs Circle Collisions "..it's just balls bangin' together init..." - @Javidx9 License ~~~~~~~ One Lone Coder Console Game Engine Copyright (C) 2018 Javidx9 This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions; See license for details. Original works located at: https://www.github.com/onelonecoder https://www.onelonecoder.com https://www.youtube.com/javidx9 GNU GPLv3 https://github.com/OneLoneCoder/videos/blob/master/LICENSE From Javidx9 :) ~~~~~~~~~~~~~~~ Hello! Ultimately I don't care what you use this for. It's intended to be educational, and perhaps to the oddly minded - a little bit of fun. Please hack this, change it and use it in any way you see fit. You acknowledge that I am not responsible for anything bad that happens as a result of your actions. However this code is protected by GNU GPLv3, see the license in the github repo. This means you must attribute me if you use it. You can view this license here: https://github.com/OneLoneCoder/videos/blob/master/LICENSE Cheers! Background ~~~~~~~~~~ Collision detection engines can get quite complicated. This program shows the interactions between circular objects of different sizes and masses. Use Left mouse button to select and drag a ball to examin static collisions, and use Right mouse button to apply velocity to the balls as if using a pool/snooker/billiards cue. Author ~~~~~~ Twitter: @javidx9 Blog: www.onelonecoder.com Video: ~~~~~~ Part #1 https://youtu.be/LPzyNOHY3A4 Last Updated: 21/01/2017 */ #include #include using namespace std; #include "olcConsoleGameEngine.h" struct sBall { float px, py; float vx, vy; float ax, ay; float radius; float mass; int id; }; class CirclePhysics : public olcConsoleGameEngine { public: CirclePhysics() { m_sAppName = L"Circle Physics"; } private: vector> modelCircle; vector vecBalls; sBall *pSelectedBall = nullptr; // Adds a ball to the vector void AddBall(float x, float y, float r = 5.0f) { sBall b; b.px = x; b.py = y; b.vx = 0; b.vy = 0; b.ax = 0; b.ay = 0; b.radius = r; b.mass = r * 10.0f; b.id = vecBalls.size(); vecBalls.emplace_back(b); } public: bool OnUserCreate() { // Define Circle Model modelCircle.push_back({ 0.0f, 0.0f }); int nPoints = 20; for (int i = 0; i < nPoints; i++) modelCircle.push_back({ cosf(i / (float)(nPoints - 1) * 2.0f * 3.14159f) , sinf(i / (float)(nPoints - 1) * 2.0f * 3.14159f) }); float fDefaultRad = 8.0f; //AddBall(ScreenWidth() * 0.25f, ScreenHeight() * 0.5f, fDefaultRad); //AddBall(ScreenWidth() * 0.75f, ScreenHeight() * 0.5f, fDefaultRad); // Add 10 Random Balls for (int i = 0; i <10; i++) AddBall(rand() % ScreenWidth(), rand() % ScreenHeight(), rand() % 16 + 2); return true; } bool OnUserUpdate(float fElapsedTime) { auto DoCirclesOverlap = [](float x1, float y1, float r1, float x2, float y2, float r2) { return fabs((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2)) <= (r1 + r2)*(r1 + r2); }; auto IsPointInCircle = [](float x1, float y1, float r1, float px, float py) { return fabs((x1 - px)*(x1 - px) + (y1 - py)*(y1 - py)) < (r1 * r1); }; if (m_mouse[0].bPressed || m_mouse[1].bPressed) { pSelectedBall = nullptr; for (auto &ball : vecBalls) { if (IsPointInCircle(ball.px, ball.py, ball.radius, m_mousePosX, m_mousePosY)) { pSelectedBall = &ball; break; } } } if (m_mouse[0].bHeld) { if (pSelectedBall != nullptr) { pSelectedBall->px = m_mousePosX; pSelectedBall->py = m_mousePosY; } } if (m_mouse[0].bReleased) { pSelectedBall = nullptr; } if (m_mouse[1].bReleased) { if (pSelectedBall != nullptr) { // Apply velocity pSelectedBall->vx = 5.0f * ((pSelectedBall->px) - (float)m_mousePosX); pSelectedBall->vy = 5.0f * ((pSelectedBall->py) - (float)m_mousePosY); } pSelectedBall = nullptr; } vector> vecCollidingPairs; // Update Ball Positions for (auto &ball : vecBalls) { // Add Drag to emulate rolling friction ball.ax = -ball.vx * 0.8f; ball.ay = -ball.vy * 0.8f; // Update ball physics ball.vx += ball.ax * fElapsedTime; ball.vy += ball.ay * fElapsedTime; ball.px += ball.vx * fElapsedTime; ball.py += ball.vy * fElapsedTime; // Wrap the balls around screen if (ball.px < 0) ball.px += (float)ScreenWidth(); if (ball.px >= ScreenWidth()) ball.px -= (float)ScreenWidth(); if (ball.py < 0) ball.py += (float)ScreenHeight(); if (ball.py >= ScreenHeight()) ball.py -= (float)ScreenHeight(); // Clamp velocity near zero if (fabs(ball.vx*ball.vx + ball.vy*ball.vy) < 0.01f) { ball.vx = 0; ball.vy = 0; } } // Static collisions, i.e. overlap for (auto &ball : vecBalls) { for (auto &target : vecBalls) { if (ball.id != target.id) { if (DoCirclesOverlap(ball.px, ball.py, ball.radius, target.px, target.py, target.radius)) { // Collision has occured vecCollidingPairs.push_back({ &ball, &target }); // Distance between ball centers float fDistance = sqrtf((ball.px - target.px)*(ball.px - target.px) + (ball.py - target.py)*(ball.py - target.py)); // Calculate displacement required float fOverlap = 0.5f * (fDistance - ball.radius - target.radius); // Displace Current Ball away from collision ball.px -= fOverlap * (ball.px - target.px) / fDistance; ball.py -= fOverlap * (ball.py - target.py) / fDistance; // Displace Target Ball away from collision target.px += fOverlap * (ball.px - target.px) / fDistance; target.py += fOverlap * (ball.py - target.py) / fDistance; } } } } // Now work out dynamic collisions for (auto c : vecCollidingPairs) { sBall *b1 = c.first; sBall *b2 = c.second; // Distance between balls float fDistance = sqrtf((b1->px - b2->px)*(b1->px - b2->px) + (b1->py - b2->py)*(b1->py - b2->py)); // Normal float nx = (b2->px - b1->px) / fDistance; float ny = (b2->py - b1->py) / fDistance; // Tangent float tx = -ny; float ty = nx; // Dot Product Tangent float dpTan1 = b1->vx * tx + b1->vy * ty; float dpTan2 = b2->vx * tx + b2->vy * ty; // Dot Product Normal float dpNorm1 = b1->vx * nx + b1->vy * ny; float dpNorm2 = b2->vx * nx + b2->vy * ny; // Conservation of momentum in 1D float m1 = (dpNorm1 * (b1->mass - b2->mass) + 2.0f * b2->mass * dpNorm2) / (b1->mass + b2->mass); float m2 = (dpNorm2 * (b2->mass - b1->mass) + 2.0f * b1->mass * dpNorm1) / (b1->mass + b2->mass); // Update ball velocities b1->vx = tx * dpTan1 + nx * m1; b1->vy = ty * dpTan1 + ny * m1; b2->vx = tx * dpTan2 + nx * m2; b2->vy = ty * dpTan2 + ny * m2; // Wikipedia Version - Maths is smarter but same //float kx = (b1->vx - b2->vx); //float ky = (b1->vy - b2->vy); //float p = 2.0 * (nx * kx + ny * ky) / (b1->mass + b2->mass); //b1->vx = b1->vx - p * b2->mass * nx; //b1->vy = b1->vy - p * b2->mass * ny; //b2->vx = b2->vx + p * b1->mass * nx; //b2->vy = b2->vy + p * b1->mass * ny; } // Clear Screen Fill(0, 0, ScreenWidth(), ScreenHeight(), ' '); // Draw Balls for (auto ball : vecBalls) DrawWireFrameModel(modelCircle, ball.px, ball.py, atan2f(ball.vy, ball.vx), ball.radius, FG_WHITE); // Draw static collisions for (auto c : vecCollidingPairs) DrawLine(c.first->px, c.first->py, c.second->px, c.second->py, PIXEL_SOLID, FG_RED); // Draw Cue if (pSelectedBall != nullptr) DrawLine(pSelectedBall->px, pSelectedBall->py, m_mousePosX, m_mousePosY, PIXEL_SOLID, FG_BLUE); return true; } }; int main() { CirclePhysics game; if (game.ConstructConsole(160, 120, 8, 8)) game.Start(); else wcout << L"Could not construct console" << endl; return 0; };