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) {
|
||||
std::lock_guard<std::mutex> lockA(m_mutex);
|
||||
std::lock_guard<std::mutex> lockB(other.m_mutex);
|
||||
|
||||
sf::Vector2f posA = getPosition();
|
||||
sf::Vector2f posB = other.getPosition();
|
||||
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 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 vB_n_new = vA_n * restitution;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include <SFML/Graphics/CircleShape.hpp>
|
||||
#include <mutex>
|
||||
|
||||
#include "PhysicalObject.hpp"
|
||||
|
||||
@@ -15,6 +16,8 @@ class Ball : public PhysicalObject {
|
||||
void handleWallCollision(const sf::Vector2f& windowSize);
|
||||
void updateColor();
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
|
||||
public:
|
||||
Ball(float radius, const sf::Vector2f& pos, const sf::Vector2f& vel,
|
||||
const sf::Color& color);
|
||||
|
||||
+2
-2
@@ -8,6 +8,6 @@ constexpr float RESTITUTION = 0.8f;
|
||||
constexpr float FRICTION = 0.9f;
|
||||
constexpr float IMPULSE = 2000.f;
|
||||
constexpr float REST_PIXEL_VELOCITY = 2.0f;
|
||||
constexpr float BALL_RADIUS = 15.f;
|
||||
constexpr unsigned BALL_QUANTITY = 500;
|
||||
constexpr float BALL_RADIUS = 6.f;
|
||||
constexpr unsigned BALL_QUANTITY = 1000;
|
||||
} // namespace Constants
|
||||
|
||||
@@ -9,8 +9,9 @@ DebugOverlay::DebugOverlay(const std::string& fontPath)
|
||||
m_text.setPosition(sf::Vector2f(5.f, 5.f));
|
||||
}
|
||||
|
||||
void DebugOverlay::update(int drawCalls, float timescale,
|
||||
const sf::RenderWindow& window) {
|
||||
void DebugOverlay::update(int drawCalls, float timeScale,
|
||||
sf::RenderWindow& window, size_t threadCount,
|
||||
size_t ballsPerThread) {
|
||||
float elapsed = m_fpsClock.restart().asSeconds();
|
||||
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 << "Frametime: " << (elapsed * 1000.f) << " ms\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());
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ class DebugOverlay {
|
||||
public:
|
||||
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);
|
||||
|
||||
private:
|
||||
|
||||
+70
-13
@@ -3,6 +3,7 @@
|
||||
#include <SFML/Window/Event.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <thread>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -17,7 +18,10 @@ Game::Game()
|
||||
std::bind(&Game::processMousePressed, this, std::placeholders::_1)),
|
||||
m_debugLines(sf::PrimitiveType::Lines),
|
||||
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}),
|
||||
"SFML Playground");
|
||||
m_window.setVerticalSyncEnabled(true);
|
||||
@@ -78,46 +82,92 @@ Game::Grid Game::buildSpatialGrid() {
|
||||
return grid;
|
||||
}
|
||||
|
||||
void Game::resolveSpatialCollisions(const Grid &grid) {
|
||||
void Game::resolveSpatialCollisionsParallel(const Grid &grid) {
|
||||
static const Cell forwardNeighbors[] = {
|
||||
{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 &offset : forwardNeighbors) {
|
||||
Cell neighborCell = {cell.first + offset.first,
|
||||
cell.second + offset.second};
|
||||
auto neighborIt = grid.find(neighborCell);
|
||||
if (neighborIt == grid.end()) continue;
|
||||
|
||||
if (neighborCell == cell) {
|
||||
for (size_t i = 0; i < cellBalls.size(); ++i) {
|
||||
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 {
|
||||
for (Ball *ballA : cellBalls) {
|
||||
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() {
|
||||
float dt = m_clock.restart().asSeconds() * m_timeScale;
|
||||
if (dt > 0.1f) dt = 0.1f;
|
||||
|
||||
for (auto &object : m_objects) {
|
||||
if (auto *ball = dynamic_cast<Ball *>(object.get())) {
|
||||
ball->update(dt, m_windowSize);
|
||||
} else {
|
||||
object->update(dt, m_windowSize);
|
||||
}
|
||||
}
|
||||
updateBallsParallel(dt);
|
||||
|
||||
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() {
|
||||
@@ -128,8 +178,12 @@ void Game::render() {
|
||||
|
||||
m_batchRenderer.clear();
|
||||
|
||||
std::vector<const Ball *> balls;
|
||||
balls.reserve(m_objects.size());
|
||||
|
||||
for (const auto &object : m_objects) {
|
||||
if (auto *ball = dynamic_cast<Ball *>(object.get())) {
|
||||
balls.push_back(ball);
|
||||
m_batchRenderer.addBall(*ball);
|
||||
} else {
|
||||
object->draw(m_window);
|
||||
@@ -148,7 +202,10 @@ void Game::render() {
|
||||
|
||||
if (m_toggleDebug) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
+12
-8
@@ -15,6 +15,7 @@
|
||||
#include "DebugOverlay.hpp"
|
||||
#include "InputManager.hpp"
|
||||
#include "PhysicalObject.hpp"
|
||||
#include "ThreadPool.hpp"
|
||||
|
||||
class Ball;
|
||||
class PhysicalObject;
|
||||
@@ -32,12 +33,7 @@ class Game {
|
||||
DebugOverlay m_debugOverlay;
|
||||
bool m_toggleDebug = true;
|
||||
BatchRenderer m_batchRenderer;
|
||||
|
||||
void processKeyPressed(const sf::Event::KeyPressed& keyPressed);
|
||||
void processMousePressed(const sf::Event::MouseButtonPressed& mousePressed);
|
||||
void handleMouseClick(const sf::Vector2i& mousePos);
|
||||
void update();
|
||||
void render();
|
||||
ThreadPool m_threadPool;
|
||||
|
||||
struct CellHash {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
using Cell = std::pair<int, int>;
|
||||
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();
|
||||
void resolveSpatialCollisions(const Grid& grid);
|
||||
|
||||
public:
|
||||
Game();
|
||||
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