Вступление и подводка
Каюсь, до сего момента я был веб-разработчиком и ничего тяжелее node в руках не держал. Тем страшнее и загадочнее для меня выглядел мир указателей, ссылок и (о ужас) типизированных массивов, да еще и фиксированной длины. Но сегодня вечером я решился наконец-то исследовать этот мир deep dark fantasies. Я джва года мечтал о своей собственной няшной двумерной симуляции движения небесных тел, и я собрался писать её на крестах!
Ни в коем случае не пособие для новичков, просто интересная статья на тему своих проектов.
В качестве библиотеки для отрисовки графики я выбрал sfml, просто потому что он выпал в поиске первым. Я еще не совсем хорошо понимаю, как заливать куда-либо c++ проект вместе с его зависимостями, поэтому вам придется самостоятельно устанавливать либу и подключать ее, если вы захотите это потестить.
Математика и логика
Законы моего мира совсем простые — имеется небесное тело, у него есть декартовы координаты x и y, скорости соответствующих компонент vx и vy, а также физические свойства, такие как масса m, радиус r и некий коэффициент «плотности» d. Радиус я считаю как произведение m * d для простоты, а «плотность» в кавычках, потому что это не плотность в прямом ее понимании, просто коэффициент.
Каждое небесное тело влияет на каждое соразмерно расстоянию — его можно посчитать просто по теореме Пифагора, думаю тут объяснения излишни:dist = sqrt( pow(x1 - x2, 2) + pow(y1 - y2, 2) );
И наконец, сила гравитационного взаимодействия между телами выражается в виде формулы:
где j12 — радиус-вектор, высчитываемый как x2-x1. Это требуется для векторных подсчетов, типа система координат все такое я сам хз почему, в школе нам такого точно не объясняли, с меня взятки гладки, просто пользуемся формулой (буду благодарен, если в комментариях люди с более старым образованием объяснят). В качестве гравитационной постоянной G будем использовать случайно подобранное значение, поскольку наши тела несоразмерны реальному миру.
Скорости vx и vy изменяются благодаря ускорению, которое в свою очередь является частным полученной выше силы и массы тела 1. Таким образом, одна масса сократится, и формула изменения скорости примет вид:vx += G * m2 / dist / dist * (x2 - x1) / dist
vy += G * m2 / dist / dist * (y2 - y1) / dist
На этом, пожалуй все, переходим к самому легкому 😀
Код
Константы, которые потребуются нам для работы:
const double PI = 3.1415926536; const double G = 1; //наша гравитационная постоянная const int boundX = 1200; //размер окна по ширине const int boundY = 800; //...и высоте
Далее, для небесного тела я создал следующую структуру (зачем нужны структуры если это тоже самое что классы?):
struct Body { float x; float y; float vx; float vy; float m; float d; float r = m * d; };
Я переживал за производительность, да и не знал как будет работать это все, поэтому на всякий случай обзавелся таймером на миллисекундах, который стопает главный поток, чтобы дать коду отдышаться:
void rest(int ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); }
Вот так выглядит main целиком:
int main() { sf::ContextSettings settings; settings.antialiasingLevel = 8.0; //уровень сглаживания RenderWindow window(VideoMode(boundX, boundY), "Planet Simulator", sf::Style::Close, settings); int delay = 50; //50ms отдыха для кода while (window.isOpen()) { std::vector<Body> bodiesToAdd = handleEvents(&window); //про обработку событий чуть позже window.clear(Color(0, 0, 0, 0)); //очищаем экран update(&window); //обновление экрана bodies.insert(bodies.end(), bodiesToAdd.begin(), bodiesToAdd.end()); //про это тоже позже :3 window.display(); //отрисовка экрана rest(delay); //отдых } return 0; }
Сердцем всего проекта служит метод void update(RenderWindow* window). Он по описанным выше законам обновляет данные тел в векторе bodies, а затем отдает их экрану для отрисовки:
void update(RenderWindow* window) { std::set<int> deleteBodies; //номера небесных тела, которые столкнулись друг с другом и должны умереть int size = bodies.size(); //количество небесных тел //используем два цикла для фиксации влияния каждого тела на каждое for (int i = 0; i < size; i++) { Body& p0 = bodies[i]; //ссылка на текущее тело for (int j = 0; j < size; j++) { //помним, что под одинаковыми индексами лежит одно и тоже тело, а сами на //себя тела не влияют, поэтому пропускаем if (i == j) continue; Body& p = bodies[j]; //ссылка на второе тело double dist = sqrt(pow(p0.x - p.x, 2) + pow(p0.y - p.y, 2)); //проверка коллизии тел if (dist > p0.r + p.r) { //собственно, изменение скоростей покомпонентно p0.vx += G * p.m / dist / dist * (p.x - p0.x) / dist; p0.vy += G * p.m / dist / dist * (p.y - p0.y) / dist; } else { deleteBodies.insert(i); deleteBodies.insert(j); } } //изменение координат покомпонентно p0.x += p0.vx; p0.y += p0.vy; CircleShape circle = renderBody(p0); //функция, которая на основе структуры просто создает кружок window->draw(circle); //рисуем кружок } //мой способ очистки вектора тел от тех, индексы которых помечены как уничтоженные //и содержатся в наборе deleteBodies std::vector<Body> copy_bodies; for (int i = 0; i < bodies.size(); ++i) { if (deleteBodies.find(i) == deleteBodies.end()) //если индекса в наборе нет { copy_bodies.push_back(bodies[i]); } } bodies = copy_bodies; //я уверен, что можно сделать это лучшее и быстрее, пока не знаю как. }
Про проверку коллизии — все достаточно прозаично, достаточно взглянуть на картинку и понять, что если расстояние между радиусами (dist) меньше сумм радиусов, то такие окружности точно пересекутся, а значит убьются.

Базовый функционал готов, но я хотел бы добавить еще создание планет по клику мыши! Для этого мы обратимся к событиям в sfml — тут нам и пригодится функцияstd::vector<Body> handleEvents(RenderWindow* window), возвращающая вектор тел, которые я бы хотел добавить, и которые я затем в main вставляю в вектор bodies. Вот как она устроена:
std::vector<Body> handleEvents(RenderWindow* window) { std::vector<Body> newBodies; //вектор с новыми телами Event event; while (window->pollEvent(event)) //пока в пуле есть новые события { if (event.type == Event::Closed) //обработка выхода из программы window->close(); if (event.type == Event::MouseButtonPressed) //если случилось нажатие мыши { sf::Vector2i position = sf::Mouse::getPosition(*window); if (event.mouseButton.button == sf::Mouse::Left) { //по нажатию на левую кнопку добавляем просто планету newBodies.push_back(Body{ static_cast<float>(position.x), static_cast<float>(position.y), 0.0, 0.0, 100.0, 0.1}); } else { //по нажатию на правую кнопку добавляем целую тяжелую звезду newBodies.push_back(Body{ static_cast<float>(position.x), static_cast<float>(position.y), 0.0, 0.0, 1000.0, 0.05}); } } } return newBodies; }
Заключение
Все хорошее имеет свойство кончаться, и мой пост к сожалению не исключение. Больше всего мне хотелось бы реализовать силу и направление начального движения планеты через клик и оттягивание мыши (как в энгри бердс). Без понятия, возможно ли такое сделать в sfml, надеюсь на талантов в комментариях. Был бы рад услышать предложения оптимизации, особенно момента очистки убитых планет. Всем добра, счастья вам ♡
Полный код
… выглядит чуточку иначе, как в примерах. В частности, в нем добавлены приколы типа того, что звезды едят планеты, которые в них врезались, и набирают массу понемногу, и прочее. Базово все так же, приятного просмотра
код
#include <SFML/Graphics.hpp> #include <chrono> #include <thread> #include <vector> #include <set> using namespace sf; const double PI = 3.1415926536; const double coef = 1; const int boundX = 1200; const int boundY = 800; const float sunMass = 1000; const float radiusCoef = 0.1; struct Body { float x; float y; float vx; float vy; float m; float r = m * radiusCoef; }; std::vector<Body> bodies = { Body{500.0, 300.0, 0.0, 0.0, 1000.0} }; void sleep(int ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); } std::vector<Body> handleEvents(RenderWindow* window) { std::vector<Body> newBodies; Event event; while (window->pollEvent(event)) { if (event.type == Event::Closed) window->close(); if (event.type == Event::MouseButtonPressed) { sf::Vector2i position = sf::Mouse::getPosition(*window); if (event.mouseButton.button == sf::Mouse::Left) { newBodies.push_back(Body{ static_cast<float>(position.x), static_cast<float>(position.y), 0.0, 0.0, 100.0}); } else { newBodies.push_back(Body{ static_cast<float>(position.x), static_cast<float>(position.y), 0.0, 0.0, 2000.0}); } } } return newBodies; } CircleShape renderBody(Body& b) { b.r = b.m * radiusCoef; CircleShape circle(b.r); circle.setOrigin(b.r, b.r); circle.setPosition(b.x, b.y); if (b.m >= sunMass) { circle.setFillColor(Color(246, 222, 1)); } return circle; } void update(RenderWindow* window) { std::set<int> deleteBodies; int size = bodies.size(); for (int i = 0; i < size; i++) { Body& p0 = bodies[i]; for (int j = 0; j < size; j++) { if (i == j) continue; Body& p = bodies[j]; double d = sqrt(pow(p0.x - p.x, 2) + pow(p0.y - p.y, 2)); if (d > p0.r + p.r) { p0.vx += coef * p.m / d / d * (p.x - p0.x) / d; p0.vy += coef * p.m / d / d * (p.y - p0.y) / d; } else { if (p0.m >= sunMass && p.m >= sunMass) { deleteBodies.insert(i); deleteBodies.insert(j); } else { if (p0.m < sunMass) { deleteBodies.insert(i); } else { p0.m += p.m * 0.1; } if (p.m < sunMass) { deleteBodies.insert(j); } else { p.m += p0.m * 0.1; } } } } p0.x += p0.vx; p0.y += p0.vy; CircleShape circle = renderBody(p0); window->draw(circle); } std::vector<Body> copy_bodies; for (int i = 0; i < bodies.size(); ++i) { if (deleteBodies.find(i) == deleteBodies.end()) { copy_bodies.push_back(bodies[i]); } } bodies = copy_bodies; } int main() { sf::ContextSettings settings; settings.antialiasingLevel = 8.0; RenderWindow window(VideoMode(boundX, boundY), "Planet Simulator", sf::Style::Close, settings); int msForFrame = 50; while (window.isOpen()) { std::vector<Body> bodiesToAdd = handleEvents(&window); window.clear(Color(0, 0, 0, 0)); update(&window); bodies.insert(bodies.end(), bodiesToAdd.begin(), bodiesToAdd.end()); bodiesToAdd.clear(); window.display(); sleep(msForFrame); } return 0; }
ссылка на оригинал статьи https://habr.com/ru/post/701702/
Добавить комментарий