Pull to refresh

[Pet] Двумерная симуляция взаимодействия небесных тел на C++

Reading time6 min
Views7.4K

Вступление и подводка

Каюсь, до сего момента я был веб-разработчиком и ничего тяжелее 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) );

И наконец, сила гравитационного взаимодействия между телами выражается в виде формулы:

 { F = G \cdot \frac{m_{1} \cdot m_{2}}{r^{2}} \cdot  \frac{j_{12}}{r}}

где j12 - радиус-вектор, высчитываемый как x2-x1. Это требуется для векторных подсчетов, типа система координат все такое я сам хз почему, в школе нам такого точно не объясняли, с меня взятки гладки, просто пользуемся формулой (буду благодарен, если в комментариях люди с более старым образованием объяснят). В качестве гравитационной постоянной G будем использовать случайно подобранное значение, поскольку наши тела несоразмерны реальному миру.

Скорости vx и vy изменяются благодаря ускорению, которое в свою очередь является частным полученной выше силы и массы тела 1. Таким образом, одна масса сократится, и формула изменения скорости примет вид:
vx += G * m2 / dist / dist * (x2 - x1) / dist
vy += G * m2 / dist / dist * (y2 - y1) / dist

На этом, пожалуй все, переходим к самому легкому :D

Код

Константы, которые потребуются нам для работы:

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;
}

Tags:
Hubs:
Total votes 14: ↑11 and ↓3+13
Comments23

Articles