diff --git a/src/Benchmarker.cpp b/src/Benchmarker.cpp new file mode 100644 index 0000000..fb89e6f --- /dev/null +++ b/src/Benchmarker.cpp @@ -0,0 +1,43 @@ +#include "Benchmarker.h" +#include +#include + +void BenchMark::bench() { + this->clock.restart(); +} + +double BenchMark::mark() { + this->last = this->clock.getElapsedTime().asSeconds(); + if (this->count != 0) { + double weight{1.0 / double(this->count + 1zu)}; + this->average = this->last * weight + this->average * (1.0 - weight); + this->highest = std::max(this->last, this->highest); + this->lowest = std::min(this->last, this->lowest); + } else { + this->average = this->highest = this->lowest = this->last; + } + this->count += 1; + //std::println("MARK: {}", this->last); + return this->last; +} + +void BenchMark::reset() { + this->average = this->highest = this->lowest = this->last = 0.0; + this->count = 0zu; +} + +double BenchMark::getLast() const { + return this->last; +} + +double BenchMark::getLowest() const { + return this->lowest; +} + +double BenchMark::getHighest() const { + return this->highest; +} + +double BenchMark::getAverage() const { + return this->average; +} diff --git a/src/Benchmarker.h b/src/Benchmarker.h new file mode 100644 index 0000000..8053b98 --- /dev/null +++ b/src/Benchmarker.h @@ -0,0 +1,26 @@ +#ifndef BENCHMARKER_H +#define BENCHMARKER_H + +#include + +class BenchMark { + private: + sf::Clock clock{}; + double last{0.0}; + double lowest{0.0}; + double highest{0.0}; + double average{0.0}; + size_t count{0}; + + public: + void bench(); + double mark(); + + void reset(); + double getLast() const; + double getLowest() const; + double getHighest() const; + double getAverage() const; +}; + +#endif diff --git a/src/collision_crisis.cpp b/src/collision_crisis.cpp index 5c747ff..465dbb1 100644 --- a/src/collision_crisis.cpp +++ b/src/collision_crisis.cpp @@ -1,4 +1,7 @@ +#include "Balls.hpp" +#include "Benchmarker.h" #include "defs.h" +#include "simulation_v2.h" #include #include #include @@ -9,15 +12,18 @@ #include #include -#include "Balls.hpp" +static bool showV1{false}; +static bool showV2{true}; +static bool tick{true}; namespace v1 { static BallGame sim; } -namespace v2 { -} + +BenchMark v1BenchMark{}; +BenchMark v2BenchMark{}; void configure(AppConfig &config) { - config.window_title = "CHANGEME"; + config.window_title = "Orbs!!!!"; config.frame_rate_limit = std::nullopt; config.vsync = true; } @@ -26,6 +32,7 @@ void setup() { ImGui::GetIO().ConfigFlags |= (ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_NavEnableGamepad | ImGuiConfigFlags_DockingEnable); sf::View view{get_window().getView()}; set_render_view(view); + v2::initialize(2500, {{50,50}, {1000, 1000}}); } void handle_input_event(sf::Event const &event) { @@ -41,36 +48,42 @@ void handle_window_event(sf::Event const &event) { } void loop(double delta) { - v1::sim.updateBalls(get_window().getSize(), delta); + if (showV1 && tick) { + v1BenchMark.bench(); + v1::sim.updateBalls(get_window().getSize(), delta); + v1BenchMark.mark(); + } + if (showV2 && tick) { + v2BenchMark.bench(); + v2::tick(delta); + v2BenchMark.mark(); + } } void draw_scene(sf::RenderTarget &target, sf::RenderStates const &states) { - v1::sim.drawBalls(get_window()); -} - -void draw_main_menu_bar() { - if (ImGui::BeginMenu("Edit")) { - if (ImGui::MenuItem("Menu item!")) { - std::println("Wahooo!!!"); - } - ImGui::EndMenu(); + if (showV1) { + v1::sim.drawBalls(get_window()); + } + if (showV2) { + v2::draw(&get_window()); } } +void draw_main_menu_bar() { } + void draw_gui() { // draw your GUI ImGui::DockSpaceOverViewport(0, NULL, ImGuiDockNodeFlags_PassthruCentralNode); - if (ImGui::Begin("My Window")) { - ImGui::Text("A window with text and a button!!"); - if (ImGui::Button("My Button")) { - std::println("Yipeeee"); - } - ImGui::End(); - } - if (ImGui::Begin("Second Window :O")) { - ImGui::Text("A window with text!"); - ImGui::End(); + if (ImGui::Begin("Tools")) { + ImGui::Checkbox("Simulation V1 (reference implementation)", &showV1); + ImGui::Text("frametime: %f", v1BenchMark.getLast()); + ImGui::Text("avg: %f", v1BenchMark.getAverage()); + ImGui::Checkbox("Simulation V2 (reimplementation)", &showV2); + ImGui::Text("frametime: %f", v2BenchMark.getLast()); + ImGui::Text("avg: %f", v2BenchMark.getAverage()); + ImGui::Checkbox("Tick", &tick); } + ImGui::End(); } void shutdown() { diff --git a/src/simulation_v2.cpp b/src/simulation_v2.cpp new file mode 100644 index 0000000..a685af2 --- /dev/null +++ b/src/simulation_v2.cpp @@ -0,0 +1,181 @@ +#include "simulation_v2.h" +#include "SFML/Graphics/RectangleShape.hpp" +#include "SFML/Graphics/Text.hpp" +#include "SFML/Graphics/Texture.hpp" +#include "SFML/Graphics/Vertex.hpp" +#include "SFML/Graphics/VertexArray.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define HASH_BUCKETS 10 +#define HASH_GRID_DIVISIONS 2 + +namespace v2 { +typedef uint8_t PointHash; + +struct Particle { + bool active{true}; + sf::CircleShape shape; + sf::Vector2f velocity; + sf::Vector2f solveMotion; + sf::Vector2f acceleration; + PointHash spatialHash; +}; + +sf::FloatRect worldBounds{}; +std::array, HASH_BUCKETS> buckets{}; //!< Hash buckets; grid of 16x16 cells stretched over the bounding area +std::vector particles{}; +std::mutex mtx{}; + +// hashing strategy: +// A 2 grid of rectangles divided evenly over the world's bounding box up to 16 * 16 buckets. +// A point is hashed by converting from world space coordinates to floored grid coordinate. +// To look it up, simply use the pointhash to index into the `buckets` array. +PointHash hashPoint(sf::Vector2f vec, sf::FloatRect bounds) { + // least- and most- significant four bits + uint8_t ls4(uint8_t(std::floor(((vec.y - bounds.position.y) / bounds.size.y) * HASH_GRID_DIVISIONS)) & 0xF); + uint8_t ms4(uint8_t(std::floor(((vec.x - bounds.position.x) / bounds.size.x) * HASH_GRID_DIVISIONS)) & 0xF); + return (ls4 | (ms4 << 4u)); +} + +sf::Vector2f particleTopLeft(Particle &particle) { + return particle.shape.getPosition() - sf::Vector2f{particle.shape.getRadius(), particle.shape.getRadius()}; +} + +PointHash hashParticlePosition(Particle &particle) { + return hashPoint(particleTopLeft(particle), worldBounds); +} + +void updateParticleHash(Particle &particle) { + PointHash previous{particle.spatialHash}; + particle.spatialHash = hashParticlePosition(particle); + if (particle.spatialHash != previous) { + buckets.at(previous % HASH_BUCKETS).erase(&particle); + buckets.at(particle.spatialHash % HASH_BUCKETS).emplace(&particle); + } +} + +void initialize(size_t particleCount, sf::FloatRect bounds) { + worldBounds = bounds; + + std::mt19937 rng{std::mt19937()}; + std::uniform_real_distribution positionDist{std::uniform_real_distribution(5.0f, 795.0f)}; + std::uniform_real_distribution velocityDist{std::uniform_real_distribution(-200.0f, 200.0f)}; + std::uniform_int_distribution colorDist{std::uniform_int_distribution(0, 255)}; + std::uniform_real_distribution radiusDist{std::uniform_real_distribution(2.5f, 2.5f)}; + particles.reserve(particleCount); + for (size_t _ : std::ranges::views::iota(0zu, particleCount)) { + particles.emplace_back(true, + sf::CircleShape(radiusDist(rng)), + sf::Vector2f{0, 0}); + Particle &particle{particles[particles.size() - 1]}; + particle.shape.setPosition({positionDist(rng), positionDist(rng)}); + particle.velocity = {velocityDist(rng), velocityDist(rng)}; + particle.spatialHash = hashParticlePosition(particle); + particle.shape.setFillColor({colorDist(rng), colorDist(rng), colorDist(rng), 255}); + particle.shape.setOrigin({particle.shape.getRadius(), particle.shape.getRadius()}); + } +} + +sf::Vector2f reflect(sf::Vector2f x, sf::Vector2f normal, float bounce = 1.0) { + float const dot{x.dot(normal)}; + if (dot > 0) + return x; + return x - (normal * -std::abs(dot) * (1 + bounce)); +} + +void particleSolveOverlap(Particle &self, Particle const &other) { + sf::Vector2f const difference{self.shape.getPosition() - other.shape.getPosition()}; + float const targetDistance{self.shape.getRadius() + other.shape.getRadius()}; + float const currentDistance{difference.length()}; + if (currentDistance < targetDistance && currentDistance > 0) { + sf::Vector2f const collisionNormal{difference.normalized()}; + float const requiredMotion{targetDistance - currentDistance}; + self.solveMotion += requiredMotion * collisionNormal * 0.5f; + self.acceleration += reflect(self.velocity, collisionNormal) - self.velocity; + } +} + +void particleSolveOverlaps(Particle &self) { + sf::Vector2f position{self.shape.getPosition()}; + sf::Vector2f offset{worldBounds.size.x / HASH_GRID_DIVISIONS, worldBounds.size.y / HASH_GRID_DIVISIONS}; + PointHash neighbours[4]{ + self.spatialHash, + hashPoint(particleTopLeft(self) + sf::Vector2f{offset.x, 0}, worldBounds), + hashPoint(particleTopLeft(self) + sf::Vector2f{offset.x, offset.y}, worldBounds), + hashPoint(particleTopLeft(self) + sf::Vector2f{0, offset.y}, worldBounds)}; + for (PointHash neighbour : neighbours) { + for (Particle *possible : buckets[neighbour % HASH_BUCKETS]) { + if (possible == &self) { + continue; + } else { + particleSolveOverlap(self, *possible); + } + } + } + if (position.x - self.shape.getRadius() < worldBounds.position.x) { + self.solveMotion.x += worldBounds.position.x - (position.x - self.shape.getRadius()); + self.acceleration += reflect(self.velocity, {1, 0}) - self.velocity; + } + if (position.x + self.shape.getRadius() > worldBounds.position.x + worldBounds.size.x) { + self.solveMotion.x += (worldBounds.position.x + worldBounds.size.x) - (position.x + self.shape.getRadius()); + self.acceleration += reflect(self.velocity, {-1, 0}) - self.velocity; + } + if (position.y - self.shape.getRadius() < worldBounds.position.y) { + self.solveMotion.y += worldBounds.position.y - (position.y - self.shape.getRadius()); + self.acceleration += reflect(self.velocity, {0, 1}) - self.velocity; + } + if (position.y + self.shape.getRadius() > worldBounds.position.y + worldBounds.size.y) { + self.solveMotion.y += (worldBounds.position.y + worldBounds.size.y) - (position.y + self.shape.getRadius()); + self.acceleration += reflect(self.velocity, {0, -1}) - self.velocity; + } +} + +void tick(double delta) { + for (Particle &particle : particles) { + if (!particle.active) { + return; + } + particleSolveOverlaps(particle); + } + + for (Particle &particle : particles) { + if (particle.active) { + // apply velocity and combined solve motion + particle.shape.move(particle.solveMotion); + particle.shape.move(particle.velocity * float(delta)); + particle.velocity += particle.acceleration; + particle.acceleration = particle.solveMotion = {0, 0}; // reset solve motion and acceleration + updateParticleHash(particle); // update spatial hash + } + } +} + +void draw(sf::RenderTarget *target) { + sf::RectangleShape rect{worldBounds.size}; + rect.setPosition(worldBounds.position); + rect.setFillColor(sf::Color::Transparent); + rect.setOutlineColor(sf::Color::White); + rect.setOutlineThickness(3); + target->draw(rect); + sf::Vertex polygon[2]; + for (Particle &particle : particles) { + if (particle.active) { + target->draw(particle.shape); +#if 0 + polygon[0].position = particle.shape.getPosition(); + polygon[1].position = particle.shape.getPosition() + particle.velocity; + target->draw(polygon, 2, sf::PrimitiveType::Lines); +#endif + } + } +} +} // namespace v2 diff --git a/src/simulation_v2.h b/src/simulation_v2.h new file mode 100644 index 0000000..8b30997 --- /dev/null +++ b/src/simulation_v2.h @@ -0,0 +1,11 @@ +#pragma once + +#include "SFML/Graphics/Rect.hpp" +#include +#include + +namespace v2 { +extern void initialize(size_t particleCount, sf::FloatRect bounds); +extern void tick(double delta); +extern void draw(sf::RenderTarget *window); +} // namespace v2