From 8d5a6a827cb45b4da4fe1ccc48f407c1972ae975 Mon Sep 17 00:00:00 2001 From: Christoph Urlacher Date: Tue, 17 Feb 2026 15:12:32 +0100 Subject: [PATCH] add basic input handling for klotski board/graph + populate graph based on klotski moves --- include/config.hpp | 6 ++ include/mass_springs.hpp | 18 ++++-- include/renderer.hpp | 9 ++- src/main.cpp | 136 +++++++++++++++++++++++++++++++++------ src/mass_springs.cpp | 41 ++++++++---- src/renderer.cpp | 101 +++++++++++++++++++++++++---- 6 files changed, 255 insertions(+), 56 deletions(-) diff --git a/include/config.hpp b/include/config.hpp index c730ee4..91d439e 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -13,12 +13,18 @@ constexpr Color EDGE_COLOR = DARKGREEN; constexpr float SIM_SPEED = 4.0; constexpr float ROTATION_SPEED = 1.0; constexpr float CAMERA_DISTANCE = 2.2; +constexpr float CULLING_TOLERANCE = 0.1; // percentage constexpr float DEFAULT_SPRING_CONSTANT = 1.5; constexpr float DEFAULT_DAMPENING_CONSTANT = 0.1; constexpr float DEFAULT_REST_LENGTH = 0.5; +constexpr float DEFAULT_REPULSION_FORCE = 0.002; constexpr int BOARD_PADDING = 5; constexpr int BLOCK_PADDING = 5; +constexpr Color BLOCK_COLOR = DARKGREEN; +constexpr Color HL_BLOCK_COLOR = GREEN; +constexpr Color TARGET_BLOCK_COLOR = RED; +constexpr Color HL_TARGET_BLOCK_COLOR = ORANGE; #endif diff --git a/include/mass_springs.hpp b/include/mass_springs.hpp index 6b28899..306eb1d 100644 --- a/include/mass_springs.hpp +++ b/include/mass_springs.hpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include class Mass { @@ -88,7 +90,7 @@ using SpringList = std::vector; class MassSpringSystem { public: - MassList masses; + std::unordered_map masses; SpringList springs; public: @@ -102,19 +104,23 @@ public: ~MassSpringSystem() {}; public: - auto AddMass(float mass, Vector3 position, bool fixed) -> void; + auto AddMass(float mass, Vector3 position, bool fixed, std::string state) + -> void; - auto GetMass(const size_t index) -> Mass &; + auto GetMass(const std::string &state) -> Mass &; - auto AddSpring(int massA, int massB, float spring_constant, - float dampening_constant, float rest_length) -> void; + auto AddSpring(const std::string &massA, const std::string &massB, + float spring_constant, float dampening_constant, + float rest_length) -> void; - auto GetSpring(const size_t index) -> Spring &; + auto Clear() -> void; auto ClearForces() -> void; auto CalculateSpringForces() -> void; + auto CalculateRepulsionForces() -> void; + auto EulerUpdate(const float delta_time) -> void; auto VerletUpdate(const float delta_time) -> void; diff --git a/include/renderer.hpp b/include/renderer.hpp index c02f332..ea42f96 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -42,7 +42,8 @@ private: auto Rotate(const Vector3 &a, const float cos_angle, const float sin_angle) -> Vector3; - auto Translate(const Vector3 &a, const float distance) -> Vector3; + auto Translate(const Vector3 &a, const float distance, const float horizontal, + const float vertical) -> Vector3; auto Project(const Vector3 &a) -> Vector2; @@ -51,12 +52,14 @@ private: public: auto Transform(Edge2Set &edges, Vertex2Set &vertices, const MassSpringSystem &mass_springs, const float angle, - const float distance) -> void; + const float distance, const float horizontal, + const float vertical) -> void; auto DrawMassSprings(const Edge2Set &edges, const Vertex2Set &vertices) -> void; - auto DrawKlotski(State &state) -> void; + auto DrawKlotski(State &state, int hov_x, int hov_y, int sel_x, int sel_y) + -> void; auto DrawTextures() -> void; }; diff --git a/src/main.cpp b/src/main.cpp index 717c204..dac0284 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,6 +9,33 @@ #include "mass_springs.hpp" #include "renderer.hpp" +auto klotski_a() -> State { + State s = State(4, 5); + Block a = Block(0, 0, 1, 2, false); + Block b = Block(1, 0, 2, 2, true); + Block c = Block(3, 0, 1, 2, false); + Block d = Block(0, 2, 1, 2, false); + Block e = Block(1, 2, 2, 1, false); + Block f = Block(3, 2, 1, 2, false); + Block g = Block(1, 3, 1, 1, false); + Block h = Block(2, 3, 1, 1, false); + Block i = Block(0, 4, 1, 1, false); + Block j = Block(3, 4, 1, 1, false); + + s.AddBlock(a); + s.AddBlock(b); + s.AddBlock(c); + s.AddBlock(d); + s.AddBlock(e); + s.AddBlock(f); + s.AddBlock(g); + s.AddBlock(h); + s.AddBlock(i); + s.AddBlock(j); + + return s; +} + auto main(int argc, char *argv[]) -> int { // if (argc < 2) { // std::cout << "Missing .klotski file." << std::endl; @@ -23,50 +50,117 @@ auto main(int argc, char *argv[]) -> int { InitWindow(WIDTH * 2, HEIGHT, "MassSprings"); + // Mass springs configuration MassSpringSystem mass_springs; - mass_springs.AddMass(1.0, Vector3(-0.5, 0.5, 0.0), true); - mass_springs.AddMass(1.0, Vector3(0.5, 0.5, 0.0), false); - mass_springs.AddMass(1.0, Vector3(0.5, 0.0, 0.0), false); - mass_springs.AddSpring(0, 1, DEFAULT_SPRING_CONSTANT, - DEFAULT_DAMPENING_CONSTANT, DEFAULT_REST_LENGTH); - mass_springs.AddSpring(1, 2, DEFAULT_SPRING_CONSTANT, - DEFAULT_DAMPENING_CONSTANT, DEFAULT_REST_LENGTH); - State s = State(4, 5); - Block a = Block(0, 0, 2, 1, false); - Block b = Block(0, 1, 1, 3, true); - Block c = Block(0, 2, "45"); - Block d = Block(0, 3, "de"); - - s.AddBlock(a); - s.AddBlock(b); - for (Block block : s) { - std::cout << "Block (" << block.x << ", " << block.y << ")" << std::endl; - } + // Klotski configuration + State board = klotski_a(); + mass_springs.AddMass(1.0, Vector3Zero(), true, board.state); + // Rendering configuration Renderer renderer(WIDTH, HEIGHT); Edge2Set edges; edges.reserve(mass_springs.springs.size()); Vertex2Set vertices; vertices.reserve(mass_springs.masses.size()); + // Game loop + float camera_distance = CAMERA_DISTANCE; + float horizontal = 0.0; + float vertical = 0.0; float frametime; float abstime = 0.0; + int hov_x = 0; + int hov_y = 0; + int sel_x = 0; + int sel_y = 0; while (!WindowShouldClose()) { - frametime = GetFrameTime(); + + // Mouse handling + Vector2 m = GetMousePosition(); + float block_size; + float x_offset = 0.0; + float y_offset = 0.0; + if (board.width > board.height) { + block_size = static_cast(WIDTH) / board.width; + y_offset = (HEIGHT - block_size * board.height) / 2.0; + } else { + block_size = static_cast(HEIGHT) / board.height; + x_offset = (WIDTH - block_size * board.width) / 2.0; + } + if (m.x < x_offset) { + hov_x = 100; + } else { + hov_x = (m.x - x_offset) / block_size; + } + if (m.y < y_offset) { + hov_y = 100; + } else { + hov_y = (m.y - y_offset) / block_size; + } + if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + sel_x = hov_x; + sel_y = hov_y; + } + camera_distance += GetMouseWheelMove() / -10.0; + + // Key handling + std::string previous_state = board.state; + if (IsKeyPressed(KEY_W)) { + if (board.MoveBlockAt(sel_x, sel_y, Direction::NOR)) { + sel_y--; + } + } else if (IsKeyPressed(KEY_A)) { + if (board.MoveBlockAt(sel_x, sel_y, Direction::WES)) { + sel_x--; + } + } else if (IsKeyPressed(KEY_S)) { + if (board.MoveBlockAt(sel_x, sel_y, Direction::SOU)) { + sel_y++; + } + } else if (IsKeyPressed(KEY_D)) { + if (board.MoveBlockAt(sel_x, sel_y, Direction::EAS)) { + sel_x++; + } + } + // TODO: Need to check for duplicate springs + if (previous_state != board.state) { + mass_springs.AddMass( + 1.0, + Vector3(static_cast(GetRandomValue(-1000, 1000)) / 1000.0, + static_cast(GetRandomValue(-1000, 1000)) / 1000.0, + static_cast(GetRandomValue(-1000, 1000)) / 1000.0), + false, board.state); + mass_springs.AddSpring(board.state, previous_state, + DEFAULT_SPRING_CONSTANT, + DEFAULT_DAMPENING_CONSTANT, DEFAULT_REST_LENGTH); + } + if (IsKeyPressed(KEY_UP)) { + vertical += 0.1; + } else if (IsKeyPressed(KEY_RIGHT)) { + horizontal += 0.1; + } else if (IsKeyPressed(KEY_DOWN)) { + vertical -= 0.1; + } else if (IsKeyPressed(KEY_LEFT)) { + horizontal -= 0.1; + } + + // Physics update mass_springs.ClearForces(); mass_springs.CalculateSpringForces(); + mass_springs.CalculateRepulsionForces(); #ifdef VERLET_UPDATE mass_springs.VerletUpdate(frametime * SIM_SPEED); #else mass_springs.EulerUpdate(frametime * SIM_SPEED); #endif + // Rendering renderer.Transform(edges, vertices, mass_springs, abstime * ROTATION_SPEED, - CAMERA_DISTANCE); + camera_distance, horizontal, vertical); renderer.DrawMassSprings(edges, vertices); - renderer.DrawKlotski(s); // TODO: Don't need to render each frame + renderer.DrawKlotski(board, hov_x, hov_y, sel_x, sel_y); renderer.DrawTextures(); abstime += frametime; diff --git a/src/mass_springs.cpp b/src/mass_springs.cpp index ee3d41e..02e41df 100644 --- a/src/mass_springs.cpp +++ b/src/mass_springs.cpp @@ -1,4 +1,5 @@ #include "mass_springs.hpp" +#include "config.hpp" #include #include @@ -72,28 +73,33 @@ auto Spring::CalculateSpringForce() -> void { massB.force = Vector3Add(massB.force, force_b); } -auto MassSpringSystem::AddMass(float mass, Vector3 position, bool fixed) - -> void { - masses.emplace_back(mass, position, fixed); +auto MassSpringSystem::AddMass(float mass, Vector3 position, bool fixed, + std::string state) -> void { + if (!masses.contains(state)) { + masses.insert(std::make_pair(state, Mass(mass, position, fixed))); + } } -auto MassSpringSystem::GetMass(const size_t index) -> Mass & { - return masses[index]; +auto MassSpringSystem::GetMass(const std::string &state) -> Mass & { + return masses.at(state); } -auto MassSpringSystem::AddSpring(int massA, int massB, float spring_constant, +auto MassSpringSystem::AddSpring(const std::string &massA, + const std::string &massB, + float spring_constant, float dampening_constant, float rest_length) -> void { - springs.emplace_back(masses[massA], masses[massB], spring_constant, + springs.emplace_back(GetMass(massA), GetMass(massB), spring_constant, dampening_constant, rest_length); } -auto MassSpringSystem::GetSpring(const size_t index) -> Spring & { - return springs[index]; +auto MassSpringSystem::Clear() -> void { + masses.clear(); + springs.clear(); } auto MassSpringSystem::ClearForces() -> void { - for (auto &mass : masses) { + for (auto &[state, mass] : masses) { mass.ClearForce(); } } @@ -104,15 +110,26 @@ auto MassSpringSystem::CalculateSpringForces() -> void { } } +auto MassSpringSystem::CalculateRepulsionForces() -> void { + for (auto &[state, mass] : masses) { + for (auto &[s, m] : masses) { + Vector3 dx = Vector3Subtract(mass.position, m.position); + mass.force = + Vector3Add(mass.force, Vector3Scale(Vector3Normalize(dx), + DEFAULT_REPULSION_FORCE)); + } + } +} + auto MassSpringSystem::EulerUpdate(const float delta_time) -> void { - for (auto &mass : masses) { + for (auto &[state, mass] : masses) { mass.CalculateVelocity(delta_time); mass.CalculatePosition(delta_time); } } auto MassSpringSystem::VerletUpdate(const float delta_time) -> void { - for (auto &mass : masses) { + for (auto &[state, mass] : masses) { mass.VerletUpdate(delta_time); } } diff --git a/src/renderer.cpp b/src/renderer.cpp index b0f9fc1..b9ebaba 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -10,8 +10,10 @@ auto Renderer::Rotate(const Vector3 &a, const float cos_angle, a.x * sin_angle + a.z * cos_angle); }; -auto Renderer::Translate(const Vector3 &a, const float distance) -> Vector3 { - return Vector3(a.x, a.y, a.z + distance); +auto Renderer::Translate(const Vector3 &a, const float distance, + const float horizontal, const float vertical) + -> Vector3 { + return Vector3(a.x + horizontal, a.y + vertical, a.z + distance); }; auto Renderer::Project(const Vector3 &a) -> Vector2 { @@ -24,27 +26,72 @@ auto Renderer::Map(const Vector2 &a) -> Vector2 { auto Renderer::Transform(Edge2Set &edges, Vertex2Set &vertices, const MassSpringSystem &mass_springs, - const float angle, const float distance) -> void { + const float angle, const float distance, + const float horizontal, const float vertical) -> void { const float cos_angle = cos(angle); const float sin_angle = sin(angle); edges.clear(); for (const auto &spring : mass_springs.springs) { - Vector2 a = Map(Project(Translate( - Rotate(spring.massA.position, cos_angle, sin_angle), distance))); - Vector2 b = Map(Project(Translate( - Rotate(spring.massB.position, cos_angle, sin_angle), distance))); + const Mass &massA = spring.massA; + const Mass &massB = spring.massB; + + // Stuff behind the camera + if (massA.position.z + distance <= 0.1) { + continue; + } + if (massB.position.z + distance <= 0.1) { + continue; + } + + Vector2 a = + Map(Project(Translate(Rotate(massA.position, cos_angle, sin_angle), + distance, horizontal, vertical))); + Vector2 b = + Map(Project(Translate(Rotate(massB.position, cos_angle, sin_angle), + distance, horizontal, vertical))); + + // Stuff outside the viewport + if (!CheckCollisionPointRec( + a, Rectangle(-1.0 * width * CULLING_TOLERANCE, + -1.0 * height * CULLING_TOLERANCE, + width + width * CULLING_TOLERANCE * 2.0, + height + height * CULLING_TOLERANCE * 2.0))) { + continue; + } + if (!CheckCollisionPointRec( + b, Rectangle(-1.0 * width * CULLING_TOLERANCE, + -1.0 * height * CULLING_TOLERANCE, + width + width * CULLING_TOLERANCE * 2.0, + height + height * CULLING_TOLERANCE * 2.0))) { + continue; + } edges.emplace_back(a, b); } // This is duplicated work, but easy to read vertices.clear(); - for (const auto &mass : mass_springs.masses) { - Vector3 a = - Translate(Rotate(mass.position, cos_angle, sin_angle), distance); + for (const auto &[state, mass] : mass_springs.masses) { + + // Stuff behind the camera + if (mass.position.z + distance <= 0.1) { + continue; + } + + Vector3 a = Translate(Rotate(mass.position, cos_angle, sin_angle), distance, + horizontal, vertical); Vector2 b = Map(Project(a)); + // Stuff outside the viewport + if (!CheckCollisionPointRec( + b, Rectangle(-1.0 * width * CULLING_TOLERANCE, + -1.0 * height * CULLING_TOLERANCE, + width + width * CULLING_TOLERANCE * 2.0, + height + height * CULLING_TOLERANCE * 2.0))) { + continue; + } + vertices.emplace_back(b.x, b.y, a.z); } } @@ -53,20 +100,26 @@ auto Renderer::DrawMassSprings(const Edge2Set &edges, const Vertex2Set &vertices) -> void { BeginTextureMode(render_target); ClearBackground(RAYWHITE); + + // Draw springs for (const auto &[a, b] : edges) { DrawLine(a.x, a.y, b.x, b.y, EDGE_COLOR); } + + // Draw masses for (const auto &a : vertices) { // Increase the perspective perception by squaring the z-coordinate - const float size = VERTEX_SIZE / (a.z * a.z); + const float size = Clamp(VERTEX_SIZE / (a.z * a.z), 0.1, 100.0); DrawRectangle(a.x - size / 2.0, a.y - size / 2.0, size, size, VERTEX_COLOR); } + DrawLine(0, 0, 0, height, BLACK); EndTextureMode(); } -auto Renderer::DrawKlotski(State &state) -> void { +auto Renderer::DrawKlotski(State &state, int hov_x, int hov_y, int sel_x, + int sel_y) -> void { BeginTextureMode(klotski_target); ClearBackground(RAYWHITE); @@ -106,9 +159,16 @@ auto Renderer::DrawKlotski(State &state) -> void { // Draw Blocks for (Block block : state) { - Color c = EDGE_COLOR; + Color c = BLOCK_COLOR; + if (block.Covers(sel_x, sel_y)) { + c = HL_BLOCK_COLOR; + } if (block.target) { - c = RED; + if (block.Covers(sel_x, sel_y)) { + c = HL_TARGET_BLOCK_COLOR; + } else { + c = TARGET_BLOCK_COLOR; + } } DrawRectangle(x_offset + BOARD_PADDING + block.x * BLOCK_PADDING * 2 + BLOCK_PADDING + block.x * block_size, @@ -119,6 +179,19 @@ auto Renderer::DrawKlotski(State &state) -> void { block.height * block_size + block.height * 2 * BLOCK_PADDING - 2 * BLOCK_PADDING, c); + + if (block.Covers(hov_x, hov_y)) { + DrawRectangleLinesEx( + Rectangle(x_offset + BOARD_PADDING + block.x * BLOCK_PADDING * 2 + + BLOCK_PADDING + block.x * block_size, + y_offset + BOARD_PADDING + block.y * BLOCK_PADDING * 2 + + BLOCK_PADDING + block.y * block_size, + block.width * block_size + block.width * 2 * BLOCK_PADDING - + 2 * BLOCK_PADDING, + block.height * block_size + + block.height * 2 * BLOCK_PADDING - 2 * BLOCK_PADDING), + 2.0, BLACK); + } } DrawLine(width - 1, 0, width - 1, height, BLACK);