mirror of
https://github.com/Floriansylvain/SFMLplayground.git
synced 2026-08-19 11:43:24 +02:00
feat: implement multithreading for collision resolution and ball updates
This commit is contained in:
+4
-1
@@ -125,6 +125,9 @@ void Ball::handleWallCollision(const sf::Vector2f& windowSize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Ball::resolveCollision(Ball& other) {
|
void Ball::resolveCollision(Ball& other) {
|
||||||
|
std::lock_guard<std::mutex> lockA(m_mutex);
|
||||||
|
std::lock_guard<std::mutex> lockB(other.m_mutex);
|
||||||
|
|
||||||
sf::Vector2f posA = getPosition();
|
sf::Vector2f posA = getPosition();
|
||||||
sf::Vector2f posB = other.getPosition();
|
sf::Vector2f posB = other.getPosition();
|
||||||
sf::Vector2f delta = posB - posA;
|
sf::Vector2f delta = posB - posA;
|
||||||
@@ -143,7 +146,7 @@ void Ball::resolveCollision(Ball& other) {
|
|||||||
float vA_n = vA.x * normal.x + vA.y * normal.y;
|
float vA_n = vA.x * normal.x + vA.y * normal.y;
|
||||||
float vB_n = vB.x * normal.x + vB.y * normal.y;
|
float vB_n = vB.x * normal.x + vB.y * normal.y;
|
||||||
|
|
||||||
float restitution = Constants::RESTITUTION;
|
float restitution = 0.95f;
|
||||||
float vA_n_new = vB_n * restitution;
|
float vA_n_new = vB_n * restitution;
|
||||||
float vB_n_new = vA_n * restitution;
|
float vB_n_new = vA_n * restitution;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <SFML/Graphics/CircleShape.hpp>
|
#include <SFML/Graphics/CircleShape.hpp>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
#include "PhysicalObject.hpp"
|
#include "PhysicalObject.hpp"
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ class Ball : public PhysicalObject {
|
|||||||
void handleWallCollision(const sf::Vector2f& windowSize);
|
void handleWallCollision(const sf::Vector2f& windowSize);
|
||||||
void updateColor();
|
void updateColor();
|
||||||
|
|
||||||
|
mutable std::mutex m_mutex;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Ball(float radius, const sf::Vector2f& pos, const sf::Vector2f& vel,
|
Ball(float radius, const sf::Vector2f& pos, const sf::Vector2f& vel,
|
||||||
const sf::Color& color);
|
const sf::Color& color);
|
||||||
|
|||||||
+2
-2
@@ -8,6 +8,6 @@ constexpr float RESTITUTION = 0.8f;
|
|||||||
constexpr float FRICTION = 0.9f;
|
constexpr float FRICTION = 0.9f;
|
||||||
constexpr float IMPULSE = 2000.f;
|
constexpr float IMPULSE = 2000.f;
|
||||||
constexpr float REST_PIXEL_VELOCITY = 2.0f;
|
constexpr float REST_PIXEL_VELOCITY = 2.0f;
|
||||||
constexpr float BALL_RADIUS = 15.f;
|
constexpr float BALL_RADIUS = 6.f;
|
||||||
constexpr unsigned BALL_QUANTITY = 500;
|
constexpr unsigned BALL_QUANTITY = 1000;
|
||||||
} // namespace Constants
|
} // namespace Constants
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ DebugOverlay::DebugOverlay(const std::string& fontPath)
|
|||||||
m_text.setPosition(sf::Vector2f(5.f, 5.f));
|
m_text.setPosition(sf::Vector2f(5.f, 5.f));
|
||||||
}
|
}
|
||||||
|
|
||||||
void DebugOverlay::update(int drawCalls, float timescale,
|
void DebugOverlay::update(int drawCalls, float timeScale,
|
||||||
const sf::RenderWindow& window) {
|
sf::RenderWindow& window, size_t threadCount,
|
||||||
|
size_t ballsPerThread) {
|
||||||
float elapsed = m_fpsClock.restart().asSeconds();
|
float elapsed = m_fpsClock.restart().asSeconds();
|
||||||
if (elapsed > 0.f) m_fps = static_cast<int>(1.f / elapsed);
|
if (elapsed > 0.f) m_fps = static_cast<int>(1.f / elapsed);
|
||||||
|
|
||||||
@@ -21,8 +22,9 @@ void DebugOverlay::update(int drawCalls, float timescale,
|
|||||||
oss << "Framerate: " << m_fps << " FPS\n";
|
oss << "Framerate: " << m_fps << " FPS\n";
|
||||||
oss << "Frametime: " << (elapsed * 1000.f) << " ms\n";
|
oss << "Frametime: " << (elapsed * 1000.f) << " ms\n";
|
||||||
oss << "Mouse: " << mousePos.x << ", " << mousePos.y << "\n";
|
oss << "Mouse: " << mousePos.x << ", " << mousePos.y << "\n";
|
||||||
oss << "Time scale: " << timescale << "\n";
|
oss << "Time scale: " << timeScale << "\n";
|
||||||
|
oss << "\nThreads: " << threadCount;
|
||||||
|
oss << "\nBalls per thread: " << ballsPerThread;
|
||||||
m_text.setString(oss.str());
|
m_text.setString(oss.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ class DebugOverlay {
|
|||||||
public:
|
public:
|
||||||
DebugOverlay(const std::string& fontPath);
|
DebugOverlay(const std::string& fontPath);
|
||||||
|
|
||||||
void update(int drawCalls, float timescale, const sf::RenderWindow& window);
|
void update(int drawCalls, float timeScale, sf::RenderWindow& window,
|
||||||
|
size_t threadCount, size_t ballsPerThread);
|
||||||
void draw(sf::RenderWindow& window);
|
void draw(sf::RenderWindow& window);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
+70
-13
@@ -3,6 +3,7 @@
|
|||||||
#include <SFML/Window/Event.hpp>
|
#include <SFML/Window/Event.hpp>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <thread>
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
@@ -17,7 +18,10 @@ Game::Game()
|
|||||||
std::bind(&Game::processMousePressed, this, std::placeholders::_1)),
|
std::bind(&Game::processMousePressed, this, std::placeholders::_1)),
|
||||||
m_debugLines(sf::PrimitiveType::Lines),
|
m_debugLines(sf::PrimitiveType::Lines),
|
||||||
m_drawCallCount(0),
|
m_drawCallCount(0),
|
||||||
m_debugOverlay("assets/consolas.ttf") {
|
m_debugOverlay("assets/consolas.ttf"),
|
||||||
|
m_threadPool(std::max(1u, std::thread::hardware_concurrency() > 2
|
||||||
|
? std::thread::hardware_concurrency() - 2
|
||||||
|
: 1u)) {
|
||||||
m_window.create(sf::VideoMode({Constants::WIDTH, Constants::HEIGHT}),
|
m_window.create(sf::VideoMode({Constants::WIDTH, Constants::HEIGHT}),
|
||||||
"SFML Playground");
|
"SFML Playground");
|
||||||
m_window.setVerticalSyncEnabled(true);
|
m_window.setVerticalSyncEnabled(true);
|
||||||
@@ -78,46 +82,92 @@ Game::Grid Game::buildSpatialGrid() {
|
|||||||
return grid;
|
return grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::resolveSpatialCollisions(const Grid &grid) {
|
void Game::resolveSpatialCollisionsParallel(const Grid &grid) {
|
||||||
static const Cell forwardNeighbors[] = {
|
static const Cell forwardNeighbors[] = {
|
||||||
{0, 0}, {1, 0}, {1, 1}, {0, 1}, {-1, 1}};
|
{0, 0}, {1, 0}, {1, 1}, {0, 1}, {-1, 1}};
|
||||||
|
|
||||||
|
std::vector<std::pair<Ball *, Ball *>> collisionPairs;
|
||||||
|
std::mutex collisionsMutex;
|
||||||
|
|
||||||
for (const auto &[cell, cellBalls] : grid) {
|
for (const auto &[cell, cellBalls] : grid) {
|
||||||
for (const auto &offset : forwardNeighbors) {
|
for (const auto &offset : forwardNeighbors) {
|
||||||
Cell neighborCell = {cell.first + offset.first,
|
Cell neighborCell = {cell.first + offset.first,
|
||||||
cell.second + offset.second};
|
cell.second + offset.second};
|
||||||
auto neighborIt = grid.find(neighborCell);
|
auto neighborIt = grid.find(neighborCell);
|
||||||
if (neighborIt == grid.end()) continue;
|
if (neighborIt == grid.end()) continue;
|
||||||
|
|
||||||
if (neighborCell == cell) {
|
if (neighborCell == cell) {
|
||||||
for (size_t i = 0; i < cellBalls.size(); ++i) {
|
for (size_t i = 0; i < cellBalls.size(); ++i) {
|
||||||
for (size_t j = i + 1; j < cellBalls.size(); ++j) {
|
for (size_t j = i + 1; j < cellBalls.size(); ++j) {
|
||||||
cellBalls[i]->resolveCollision(*cellBalls[j]);
|
std::lock_guard<std::mutex> lock(collisionsMutex);
|
||||||
|
collisionPairs.emplace_back(cellBalls[i], cellBalls[j]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (Ball *ballA : cellBalls) {
|
for (Ball *ballA : cellBalls) {
|
||||||
for (Ball *ballB : neighborIt->second) {
|
for (Ball *ballB : neighborIt->second) {
|
||||||
ballA->resolveCollision(*ballB);
|
std::lock_guard<std::mutex> lock(collisionsMutex);
|
||||||
|
collisionPairs.emplace_back(ballA, ballB);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const size_t chunkSize = std::max(
|
||||||
|
size_t(1), collisionPairs.size() / std::thread::hardware_concurrency());
|
||||||
|
std::vector<std::future<void>> futures;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < collisionPairs.size(); i += chunkSize) {
|
||||||
|
size_t end = std::min(i + chunkSize, collisionPairs.size());
|
||||||
|
|
||||||
|
futures.push_back(m_threadPool.enqueue([&collisionPairs, i, end]() {
|
||||||
|
for (size_t j = i; j < end; ++j) {
|
||||||
|
auto &[ballA, ballB] = collisionPairs[j];
|
||||||
|
ballA->resolveCollision(*ballB);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto &future : futures) {
|
||||||
|
future.get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::update() {
|
void Game::update() {
|
||||||
float dt = m_clock.restart().asSeconds() * m_timeScale;
|
float dt = m_clock.restart().asSeconds() * m_timeScale;
|
||||||
if (dt > 0.1f) dt = 0.1f;
|
if (dt > 0.1f) dt = 0.1f;
|
||||||
|
|
||||||
for (auto &object : m_objects) {
|
updateBallsParallel(dt);
|
||||||
if (auto *ball = dynamic_cast<Ball *>(object.get())) {
|
|
||||||
ball->update(dt, m_windowSize);
|
|
||||||
} else {
|
|
||||||
object->update(dt, m_windowSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto grid = buildSpatialGrid();
|
auto grid = buildSpatialGrid();
|
||||||
resolveSpatialCollisions(grid);
|
|
||||||
|
resolveSpatialCollisionsParallel(grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::updateBallsParallel(float dt) {
|
||||||
|
const size_t chunkSize = std::max(
|
||||||
|
size_t(1), m_objects.size() / std::thread::hardware_concurrency());
|
||||||
|
|
||||||
|
std::vector<std::future<void>> futures;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < m_objects.size(); i += chunkSize) {
|
||||||
|
size_t end = std::min(i + chunkSize, m_objects.size());
|
||||||
|
|
||||||
|
futures.push_back(m_threadPool.enqueue([&, i, end, dt]() {
|
||||||
|
for (size_t j = i; j < end; ++j) {
|
||||||
|
if (auto *ball = dynamic_cast<Ball *>(m_objects[j].get())) {
|
||||||
|
ball->update(dt, m_windowSize);
|
||||||
|
} else {
|
||||||
|
m_objects[j]->update(dt, m_windowSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto &future : futures) {
|
||||||
|
future.get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::render() {
|
void Game::render() {
|
||||||
@@ -128,8 +178,12 @@ void Game::render() {
|
|||||||
|
|
||||||
m_batchRenderer.clear();
|
m_batchRenderer.clear();
|
||||||
|
|
||||||
|
std::vector<const Ball *> balls;
|
||||||
|
balls.reserve(m_objects.size());
|
||||||
|
|
||||||
for (const auto &object : m_objects) {
|
for (const auto &object : m_objects) {
|
||||||
if (auto *ball = dynamic_cast<Ball *>(object.get())) {
|
if (auto *ball = dynamic_cast<Ball *>(object.get())) {
|
||||||
|
balls.push_back(ball);
|
||||||
m_batchRenderer.addBall(*ball);
|
m_batchRenderer.addBall(*ball);
|
||||||
} else {
|
} else {
|
||||||
object->draw(m_window);
|
object->draw(m_window);
|
||||||
@@ -148,7 +202,10 @@ void Game::render() {
|
|||||||
|
|
||||||
if (m_toggleDebug) {
|
if (m_toggleDebug) {
|
||||||
DebugDraw::drawBatchedLines(m_window, m_debugLines);
|
DebugDraw::drawBatchedLines(m_window, m_debugLines);
|
||||||
m_debugOverlay.update(m_drawCallCount + 2, m_timeScale, m_window);
|
size_t threadCount = m_threadPool.getThreadCount();
|
||||||
|
size_t ballsPerThread = m_objects.size() / threadCount;
|
||||||
|
m_debugOverlay.update(m_drawCallCount + 2, m_timeScale, m_window,
|
||||||
|
threadCount, ballsPerThread);
|
||||||
m_debugOverlay.draw(m_window);
|
m_debugOverlay.draw(m_window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-8
@@ -15,6 +15,7 @@
|
|||||||
#include "DebugOverlay.hpp"
|
#include "DebugOverlay.hpp"
|
||||||
#include "InputManager.hpp"
|
#include "InputManager.hpp"
|
||||||
#include "PhysicalObject.hpp"
|
#include "PhysicalObject.hpp"
|
||||||
|
#include "ThreadPool.hpp"
|
||||||
|
|
||||||
class Ball;
|
class Ball;
|
||||||
class PhysicalObject;
|
class PhysicalObject;
|
||||||
@@ -32,12 +33,7 @@ class Game {
|
|||||||
DebugOverlay m_debugOverlay;
|
DebugOverlay m_debugOverlay;
|
||||||
bool m_toggleDebug = true;
|
bool m_toggleDebug = true;
|
||||||
BatchRenderer m_batchRenderer;
|
BatchRenderer m_batchRenderer;
|
||||||
|
ThreadPool m_threadPool;
|
||||||
void processKeyPressed(const sf::Event::KeyPressed& keyPressed);
|
|
||||||
void processMousePressed(const sf::Event::MouseButtonPressed& mousePressed);
|
|
||||||
void handleMouseClick(const sf::Vector2i& mousePos);
|
|
||||||
void update();
|
|
||||||
void render();
|
|
||||||
|
|
||||||
struct CellHash {
|
struct CellHash {
|
||||||
std::size_t operator()(const std::pair<int, int>& k) const {
|
std::size_t operator()(const std::pair<int, int>& k) const {
|
||||||
@@ -45,13 +41,21 @@ class Game {
|
|||||||
static_cast<std::size_t>(k.second) * 19349663;
|
static_cast<std::size_t>(k.second) * 19349663;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
using Cell = std::pair<int, int>;
|
using Cell = std::pair<int, int>;
|
||||||
using Grid = std::unordered_map<Cell, std::vector<Ball*>, CellHash>;
|
using Grid = std::unordered_map<Cell, std::vector<Ball*>, CellHash>;
|
||||||
|
|
||||||
|
void processKeyPressed(const sf::Event::KeyPressed& keyPressed);
|
||||||
|
void processMousePressed(const sf::Event::MouseButtonPressed& mousePressed);
|
||||||
|
void handleMouseClick(const sf::Vector2i& mousePos);
|
||||||
|
void update();
|
||||||
|
void render();
|
||||||
|
void updateBallsParallel(float dt);
|
||||||
|
void resolveSpatialCollisionsParallel(const Grid& grid);
|
||||||
|
|
||||||
Grid buildSpatialGrid();
|
Grid buildSpatialGrid();
|
||||||
void resolveSpatialCollisions(const Grid& grid);
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Game();
|
Game();
|
||||||
void run();
|
void run();
|
||||||
|
size_t getThreadCount() const { return m_threadPool.getThreadCount(); }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <functional>
|
||||||
|
#include <future>
|
||||||
|
#include <mutex>
|
||||||
|
#include <queue>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class ThreadPool {
|
||||||
|
public:
|
||||||
|
explicit ThreadPool(size_t numThreads);
|
||||||
|
~ThreadPool();
|
||||||
|
size_t getThreadCount() const { return workers.size(); }
|
||||||
|
|
||||||
|
template <class F, class... Args>
|
||||||
|
auto enqueue(F&& f, Args&&... args)
|
||||||
|
-> std::future<typename std::invoke_result<F, Args...>::type>;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<std::thread> workers;
|
||||||
|
|
||||||
|
std::queue<std::function<void()>> tasks;
|
||||||
|
|
||||||
|
std::mutex queueMutex;
|
||||||
|
std::condition_variable condition;
|
||||||
|
bool stop;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline ThreadPool::ThreadPool(size_t numThreads) : stop(false) {
|
||||||
|
for (size_t i = 0; i < numThreads; ++i) {
|
||||||
|
workers.emplace_back([this] {
|
||||||
|
while (true) {
|
||||||
|
std::function<void()> task;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(this->queueMutex);
|
||||||
|
this->condition.wait(lock, [this] {
|
||||||
|
return this->stop || !this->tasks.empty();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this->stop && this->tasks.empty()) return;
|
||||||
|
|
||||||
|
task = std::move(this->tasks.front());
|
||||||
|
this->tasks.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
task();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline ThreadPool::~ThreadPool() {
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(queueMutex);
|
||||||
|
stop = true;
|
||||||
|
}
|
||||||
|
condition.notify_all();
|
||||||
|
|
||||||
|
for (auto& worker : workers) worker.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class F, class... Args>
|
||||||
|
inline auto ThreadPool::enqueue(F&& f, Args&&... args)
|
||||||
|
-> std::future<typename std::invoke_result<F, Args...>::type> {
|
||||||
|
using return_type = typename std::invoke_result<F, Args...>::type;
|
||||||
|
|
||||||
|
auto task = std::make_shared<std::packaged_task<return_type()>>(
|
||||||
|
std::bind(std::forward<F>(f), std::forward<Args>(args)...));
|
||||||
|
|
||||||
|
std::future<return_type> res = task->get_future();
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(queueMutex);
|
||||||
|
if (stop) throw std::runtime_error("enqueue on stopped ThreadPool");
|
||||||
|
|
||||||
|
tasks.emplace([task]() { (*task)(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
condition.notify_one();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user