feat: implemented simulation v2 and glue

This commit is contained in:
Sara Gerretsen 2025-10-28 21:46:48 +01:00
parent fc74361bb3
commit 5c67286175
5 changed files with 298 additions and 24 deletions

43
src/Benchmarker.cpp Normal file
View file

@ -0,0 +1,43 @@
#include "Benchmarker.h"
#include <SFML/System/Time.hpp>
#include <print>
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;
}

26
src/Benchmarker.h Normal file
View file

@ -0,0 +1,26 @@
#ifndef BENCHMARKER_H
#define BENCHMARKER_H
#include <SFML/System/Clock.hpp>
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

View file

@ -1,4 +1,7 @@
#include "Balls.hpp"
#include "Benchmarker.h"
#include "defs.h"
#include "simulation_v2.h"
#include <SFML/Graphics.hpp>
#include <SFML/Graphics/Rect.hpp>
#include <SFML/Graphics/RectangleShape.hpp>
@ -9,15 +12,18 @@
#include <imgui.h>
#include <print>
#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() {

181
src/simulation_v2.cpp Normal file
View file

@ -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 <SFML/Graphics/CircleShape.hpp>
#include <array>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <mutex>
#include <print>
#include <random>
#include <ranges>
#include <set>
#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<std::set<Particle *>, HASH_BUCKETS> buckets{}; //!< Hash buckets; grid of 16x16 cells stretched over the bounding area
std::vector<Particle> 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<float> positionDist{std::uniform_real_distribution<float>(5.0f, 795.0f)};
std::uniform_real_distribution<float> velocityDist{std::uniform_real_distribution<float>(-200.0f, 200.0f)};
std::uniform_int_distribution<uint8_t> colorDist{std::uniform_int_distribution<uint8_t>(0, 255)};
std::uniform_real_distribution<float> radiusDist{std::uniform_real_distribution<float>(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

11
src/simulation_v2.h Normal file
View file

@ -0,0 +1,11 @@
#pragma once
#include "SFML/Graphics/Rect.hpp"
#include <SFML/Graphics.hpp>
#include <SFML/System/Vector2.hpp>
namespace v2 {
extern void initialize(size_t particleCount, sf::FloatRect bounds);
extern void tick(double delta);
extern void draw(sf::RenderTarget *window);
} // namespace v2