Верле: разрешаем коллизии (часть 1)
Всех приветствую! Сегодня мы попробуем написать некое подобие простейшего физического движка.
Введение:
Из жизни мы знаем:
Другими словами:
Произведем несколько преобразований, зная, что скорость — производная координаты по времени, а ускорение — производная скорости по времени:
Получаем такую формулу:
Если интересно, можно ознакомиться со статьей на Википедии.
Начинаем писать код:
Для начала напишем простенький класс для вектора (x; y):
#pragma once
#include
namespace eng {
template struct Vec2 {
T x, y;
Vec2() : x{0}, y{0} {};
Vec2(T _x, T _y) : x{_x}, y{_y} {};
T length() const { return std::sqrt(x * x + y * y); }
Vec2 &operator=(const Vec2 &other) {
x = other.x;
y = other.y;
return *this;
}
Vec2 operator+(const Vec2 &other) const {
return Vec2{x + other.x, y + other.y};
}
Vec2 operator-(const Vec2 &other) const {
return Vec2{x - other.x, y - other.y};
}
void operator+=(const Vec2 &other) {
x += other.x;
y += other.y;
}
void operator-=(const Vec2 &other) {
x -= other.x;
y -= other.y;
}
Vec2 operator*(const T value) const { return Vec2{x * value, y * value}; }
Vec2 operator/(const T value) const { return Vec2{x / value, y / value}; }
};
}
Такого функционала нам будет достаточно; думаю пояснять здесь ничего не нужно. Вообще, даже не нужно было делать класс шаблонным.
Теперь пропишем константы:
#pragma once
#include
namespace constants {
const inline eng::Vec2 gravity = {0.0f, 1000.0f};
const inline int screenWidth = 1280;
const inline int screenHeight = 720;
// область, которую нельзя покидать нашим объектам
const inline float areaRadius = 300.f;
const inline float areaX = constants::screenWidth / 2.f;
const inline float areaY = constants::screenHeight / 2.f;
} // namespace constants
Перейдем к написанию класса для сущностей нашего движка:
#pragma once
#include "Constants.hpp"
#include "Vector2.hpp"
#include
#include
#include
namespace eng {
struct VerletObject {
// вектора из формулы
Vec2 positionCurrent;
Vec2 positionOld;
Vec2 acceleration;
// в качестве графической библиотеки будем использовать СФМЛ
sf::CircleShape sfShape;
float radius;
// не забываем сделать центр окружности центром шейпа, по умолчанию
// им является левый верхний угол
VerletObject(float xPos, float yPos, float _radius, sf::Color color)
: positionCurrent{xPos, yPos}, positionOld{xPos, yPos}, radius{_radius} {
sfShape.setRadius(radius);
sfShape.setOrigin(radius, radius);
sfShape.setPosition(xPos, yPos);
sfShape.setFillColor(color);
}
// все в соответствии с формулой
void updatePosition(float dt) {
Vec2 velocity = positionCurrent - positionOld;
positionOld = positionCurrent;
positionCurrent += velocity + constants::gravity * dt * dt;
sfShape.setPosition(positionCurrent.x, positionCurrent.y);
}
};
}
Начинаем реализовывать класс, который будет хранить в себе все VerletObject, обновлять позиции, искать коллизии, разрешать их:
#pragma once
#include "Constants.hpp"
#include "Vector2.hpp"
#include "VerletObject.hpp"
#include
#include
namespace eng{
class Game {
private:
//в массиве храним объекты движка
std::vector objects;
sf::RenderWindow *window;
// так обновляем позицию им всем
void updatePositions(float dt) {
for (auto *object : objects) {
object->updatePosition(dt);
}
}
// так не даем объекту покидать разрешенную область
void applyConstraint() {
const Vec2 centerPosition{constants::areaX, constants::areaY};
// для каждого объекта
for (auto *object : objects) {
// считаем радиус-вектор от центра допустимой области к объекту
// ищем его модуль
const Vec2 vecToObj = object->positionCurrent - centerPosition;
const float distToObj = vecToObj.length();
// если объект выходит за границы области
if (distToObj > constants::areaRadius - object->radius) {
// берем единичный вектор (направление от ц. области к ц. объекта)
const Vec2 normalized = vecToObj / distToObj;
// обновляем позицию так, чтобы наш объект был внутри области
// по-сути, мы двигаем его ближе к центру области по прямой, проходящей
// через центр объекта и центр области
object->positionCurrent =
centerPosition +
normalized * (constants::areaRadius - object->radius);
}
}
}
}
}
Чек-поинт:
Пора посмотреть, что у нас получается.
Для этого напишем конструктор и два метода: один для добавления объектов, второй вызвающий два предыдущих, рисующий все шейпы:
//...
public:
Game(sf::RenderWindow *_window) : window{_window} {};
void addObject(float xPos, float yPos, float radius,
sf::Color color = sf::Color(sf::Color::Blue)) {
VerletObject *obj = new VerletObject(xPos, yPos, radius, color);
objects.push_back(obj);
}
void update(float dt) {
applyConstraint();
updatePositions(dt);
for (auto *object : objects) {
window->draw(object->sfShape);
}
}
//...
И, наконец, main.cpp:
#include "Constants.hpp"
#include "Game.hpp"
#include "Random.hpp"
#include "Vector2.hpp"
#include
int main() {
sf::RenderWindow window(
sf::VideoMode(constants::screenWidth, constants::screenHeight), "Verlet");
window.setVerticalSyncEnabled(1);
eng::Game game(&window);
// та самая область
sf::CircleShape area;
area.setOrigin(constants::areaRadius, constants::areaRadius);
area.setPosition(constants::areaX, constants::areaY);
area.setRadius(constants::areaRadius);
area.setFillColor(sf::Color::White);
area.setPointCount(200);
sf::Clock deltaClock;
sf::Time dt;
while (window.isOpen()) {
sf::Event event;
while (window.pollEvent(event)) {
if (event.type == sf::Event::Closed)
window.close();
}
// будем генерировать объекты по нажатию ПКМ
if (sf::Mouse::isButtonPressed(sf::Mouse::Right)) {
sf::Vector2i position = sf::Mouse::getPosition(window);
game.addObject(position.x, position.y, eng::getRandomInt(5, 30),
sf::Color(eng::getRandomInt(0, 255),
eng::getRandomInt(0, 255),
eng::getRandomInt(0, 255)));
}
window.clear();
// сначала рисуем область, потом уже объекты, иначе мы их не увидим
window.draw(area);
game.update(dt.asSeconds());
// отображаем что получилось
window.display();
// записываем время итерации
dt = deltaClock.restart();
}
return 0;
}
Забыл еще одну вспомогательную функцию:
int getRandomInt(int l, int r) {
std::random_device rd;
std::uniform_int_distribution gen(l, r);
return gen(rd);
}
Результат следующий:
Перейдем к написанию коллизии. Пока что мы будем использовать примитивный алгоритм, заключающийся в переборе всех пар объектов и сравнению расстояния между их центрами и суммой их радиусов: если оно меньше суммы радиусов, то окружности надо «растолкнуть»:
//...
void solveCollisions() {
// перебираем все пары объектов
for (int i = 0; i < objects.size(); ++i) {
for (int j = 0; j < objects.size(); ++j) {
// самому с собой столкнуться невозможно
if (j == i)
continue;
// вектор от центра первой окр. к центру второй
Vec2 collisionAxis =
objects[i]->positionCurrent - objects[j]->positionCurrent;
// если расстояние между ними больше, чем сумма радиусов
// то они не контактируют
const float dist = collisionAxis.length();
if (dist > objects[i]->radius + objects[j]->radius)
continue;
// единичная версия нашего вектора от ц. первой окр. к ц. второй окр
Vec2 normalized = collisionAxis / dist;
// расстояние, на которое нам нужно отодвинуть друг от друга окружности
// чтобы одна не была в другой
const float delta = objects[i]->radius + objects[j]->radius - dist;
// рассталкиваем их вдоль пряиой
// соблюдая некое подобие закона сохранения импульса
float weightDiff =
objects[j]->radius / (objects[i]->radius + objects[j]->radius);
objects[i]->positionCurrent += normalized * delta * weightDiff;
objects[j]->positionCurrent -= normalized * delta * (1 - weightDiff);
}
}
}
//...
Осталось добавить вызов этого метода в update.
И запускаем!
Получилось просто, хоть и не идеально.
Во второй части попробуем стать к идеалу чуть ближе. Спасибо тем, кому хватило терпения дочитать