From 02a2ba28182fab8580ea52014fbd2a1a2f48afaf Mon Sep 17 00:00:00 2001 From: Jan Mrna Date: Mon, 29 Sep 2025 14:15:08 +0200 Subject: [PATCH 01/23] Window member variables set as private --- cpp/src/window.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/src/window.hpp b/cpp/src/window.hpp index b582b53..8813747 100644 --- a/cpp/src/window.hpp +++ b/cpp/src/window.hpp @@ -30,11 +30,11 @@ public: void DrawCircle(const WorldPos &position, float radius); void DrawLine(const WorldPos &A, const WorldPos &B); +private: + uint32_t m_Width; + uint32_t m_Height; std::shared_ptr m_Renderer = nullptr; SDL_Window *m_Window; SDL_GLContext m_Context; -private: - uint32_t m_Width; - uint32_t m_Height; }; From 1defbe8a0085a63ffdbdb60c632ee4907064c117 Mon Sep 17 00:00:00 2001 From: Jan Mrna Date: Tue, 30 Sep 2025 14:00:08 +0200 Subject: [PATCH 02/23] Moved TODOs from Readme to GitHub project --- README.md | 52 ---------------------------------------------------- 1 file changed, 52 deletions(-) diff --git a/README.md b/README.md index 3c3bdf0..c95561a 100644 --- a/README.md +++ b/README.md @@ -52,55 +52,3 @@ make -j $(nproc) Run the `pathfinding` binary in the [cpp](./cpp/) folder. -## TODO - -- [x] python - - [x] get jupyter lab running - - [x] drawing utility - - [x] interface for pathfinding - - [x] research methods - - [x] implement methods - - [x] DFS - - [x] BFS - - [x] Dijsktra - - [x] GBFS - - [x] A* - - [x] performance measurement: time/visited nodes - - [x] finalize the script and copy back to the jupyter notebook - - [x] finish text on the page - - [x] create a dedicated python script -- [ ] C++ - - [x] re-use 2D game engine - - [x] add tiles (with cost) to map - - [x] conversion functions from tile coords to world coords - - [x] drawing tiles - - [x] add "terrain tiles" with different costs - - [x] add mouse-click action - - [x] add direct movement (through mouse click action, no pathfinding) - - [x] implement pathfinding - - [x] BFS - - [x] GBFS - - [x] Dijkstra - - [ ] A* - - [ ] windows build? - - [x] VS solution - - [ ] merge to master - - [ ] cmake? - - [x] add screenshot - - [ ] zoom + pan of the map - - [ ] maze generator? - - [ ] collisions - - [ ] multiple units - - change from single unit (player) to RTS-style multiple units - - [ ] unit selection - - selection rectangle? - - [ ] pathfinding for multiple units - - [ ] pathfinding for multiple units - - group formation, local cohesion, etc - - [ ] cpython interface - - control the game through the interpreter - - [ ] clang-format config - - [ ] git hooks? - - [ ] [gcovr](https://gcovr.com/en/stable/) - - [ ] [clang-tidy](https://clang.llvm.org/extra/clang-tidy/) - From 8e00c1fed384f6c6009b8c3543bcfc5073d62dbc Mon Sep 17 00:00:00 2001 From: Jan Mrna Date: Tue, 30 Sep 2025 14:23:00 +0200 Subject: [PATCH 03/23] Added .aider to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index fcf12b2..7f1c441 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ tags python/.ipynb_checkpoints cpp/build cpp/pathfinding +.aider* From a022c3321e5cd85932dc462d2de19cd85340f6b7 Mon Sep 17 00:00:00 2001 From: Jan Mrna Date: Thu, 2 Oct 2025 17:56:37 +0200 Subject: [PATCH 04/23] Add test target --- cpp/Makefile | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cpp/Makefile b/cpp/Makefile index bb0b33e..eeca937 100644 --- a/cpp/Makefile +++ b/cpp/Makefile @@ -9,16 +9,23 @@ LDLIBS := -lSDL3 -lSDL3_image -lGLEW -lGL SRC_DIR := src BUILD_DIR:= build TARGET := pathfinding +TEST_TARGET := unittest #---------------------------------- SOURCES := $(shell find $(SRC_DIR) -name '*.cpp') OBJECTS := $(SOURCES:$(SRC_DIR)/%.cpp=$(BUILD_DIR)/%.o) #---------------------------------- -.PHONY: all clean +.PHONY: all clean test all: $(TARGET) +test: $(TEST_TARGET) + ./$(TEST_TARGET) + +$(TEST_TARGET): test/test.cpp + $(CXX) -std=c++23 -lgtest -Isrc -o $@ $< + # link step $(TARGET): $(OBJECTS) $(CXX) $(LDFLAGS) -o $@ $^ $(LDLIBS) @@ -32,4 +39,4 @@ $(BUILD_DIR): mkdir -p $@ clean: - rm -rf $(BUILD_DIR) $(TARGET) + rm -rf $(BUILD_DIR) $(TARGET) $(TEST_TARGET) From 0e17c84eb9b54e79eee6dd7723b407bda2ef0633 Mon Sep 17 00:00:00 2001 From: Jan Mrna Date: Thu, 2 Oct 2025 19:29:08 +0200 Subject: [PATCH 05/23] WIP new vec class and tests --- cpp/src/math.hpp | 204 ++++++++++++++++++++ cpp/test/test.cpp | 468 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 558 insertions(+), 114 deletions(-) diff --git a/cpp/src/math.hpp b/cpp/src/math.hpp index f7e5f5e..ad5e5c4 100644 --- a/cpp/src/math.hpp +++ b/cpp/src/math.hpp @@ -6,6 +6,209 @@ #include #include #include +#include +#include +#include + + +template +requires std::floating_point +static inline bool equalEpsilon(const T& a, const T& b) +{ + constexpr auto epsilon = [](){ + if constexpr (std::is_same_v) { + return T{1e-5}; + } else { + return T{1e-12}; // double, long double + } + }(); + if (a == b) { + // handle special cases: bit equality, Inf... + return true; + } + return std::abs(a - b) < epsilon; +} + + +template +class vec { +public: + vec() : m_Array{} {} + + template + requires (std::same_as && ...) && (sizeof...(ArgsT) == N) + vec(ArgsT... args) : m_Array{args...} {} + + const T& operator[](size_t index) const + { + // we leave run-time checks to the underlying std::array + return m_Array[index]; + } + + T& operator[](size_t index) + { + // we leave run-time checks to the underlying std::array + return m_Array[index]; + } + + friend std::ostream &operator<<(std::ostream &os, const vec &obj) + { + os << "( "; + for (const auto& element : obj.m_Array) { + os << element << " "; + } + os << ")"; + return os; + } + + + // + // binary operators + // + + friend bool operator==(const vec& a, const vec& b) + { + for (const auto& [u, v] : std::views::zip(a.m_Array,b.m_Array)) { + if (!equalEpsilon(u, v)) { + return false; + } + } + return true; + } + + friend bool operator!=(const vec& a, const vec& b) + { + return !(a == b); + } + + friend vec operator+(const vec& a, const vec& b) + { + vec c; + std::ranges::transform(a.m_Array, b.m_Array, c.m_Array.begin(), std::plus{}); + return c; + } + + friend vec operator-(const vec& a, const vec& b) + { + vec c; + std::ranges::transform(a.m_Array, b.m_Array, c.m_Array.begin(), std::minus{}); + return c; + } + + friend vec operator*(const vec& a, const T& scalar) + { + vec c; + std::ranges::transform(a.m_Array, std::views::repeat(scalar), c.m_Array.begin(), std::multiplies{}); + return c; + } + + friend vec operator*(const T& scalar, const vec& a) { + return a * scalar; + } + + friend vec operator/(const vec& a, const T& scalar) + { + vec c; + std::ranges::transform(a.m_Array, std::views::repeat(scalar), c.m_Array.begin(), std::divides{}); + return c; + } + + // + // compound-assignment operators + // + + vec& operator+=(const vec& b) + { + vec& a = *this; + std::ranges::transform(a.m_Array, b.m_Array, a.m_Array.begin(), std::plus{}); + return a; + } + + vec& operator-=(const vec& b) + { + vec& a = *this; + std::ranges::transform(a.m_Array, b.m_Array, a.m_Array.begin(), std::minus{}); + return a; + } + + + // + // Utility functions + // + + T LengthSquared() const + { + return std::transform_reduce( + m_Array.begin(), m_Array.end(), + T{0.0}, + std::plus{}, + [](T x){ + return x*x; + } + ); + } + + T Length() const + { + return std::sqrt(LengthSquared()); + } + + // + // In-place vector operations + // + + void Normalize() + { + T length = Length(); + if (equalEpsilon(length, T{0})) + return; + std::ranges::transform(m_Array, std::views::repeat(length), m_Array.begin(), std::divides{}); + } + + + // + // Methods returning new object + // + + vec GetNormalized() const + { + vec tmp = *this; + tmp.Normalize(); + return tmp; + } + + vec GetOrthogonal() const requires (N == 2) + { + vec tmp = *this; + + std::swap(tmp.m_Array[0], tmp.m_Array[1]); + tmp.m_Array[0] *= -1; + + return tmp; + } + +// const T& x = m_Array[0]; +// const T& y = m_Array[1]; +// const T& z = m_Array[2]; + + +private: + std::array m_Array; +}; + + +using vec2 = vec; +using vec3 = vec; +using vec4 = vec; +using dvec2 = vec; +using dvec3 = vec; +using dvec4 = vec; +using ivec2 = vec; +using ivec3 = vec; +using ivec4 = vec; +using uvec2 = vec; +using uvec3 = vec; +using uvec4 = vec; constexpr double EQUALITY_LIMIT = 1e-6; @@ -132,6 +335,7 @@ public: using TilePos = Vec2D; using WorldPos = Vec2D; +using WindowPos = Vec2D; struct TilePosHash { std::size_t operator()(const TilePos& p) const noexcept { diff --git a/cpp/test/test.cpp b/cpp/test/test.cpp index 403ed83..b4b2ae5 100644 --- a/cpp/test/test.cpp +++ b/cpp/test/test.cpp @@ -5,151 +5,391 @@ #include #include -#include "array.hpp" #include "log.hpp" #include "math.hpp" -// Vec2D Tests -TEST(Vec2D, DefaultConstruction) { - Vec2D v; - // Default values are uninitialized, but we can test basic functionality - v.x = 0; - v.y = 0; - ASSERT_EQ(v.x, 0); - ASSERT_EQ(v.y, 0); + +TEST(vec, DefaultConstruction) { + // Test that default-constucted vector + // has all elements equal to zero + vec3 v1; + ASSERT_EQ(v1[0], 0.0); + ASSERT_EQ(v1[1], 0.0); + ASSERT_EQ(v1[2], 0.0); } -TEST(Vec2D, InitializerListConstruction) { - Vec2D v{3, 4}; - ASSERT_EQ(v.x, 3); - ASSERT_EQ(v.y, 4); - - Vec2D vf{1.5f, 2.5f}; - ASSERT_FLOAT_EQ(vf.x, 1.5f); - ASSERT_FLOAT_EQ(vf.y, 2.5f); - - // Test type conversion - Vec2D vd{1, 2}; // int to float - ASSERT_FLOAT_EQ(vd.x, 1.0f); - ASSERT_FLOAT_EQ(vd.y, 2.0f); +TEST(vec, GetElements) { + // Test operator[] + ivec3 v1{12, 34, 56}; + ASSERT_EQ(v1[0], 12); + ASSERT_EQ(v1[1], 34); + ASSERT_EQ(v1[2], 56); } -TEST(Vec2D, Addition) { - Vec2D a{1, 2}; - Vec2D b{3, 4}; - Vec2D c = a + b; - ASSERT_EQ(c.x, 4); - ASSERT_EQ(c.y, 6); +TEST(vec, equalEpsilon) { + // Test equalEpsilon + // TODO just an ad-hoc test, + // can possibly fail for other machines. + // This needs some work + vec3 v1{1.0f, 2.0f, 3.0f}; + vec3 v2{0.999999f, 1.9999999f, 2.9999999f}; + ASSERT_EQ(v1, v2); +} +TEST(vec, nonEqualEpsilon) { + // Test operator!= + vec3 v1{1.0f, 2.0f, 3.0f}; + vec3 v2{2.0f, 4.0f, 6.0f}; + ASSERT_NE(v1, v2); +} + +TEST(vec, LogPrint) { + // Test that logger can print the vector of different types + // and sizes + vec2 v2(1.2f, 3.4f); + vec3 v3(1.2f, 3.4f, 5.6f); + vec4 v4(1.2f, 3.4f, 5.6f, 7.8f); + dvec2 dv2(1.2, 3.4); + dvec3 dv3(1.2, 3.4, 5.6); + dvec4 dv4(1.2, 3.4, 5.6, 7.8); + ivec2 iv2(1, 3); + ivec3 iv3(1, 3, 5); + ivec4 iv4(1, 3, 5, 7); + uvec2 uv2(1u, 3u); + uvec3 uv3(1u, 3u, 5u); + uvec4 uv4(1u, 3u, 5u, 7u); + + LOG_DEBUG("vec2 ", v2); + LOG_DEBUG("vec3 ", v3); + LOG_DEBUG("vec4 ", v4); + LOG_DEBUG("dvec2 ", dv2); + LOG_DEBUG("dvec3 ", dv3); + LOG_DEBUG("dvec4 ", dv4); + LOG_DEBUG("ivec2 ", iv2); + LOG_DEBUG("ivec3 ", iv3); + LOG_DEBUG("ivec4 ", iv4); + LOG_DEBUG("uvec2 ", uv2); + LOG_DEBUG("uvec3 ", uv3); + LOG_DEBUG("uvec4 ", uv4); +} + +TEST(vec, Add) +{ + // Test operator+ with float vectors + vec3 v1{1.0f, 2.0f, 3.0f}; + vec3 v2{4.0f, 5.0f, 6.0f}; + vec3 result = v1 + v2; + + ASSERT_FLOAT_EQ(result[0], 5.0f); + ASSERT_FLOAT_EQ(result[1], 7.0f); + ASSERT_FLOAT_EQ(result[2], 9.0f); + + // Test operator+ with integer vectors + ivec3 iv1{1, 2, 3}; + ivec3 iv2{10, 20, 30}; + ivec3 iresult = iv1 + iv2; + + ASSERT_EQ(iresult[0], 11); + ASSERT_EQ(iresult[1], 22); + ASSERT_EQ(iresult[2], 33); + // Test that original vectors are unchanged - ASSERT_EQ(a.x, 1); - ASSERT_EQ(a.y, 2); - ASSERT_EQ(b.x, 3); - ASSERT_EQ(b.y, 4); + ASSERT_FLOAT_EQ(v1[0], 1.0f); + ASSERT_FLOAT_EQ(v1[1], 2.0f); + ASSERT_FLOAT_EQ(v1[2], 3.0f); } -TEST(Vec2D, AdditionAssignment) { - Vec2D a{1, 2}; - Vec2D b{3, 4}; - - a += b; - ASSERT_EQ(a.x, 4); - ASSERT_EQ(a.y, 6); - - // Test that b is unchanged - ASSERT_EQ(b.x, 3); - ASSERT_EQ(b.y, 4); +TEST(vec, Sub) +{ + // Test operator- with float vectors + vec3 v1{5.0f, 7.0f, 9.0f}; + vec3 v2{1.0f, 2.0f, 3.0f}; + vec3 result = v1 - v2; + + ASSERT_FLOAT_EQ(result[0], 4.0f); + ASSERT_FLOAT_EQ(result[1], 5.0f); + ASSERT_FLOAT_EQ(result[2], 6.0f); + + // Test operator- with integer vectors + ivec3 iv1{30, 20, 10}; + ivec3 iv2{5, 3, 1}; + ivec3 iresult = iv1 - iv2; + + ASSERT_EQ(iresult[0], 25); + ASSERT_EQ(iresult[1], 17); + ASSERT_EQ(iresult[2], 9); + + // Test that original vectors are unchanged + ASSERT_FLOAT_EQ(v1[0], 5.0f); + ASSERT_FLOAT_EQ(v1[1], 7.0f); + ASSERT_FLOAT_EQ(v1[2], 9.0f); + + // Test subtraction resulting in negative values + vec3 v3{1.0f, 2.0f, 3.0f}; + vec3 v4{4.0f, 5.0f, 6.0f}; + vec3 negative_result = v3 - v4; + + ASSERT_FLOAT_EQ(negative_result[0], -3.0f); + ASSERT_FLOAT_EQ(negative_result[1], -3.0f); + ASSERT_FLOAT_EQ(negative_result[2], -3.0f); } -TEST(Vec2D, ScalarMultiplication) { - Vec2D v{2, 3}; - - Vec2D result = v * 2.0f; - ASSERT_EQ(result.x, 4); - ASSERT_EQ(result.y, 6); - - // Test with float vector - Vec2D vf{1.5f, 2.5f}; - Vec2D resultf = vf * 2.0f; - ASSERT_FLOAT_EQ(resultf.x, 3.0f); - ASSERT_FLOAT_EQ(resultf.y, 5.0f); +TEST(vec, ScalarMultiplication) +{ + // Test scalar * vector with float vectors + vec3 v1{2.0f, 3.0f, 4.0f}; + vec3 result = v1 * 2.5f; + + ASSERT_FLOAT_EQ(result[0], 5.0f); + ASSERT_FLOAT_EQ(result[1], 7.5f); + ASSERT_FLOAT_EQ(result[2], 10.0f); + + // Test scalar * vector with integer vectors + ivec3 iv1{3, 5, 7}; + ivec3 iresult = iv1 * 2; + + ASSERT_EQ(iresult[0], 6); + ASSERT_EQ(iresult[1], 10); + ASSERT_EQ(iresult[2], 14); + + // Test that original vector is unchanged + ASSERT_FLOAT_EQ(v1[0], 2.0f); + ASSERT_FLOAT_EQ(v1[1], 3.0f); + ASSERT_FLOAT_EQ(v1[2], 4.0f); + + // Test multiplication by zero + vec3 v2{1.0f, 2.0f, 3.0f}; + vec3 zero_result = v2 * 0.0f; + + ASSERT_FLOAT_EQ(zero_result[0], 0.0f); + ASSERT_FLOAT_EQ(zero_result[1], 0.0f); + ASSERT_FLOAT_EQ(zero_result[2], 0.0f); + + // Test multiplication by negative scalar (and different ordering) + vec3 v3{1.0f, -2.0f, 3.0f}; + vec3 negative_result = -2.0f * v3; + + ASSERT_FLOAT_EQ(negative_result[0], -2.0f); + ASSERT_FLOAT_EQ(negative_result[1], 4.0f); + ASSERT_FLOAT_EQ(negative_result[2], -6.0f); } -TEST(Vec2D, Normalization) { - Vec2D v{3.0f, 4.0f}; // Length = 5 - - v.normalize(); - ASSERT_FLOAT_EQ(v.x, 0.6f); - ASSERT_FLOAT_EQ(v.y, 0.8f); - - // Check that length is approximately 1 - float length = sqrt(v.x * v.x + v.y * v.y); - ASSERT_NEAR(length, 1.0f, 1e-6f); +TEST(vec, ScalarDivision) +{ + // Test vector / scalar with float vectors + vec3 v1{10.0f, 15.0f, 20.0f}; + vec3 result = v1 / 2.5f; + + ASSERT_FLOAT_EQ(result[0], 4.0f); + ASSERT_FLOAT_EQ(result[1], 6.0f); + ASSERT_FLOAT_EQ(result[2], 8.0f); + + // Test vector / scalar with integer vectors + ivec3 iv1{12, 18, 24}; + ivec3 iresult = iv1 / 2; + + ASSERT_EQ(iresult[0], 6); + ASSERT_EQ(iresult[1], 9); + ASSERT_EQ(iresult[2], 12); + + // Test that original vector is unchanged + ASSERT_FLOAT_EQ(v1[0], 10.0f); + ASSERT_FLOAT_EQ(v1[1], 15.0f); + ASSERT_FLOAT_EQ(v1[2], 20.0f); + + // Test division by negative scalar + vec3 v2{6.0f, -9.0f, 12.0f}; + vec3 negative_result = v2 / -3.0f; + + ASSERT_FLOAT_EQ(negative_result[0], -2.0f); + ASSERT_FLOAT_EQ(negative_result[1], 3.0f); + ASSERT_FLOAT_EQ(negative_result[2], -4.0f); + + // Test division by fractional scalar + vec3 v3{1.0f, 2.0f, 3.0f}; + vec3 fractional_result = v3 / 0.5f; + + ASSERT_FLOAT_EQ(fractional_result[0], 2.0f); + ASSERT_FLOAT_EQ(fractional_result[1], 4.0f); + ASSERT_FLOAT_EQ(fractional_result[2], 6.0f); } -TEST(Vec2D, NormalizedCopy) { - Vec2D v{3.0f, 4.0f}; - Vec2D normalized = v.normalized(); +TEST(vec, AdditionAssignment) +{ + // Test operator+= with float vectors + vec3 v1{1.0f, 2.0f, 3.0f}; + vec3 v2{4.0f, 5.0f, 6.0f}; + v1 += v2; + + ASSERT_FLOAT_EQ(v1[0], 5.0f); + ASSERT_FLOAT_EQ(v1[1], 7.0f); + ASSERT_FLOAT_EQ(v1[2], 9.0f); + + // Test that v2 is unchanged + ASSERT_FLOAT_EQ(v2[0], 4.0f); + ASSERT_FLOAT_EQ(v2[1], 5.0f); + ASSERT_FLOAT_EQ(v2[2], 6.0f); + + // Test operator+= with integer vectors + ivec3 iv1{10, 20, 30}; + ivec3 iv2{1, 2, 3}; + iv1 += iv2; + + ASSERT_EQ(iv1[0], 11); + ASSERT_EQ(iv1[1], 22); + ASSERT_EQ(iv1[2], 33); + + // Test chaining + vec3 v3{1.0f, 1.0f, 1.0f}; + vec3 v4{2.0f, 2.0f, 2.0f}; + vec3 v5{3.0f, 3.0f, 3.0f}; + v3 += v4 += v5; + + ASSERT_FLOAT_EQ(v3[0], 6.0f); + ASSERT_FLOAT_EQ(v3[1], 6.0f); + ASSERT_FLOAT_EQ(v3[2], 6.0f); + ASSERT_FLOAT_EQ(v4[0], 5.0f); + ASSERT_FLOAT_EQ(v4[1], 5.0f); + ASSERT_FLOAT_EQ(v4[2], 5.0f); +} - // Original should be unchanged - ASSERT_FLOAT_EQ(v.x, 3.0f); - ASSERT_FLOAT_EQ(v.y, 4.0f); +TEST(vec, SubtractionAssignment) +{ + // Test operator-= with float vectors + vec3 v1{10.0f, 15.0f, 20.0f}; + vec3 v2{3.0f, 5.0f, 7.0f}; + v1 -= v2; + + ASSERT_FLOAT_EQ(v1[0], 7.0f); + ASSERT_FLOAT_EQ(v1[1], 10.0f); + ASSERT_FLOAT_EQ(v1[2], 13.0f); + + // Test that v2 is unchanged + ASSERT_FLOAT_EQ(v2[0], 3.0f); + ASSERT_FLOAT_EQ(v2[1], 5.0f); + ASSERT_FLOAT_EQ(v2[2], 7.0f); + + // Test operator-= with integer vectors + ivec3 iv1{50, 40, 30}; + ivec3 iv2{5, 10, 15}; + iv1 -= iv2; + + ASSERT_EQ(iv1[0], 45); + ASSERT_EQ(iv1[1], 30); + ASSERT_EQ(iv1[2], 15); + + // Test subtraction resulting in negative values + vec3 v3{1.0f, 2.0f, 3.0f}; + vec3 v4{4.0f, 5.0f, 6.0f}; + v3 -= v4; + + ASSERT_FLOAT_EQ(v3[0], -3.0f); + ASSERT_FLOAT_EQ(v3[1], -3.0f); + ASSERT_FLOAT_EQ(v3[2], -3.0f); +} +TEST(vec, LengthSquared) +{ + // Test LengthSquared with float vectors + vec3 v1{3.0f, 4.0f, 0.0f}; + ASSERT_FLOAT_EQ(v1.LengthSquared(), 25.0f); // 3² + 4² + 0² = 25 + + vec2 v2{1.0f, 1.0f}; + ASSERT_FLOAT_EQ(v2.LengthSquared(), 2.0f); // 1² + 1² = 2 + + // Test with zero vector + vec3 zero{0.0f, 0.0f, 0.0f}; + ASSERT_FLOAT_EQ(zero.LengthSquared(), 0.0f); +} + +TEST(vec, Length) +{ + // Test Length with float vectors + vec3 v1{3.0f, 4.0f, 0.0f}; + ASSERT_FLOAT_EQ(v1.Length(), 5.0f); // sqrt(3² + 4² + 0²) = 5 + + vec2 v2{1.0f, 1.0f}; + ASSERT_NEAR(v2.Length(), 1.414213f, 1e-5f); // sqrt(2) ≈ 1.414213 + + // Test with zero vector + vec3 zero{0.0f, 0.0f, 0.0f}; + ASSERT_FLOAT_EQ(zero.Length(), 0.0f); +} + +TEST(vec, Normalize) +{ + // Test Normalize with float vectors + vec3 v1{3.0f, 4.0f, 0.0f}; + v1.Normalize(); + + ASSERT_FLOAT_EQ(v1[0], 0.6f); // 3/5 + ASSERT_FLOAT_EQ(v1[1], 0.8f); // 4/5 + ASSERT_FLOAT_EQ(v1[2], 0.0f); + ASSERT_NEAR(v1.Length(), 1.0f, 1e-6f); + + // Test with zero vector (may produce NaN - implementation dependent) + vec3 zero{0.0f, 0.0f, 0.0f}; + zero.Normalize(); + // Check if result is NaN (which is expected for zero vector normalization) + ASSERT_TRUE(zero[0] == 0.0f); + ASSERT_TRUE(zero[1] == 0.0f); + ASSERT_TRUE(zero[2] == 0.0f); +} + +TEST(vec, GetNormalized) +{ + // Test GetNormalized with float vectors + const vec3 v1{3.0f, 4.0f, 0.0f}; + vec3 normalized = v1.GetNormalized(); + + // Original vector should be unchanged + ASSERT_FLOAT_EQ(v1[0], 3.0f); + ASSERT_FLOAT_EQ(v1[1], 4.0f); + ASSERT_FLOAT_EQ(v1[2], 0.0f); + // Normalized copy should be unit length - ASSERT_FLOAT_EQ(normalized.x, 0.6f); - ASSERT_FLOAT_EQ(normalized.y, 0.8f); - - float length = - sqrt(normalized.x * normalized.x + normalized.y * normalized.y); - ASSERT_NEAR(length, 1.0f, 1e-6f); + ASSERT_FLOAT_EQ(normalized[0], 0.6f); // 3/5 + ASSERT_FLOAT_EQ(normalized[1], 0.8f); // 4/5 + ASSERT_FLOAT_EQ(normalized[2], 0.0f); + ASSERT_NEAR(normalized.Length(), 1.0f, 1e-6f); + + // Test with zero vector + vec3 zero{0.0f, 0.0f, 0.0f}; + vec3 zero_normalized = zero.GetNormalized(); + + ASSERT_FLOAT_EQ(zero_normalized[0], 0.0f); + ASSERT_FLOAT_EQ(zero_normalized[1], 0.0f); + ASSERT_FLOAT_EQ(zero_normalized[2], 0.0f); + + // Original zero vector should be unchanged + ASSERT_FLOAT_EQ(zero[0], 0.0f); + ASSERT_FLOAT_EQ(zero[1], 0.0f); + ASSERT_FLOAT_EQ(zero[2], 0.0f); } -TEST(Vec2D, ZeroVectorNormalization) { - Vec2D v{0.0f, 0.0f}; - - v.normalize(); - ASSERT_FLOAT_EQ(v.x, 0.0f); - ASSERT_FLOAT_EQ(v.y, 0.0f); - - // Test normalized() as well - Vec2D v2{0.0f, 0.0f}; - Vec2D normalized = v2.normalized(); - ASSERT_FLOAT_EQ(normalized.x, 0.0f); - ASSERT_FLOAT_EQ(normalized.y, 0.0f); +TEST(vec, GetOrthogonal) +{ + const vec2 v1{5.0f, 1.0f}; + auto v2 = v1.GetOrthogonal(); + ASSERT_FLOAT_EQ(v2[0], -1.0f); + ASSERT_FLOAT_EQ(v2[1], 5.0f); } -TEST(Vec2D, VerySmallVectorNormalization) { - Vec2D v{1e-7f, 1e-7f}; // Very small vector - - v.normalize(); - // Should be treated as zero vector - ASSERT_FLOAT_EQ(v.x, 0.0f); - ASSERT_FLOAT_EQ(v.y, 0.0f); -} - -TEST(Vec2D, OutputOperator) { - Vec2D v{42, 24}; - - std::ostringstream oss; - oss << v; - ASSERT_EQ(oss.str(), "( 42, 24)"); -} - -TEST(Vec2D, ChainedOperations) { - Vec2D a{1.0f, 2.0f}; - Vec2D b{3.0f, 4.0f}; +TEST(vec, ChainedOperations) { + vec2 a{1.0f, 2.0f}; + vec2 b{3.0f, 4.0f}; // Test chaining: (a + b) * 2.0f - Vec2D result = (a + b) * 2.0f; - ASSERT_FLOAT_EQ(result.x, 8.0f); - ASSERT_FLOAT_EQ(result.y, 12.0f); + auto result = (a + b) * 2.0f; + ASSERT_FLOAT_EQ(result[0], 8.0f); + ASSERT_FLOAT_EQ(result[1], 12.0f); // Test chaining with assignment a += b; a = a * 0.5f; - ASSERT_FLOAT_EQ(a.x, 2.0f); - ASSERT_FLOAT_EQ(a.y, 3.0f); + ASSERT_FLOAT_EQ(a[0], 2.0f); + ASSERT_FLOAT_EQ(a[1], 3.0f); } int main(int argc, char **argv) { From 92b36a8943dd942ef5603d4dbbd298c7fe2ce4b1 Mon Sep 17 00:00:00 2001 From: Jan Mrna Date: Thu, 2 Oct 2025 20:48:05 +0200 Subject: [PATCH 06/23] Added DistanceTo to vec class --- cpp/src/math.hpp | 5 +++++ cpp/test/test.cpp | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/cpp/src/math.hpp b/cpp/src/math.hpp index ad5e5c4..4f148c5 100644 --- a/cpp/src/math.hpp +++ b/cpp/src/math.hpp @@ -153,6 +153,11 @@ public: return std::sqrt(LengthSquared()); } + T DistanceTo(const vec& b) const { + const vec& a = *this; + return (a - b).Length(); + } + // // In-place vector operations // diff --git a/cpp/test/test.cpp b/cpp/test/test.cpp index b4b2ae5..c7d4523 100644 --- a/cpp/test/test.cpp +++ b/cpp/test/test.cpp @@ -376,6 +376,38 @@ TEST(vec, GetOrthogonal) ASSERT_FLOAT_EQ(v2[1], 5.0f); } +TEST(vec, DistanceTo) +{ + // Test DistanceTo with 3D vectors + vec3 v1{0.0f, 0.0f, 0.0f}; + vec3 v2{3.0f, 4.0f, 0.0f}; + + float distance = v1.DistanceTo(v2); + ASSERT_FLOAT_EQ(distance, 5.0f); // 3-4-5 triangle + + // Distance should be symmetric + ASSERT_FLOAT_EQ(v2.DistanceTo(v1), distance); + + // Test with 2D vectors + vec2 a{1.0f, 1.0f}; + vec2 b{4.0f, 5.0f}; + + float distance_2d = a.DistanceTo(b); + ASSERT_FLOAT_EQ(distance_2d, 5.0f); // sqrt((4-1)² + (5-1)²) = sqrt(9+16) = 5 + + // Distance to self should be zero + ASSERT_FLOAT_EQ(v1.DistanceTo(v1), 0.0f); + ASSERT_FLOAT_EQ(a.DistanceTo(a), 0.0f); + + // Test that original vectors are unchanged + ASSERT_FLOAT_EQ(v1[0], 0.0f); + ASSERT_FLOAT_EQ(v1[1], 0.0f); + ASSERT_FLOAT_EQ(v1[2], 0.0f); + ASSERT_FLOAT_EQ(v2[0], 3.0f); + ASSERT_FLOAT_EQ(v2[1], 4.0f); + ASSERT_FLOAT_EQ(v2[2], 0.0f); +} + TEST(vec, ChainedOperations) { vec2 a{1.0f, 2.0f}; vec2 b{3.0f, 4.0f}; From 47977d9979c9955c507aa27fc915bb3af2e16f3a Mon Sep 17 00:00:00 2001 From: Jan Mrna Date: Thu, 2 Oct 2025 21:42:44 +0200 Subject: [PATCH 07/23] Refactor vec, add domain-specific types (world, window, tile) --- cpp/src/entities.cpp | 9 +- cpp/src/math.hpp | 447 ++++++++++++++++++++++--------------------- 2 files changed, 238 insertions(+), 218 deletions(-) diff --git a/cpp/src/entities.cpp b/cpp/src/entities.cpp index e3e0f50..b8f78b8 100644 --- a/cpp/src/entities.cpp +++ b/cpp/src/entities.cpp @@ -5,6 +5,7 @@ #include "log.hpp" #include "math.hpp" #include "sprite.hpp" +#include "coorginates.hpp" Entity::Entity(WorldPos position) : m_Position(position) { LOG_DEBUG("spawning entity at position ", position); @@ -25,13 +26,13 @@ void Entity::ZeroActualVelocityInDirection(WorldPos direction) { // where e1 is formed by the direction where we want to zero-out // the velocity, and e2 is the orthogonal vector. // Scalars q1, q2 are coordinates for e1, e2 basis. - WorldPos e1 = direction.normalized(); - WorldPos e2 = e1.orthogonal(); + WorldPos e1 = direction.GetNormalized(); + WorldPos e2 = e1.GetOrthogonal(); // q1 * e1 + q2 * e2 = v, from this follows: auto &v = GetActualVelocity(); - float q2 = (v.y * e1.x - v.x * e1.y) / (e2.y * e1.x - e2.x * e1.y); - float q1 = (v.x - q2 * e2.x) / e1.x; + float q2 = (v.y() * e1.x() - v.x() * e1.y()) / (e2.y() * e1.x() - e2.x() * e1.y()); + float q1 = (v.x() - q2 * e2.x()) / e1.x(); // We then zero-out the q1, but only if it's positive - meaning // it is aiming in the direction of "direction", not out. diff --git a/cpp/src/math.hpp b/cpp/src/math.hpp index 4f148c5..01e900e 100644 --- a/cpp/src/math.hpp +++ b/cpp/src/math.hpp @@ -1,21 +1,19 @@ #pragma once +#include #include #include #include #include #include -#include -#include -#include #include - +#include +#include template -requires std::floating_point -static inline bool equalEpsilon(const T& a, const T& b) -{ - constexpr auto epsilon = [](){ + requires std::floating_point +static inline bool equalEpsilon(const T &a, const T &b) { + constexpr auto epsilon = []() { if constexpr (std::is_same_v) { return T{1e-5}; } else { @@ -29,46 +27,41 @@ static inline bool equalEpsilon(const T& a, const T& b) return std::abs(a - b) < epsilon; } +struct Any {}; -template -class vec { +template class vec { public: vec() : m_Array{} {} template - requires (std::same_as && ...) && (sizeof...(ArgsT) == N) + requires(std::same_as && ...) && (sizeof...(ArgsT) == N) vec(ArgsT... args) : m_Array{args...} {} - const T& operator[](size_t index) const - { + const T &operator[](size_t index) const { // we leave run-time checks to the underlying std::array return m_Array[index]; } - T& operator[](size_t index) - { + T &operator[](size_t index) { // we leave run-time checks to the underlying std::array return m_Array[index]; } - friend std::ostream &operator<<(std::ostream &os, const vec &obj) - { + friend std::ostream &operator<<(std::ostream &os, const vec &obj) { os << "( "; - for (const auto& element : obj.m_Array) { + for (const auto &element : obj.m_Array) { os << element << " "; } os << ")"; return os; } - // // binary operators // - friend bool operator==(const vec& a, const vec& b) - { - for (const auto& [u, v] : std::views::zip(a.m_Array,b.m_Array)) { + friend bool operator==(const vec &a, const vec &b) { + for (const auto &[u, v] : std::views::zip(a.m_Array, b.m_Array)) { if (!equalEpsilon(u, v)) { return false; } @@ -76,113 +69,96 @@ public: return true; } - friend bool operator!=(const vec& a, const vec& b) - { - return !(a == b); - } + friend bool operator!=(const vec &a, const vec &b) { return !(a == b); } - friend vec operator+(const vec& a, const vec& b) - { - vec c; - std::ranges::transform(a.m_Array, b.m_Array, c.m_Array.begin(), std::plus{}); + friend vec operator+(const vec &a, const vec &b) { + vec c; + std::ranges::transform(a.m_Array, b.m_Array, c.m_Array.begin(), + std::plus{}); return c; } - friend vec operator-(const vec& a, const vec& b) - { - vec c; - std::ranges::transform(a.m_Array, b.m_Array, c.m_Array.begin(), std::minus{}); + friend vec operator-(const vec &a, const vec &b) { + vec c; + std::ranges::transform(a.m_Array, b.m_Array, c.m_Array.begin(), + std::minus{}); return c; } - friend vec operator*(const vec& a, const T& scalar) - { - vec c; - std::ranges::transform(a.m_Array, std::views::repeat(scalar), c.m_Array.begin(), std::multiplies{}); + friend vec operator*(const vec &a, const T &scalar) { + vec c; + std::ranges::transform(a.m_Array, std::views::repeat(scalar), + c.m_Array.begin(), std::multiplies{}); return c; } - friend vec operator*(const T& scalar, const vec& a) { - return a * scalar; - } + friend vec operator*(const T &scalar, const vec &a) { return a * scalar; } - friend vec operator/(const vec& a, const T& scalar) - { - vec c; - std::ranges::transform(a.m_Array, std::views::repeat(scalar), c.m_Array.begin(), std::divides{}); + friend vec operator/(const vec &a, const T &scalar) { + vec c; + std::ranges::transform(a.m_Array, std::views::repeat(scalar), + c.m_Array.begin(), std::divides{}); return c; } // // compound-assignment operators // - - vec& operator+=(const vec& b) - { - vec& a = *this; - std::ranges::transform(a.m_Array, b.m_Array, a.m_Array.begin(), std::plus{}); + + vec &operator+=(const vec &b) { + vec &a = *this; + std::ranges::transform(a.m_Array, b.m_Array, a.m_Array.begin(), + std::plus{}); return a; } - vec& operator-=(const vec& b) - { - vec& a = *this; - std::ranges::transform(a.m_Array, b.m_Array, a.m_Array.begin(), std::minus{}); + vec &operator-=(const vec &b) { + vec &a = *this; + std::ranges::transform(a.m_Array, b.m_Array, a.m_Array.begin(), + std::minus{}); return a; } - // // Utility functions // - T LengthSquared() const - { - return std::transform_reduce( - m_Array.begin(), m_Array.end(), - T{0.0}, - std::plus{}, - [](T x){ - return x*x; - } - ); + T LengthSquared() const { + return std::transform_reduce(m_Array.begin(), m_Array.end(), T{0.0}, + std::plus{}, [](T x) { return x * x; }); } - T Length() const - { - return std::sqrt(LengthSquared()); - } + T Length() const { return std::sqrt(LengthSquared()); } - T DistanceTo(const vec& b) const { - const vec& a = *this; + T DistanceTo(const vec &b) const { + const vec &a = *this; return (a - b).Length(); - } + } // // In-place vector operations // - void Normalize() - { + void Normalize() { T length = Length(); if (equalEpsilon(length, T{0})) - return; - std::ranges::transform(m_Array, std::views::repeat(length), m_Array.begin(), std::divides{}); + return; + std::ranges::transform(m_Array, std::views::repeat(length), m_Array.begin(), + std::divides{}); } - // // Methods returning new object // - - vec GetNormalized() const - { + + vec GetNormalized() const { vec tmp = *this; tmp.Normalize(); return tmp; } - vec GetOrthogonal() const requires (N == 2) + vec GetOrthogonal() const + requires(N == 2) { vec tmp = *this; @@ -192,15 +168,53 @@ public: return tmp; } -// const T& x = m_Array[0]; -// const T& y = m_Array[1]; -// const T& z = m_Array[2]; + // + // Helpers + // + const T &x() const + requires(N >= 1) + { + return m_Array[0]; + } + + T &x() + requires(N >= 1) + { + return m_Array[0]; + } + + const T &y() const + requires(N >= 2) + { + return m_Array[1]; + } + + T &y() + requires(N >= 2) + { + return m_Array[1]; + } + + const T &z() const + requires(N >= 3) + { + return m_Array[2]; + } + + T &z() + requires(N >= 3) + { + return m_Array[2]; + } private: - std::array m_Array; + std::array m_Array; }; +// +// Aliases +// using vec2 = vec; using vec3 = vec; @@ -215,138 +229,143 @@ using uvec2 = vec; using uvec3 = vec; using uvec4 = vec; -constexpr double EQUALITY_LIMIT = 1e-6; +// tags for differentiating between domains +struct WorldTag {}; +struct WindowTag {}; +struct TileTag {}; -#ifdef _WIN32 -#include -#define M_PI std::numbers::pi -#endif +// types for each domain +using WorldPos = vec; +using WindowPos = vec; +using TilePos = vec; -template struct Vec2D { -public: - Vec2D() = default; - ~Vec2D() = default; - - template - Vec2D(Vec2D other) { - this->x = static_cast(other.x); - this->y = static_cast(other.y); - } - - Vec2D& operator+=(const Vec2D &other) { - x += other.x; - y += other.y; - return *this; - } - - template - requires std::is_arithmetic_v - Vec2D& operator/=(U k) { - this->x /= static_cast(k); - this->y /= static_cast(k); - return *this; - } - - friend Vec2D operator+(const Vec2D &a, const Vec2D &b) { - return Vec2D{a.x + b.x, a.y + b.y}; - } - - friend Vec2D operator-(const Vec2D &a, const Vec2D &b) { - return Vec2D{a.x - b.x, a.y - b.y}; - } - - template - requires std::is_arithmetic_v - friend Vec2D operator*(U k, const Vec2D &v) - { - return Vec2D{k * v.x, k * v.y}; - } - - template - requires std::is_arithmetic_v - friend Vec2D operator/(const Vec2D &v, U k) - { - return Vec2D{v.x / k, v.y / k}; - } - - friend bool operator==(const Vec2D &a, const Vec2D &b) { - if constexpr (std::is_integral_v) { - return a.x == b.x && a.y == b.y; - } else if constexpr (std::is_floating_point_v) { - return a.distance(b) < EQUALITY_LIMIT; - } else { - static_assert("Unhandled comparison"); - } - } - - Vec2D operator*(float b) const { return Vec2D{b * x, b * y}; } - - T distance_squared(const Vec2D &other) const { - T dx = x - other.x; - T dy = y - other.y; - return dx * dx + dy * dy; - } - - T distance(const Vec2D &other) const - requires std::floating_point - { - return sqrt(distance_squared(other)); - } - - void normalize() - requires std::floating_point - { - auto length = sqrt(x * x + y * y); - if (length < EQUALITY_LIMIT) { - x = y = 0; - } else { - x /= length; - y /= length; - } - } - - Vec2D normalized() - requires std::floating_point - { - Vec2D v(*this); - v.normalize(); - return v; - } - - Vec2D orthogonal() const - { - Vec2D v(*this); - - std::swap(v.x, v.y); - v.x = -v.x; - return v; - } - - template Vec2D(std::initializer_list list) { - assert(list.size() == 2); - auto first_element = *list.begin(); - auto second_element = *(list.begin() + 1); - x = static_cast(first_element); - y = static_cast(second_element); - } - - T x, y; - - friend std::ostream &operator<<(std::ostream &os, const Vec2D &obj) { - os << "( " << obj.x << ", " << obj.y << ")"; - return os; - } -}; - -using TilePos = Vec2D; -using WorldPos = Vec2D; -using WindowPos = Vec2D; +// +// Utils +// struct TilePosHash { - std::size_t operator()(const TilePos& p) const noexcept { - std::size_t h1 = std::hash{}(p.x); - std::size_t h2 = std::hash{}(p.y); - return h1 ^ (h2 + 0x9e3779b9 + (h1<<6) + (h1>>2)); - } + std::size_t operator()(const TilePos &p) const noexcept { + std::size_t h1 = std::hash{}(p.x()); + std::size_t h2 = std::hash{}(p.y()); + return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2)); + } }; +// old stuff - TODO delete + +// constexpr double EQUALITY_LIMIT = 1e-6; +// template struct Vec2D { +// public: +// Vec2D() = default; +// ~Vec2D() = default; +// +// template +// Vec2D(Vec2D other) { +// this->x = static_cast(other.x); +// this->y = static_cast(other.y); +// } +// +// Vec2D& operator+=(const Vec2D &other) { +// x += other.x; +// y += other.y; +// return *this; +// } +// +// template +// requires std::is_arithmetic_v +// Vec2D& operator/=(U k) { +// this->x /= static_cast(k); +// this->y /= static_cast(k); +// return *this; +// } +// +// friend Vec2D operator+(const Vec2D &a, const Vec2D &b) { +// return Vec2D{a.x + b.x, a.y + b.y}; +// } +// +// friend Vec2D operator-(const Vec2D &a, const Vec2D &b) { +// return Vec2D{a.x - b.x, a.y - b.y}; +// } +// +// template +// requires std::is_arithmetic_v +// friend Vec2D operator*(U k, const Vec2D &v) +// { +// return Vec2D{k * v.x, k * v.y}; +// } +// +// template +// requires std::is_arithmetic_v +// friend Vec2D operator/(const Vec2D &v, U k) +// { +// return Vec2D{v.x / k, v.y / k}; +// } +// +// friend bool operator==(const Vec2D &a, const Vec2D &b) { +// if constexpr (std::is_integral_v) { +// return a.x == b.x && a.y == b.y; +// } else if constexpr (std::is_floating_point_v) { +// return a.distance(b) < EQUALITY_LIMIT; +// } else { +// static_assert("Unhandled comparison"); +// } +// } +// +// Vec2D operator*(float b) const { return Vec2D{b * x, b * y}; } +// +// T distance_squared(const Vec2D &other) const { +// T dx = x - other.x; +// T dy = y - other.y; +// return dx * dx + dy * dy; +// } +// +// T distance(const Vec2D &other) const +// requires std::floating_point +// { +// return sqrt(distance_squared(other)); +// } +// +// void normalize() +// requires std::floating_point +// { +// auto length = sqrt(x * x + y * y); +// if (length < EQUALITY_LIMIT) { +// x = y = 0; +// } else { +// x /= length; +// y /= length; +// } +// } +// +// Vec2D normalized() +// requires std::floating_point +// { +// Vec2D v(*this); +// v.normalize(); +// return v; +// } +// +// Vec2D orthogonal() const +// { +// Vec2D v(*this); +// +// std::swap(v.x, v.y); +// v.x = -v.x; +// return v; +// } +// +// template Vec2D(std::initializer_list list) { +// assert(list.size() == 2); +// auto first_element = *list.begin(); +// auto second_element = *(list.begin() + 1); +// x = static_cast(first_element); +// y = static_cast(second_element); +// } +// +// T x, y; +// +// friend std::ostream &operator<<(std::ostream &os, const Vec2D &obj) { +// os << "( " << obj.x << ", " << obj.y << ")"; +// return os; +// } +// }; From 8a49c12909da09c4fcca2e7a387bdc903e745ebb Mon Sep 17 00:00:00 2001 From: Jan Mrna Date: Sat, 4 Oct 2025 16:54:29 +0200 Subject: [PATCH 08/23] Refactor WorldPos, WindowsPos done --- cpp/src/entities.cpp | 1 - cpp/src/gameloop.cpp | 15 ++++++++--- cpp/src/map.cpp | 52 +++++++++++++++++++------------------ cpp/src/math.hpp | 22 +++++++++++----- cpp/src/pathfinder/gbfs.cpp | 2 +- cpp/src/pathfindingdemo.cpp | 6 ++--- cpp/src/pathfindingdemo.hpp | 3 +++ cpp/src/sprite.cpp | 4 +-- cpp/src/sprite.hpp | 4 +-- cpp/src/user_input.hpp | 7 +++-- cpp/src/window.cpp | 20 +++++++------- cpp/src/window.hpp | 8 +++--- cpp/test/test.cpp | 6 +++++ 13 files changed, 89 insertions(+), 61 deletions(-) diff --git a/cpp/src/entities.cpp b/cpp/src/entities.cpp index b8f78b8..d8e1ad2 100644 --- a/cpp/src/entities.cpp +++ b/cpp/src/entities.cpp @@ -5,7 +5,6 @@ #include "log.hpp" #include "math.hpp" #include "sprite.hpp" -#include "coorginates.hpp" Entity::Entity(WorldPos position) : m_Position(position) { LOG_DEBUG("spawning entity at position ", position); diff --git a/cpp/src/gameloop.cpp b/cpp/src/gameloop.cpp index 85d825e..0c0466f 100644 --- a/cpp/src/gameloop.cpp +++ b/cpp/src/gameloop.cpp @@ -26,10 +26,15 @@ void GameLoop::Run() { const auto &tiles = map.GetMapTiles(); for (size_t row = 0; row < tiles.size(); row++) { for (size_t col = 0; col < tiles[row].size(); col++) { + const auto& camera = m_Game->GetCamera(); // LOG_DEBUG("Drawing rect (", row, ", ", col, ")"); m_Window->DrawRect( - map.TileEdgeToWorld(TilePos{row, col}), - map.GetTileSize(), tiles[row][col]->R, tiles[row][col]->G, + camera.WorldToWindow( + map.TileEdgeToWorld( + TilePos{static_cast(row), static_cast(col)} + ) + ), + camera.WorldToWindow(map.GetTileSize()), tiles[row][col]->R, tiles[row][col]->G, tiles[row][col]->B, tiles[row][col]->A); } } @@ -37,13 +42,15 @@ void GameLoop::Run() { // draw the path, if it exists WorldPos start_pos = m_Game->GetPlayer()->GetPosition(); for (const auto& next_pos: m_Game->GetPath()) { - m_Window->DrawLine(start_pos, next_pos); + const auto& camera = m_Game->GetCamera(); + m_Window->DrawLine(camera.WorldToWindow(start_pos), camera.WorldToWindow(next_pos)); start_pos = next_pos; } // draw all the entities (player etc) for (auto &entity : m_Game->GetEntities()) { - m_Window->DrawSprite(entity->GetPosition(), entity->GetSprite()); + const auto& camera = m_Game->GetCamera(); + m_Window->DrawSprite(camera.WorldToWindow(entity->GetPosition()), entity->GetSprite()); } m_Window->Flush(); diff --git a/cpp/src/map.cpp b/cpp/src/map.cpp index aa48775..4ef7e51 100644 --- a/cpp/src/map.cpp +++ b/cpp/src/map.cpp @@ -17,23 +17,24 @@ Map::Map(int rows, int cols) : m_Cols(cols), m_Rows(rows) { } WorldPos Map::TileToWorld(TilePos p) const { - return WorldPos{(p.x + 0.5) * TILE_SIZE, (p.y + 0.5) * TILE_SIZE}; + return WorldPos{(p.x() + 0.5f) * TILE_SIZE, (p.y() + 0.5f) * TILE_SIZE}; } WorldPos Map::TileEdgeToWorld(TilePos p) const { - return WorldPos{p.x * TILE_SIZE, p.y * TILE_SIZE}; + return WorldPos{p.x() * TILE_SIZE, p.y() * TILE_SIZE}; } TilePos Map::WorldToTile(WorldPos p) const { - return TilePos{p.x / TILE_SIZE, p.y / TILE_SIZE}; + return TilePos{static_cast(p.x() / TILE_SIZE), static_cast(p.y() / TILE_SIZE)}; } +// TODO this should probably use something like WorldSize or WorldVec to make the distinction clear WorldPos Map::GetTileSize() const { return WorldPos{TILE_SIZE, TILE_SIZE}; } const Tile *Map::GetTileAt(TilePos p) const { assert(IsTilePosValid(p)); - size_t row = p.x; - size_t col = p.y; + size_t row = p.x(); + size_t col = p.y(); return m_Tiles[row][col]; } @@ -43,10 +44,10 @@ const Tile *Map::GetTileAt(WorldPos p) const { } bool Map::IsTilePosValid(TilePos p) const { - if (p.x < 0 || p.y < 0) + if (p.x() < 0 || p.y() < 0) return false; - size_t row = static_cast(p.x); - size_t col = static_cast(p.y); + size_t row = static_cast(p.x()); + size_t col = static_cast(p.y()); return row < m_Tiles.size() && col < m_Tiles[0].size(); } @@ -75,14 +76,14 @@ std::vector Map::GetNeighbors(TilePos center) const void Map::PaintCircle(TilePos center, unsigned radius, TileType tile_type) { // get rectangle that wraps the circle - TilePos corner1 = TilePos{center.x - radius, center.y - radius}; - TilePos corner2 = TilePos{center.x + radius, center.y + radius}; + TilePos corner1 = TilePos{center.x() - static_cast(radius), center.y() - static_cast(radius)}; + TilePos corner2 = TilePos{center.x() + static_cast(radius), center.y() + static_cast(radius)}; // iterate through all valid points, setting the type const unsigned radius_squared = radius * radius; - for (int x = corner1.x; x < corner2.x; x++) { - for (int y = corner1.y; y < corner2.y; y++) { + for (int x = corner1.x(); x < corner2.x(); x++) { + for (int y = corner1.y(); y < corner2.y(); y++) { TilePos current_tile = {x, y}; - unsigned distance_squared = center.distance_squared(current_tile); + unsigned distance_squared = static_cast(center.DistanceTo(current_tile) * center.DistanceTo(current_tile)); if (IsTilePosValid(current_tile) && distance_squared < radius_squared) { // y is row, x is col @@ -94,19 +95,20 @@ void Map::PaintCircle(TilePos center, unsigned radius, TileType tile_type) void Map::PaintLine(TilePos start_tile, TilePos stop_tile, double width, TileType tile_type) { - const Vec2D start(start_tile); - const Vec2D stop(stop_tile); - const double line_length = start.distance(stop); - const Vec2D step((stop-start)/line_length); - const Vec2D ortho = step.orthogonal(); + const vec start{static_cast(start_tile.x()), static_cast(start_tile.y())}; + const vec stop{static_cast(stop_tile.x()), static_cast(stop_tile.y())}; + const double line_length = start.DistanceTo(stop); + const vec step = (stop - start) / line_length; + const vec ortho = step.GetOrthogonal(); LOG_DEBUG("step = ", step, " ortho = ", ortho); for (double t = 0; t < line_length; t += 1.0) { for (double ortho_t = 0; ortho_t < width; ortho_t += 0.1) { auto tile_pos = start + step * t + ortho * ortho_t; - if (IsTilePosValid(tile_pos)) { - size_t row = static_cast(tile_pos.x); - size_t col = static_cast(tile_pos.y); + TilePos tile_pos_int{static_cast(tile_pos.x()), static_cast(tile_pos.y())}; + if (IsTilePosValid(tile_pos_int)) { + size_t row = static_cast(tile_pos.x()); + size_t col = static_cast(tile_pos.y()); m_Tiles[row][col] = &tile_types.at(tile_type); } } @@ -116,15 +118,15 @@ void Map::PaintLine(TilePos start_tile, TilePos stop_tile, double width, TileTyp void Map::PaintRectangle(TilePos first_corner, TilePos second_corner, TileType tile_type) { - std::initializer_list xvals = {first_corner.x, second_corner.x}; - std::initializer_list yvals = {first_corner.y, second_corner.y}; + std::initializer_list xvals = {first_corner.x(), second_corner.x()}; + std::initializer_list yvals = {first_corner.y(), second_corner.y()}; for (int x = std::min(xvals); x < std::max(xvals); x++) { for (int y = std::min(yvals); y < std::max(yvals); y++) { TilePos tile_pos{x,y}; LOG_DEBUG("tile_pos = ", tile_pos); if (IsTilePosValid(tile_pos)) { - size_t row = static_cast(tile_pos.x); - size_t col = static_cast(tile_pos.y); + size_t row = static_cast(tile_pos.x()); + size_t col = static_cast(tile_pos.y()); m_Tiles[row][col] = &tile_types.at(tile_type); } } diff --git a/cpp/src/math.hpp b/cpp/src/math.hpp index 01e900e..5d214b0 100644 --- a/cpp/src/math.hpp +++ b/cpp/src/math.hpp @@ -60,7 +60,15 @@ public: // binary operators // - friend bool operator==(const vec &a, const vec &b) { + friend bool operator==(const vec &a, const vec &b) + requires (std::is_integral_v) + { + return std::ranges::equal(a.m_Array, b.m_Array); + } + + friend bool operator==(const vec &a, const vec &b) + requires (std::is_floating_point_v) + { for (const auto &[u, v] : std::views::zip(a.m_Array, b.m_Array)) { if (!equalEpsilon(u, v)) { return false; @@ -72,21 +80,21 @@ public: friend bool operator!=(const vec &a, const vec &b) { return !(a == b); } friend vec operator+(const vec &a, const vec &b) { - vec c; + vec c; std::ranges::transform(a.m_Array, b.m_Array, c.m_Array.begin(), std::plus{}); return c; } friend vec operator-(const vec &a, const vec &b) { - vec c; + vec c; std::ranges::transform(a.m_Array, b.m_Array, c.m_Array.begin(), std::minus{}); return c; } friend vec operator*(const vec &a, const T &scalar) { - vec c; + vec c; std::ranges::transform(a.m_Array, std::views::repeat(scalar), c.m_Array.begin(), std::multiplies{}); return c; @@ -95,7 +103,7 @@ public: friend vec operator*(const T &scalar, const vec &a) { return a * scalar; } friend vec operator/(const vec &a, const T &scalar) { - vec c; + vec c; std::ranges::transform(a.m_Array, std::views::repeat(scalar), c.m_Array.begin(), std::divides{}); return c; @@ -124,7 +132,7 @@ public: // T LengthSquared() const { - return std::transform_reduce(m_Array.begin(), m_Array.end(), T{0.0}, + return std::transform_reduce(m_Array.begin(), m_Array.end(), T{}, std::plus{}, [](T x) { return x * x; }); } @@ -237,7 +245,7 @@ struct TileTag {}; // types for each domain using WorldPos = vec; using WindowPos = vec; -using TilePos = vec; +using TilePos = vec; // // Utils diff --git a/cpp/src/pathfinder/gbfs.cpp b/cpp/src/pathfinder/gbfs.cpp index 8b9bbfd..02684ca 100644 --- a/cpp/src/pathfinder/gbfs.cpp +++ b/cpp/src/pathfinder/gbfs.cpp @@ -11,7 +11,7 @@ namespace pathfinder { float GBFS::Heuristic(const TilePos& a, const TilePos& b) { - return static_cast(std::abs(a.x- b.x) + std::abs(a.y - b.y)); + return static_cast(std::abs(a.x() - b.x()) + std::abs(a.y() - b.y())); } Path GBFS::CalculatePath(WorldPos start_world, WorldPos end_world) diff --git a/cpp/src/pathfindingdemo.cpp b/cpp/src/pathfindingdemo.cpp index aa2b899..8fe7d8e 100644 --- a/cpp/src/pathfindingdemo.cpp +++ b/cpp/src/pathfindingdemo.cpp @@ -63,7 +63,7 @@ void PathFindingDemo::CreateMap() { } WorldPos PathFindingDemo::GetRandomPosition() const { - return WorldPos{0.0, 0.0}; // totally random! + return WorldPos{0.0f, 0.0f}; // totally random! } std::optional PathFindingDemo::GetMoveTarget() { @@ -75,7 +75,7 @@ std::optional PathFindingDemo::GetMoveTarget() { WorldPos next_player_pos = m_Path.front(); - if (current_player_pos.distance(next_player_pos) > 1.0) { + if (current_player_pos.DistanceTo(next_player_pos) > 1.0) { // target not reached yet return next_player_pos; } @@ -95,7 +95,7 @@ void PathFindingDemo::UpdatePlayerVelocity() { WorldPos velocity = WorldPos{}; if (next_pos) { velocity = next_pos.value() - current_pos; - velocity.normalize(); + velocity.Normalize(); //LOG_DEBUG("I want to move to: ", next_pos.value(), // ", velocity: ", velocity); } diff --git a/cpp/src/pathfindingdemo.hpp b/cpp/src/pathfindingdemo.hpp index 20532b3..9743c86 100644 --- a/cpp/src/pathfindingdemo.hpp +++ b/cpp/src/pathfindingdemo.hpp @@ -10,6 +10,7 @@ #include "map.hpp" #include "user_input.hpp" #include "pathfinder/base.hpp" +#include "camera.hpp" class PathFindingDemo { public: @@ -24,6 +25,7 @@ public: std::shared_ptr GetPlayer() { return m_Player; } std::vector>& GetEntities() { return m_Entities; } const Map& GetMap() const { return m_Map; } + const Camera& GetCamera() const { return m_Camera; } const pathfinder::Path& GetPath() const { return m_Path; } bool IsExitRequested() const { return m_ExitRequested; } @@ -37,6 +39,7 @@ public: private: bool m_ExitRequested = false; Map m_Map; + Camera m_Camera; std::vector> m_Entities; std::shared_ptr m_Player; pathfinder::Path m_Path; diff --git a/cpp/src/sprite.cpp b/cpp/src/sprite.cpp index 41b40ca..be5164f 100644 --- a/cpp/src/sprite.cpp +++ b/cpp/src/sprite.cpp @@ -12,13 +12,13 @@ Sprite::Sprite() : m_Texture(nullptr, SDL_DestroyTexture) {} -Sprite::Sprite(std::string path, Vec2D center) : Sprite() { +Sprite::Sprite(std::string path, WorldPos center) : Sprite() { LoadImage(path, center); } Sprite::~Sprite() { LOG_DEBUG("."); } -void Sprite::LoadImage(std::string path, Vec2D image_center) { +void Sprite::LoadImage(std::string path, WorldPos image_center) { LOG_INFO("Loading image ", path); assert(m_Renderer != nullptr); diff --git a/cpp/src/sprite.hpp b/cpp/src/sprite.hpp index fd86167..5b9b7e6 100644 --- a/cpp/src/sprite.hpp +++ b/cpp/src/sprite.hpp @@ -14,7 +14,7 @@ class Sprite { public: Sprite(); ~Sprite(); - explicit Sprite(std::string path, WorldPos center = {0, 0}); + explicit Sprite(std::string path, WorldPos center = WorldPos{}); Sprite(const Sprite &) = delete; Sprite &operator=(const Sprite &) = delete; @@ -28,7 +28,7 @@ public: WorldPos GetSize() const { return m_Size; } WorldPos GetCenter() const { return m_ImageCenter; } - void LoadImage(std::string path, WorldPos image_center = {0.0, 0.0}); + void LoadImage(std::string path, WorldPos image_center = WorldPos{}); private: static std::shared_ptr m_Renderer; diff --git a/cpp/src/user_input.hpp b/cpp/src/user_input.hpp index 2ea98b5..5c328a5 100644 --- a/cpp/src/user_input.hpp +++ b/cpp/src/user_input.hpp @@ -11,8 +11,8 @@ class UserAction { public: enum class Type { NONE, EXIT, SET_MOVE_TARGET, SELECT_PATHFINDER }; - UserAction() = default; - UserAction(Type t) : type(t) {} + UserAction() : type(Type::NONE), Argument{.number = 0} {} + UserAction(Type t) : type(t), Argument{.number = 0} {} UserAction(Type t, char key) : type(t), Argument{.key = key} {} UserAction(Type t, WorldPos v) : type(t), Argument{.position = v} {} UserAction(Type t, int arg) : type(t), Argument{.number = arg} {} @@ -25,6 +25,9 @@ public: char key; int number; } Argument; + + // TODO use std::variant + //std::variant Argument; }; class UserInput { diff --git a/cpp/src/window.cpp b/cpp/src/window.cpp index a488258..27f013e 100644 --- a/cpp/src/window.cpp +++ b/cpp/src/window.cpp @@ -76,17 +76,17 @@ Window::~Window() { LOG_DEBUG("."); } -void Window::DrawSprite(const WorldPos &position, Sprite &s) { +void Window::DrawSprite(const WindowPos &position, Sprite &s) { WorldPos size = s.GetSize(); WorldPos img_center = s.GetCenter(); - SDL_FRect rect = {position.x - img_center.x, position.y - img_center.y, - size.x, size.y}; + SDL_FRect rect = {position.x() - img_center.x(), position.y() - img_center.y(), + size.x(), size.y()}; SDL_RenderTexture(m_Renderer.get(), s.GetTexture(), nullptr, &rect); } -void Window::DrawRect(const WorldPos &position, const WorldPos size, uint8_t R, +void Window::DrawRect(const WindowPos &position, const WindowPos size, uint8_t R, uint8_t G, uint8_t B, uint8_t A) { - SDL_FRect rect = {position.x, position.y, size.x, size.y}; + SDL_FRect rect = {position.x(), position.y(), size.x(), size.y()}; SDL_SetRenderDrawColor(m_Renderer.get(), R, G, B, A); SDL_RenderFillRect(m_Renderer.get(), &rect); } @@ -98,9 +98,9 @@ void Window::ClearWindow() { void Window::Flush() { SDL_RenderPresent(m_Renderer.get()); } -void Window::DrawCircle(const WorldPos &position, float radius) { - int cx = static_cast(position.x); - int cy = static_cast(position.y); +void Window::DrawCircle(const WindowPos &position, float radius) { + int cx = static_cast(position.x()); + int cy = static_cast(position.y()); SDL_SetRenderDrawColor(m_Renderer.get(), 255, 0, 0, 255); for (int i = 0; i < 360; ++i) { double a = i * M_PI / 180.0; @@ -110,9 +110,9 @@ void Window::DrawCircle(const WorldPos &position, float radius) { } } -void Window::DrawLine(const WorldPos &A, const WorldPos &B) +void Window::DrawLine(const WindowPos &A, const WindowPos &B) { SDL_SetRenderDrawColor(m_Renderer.get(), 255, 0, 0, 255); - SDL_RenderLine(m_Renderer.get(), A.x, A.y, B.x, B.y); + SDL_RenderLine(m_Renderer.get(), A.x(), A.y(), B.x(), B.y()); } diff --git a/cpp/src/window.hpp b/cpp/src/window.hpp index 8813747..cd39983 100644 --- a/cpp/src/window.hpp +++ b/cpp/src/window.hpp @@ -22,13 +22,13 @@ public: Window &operator=(Window &&) = delete; std::expected Init(); - void DrawSprite(const WorldPos &position, Sprite &s); - void DrawRect(const WorldPos &position, const WorldPos size, uint8_t R, + void DrawSprite(const WindowPos &position, Sprite &s); + void DrawRect(const WindowPos &position, const WindowPos size, uint8_t R, uint8_t G, uint8_t B, uint8_t A); void ClearWindow(); void Flush(); - void DrawCircle(const WorldPos &position, float radius); - void DrawLine(const WorldPos &A, const WorldPos &B); + void DrawCircle(const WindowPos &position, float radius); + void DrawLine(const WindowPos &A, const WindowPos &B); private: uint32_t m_Width; diff --git a/cpp/test/test.cpp b/cpp/test/test.cpp index c7d4523..1f45431 100644 --- a/cpp/test/test.cpp +++ b/cpp/test/test.cpp @@ -37,6 +37,12 @@ TEST(vec, equalEpsilon) { ASSERT_EQ(v1, v2); } +TEST(vec, equalInt) { + ivec2 v1{1,2}; + ivec2 v2{1,2}; + ASSERT_EQ(v1, v2); +} + TEST(vec, nonEqualEpsilon) { // Test operator!= vec3 v1{1.0f, 2.0f, 3.0f}; From 326094caf3f41c56feddf3b7a33f9d4d04606dcb Mon Sep 17 00:00:00 2001 From: Jan Mrna Date: Sat, 4 Oct 2025 17:10:20 +0200 Subject: [PATCH 09/23] Adde camera class --- cpp/src/camera.cpp | 16 ++++++++++++++++ cpp/src/camera.hpp | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 cpp/src/camera.cpp create mode 100644 cpp/src/camera.hpp diff --git a/cpp/src/camera.cpp b/cpp/src/camera.cpp new file mode 100644 index 0000000..322fe1f --- /dev/null +++ b/cpp/src/camera.cpp @@ -0,0 +1,16 @@ +#include "camera.hpp" +#include "math.hpp" + + +// for now only pass-through placeholder functions, +// since we draw the whole map + +WindowPos Camera::WorldToWindow(WorldPos world) const +{ + return WindowPos{world[0], world[1]}; +} + +WorldPos Camera::WindowToWorld(WindowPos window) const +{ + return WorldPos{window[0], window[1]}; +} diff --git a/cpp/src/camera.hpp b/cpp/src/camera.hpp new file mode 100644 index 0000000..1df41ec --- /dev/null +++ b/cpp/src/camera.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "math.hpp" + +class Camera +{ +public: + WindowPos WorldToWindow(WorldPos) const; + WorldPos WindowToWorld(WindowPos) const; + +private: + // TODO this should be replaced with a matrix + float m_Zoom; + WorldPos m_RectCorner; // upper left corner (0,0) of the drawn regios + +}; From 75eeac06df9805a3f4c286a8fc34418e2f679561 Mon Sep 17 00:00:00 2001 From: Jan Mrna Date: Sat, 4 Oct 2025 17:10:40 +0200 Subject: [PATCH 10/23] Removed unused coded, fix compiler warnings --- cpp/src/math.hpp | 118 ------------------------------------ cpp/src/pathfinder/base.cpp | 2 +- cpp/src/user_input.cpp | 1 - 3 files changed, 1 insertion(+), 120 deletions(-) diff --git a/cpp/src/math.hpp b/cpp/src/math.hpp index 5d214b0..a6a3400 100644 --- a/cpp/src/math.hpp +++ b/cpp/src/math.hpp @@ -259,121 +259,3 @@ struct TilePosHash { } }; -// old stuff - TODO delete - -// constexpr double EQUALITY_LIMIT = 1e-6; -// template struct Vec2D { -// public: -// Vec2D() = default; -// ~Vec2D() = default; -// -// template -// Vec2D(Vec2D other) { -// this->x = static_cast(other.x); -// this->y = static_cast(other.y); -// } -// -// Vec2D& operator+=(const Vec2D &other) { -// x += other.x; -// y += other.y; -// return *this; -// } -// -// template -// requires std::is_arithmetic_v -// Vec2D& operator/=(U k) { -// this->x /= static_cast(k); -// this->y /= static_cast(k); -// return *this; -// } -// -// friend Vec2D operator+(const Vec2D &a, const Vec2D &b) { -// return Vec2D{a.x + b.x, a.y + b.y}; -// } -// -// friend Vec2D operator-(const Vec2D &a, const Vec2D &b) { -// return Vec2D{a.x - b.x, a.y - b.y}; -// } -// -// template -// requires std::is_arithmetic_v -// friend Vec2D operator*(U k, const Vec2D &v) -// { -// return Vec2D{k * v.x, k * v.y}; -// } -// -// template -// requires std::is_arithmetic_v -// friend Vec2D operator/(const Vec2D &v, U k) -// { -// return Vec2D{v.x / k, v.y / k}; -// } -// -// friend bool operator==(const Vec2D &a, const Vec2D &b) { -// if constexpr (std::is_integral_v) { -// return a.x == b.x && a.y == b.y; -// } else if constexpr (std::is_floating_point_v) { -// return a.distance(b) < EQUALITY_LIMIT; -// } else { -// static_assert("Unhandled comparison"); -// } -// } -// -// Vec2D operator*(float b) const { return Vec2D{b * x, b * y}; } -// -// T distance_squared(const Vec2D &other) const { -// T dx = x - other.x; -// T dy = y - other.y; -// return dx * dx + dy * dy; -// } -// -// T distance(const Vec2D &other) const -// requires std::floating_point -// { -// return sqrt(distance_squared(other)); -// } -// -// void normalize() -// requires std::floating_point -// { -// auto length = sqrt(x * x + y * y); -// if (length < EQUALITY_LIMIT) { -// x = y = 0; -// } else { -// x /= length; -// y /= length; -// } -// } -// -// Vec2D normalized() -// requires std::floating_point -// { -// Vec2D v(*this); -// v.normalize(); -// return v; -// } -// -// Vec2D orthogonal() const -// { -// Vec2D v(*this); -// -// std::swap(v.x, v.y); -// v.x = -v.x; -// return v; -// } -// -// template Vec2D(std::initializer_list list) { -// assert(list.size() == 2); -// auto first_element = *list.begin(); -// auto second_element = *(list.begin() + 1); -// x = static_cast(first_element); -// y = static_cast(second_element); -// } -// -// T x, y; -// -// friend std::ostream &operator<<(std::ostream &os, const Vec2D &obj) { -// os << "( " << obj.x << ", " << obj.y << ")"; -// return os; -// } -// }; diff --git a/cpp/src/pathfinder/base.cpp b/cpp/src/pathfinder/base.cpp index 5468672..cccaf5b 100644 --- a/cpp/src/pathfinder/base.cpp +++ b/cpp/src/pathfinder/base.cpp @@ -13,7 +13,7 @@ PathFinderBase::PathFinderBase(const Map* map) : m_Map(map) {} // LinearPathFinder also lives here, since it is too small to get it's // own implementation file -Path LinearPathFinder::CalculatePath(WorldPos start, WorldPos end) +Path LinearPathFinder::CalculatePath(WorldPos, WorldPos end) // first argument (start pos) not used { auto path = Path{end}; return path; diff --git a/cpp/src/user_input.cpp b/cpp/src/user_input.cpp index 46a6cdb..2c252f3 100644 --- a/cpp/src/user_input.cpp +++ b/cpp/src/user_input.cpp @@ -21,7 +21,6 @@ std::expected UserInput::Init() { return {}; } const std::vector &UserInput::GetActions() { m_Actions.clear(); - static WorldPos move_direction = {0.0f, 0.0f}; SDL_Event event; while (SDL_PollEvent(&event)) { From f458468644f101ed9a533c5ac00d5b8b059b1065 Mon Sep 17 00:00:00 2001 From: Jan Mrna Date: Sun, 5 Oct 2025 12:11:06 +0200 Subject: [PATCH 11/23] Add map pan --- cpp/src/camera.cpp | 33 ++++++++- cpp/src/camera.hpp | 12 ++-- cpp/src/gameloop.cpp | 17 +++-- cpp/src/map.cpp | 3 +- cpp/src/map.hpp | 4 +- cpp/src/math.hpp | 138 ++++++++++++++++++++++++++++++++++-- cpp/src/pathfindingdemo.cpp | 7 +- cpp/src/user_input.cpp | 26 ++++++- cpp/src/user_input.hpp | 8 +-- cpp/src/window.cpp | 2 +- cpp/src/window.hpp | 2 +- 11 files changed, 219 insertions(+), 33 deletions(-) diff --git a/cpp/src/camera.cpp b/cpp/src/camera.cpp index 322fe1f..3134796 100644 --- a/cpp/src/camera.cpp +++ b/cpp/src/camera.cpp @@ -1,16 +1,43 @@ #include "camera.hpp" #include "math.hpp" - +#include "log.hpp" // for now only pass-through placeholder functions, // since we draw the whole map + +void Camera::Pan(const WorldPos& delta) +{ + LOG_DEBUG("m_Pan before: ", m_Pan, " delta ", delta); + m_Pan += delta; + LOG_DEBUG("m_Pan after: ", m_Pan); +} + +void Camera::Zoom(const WorldPos&) +{ + +} + WindowPos Camera::WorldToWindow(WorldPos world) const { - return WindowPos{world[0], world[1]}; + const auto& v = world + m_Pan; + return WindowPos{v[0], v[1]}; } WorldPos Camera::WindowToWorld(WindowPos window) const { - return WorldPos{window[0], window[1]}; + return WorldPos{window[0], window[1]} - m_Pan; +} + + +WindowSize Camera::WorldToWindowSize(WorldSize world) const +{ + const auto& v = world; + // no zoom yet, just pass-through + return WindowSize{v[0], v[1]}; +} + +WorldSize Camera::WindowToWorldSize(WindowSize window) const +{ + return WorldSize{window[0], window[1]}; } diff --git a/cpp/src/camera.hpp b/cpp/src/camera.hpp index 1df41ec..310451b 100644 --- a/cpp/src/camera.hpp +++ b/cpp/src/camera.hpp @@ -5,12 +5,16 @@ class Camera { public: - WindowPos WorldToWindow(WorldPos) const; - WorldPos WindowToWorld(WindowPos) const; + void Pan(const WorldPos& delta); + void Zoom(const WorldPos& delta); + + WindowPos WorldToWindow(WorldPos) const; + WorldPos WindowToWorld(WindowPos) const; + WindowSize WorldToWindowSize(WorldSize) const; + WorldSize WindowToWorldSize(WindowSize) const; private: // TODO this should be replaced with a matrix float m_Zoom; - WorldPos m_RectCorner; // upper left corner (0,0) of the drawn regios - + WorldPos m_Pan; }; diff --git a/cpp/src/gameloop.cpp b/cpp/src/gameloop.cpp index 0c0466f..671548c 100644 --- a/cpp/src/gameloop.cpp +++ b/cpp/src/gameloop.cpp @@ -27,14 +27,19 @@ void GameLoop::Run() { for (size_t row = 0; row < tiles.size(); row++) { for (size_t col = 0; col < tiles[row].size(); col++) { const auto& camera = m_Game->GetCamera(); + const auto& position = camera.WorldToWindow( + map.TileEdgeToWorld( + TilePos{static_cast(row), static_cast(col)} + ) + ); + const auto& size = camera.WorldToWindowSize( + map.GetTileSize() + ); // LOG_DEBUG("Drawing rect (", row, ", ", col, ")"); m_Window->DrawRect( - camera.WorldToWindow( - map.TileEdgeToWorld( - TilePos{static_cast(row), static_cast(col)} - ) - ), - camera.WorldToWindow(map.GetTileSize()), tiles[row][col]->R, tiles[row][col]->G, + position, + size, + tiles[row][col]->R, tiles[row][col]->G, tiles[row][col]->B, tiles[row][col]->A); } } diff --git a/cpp/src/map.cpp b/cpp/src/map.cpp index 4ef7e51..902880f 100644 --- a/cpp/src/map.cpp +++ b/cpp/src/map.cpp @@ -28,8 +28,7 @@ TilePos Map::WorldToTile(WorldPos p) const { return TilePos{static_cast(p.x() / TILE_SIZE), static_cast(p.y() / TILE_SIZE)}; } -// TODO this should probably use something like WorldSize or WorldVec to make the distinction clear -WorldPos Map::GetTileSize() const { return WorldPos{TILE_SIZE, TILE_SIZE}; } +WorldSize Map::GetTileSize() const { return WorldSize{TILE_SIZE, TILE_SIZE}; } const Tile *Map::GetTileAt(TilePos p) const { assert(IsTilePosValid(p)); diff --git a/cpp/src/map.hpp b/cpp/src/map.hpp index 2be7b64..6b400f6 100644 --- a/cpp/src/map.hpp +++ b/cpp/src/map.hpp @@ -25,8 +25,8 @@ public: WorldPos TileToWorld(TilePos p) const; WorldPos TileEdgeToWorld(TilePos p) const; TilePos WorldToTile(WorldPos p) const; - - WorldPos GetTileSize() const; + + WorldSize GetTileSize() const; const Tile *GetTileAt(TilePos p) const; const Tile *GetTileAt(WorldPos p) const; diff --git a/cpp/src/math.hpp b/cpp/src/math.hpp index a6a3400..3cb0dda 100644 --- a/cpp/src/math.hpp +++ b/cpp/src/math.hpp @@ -238,14 +238,22 @@ using uvec3 = vec; using uvec4 = vec; // tags for differentiating between domains -struct WorldTag {}; -struct WindowTag {}; -struct TileTag {}; +struct WorldPosTag {}; +struct WorldSizeTag {}; +struct WindowPosTag {}; +struct WindowSizeTag {}; +struct TilePosTag {}; +struct TileSizeTag {}; // types for each domain -using WorldPos = vec; -using WindowPos = vec; -using TilePos = vec; +using WorldPos = vec; +using WindowPos = vec; +using TilePos = vec; +// Size +using WorldSize = vec; +using WindowSize = vec; +using TileSize = vec; + // // Utils @@ -259,3 +267,121 @@ struct TilePosHash { } }; +// old stuff - TODO delete + +// constexpr double EQUALITY_LIMIT = 1e-6; +// template struct Vec2D { +// public: +// Vec2D() = default; +// ~Vec2D() = default; +// +// template +// Vec2D(Vec2D other) { +// this->x = static_cast(other.x); +// this->y = static_cast(other.y); +// } +// +// Vec2D& operator+=(const Vec2D &other) { +// x += other.x; +// y += other.y; +// return *this; +// } +// +// template +// requires std::is_arithmetic_v +// Vec2D& operator/=(U k) { +// this->x /= static_cast(k); +// this->y /= static_cast(k); +// return *this; +// } +// +// friend Vec2D operator+(const Vec2D &a, const Vec2D &b) { +// return Vec2D{a.x + b.x, a.y + b.y}; +// } +// +// friend Vec2D operator-(const Vec2D &a, const Vec2D &b) { +// return Vec2D{a.x - b.x, a.y - b.y}; +// } +// +// template +// requires std::is_arithmetic_v +// friend Vec2D operator*(U k, const Vec2D &v) +// { +// return Vec2D{k * v.x, k * v.y}; +// } +// +// template +// requires std::is_arithmetic_v +// friend Vec2D operator/(const Vec2D &v, U k) +// { +// return Vec2D{v.x / k, v.y / k}; +// } +// +// friend bool operator==(const Vec2D &a, const Vec2D &b) { +// if constexpr (std::is_integral_v) { +// return a.x == b.x && a.y == b.y; +// } else if constexpr (std::is_floating_point_v) { +// return a.distance(b) < EQUALITY_LIMIT; +// } else { +// static_assert("Unhandled comparison"); +// } +// } +// +// Vec2D operator*(float b) const { return Vec2D{b * x, b * y}; } +// +// T distance_squared(const Vec2D &other) const { +// T dx = x - other.x; +// T dy = y - other.y; +// return dx * dx + dy * dy; +// } +// +// T distance(const Vec2D &other) const +// requires std::floating_point +// { +// return sqrt(distance_squared(other)); +// } +// +// void normalize() +// requires std::floating_point +// { +// auto length = sqrt(x * x + y * y); +// if (length < EQUALITY_LIMIT) { +// x = y = 0; +// } else { +// x /= length; +// y /= length; +// } +// } +// +// Vec2D normalized() +// requires std::floating_point +// { +// Vec2D v(*this); +// v.normalize(); +// return v; +// } +// +// Vec2D orthogonal() const +// { +// Vec2D v(*this); +// +// std::swap(v.x, v.y); +// v.x = -v.x; +// return v; +// } +// +// template Vec2D(std::initializer_list list) { +// assert(list.size() == 2); +// auto first_element = *list.begin(); +// auto second_element = *(list.begin() + 1); +// x = static_cast(first_element); +// y = static_cast(second_element); +// } +// +// T x, y; +// +// friend std::ostream &operator<<(std::ostream &os, const Vec2D &obj) { +// os << "( " << obj.x << ", " << obj.y << ")"; +// return os; +// } +// }; diff --git a/cpp/src/pathfindingdemo.cpp b/cpp/src/pathfindingdemo.cpp index 8fe7d8e..5e985dc 100644 --- a/cpp/src/pathfindingdemo.cpp +++ b/cpp/src/pathfindingdemo.cpp @@ -110,7 +110,7 @@ void PathFindingDemo::HandleActions(const std::vector &actions) { LOG_INFO("Exit requested"); m_ExitRequested = true; } else if (action.type == UserAction::Type::SET_MOVE_TARGET) { - WorldPos wp = action.Argument.position; + WorldPos wp = m_Camera.WindowToWorld(action.Argument.position); LOG_INFO("Calculating path to target: ", wp); m_Path = m_PathFinder->CalculatePath(m_Player->GetPosition(), wp); LOG_INFO("Done, path node count: ", m_Path.size()); @@ -119,6 +119,11 @@ void PathFindingDemo::HandleActions(const std::vector &actions) { PathFinderType type = static_cast(action.Argument.number); m_PathFinder = pathfinder::utils::create(type, (const Map*)&m_Map); LOG_INFO("Switched to path finding method: ", m_PathFinder->GetName()); + } else if (action.type == UserAction::Type::CAMERA_PAN) { + const auto& window_pan = action.Argument.position; + WorldPos world_pan{window_pan.x(), window_pan.y()}; + m_Camera.Pan(world_pan); + LOG_INFO("Camera pan delta: ", world_pan); } }; } diff --git a/cpp/src/user_input.cpp b/cpp/src/user_input.cpp index 2c252f3..fc98968 100644 --- a/cpp/src/user_input.cpp +++ b/cpp/src/user_input.cpp @@ -22,8 +22,12 @@ std::expected UserInput::Init() { return {}; } const std::vector &UserInput::GetActions() { m_Actions.clear(); SDL_Event event; + + static WindowPos mouse_pan_start; + static bool mouse_pan = false; while (SDL_PollEvent(&event)) { + // TODO refactor mouse / kbd handling into separate functions if (event.type == SDL_EVENT_KEY_DOWN || event.type == SDL_EVENT_KEY_UP) { bool key_down = event.type == SDL_EVENT_KEY_DOWN ? true : false; SDL_KeyboardEvent kbd_event = event.key; @@ -55,9 +59,25 @@ const std::vector &UserInput::GetActions() { } } else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { SDL_MouseButtonEvent mouse_event = event.button; - LOG_DEBUG("Mouse down: ", mouse_event.x, ", ", mouse_event.y); - m_Actions.emplace_back(UserAction::Type::SET_MOVE_TARGET, - WorldPos{mouse_event.x, mouse_event.y}); + if (mouse_event.button == 1) { + LOG_DEBUG("Mouse down: ", mouse_event.x, ", ", mouse_event.y); + m_Actions.emplace_back(UserAction::Type::SET_MOVE_TARGET, + WindowPos{mouse_event.x, mouse_event.y}); + } else if (mouse_event.button == 2) { + mouse_pan = true; + } + } else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP) { + SDL_MouseButtonEvent mouse_event = event.button; + if (mouse_event.button == 2) { + mouse_pan = false; + } + } else if (event.type == SDL_EVENT_MOUSE_MOTION) { + SDL_MouseMotionEvent motion_event = event.motion; + if (mouse_pan) { + m_Actions.emplace_back(UserAction::Type::CAMERA_PAN, + WindowPos{motion_event.xrel, motion_event.yrel}); + + } } else { // TODO uncomment, for now too much noise // LOG_WARNING("Action not processed"); diff --git a/cpp/src/user_input.hpp b/cpp/src/user_input.hpp index 5c328a5..62631e5 100644 --- a/cpp/src/user_input.hpp +++ b/cpp/src/user_input.hpp @@ -9,25 +9,25 @@ class UserAction { public: - enum class Type { NONE, EXIT, SET_MOVE_TARGET, SELECT_PATHFINDER }; + enum class Type { NONE, EXIT, SET_MOVE_TARGET, SELECT_PATHFINDER, CAMERA_PAN, CAMERA_ZOOM }; UserAction() : type(Type::NONE), Argument{.number = 0} {} UserAction(Type t) : type(t), Argument{.number = 0} {} UserAction(Type t, char key) : type(t), Argument{.key = key} {} - UserAction(Type t, WorldPos v) : type(t), Argument{.position = v} {} + UserAction(Type t, WindowPos v) : type(t), Argument{.position = v} {} UserAction(Type t, int arg) : type(t), Argument{.number = arg} {} ~UserAction() = default; Type type; union { - WorldPos position; + WindowPos position; char key; int number; } Argument; // TODO use std::variant - //std::variant Argument; + //std::variant Argument; }; class UserInput { diff --git a/cpp/src/window.cpp b/cpp/src/window.cpp index 27f013e..b49d15b 100644 --- a/cpp/src/window.cpp +++ b/cpp/src/window.cpp @@ -84,7 +84,7 @@ void Window::DrawSprite(const WindowPos &position, Sprite &s) { SDL_RenderTexture(m_Renderer.get(), s.GetTexture(), nullptr, &rect); } -void Window::DrawRect(const WindowPos &position, const WindowPos size, uint8_t R, +void Window::DrawRect(const WindowPos &position, const WindowSize size, uint8_t R, uint8_t G, uint8_t B, uint8_t A) { SDL_FRect rect = {position.x(), position.y(), size.x(), size.y()}; SDL_SetRenderDrawColor(m_Renderer.get(), R, G, B, A); diff --git a/cpp/src/window.hpp b/cpp/src/window.hpp index cd39983..5a16152 100644 --- a/cpp/src/window.hpp +++ b/cpp/src/window.hpp @@ -23,7 +23,7 @@ public: std::expected Init(); void DrawSprite(const WindowPos &position, Sprite &s); - void DrawRect(const WindowPos &position, const WindowPos size, uint8_t R, + void DrawRect(const WindowPos &position, const WindowSize size, uint8_t R, uint8_t G, uint8_t B, uint8_t A); void ClearWindow(); void Flush(); From 4a9498a5206c3beb79e4bbd33117063cfc7c8a3f Mon Sep 17 00:00:00 2001 From: Jan Mrna Date: Sun, 5 Oct 2025 12:16:53 +0200 Subject: [PATCH 12/23] Add mouse button type --- cpp/src/user_input.cpp | 8 +++++--- cpp/src/user_input.hpp | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/cpp/src/user_input.cpp b/cpp/src/user_input.cpp index fc98968..59dd83b 100644 --- a/cpp/src/user_input.cpp +++ b/cpp/src/user_input.cpp @@ -59,16 +59,18 @@ const std::vector &UserInput::GetActions() { } } else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { SDL_MouseButtonEvent mouse_event = event.button; - if (mouse_event.button == 1) { + MouseButton button = static_cast(mouse_event.button); + if (button == MouseButton::LEFT) { LOG_DEBUG("Mouse down: ", mouse_event.x, ", ", mouse_event.y); m_Actions.emplace_back(UserAction::Type::SET_MOVE_TARGET, WindowPos{mouse_event.x, mouse_event.y}); - } else if (mouse_event.button == 2) { + } else if (button == MouseButton::MIDDLE) { mouse_pan = true; } } else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP) { SDL_MouseButtonEvent mouse_event = event.button; - if (mouse_event.button == 2) { + MouseButton button = static_cast(mouse_event.button); + if (button == MouseButton::MIDDLE) { mouse_pan = false; } } else if (event.type == SDL_EVENT_MOUSE_MOTION) { diff --git a/cpp/src/user_input.hpp b/cpp/src/user_input.hpp index 62631e5..48a2457 100644 --- a/cpp/src/user_input.hpp +++ b/cpp/src/user_input.hpp @@ -7,6 +7,9 @@ #include "log.hpp" #include "math.hpp" +// Seems like SDL doesn't have named constants for mouse button +enum class MouseButton { LEFT = 1, MIDDLE, RIGHT }; + class UserAction { public: enum class Type { NONE, EXIT, SET_MOVE_TARGET, SELECT_PATHFINDER, CAMERA_PAN, CAMERA_ZOOM }; From d924e7dbca777700a9c45170bf20f40533c079b9 Mon Sep 17 00:00:00 2001 From: Jan Mrna Date: Sun, 5 Oct 2025 20:17:24 +0200 Subject: [PATCH 13/23] Refactor mouse and keyboard events --- cpp/src/user_input.cpp | 87 +++++++++++++++++++++++++----------------- cpp/src/user_input.hpp | 3 ++ 2 files changed, 55 insertions(+), 35 deletions(-) diff --git a/cpp/src/user_input.cpp b/cpp/src/user_input.cpp index 59dd83b..c9cfc5f 100644 --- a/cpp/src/user_input.cpp +++ b/cpp/src/user_input.cpp @@ -19,21 +19,43 @@ UserInput::~UserInput() { LOG_DEBUG("."); }; std::expected UserInput::Init() { return {}; } -const std::vector &UserInput::GetActions() { - m_Actions.clear(); - SDL_Event event; - - static WindowPos mouse_pan_start; +void UserInput::GetActions_mouse(const SDL_Event& event) +{ static bool mouse_pan = false; - while (SDL_PollEvent(&event)) { - // TODO refactor mouse / kbd handling into separate functions - if (event.type == SDL_EVENT_KEY_DOWN || event.type == SDL_EVENT_KEY_UP) { + if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { + SDL_MouseButtonEvent mouse_event = event.button; + MouseButton button = static_cast(mouse_event.button); + if (button == MouseButton::LEFT) { + LOG_DEBUG("Mouse down: ", mouse_event.x, ", ", mouse_event.y); + m_Actions.emplace_back(UserAction::Type::SET_MOVE_TARGET, + WindowPos{mouse_event.x, mouse_event.y}); + } else if (button == MouseButton::MIDDLE) { + mouse_pan = true; + } + } else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP) { + SDL_MouseButtonEvent mouse_event = event.button; + MouseButton button = static_cast(mouse_event.button); + if (button == MouseButton::MIDDLE) { + mouse_pan = false; + } + } else if (event.type == SDL_EVENT_MOUSE_MOTION) { + SDL_MouseMotionEvent motion_event = event.motion; + if (mouse_pan) { + m_Actions.emplace_back(UserAction::Type::CAMERA_PAN, + WindowPos{motion_event.xrel, motion_event.yrel}); + + } + } +} + +void UserInput::GetActions_kbd(const SDL_Event& event) +{ bool key_down = event.type == SDL_EVENT_KEY_DOWN ? true : false; SDL_KeyboardEvent kbd_event = event.key; if (kbd_event.repeat) { // SDL repeats KEY_DOWN if key is held down, we ignore that - continue; + return; } LOG_DEBUG("Key '", static_cast(kbd_event.key), key_down ? "' down" : "' up"); @@ -41,8 +63,7 @@ const std::vector &UserInput::GetActions() { switch (kbd_event.key) { case 'q': m_Actions.emplace_back(UserAction::Type::EXIT); - // further processing of inputs is not needed - return m_Actions; + return; case '1': case '2': case '3': @@ -57,30 +78,26 @@ const std::vector &UserInput::GetActions() { LOG_INFO("Key '", static_cast(kbd_event.key), "' not mapped"); break; } - } else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { - SDL_MouseButtonEvent mouse_event = event.button; - MouseButton button = static_cast(mouse_event.button); - if (button == MouseButton::LEFT) { - LOG_DEBUG("Mouse down: ", mouse_event.x, ", ", mouse_event.y); - m_Actions.emplace_back(UserAction::Type::SET_MOVE_TARGET, - WindowPos{mouse_event.x, mouse_event.y}); - } else if (button == MouseButton::MIDDLE) { - mouse_pan = true; - } - } else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP) { - SDL_MouseButtonEvent mouse_event = event.button; - MouseButton button = static_cast(mouse_event.button); - if (button == MouseButton::MIDDLE) { - mouse_pan = false; - } - } else if (event.type == SDL_EVENT_MOUSE_MOTION) { - SDL_MouseMotionEvent motion_event = event.motion; - if (mouse_pan) { - m_Actions.emplace_back(UserAction::Type::CAMERA_PAN, - WindowPos{motion_event.xrel, motion_event.yrel}); - - } - } else { +} + +const std::vector& UserInput::GetActions() { + m_Actions.clear(); + SDL_Event event; + + while (SDL_PollEvent(&event)) { + if (event.type == SDL_EVENT_KEY_DOWN + || event.type == SDL_EVENT_KEY_UP) + { + GetActions_kbd(event); + } + else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN + || event.type == SDL_EVENT_MOUSE_BUTTON_UP + || event.type == SDL_EVENT_MOUSE_MOTION) + { + GetActions_mouse(event); + } + else + { // TODO uncomment, for now too much noise // LOG_WARNING("Action not processed"); } diff --git a/cpp/src/user_input.hpp b/cpp/src/user_input.hpp index 48a2457..8c6b2fc 100644 --- a/cpp/src/user_input.hpp +++ b/cpp/src/user_input.hpp @@ -49,4 +49,7 @@ public: private: std::vector m_Actions; + + void GetActions_kbd(const SDL_Event&); + void GetActions_mouse(const SDL_Event&); }; From f5304d045d0c6f136bc5dc686e96ed640c96e140 Mon Sep 17 00:00:00 2001 From: Jan Mrna Date: Sun, 5 Oct 2025 20:45:09 +0200 Subject: [PATCH 14/23] Refactor user input --- cpp/src/user_input.cpp | 34 ++++++++++++++++++++++++---------- cpp/src/user_input.hpp | 2 +- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/cpp/src/user_input.cpp b/cpp/src/user_input.cpp index c9cfc5f..9d65e6e 100644 --- a/cpp/src/user_input.cpp +++ b/cpp/src/user_input.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "user_input.hpp" @@ -49,7 +50,7 @@ void UserInput::GetActions_mouse(const SDL_Event& event) } } -void UserInput::GetActions_kbd(const SDL_Event& event) +void UserInput::GetActions_keyboard(const SDL_Event& event) { bool key_down = event.type == SDL_EVENT_KEY_DOWN ? true : false; SDL_KeyboardEvent kbd_event = event.key; @@ -81,18 +82,31 @@ void UserInput::GetActions_kbd(const SDL_Event& event) } const std::vector& UserInput::GetActions() { - m_Actions.clear(); - SDL_Event event; + + static std::unordered_set mouse_events = { + SDL_EVENT_MOUSE_MOTION, + SDL_EVENT_MOUSE_BUTTON_DOWN, + SDL_EVENT_MOUSE_BUTTON_UP, + SDL_EVENT_MOUSE_WHEEL, + SDL_EVENT_MOUSE_ADDED, + SDL_EVENT_MOUSE_REMOVED, + }; - while (SDL_PollEvent(&event)) { - if (event.type == SDL_EVENT_KEY_DOWN - || event.type == SDL_EVENT_KEY_UP) + static std::unordered_set keyboard_events = { + SDL_EVENT_KEY_DOWN, + SDL_EVENT_KEY_UP, + }; + + SDL_Event event; + m_Actions.clear(); + + while (SDL_PollEvent(&event)) + { + if (keyboard_events.contains(event.type)) { - GetActions_kbd(event); + GetActions_keyboard(event); } - else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN - || event.type == SDL_EVENT_MOUSE_BUTTON_UP - || event.type == SDL_EVENT_MOUSE_MOTION) + else if (mouse_events.contains(event.type)) { GetActions_mouse(event); } diff --git a/cpp/src/user_input.hpp b/cpp/src/user_input.hpp index 8c6b2fc..8ed139b 100644 --- a/cpp/src/user_input.hpp +++ b/cpp/src/user_input.hpp @@ -50,6 +50,6 @@ public: private: std::vector m_Actions; - void GetActions_kbd(const SDL_Event&); + void GetActions_keyboard(const SDL_Event&); void GetActions_mouse(const SDL_Event&); }; From 71e6bdb9046b808978ab4c9a9a86d7923ac24d6c Mon Sep 17 00:00:00 2001 From: Jan Mrna Date: Sun, 5 Oct 2025 21:20:30 +0200 Subject: [PATCH 15/23] Basic zoom implemented --- cpp/src/camera.cpp | 13 +++++++------ cpp/src/camera.hpp | 4 ++-- cpp/src/math.hpp | 9 +++++++++ cpp/src/pathfindingdemo.cpp | 29 ++++++++++++++++++++------- cpp/src/user_input.cpp | 39 ++++++++++++++++++++++++------------- cpp/src/user_input.hpp | 6 ++++-- 6 files changed, 70 insertions(+), 30 deletions(-) diff --git a/cpp/src/camera.cpp b/cpp/src/camera.cpp index 3134796..d24a74b 100644 --- a/cpp/src/camera.cpp +++ b/cpp/src/camera.cpp @@ -8,25 +8,26 @@ void Camera::Pan(const WorldPos& delta) { - LOG_DEBUG("m_Pan before: ", m_Pan, " delta ", delta); m_Pan += delta; - LOG_DEBUG("m_Pan after: ", m_Pan); } -void Camera::Zoom(const WorldPos&) +void Camera::Zoom(float delta) { - + constexpr float ZOOM_SCALE = 0.1f; + m_Zoom += delta * ZOOM_SCALE; + LOG_DEBUG("Zoom: ", m_Zoom); } WindowPos Camera::WorldToWindow(WorldPos world) const { const auto& v = world + m_Pan; - return WindowPos{v[0], v[1]}; + return WindowPos{v[0], v[1]} * m_Zoom; } WorldPos Camera::WindowToWorld(WindowPos window) const { - return WorldPos{window[0], window[1]} - m_Pan; + window /= m_Zoom; + return WorldPos{window[0], window[1]} - m_Pan; } diff --git a/cpp/src/camera.hpp b/cpp/src/camera.hpp index 310451b..ed61e00 100644 --- a/cpp/src/camera.hpp +++ b/cpp/src/camera.hpp @@ -6,7 +6,7 @@ class Camera { public: void Pan(const WorldPos& delta); - void Zoom(const WorldPos& delta); + void Zoom(float delta); WindowPos WorldToWindow(WorldPos) const; WorldPos WindowToWorld(WindowPos) const; @@ -15,6 +15,6 @@ public: private: // TODO this should be replaced with a matrix - float m_Zoom; + float m_Zoom = 1.0f; WorldPos m_Pan; }; diff --git a/cpp/src/math.hpp b/cpp/src/math.hpp index 3cb0dda..4068b7e 100644 --- a/cpp/src/math.hpp +++ b/cpp/src/math.hpp @@ -127,6 +127,15 @@ public: return a; } + vec& operator/=(float scalar) + { + vec& a = *this; + auto b = std::views::repeat(scalar); + std::ranges::transform(a.m_Array, b, a.m_Array.begin(), std::divides{}); + // TODO check all of this, could be done better with views instead of ranges? + return a; + } + // // Utility functions // diff --git a/cpp/src/pathfindingdemo.cpp b/cpp/src/pathfindingdemo.cpp index 5e985dc..eef2382 100644 --- a/cpp/src/pathfindingdemo.cpp +++ b/cpp/src/pathfindingdemo.cpp @@ -93,7 +93,8 @@ void PathFindingDemo::UpdatePlayerVelocity() { double tile_velocity_coeff = m_Map.GetTileVelocityCoeff(current_pos); auto next_pos = GetMoveTarget(); WorldPos velocity = WorldPos{}; - if (next_pos) { + if (next_pos) + { velocity = next_pos.value() - current_pos; velocity.Normalize(); //LOG_DEBUG("I want to move to: ", next_pos.value(), @@ -104,26 +105,40 @@ void PathFindingDemo::UpdatePlayerVelocity() { player->Update(time_delta); } -void PathFindingDemo::HandleActions(const std::vector &actions) { - for (const auto &action : actions) { - if (action.type == UserAction::Type::EXIT) { +void PathFindingDemo::HandleActions(const std::vector &actions) +{ + for (const auto &action : actions) + { + if (action.type == UserAction::Type::EXIT) + { LOG_INFO("Exit requested"); m_ExitRequested = true; - } else if (action.type == UserAction::Type::SET_MOVE_TARGET) { + } + else if (action.type == UserAction::Type::SET_MOVE_TARGET) + { WorldPos wp = m_Camera.WindowToWorld(action.Argument.position); LOG_INFO("Calculating path to target: ", wp); m_Path = m_PathFinder->CalculatePath(m_Player->GetPosition(), wp); LOG_INFO("Done, path node count: ", m_Path.size()); - } else if (action.type == UserAction::Type::SELECT_PATHFINDER) { + } + else if (action.type == UserAction::Type::SELECT_PATHFINDER) + { using namespace pathfinder; PathFinderType type = static_cast(action.Argument.number); m_PathFinder = pathfinder::utils::create(type, (const Map*)&m_Map); LOG_INFO("Switched to path finding method: ", m_PathFinder->GetName()); - } else if (action.type == UserAction::Type::CAMERA_PAN) { + } + else if (action.type == UserAction::Type::CAMERA_PAN) + { const auto& window_pan = action.Argument.position; WorldPos world_pan{window_pan.x(), window_pan.y()}; m_Camera.Pan(world_pan); LOG_INFO("Camera pan delta: ", world_pan); } + else if (action.type == UserAction::Type::CAMERA_ZOOM) + { + m_Camera.Zoom(action.Argument.float_number); + LOG_INFO("Camera zoom: ", action.Argument.float_number); + } }; } diff --git a/cpp/src/user_input.cpp b/cpp/src/user_input.cpp index 9d65e6e..1604f94 100644 --- a/cpp/src/user_input.cpp +++ b/cpp/src/user_input.cpp @@ -24,30 +24,43 @@ void UserInput::GetActions_mouse(const SDL_Event& event) { static bool mouse_pan = false; - if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { - SDL_MouseButtonEvent mouse_event = event.button; - MouseButton button = static_cast(mouse_event.button); - if (button == MouseButton::LEFT) { + SDL_MouseButtonEvent mouse_event = event.button; + MouseButton button = static_cast(mouse_event.button); + + if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) + { + if (button == MouseButton::LEFT) + { LOG_DEBUG("Mouse down: ", mouse_event.x, ", ", mouse_event.y); m_Actions.emplace_back(UserAction::Type::SET_MOVE_TARGET, WindowPos{mouse_event.x, mouse_event.y}); - } else if (button == MouseButton::MIDDLE) { + } + else if (button == MouseButton::MIDDLE) + { mouse_pan = true; } - } else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP) { - SDL_MouseButtonEvent mouse_event = event.button; - MouseButton button = static_cast(mouse_event.button); - if (button == MouseButton::MIDDLE) { + } + else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP) + { + if (button == MouseButton::MIDDLE) + { mouse_pan = false; } - } else if (event.type == SDL_EVENT_MOUSE_MOTION) { + } + else if (event.type == SDL_EVENT_MOUSE_MOTION) + { SDL_MouseMotionEvent motion_event = event.motion; - if (mouse_pan) { + if (mouse_pan) + { m_Actions.emplace_back(UserAction::Type::CAMERA_PAN, - WindowPos{motion_event.xrel, motion_event.yrel}); - + WindowPos{motion_event.xrel, motion_event.yrel}); } } + else if(event.type == SDL_EVENT_MOUSE_WHEEL) + { + SDL_MouseWheelEvent mouse_wheel = event.wheel; + m_Actions.emplace_back(UserAction::Type::CAMERA_ZOOM, mouse_wheel.y); + } } void UserInput::GetActions_keyboard(const SDL_Event& event) diff --git a/cpp/src/user_input.hpp b/cpp/src/user_input.hpp index 8ed139b..c32b071 100644 --- a/cpp/src/user_input.hpp +++ b/cpp/src/user_input.hpp @@ -18,7 +18,8 @@ public: UserAction(Type t) : type(t), Argument{.number = 0} {} UserAction(Type t, char key) : type(t), Argument{.key = key} {} UserAction(Type t, WindowPos v) : type(t), Argument{.position = v} {} - UserAction(Type t, int arg) : type(t), Argument{.number = arg} {} + UserAction(Type t, int32_t arg) : type(t), Argument{.number = arg} {} + UserAction(Type t, float arg) : type(t), Argument{.float_number = arg} {} ~UserAction() = default; Type type; @@ -26,7 +27,8 @@ public: union { WindowPos position; char key; - int number; + int32_t number; + float float_number; } Argument; // TODO use std::variant From 11fbcbbb105cf26b94ad02be04c07e396c186222 Mon Sep 17 00:00:00 2001 From: Mrna Date: Mon, 6 Oct 2025 14:07:15 +0200 Subject: [PATCH 16/23] Added missing header (win compiler complained) --- cpp/src/math.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/math.hpp b/cpp/src/math.hpp index 4068b7e..f6ac078 100644 --- a/cpp/src/math.hpp +++ b/cpp/src/math.hpp @@ -9,6 +9,7 @@ #include #include #include +#include template requires std::floating_point From 4e950c15a5be7e0e9ca8d9de99c4965239d8de89 Mon Sep 17 00:00:00 2001 From: Mrna Date: Tue, 7 Oct 2025 09:35:52 +0200 Subject: [PATCH 17/23] Added Matrix class --- cpp/src/math.hpp | 230 +++++++++++++++++++++++------------------------ 1 file changed, 112 insertions(+), 118 deletions(-) diff --git a/cpp/src/math.hpp b/cpp/src/math.hpp index f6ac078..ad3dcd0 100644 --- a/cpp/src/math.hpp +++ b/cpp/src/math.hpp @@ -38,6 +38,10 @@ public: requires(std::same_as && ...) && (sizeof...(ArgsT) == N) vec(ArgsT... args) : m_Array{args...} {} + // + // Access to elements & data + // + const T &operator[](size_t index) const { // we leave run-time checks to the underlying std::array return m_Array[index]; @@ -57,6 +61,8 @@ public: return os; } + std::array& Data() { return m_Array; } + // // binary operators // @@ -186,6 +192,19 @@ public: return tmp; } + static T DotProduct(const vec& a, const vec& b) + { + return std::inner_product( + a.m_Array.begin(), a.m_Array.end(), b.m_Array.begin(), + T{}, std::plus{}, std::multiplies{}); + } + + T DotProduct(const vec& b) const + { + const auto& a = *this; + return DotProduct(a, b); + } + // // Helpers // @@ -264,7 +283,6 @@ using WorldSize = vec; using WindowSize = vec; using TileSize = vec; - // // Utils // @@ -277,121 +295,97 @@ struct TilePosHash { } }; -// old stuff - TODO delete +// +// Matrix +// -// constexpr double EQUALITY_LIMIT = 1e-6; -// template struct Vec2D { -// public: -// Vec2D() = default; -// ~Vec2D() = default; -// -// template -// Vec2D(Vec2D other) { -// this->x = static_cast(other.x); -// this->y = static_cast(other.y); -// } -// -// Vec2D& operator+=(const Vec2D &other) { -// x += other.x; -// y += other.y; -// return *this; -// } -// -// template -// requires std::is_arithmetic_v -// Vec2D& operator/=(U k) { -// this->x /= static_cast(k); -// this->y /= static_cast(k); -// return *this; -// } -// -// friend Vec2D operator+(const Vec2D &a, const Vec2D &b) { -// return Vec2D{a.x + b.x, a.y + b.y}; -// } -// -// friend Vec2D operator-(const Vec2D &a, const Vec2D &b) { -// return Vec2D{a.x - b.x, a.y - b.y}; -// } -// -// template -// requires std::is_arithmetic_v -// friend Vec2D operator*(U k, const Vec2D &v) -// { -// return Vec2D{k * v.x, k * v.y}; -// } -// -// template -// requires std::is_arithmetic_v -// friend Vec2D operator/(const Vec2D &v, U k) -// { -// return Vec2D{v.x / k, v.y / k}; -// } -// -// friend bool operator==(const Vec2D &a, const Vec2D &b) { -// if constexpr (std::is_integral_v) { -// return a.x == b.x && a.y == b.y; -// } else if constexpr (std::is_floating_point_v) { -// return a.distance(b) < EQUALITY_LIMIT; -// } else { -// static_assert("Unhandled comparison"); -// } -// } -// -// Vec2D operator*(float b) const { return Vec2D{b * x, b * y}; } -// -// T distance_squared(const Vec2D &other) const { -// T dx = x - other.x; -// T dy = y - other.y; -// return dx * dx + dy * dy; -// } -// -// T distance(const Vec2D &other) const -// requires std::floating_point -// { -// return sqrt(distance_squared(other)); -// } -// -// void normalize() -// requires std::floating_point -// { -// auto length = sqrt(x * x + y * y); -// if (length < EQUALITY_LIMIT) { -// x = y = 0; -// } else { -// x /= length; -// y /= length; -// } -// } -// -// Vec2D normalized() -// requires std::floating_point -// { -// Vec2D v(*this); -// v.normalize(); -// return v; -// } -// -// Vec2D orthogonal() const -// { -// Vec2D v(*this); -// -// std::swap(v.x, v.y); -// v.x = -v.x; -// return v; -// } -// -// template Vec2D(std::initializer_list list) { -// assert(list.size() == 2); -// auto first_element = *list.begin(); -// auto second_element = *(list.begin() + 1); -// x = static_cast(first_element); -// y = static_cast(second_element); -// } -// -// T x, y; -// -// friend std::ostream &operator<<(std::ostream &os, const Vec2D &obj) { -// os << "( " << obj.x << ", " << obj.y << ")"; -// return os; -// } -// }; +// Collumn major square matrix +template +class Matrix { + +using vec_type = vec; + +public: + Matrix() = default; + + // Initialization using flat array of N*N elements + template + requires (M == N*N && std::same_as) + Matrix(std::array array) : m_Array{} + { + std::size_t idx = 0; + for (auto col : array | std::views::chunk(N)) + { + std::ranges::copy(col, m_Array[idx++].Data().begin()); + } + } + + const vec_type& operator[](size_t index) const { return m_Array[index]; } + vec_type& operator[](size_t index) { return m_Array[index]; } + + friend std::ostream &operator<<(std::ostream &os, const Matrix &obj) + { + os << "( "; + for (const auto &element : obj.m_Array) { + os << element << " "; + } + os << ")"; + return os; + } + + + friend Matrix operator+(const Matrix& A, const Matrix& B) + { + Matrix C; + std::ranges::transform(A.m_Array, B.m_Array, C.m_Array.begin(), std::plus{}); + return C; + } + + friend Matrix operator-(const Matrix& A, const Matrix& B) + { + Matrix C; + std::ranges::transform(A.m_Array, B.m_Array, C.m_Array.begin(), std::minus{}); + return C; + } + + friend Matrix operator*(const Matrix& A, const Matrix& B) + { + Matrix C; + + for (size_t i = 0; i < N; i++) + { + for (size_t j = 0; j < N; j++) + { + T sum = 0; + for (size_t k = 0; k < N; ++k) sum += A[i][k] * B[k][j]; + C[i][j] = sum; + } + } + return C; + } + + friend vec_type operator*(const Matrix& A, const vec_type& b) + { + // we assume that b is row vector + vec_type c; + for (size_t i = 0; i < N; i++) + { + c[i] = b.DotProduct(A[i]); + } + return c; + } + + static constexpr Matrix Eye() + { + Matrix E; + for (size_t i = 0; i < N; i++) + { + E[i][i] = T{1}; + } + return E; + } + + +private: + std::array m_Array; +}; \ No newline at end of file From 81f7de05fb1ed4e9b857fdffbf0fbd4142333d80 Mon Sep 17 00:00:00 2001 From: Mrna Date: Tue, 7 Oct 2025 12:40:42 +0200 Subject: [PATCH 18/23] Windows fix for M_PI --- cpp/src/math.hpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cpp/src/math.hpp b/cpp/src/math.hpp index ad3dcd0..fbd0682 100644 --- a/cpp/src/math.hpp +++ b/cpp/src/math.hpp @@ -11,6 +11,12 @@ #include #include +#ifdef _WIN32 +#include +#define M_PI std::numbers::pi +// TODO use std::numbers::pi instead of M_PI +#endif + template requires std::floating_point static inline bool equalEpsilon(const T &a, const T &b) { From 582e3e1adc0597aa4a9410f8f86b648a0b4b13b6 Mon Sep 17 00:00:00 2001 From: Mrna Date: Tue, 7 Oct 2025 12:41:03 +0200 Subject: [PATCH 19/23] Remove temporary file from VS solution --- vs/pathfinding_demo/pathfinding_demo.vcxproj | 40 +++++++- .../pathfinding_demo.vcxproj.filters | 96 +++++++++++++++++++ 2 files changed, 132 insertions(+), 4 deletions(-) diff --git a/vs/pathfinding_demo/pathfinding_demo.vcxproj b/vs/pathfinding_demo/pathfinding_demo.vcxproj index c7828c9..2908d8d 100644 --- a/vs/pathfinding_demo/pathfinding_demo.vcxproj +++ b/vs/pathfinding_demo/pathfinding_demo.vcxproj @@ -19,12 +19,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -96,7 +128,7 @@ GLEW_STATIC true stdcpplatest - ..\glew\include;..\SDL\include;..\SDL_image\include;..\;%(AdditionalIncludeDirectories) + ..\glew\include;..\SDL\include;..\SDL_image\include;..\;..\..\cpp\src;%(AdditionalIncludeDirectories) Console @@ -114,7 +146,7 @@ GLEW_STATIC true stdcpplatest - ..\glew\include;..\SDL\include;..\SDL_image\include;..\;%(AdditionalIncludeDirectories) + ..\glew\include;..\SDL\include;..\SDL_image\include;..\;..\..\cpp\src;%(AdditionalIncludeDirectories) Console @@ -132,7 +164,7 @@ GLEW_STATIC true stdcpplatest - ..\glew\include;..\SDL\include;..\SDL_image\include;..\;%(AdditionalIncludeDirectories) + ..\glew\include;..\SDL\include;..\SDL_image\include;..\;..\..\cpp\src;%(AdditionalIncludeDirectories) Console @@ -150,7 +182,7 @@ GLEW_STATIC true stdcpplatest - ..\glew\include;..\SDL\include;..\SDL_image\include;..\;%(AdditionalIncludeDirectories) + ..\glew\include;..\SDL\include;..\SDL_image\include;..\;..\..\cpp\src;%(AdditionalIncludeDirectories) Console diff --git a/vs/pathfinding_demo/pathfinding_demo.vcxproj.filters b/vs/pathfinding_demo/pathfinding_demo.vcxproj.filters index fca6151..5b5e6b6 100644 --- a/vs/pathfinding_demo/pathfinding_demo.vcxproj.filters +++ b/vs/pathfinding_demo/pathfinding_demo.vcxproj.filters @@ -18,6 +18,54 @@ Source Files + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + @@ -29,5 +77,53 @@ Header Files + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + \ No newline at end of file From 783200733ad7c904dac5784fd7d5e27568bab466 Mon Sep 17 00:00:00 2001 From: Mrna Date: Tue, 7 Oct 2025 13:46:34 +0200 Subject: [PATCH 20/23] Fixed tile size and mouse pan when zoom != 1 --- cpp/src/camera.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cpp/src/camera.cpp b/cpp/src/camera.cpp index d24a74b..8e04ae7 100644 --- a/cpp/src/camera.cpp +++ b/cpp/src/camera.cpp @@ -8,7 +8,7 @@ void Camera::Pan(const WorldPos& delta) { - m_Pan += delta; + m_Pan += (delta / m_Zoom); } void Camera::Zoom(float delta) @@ -35,10 +35,11 @@ WindowSize Camera::WorldToWindowSize(WorldSize world) const { const auto& v = world; // no zoom yet, just pass-through - return WindowSize{v[0], v[1]}; + return WindowSize{v[0], v[1]} * m_Zoom; } WorldSize Camera::WindowToWorldSize(WindowSize window) const { + window /= m_Zoom; return WorldSize{window[0], window[1]}; } From 69e319a7303c471a29329f5e1bb2de4ef0a5a0ce Mon Sep 17 00:00:00 2001 From: Mrna Date: Tue, 7 Oct 2025 14:03:00 +0200 Subject: [PATCH 21/23] Fixed sprite size on zoom --- cpp/src/camera.hpp | 3 +++ cpp/src/gameloop.cpp | 2 +- cpp/src/sprite.hpp | 4 ++-- cpp/src/window.cpp | 6 +++--- cpp/src/window.hpp | 2 +- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/cpp/src/camera.hpp b/cpp/src/camera.hpp index ed61e00..f57b441 100644 --- a/cpp/src/camera.hpp +++ b/cpp/src/camera.hpp @@ -8,6 +8,9 @@ public: void Pan(const WorldPos& delta); void Zoom(float delta); + WorldPos GetPan() const { return m_Pan; } + float GetZoom() const { return m_Zoom; } + WindowPos WorldToWindow(WorldPos) const; WorldPos WindowToWorld(WindowPos) const; WindowSize WorldToWindowSize(WorldSize) const; diff --git a/cpp/src/gameloop.cpp b/cpp/src/gameloop.cpp index 671548c..3f97015 100644 --- a/cpp/src/gameloop.cpp +++ b/cpp/src/gameloop.cpp @@ -55,7 +55,7 @@ void GameLoop::Run() { // draw all the entities (player etc) for (auto &entity : m_Game->GetEntities()) { const auto& camera = m_Game->GetCamera(); - m_Window->DrawSprite(camera.WorldToWindow(entity->GetPosition()), entity->GetSprite()); + m_Window->DrawSprite(camera.WorldToWindow(entity->GetPosition()), entity->GetSprite(), camera.GetZoom()); } m_Window->Flush(); diff --git a/cpp/src/sprite.hpp b/cpp/src/sprite.hpp index 5b9b7e6..051d732 100644 --- a/cpp/src/sprite.hpp +++ b/cpp/src/sprite.hpp @@ -25,7 +25,7 @@ public: // GetTexture cannot return pointer to const, as SDL_RenderTexture modifies it SDL_Texture *GetTexture() { return m_Texture.get(); } - WorldPos GetSize() const { return m_Size; } + WorldSize GetSize() const { return m_Size; } WorldPos GetCenter() const { return m_ImageCenter; } void LoadImage(std::string path, WorldPos image_center = WorldPos{}); @@ -33,7 +33,7 @@ public: private: static std::shared_ptr m_Renderer; std::unique_ptr m_Texture; - WorldPos m_Size; + WorldSize m_Size; WorldPos m_ImageCenter; float m_TextureWidth = 0; float m_TextureHeight = 0; diff --git a/cpp/src/window.cpp b/cpp/src/window.cpp index b49d15b..91b9ab6 100644 --- a/cpp/src/window.cpp +++ b/cpp/src/window.cpp @@ -76,9 +76,9 @@ Window::~Window() { LOG_DEBUG("."); } -void Window::DrawSprite(const WindowPos &position, Sprite &s) { - WorldPos size = s.GetSize(); - WorldPos img_center = s.GetCenter(); +void Window::DrawSprite(const WindowPos &position, Sprite &s, float scale) { + WorldSize size = s.GetSize() * scale; + WorldPos img_center = s.GetCenter() * scale; SDL_FRect rect = {position.x() - img_center.x(), position.y() - img_center.y(), size.x(), size.y()}; SDL_RenderTexture(m_Renderer.get(), s.GetTexture(), nullptr, &rect); diff --git a/cpp/src/window.hpp b/cpp/src/window.hpp index 5a16152..8453d67 100644 --- a/cpp/src/window.hpp +++ b/cpp/src/window.hpp @@ -22,7 +22,7 @@ public: Window &operator=(Window &&) = delete; std::expected Init(); - void DrawSprite(const WindowPos &position, Sprite &s); + void DrawSprite(const WindowPos &position, Sprite &s, float scale = 1.0f); void DrawRect(const WindowPos &position, const WindowSize size, uint8_t R, uint8_t G, uint8_t B, uint8_t A); void ClearWindow(); From e1fa58e11a2982cb19462153e0b15d82e86dcd20 Mon Sep 17 00:00:00 2001 From: Mrna Date: Wed, 8 Oct 2025 14:32:51 +0200 Subject: [PATCH 22/23] Added GHCP generated tests for Matrix class (NOT TESTED) --- cpp/test/test.cpp | 260 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) diff --git a/cpp/test/test.cpp b/cpp/test/test.cpp index 1f45431..578586e 100644 --- a/cpp/test/test.cpp +++ b/cpp/test/test.cpp @@ -430,6 +430,266 @@ TEST(vec, ChainedOperations) { ASSERT_FLOAT_EQ(a[1], 3.0f); } +TEST(Matrix, DefaultConstruction) { + // Test that default-constructed matrix has all elements equal to zero + Matrix m1; + ASSERT_FLOAT_EQ(m1[0][0], 0.0f); + ASSERT_FLOAT_EQ(m1[0][1], 0.0f); + ASSERT_FLOAT_EQ(m1[1][0], 0.0f); + ASSERT_FLOAT_EQ(m1[1][1], 0.0f); +} + +TEST(Matrix, ArrayConstruction) { + // Test construction from array (column major) + Matrix m1(std::array{1.0f, 2.0f, 3.0f, 4.0f}); + + // Column 0: [1, 2] + ASSERT_FLOAT_EQ(m1[0][0], 1.0f); + ASSERT_FLOAT_EQ(m1[0][1], 2.0f); + + // Column 1: [3, 4] + ASSERT_FLOAT_EQ(m1[1][0], 3.0f); + ASSERT_FLOAT_EQ(m1[1][1], 4.0f); + + // Test with 3x3 matrix + Matrix m2(std::array{1, 2, 3, 4, 5, 6, 7, 8, 9}); + + // Column 0: [1, 2, 3] + ASSERT_EQ(m2[0][0], 1); + ASSERT_EQ(m2[0][1], 2); + ASSERT_EQ(m2[0][2], 3); + + // Column 1: [4, 5, 6] + ASSERT_EQ(m2[1][0], 4); + ASSERT_EQ(m2[1][1], 5); + ASSERT_EQ(m2[1][2], 6); + + // Column 2: [7, 8, 9] + ASSERT_EQ(m2[2][0], 7); + ASSERT_EQ(m2[2][1], 8); + ASSERT_EQ(m2[2][2], 9); +} + +TEST(Matrix, ElementAccess) { + // Test element access (both const and non-const) + Matrix m1(std::array{1.0f, 2.0f, 3.0f, 4.0f}); + + // Test const access + const Matrix& const_ref = m1; + ASSERT_FLOAT_EQ(const_ref[0][0], 1.0f); + ASSERT_FLOAT_EQ(const_ref[1][1], 4.0f); + + // Test non-const access and modification + m1[0][0] = 10.0f; + m1[1][1] = 40.0f; + + ASSERT_FLOAT_EQ(m1[0][0], 10.0f); + ASSERT_FLOAT_EQ(m1[1][1], 40.0f); + + // Verify other elements unchanged + ASSERT_FLOAT_EQ(m1[0][1], 2.0f); + ASSERT_FLOAT_EQ(m1[1][0], 3.0f); +} + +TEST(Matrix, Addition) { + // Test matrix addition + Matrix m1(std::array{1.0f, 2.0f, 3.0f, 4.0f}); + Matrix m2(std::array{5.0f, 6.0f, 7.0f, 8.0f}); + + Matrix result = m1 + m2; + + ASSERT_FLOAT_EQ(result[0][0], 6.0f); // 1 + 5 + ASSERT_FLOAT_EQ(result[0][1], 8.0f); // 2 + 6 + ASSERT_FLOAT_EQ(result[1][0], 10.0f); // 3 + 7 + ASSERT_FLOAT_EQ(result[1][1], 12.0f); // 4 + 8 + + // Test that original matrices are unchanged + ASSERT_FLOAT_EQ(m1[0][0], 1.0f); + ASSERT_FLOAT_EQ(m1[1][1], 4.0f); + ASSERT_FLOAT_EQ(m2[0][0], 5.0f); + ASSERT_FLOAT_EQ(m2[1][1], 8.0f); + + // Test with integer matrices + Matrix im1(std::array{1, 2, 3, 4}); + Matrix im2(std::array{10, 20, 30, 40}); + Matrix iresult = im1 + im2; + + ASSERT_EQ(iresult[0][0], 11); + ASSERT_EQ(iresult[0][1], 22); + ASSERT_EQ(iresult[1][0], 33); + ASSERT_EQ(iresult[1][1], 44); +} + +TEST(Matrix, Subtraction) { + // Test matrix subtraction + Matrix m1(std::array{10.0f, 8.0f, 6.0f, 4.0f}); + Matrix m2(std::array{1.0f, 2.0f, 3.0f, 4.0f}); + + Matrix result = m1 - m2; + + ASSERT_FLOAT_EQ(result[0][0], 9.0f); // 10 - 1 + ASSERT_FLOAT_EQ(result[0][1], 6.0f); // 8 - 2 + ASSERT_FLOAT_EQ(result[1][0], 3.0f); // 6 - 3 + ASSERT_FLOAT_EQ(result[1][1], 0.0f); // 4 - 4 + + // Test that original matrices are unchanged + ASSERT_FLOAT_EQ(m1[0][0], 10.0f); + ASSERT_FLOAT_EQ(m1[1][1], 4.0f); + ASSERT_FLOAT_EQ(m2[0][0], 1.0f); + ASSERT_FLOAT_EQ(m2[1][1], 4.0f); + + // Test subtraction resulting in negative values + Matrix m3(std::array{1.0f, 2.0f, 3.0f, 4.0f}); + Matrix m4(std::array{5.0f, 6.0f, 7.0f, 8.0f}); + Matrix negative_result = m3 - m4; + + ASSERT_FLOAT_EQ(negative_result[0][0], -4.0f); + ASSERT_FLOAT_EQ(negative_result[0][1], -4.0f); + ASSERT_FLOAT_EQ(negative_result[1][0], -4.0f); + ASSERT_FLOAT_EQ(negative_result[1][1], -4.0f); +} + +TEST(Matrix, MatrixMultiplication) { + // Test 2x2 matrix multiplication + Matrix m1(std::array{1.0f, 2.0f, 3.0f, 4.0f}); + Matrix m2(std::array{5.0f, 6.0f, 7.0f, 8.0f}); + + Matrix result = m1 * m2; + + // m1 = [1 3] m2 = [5 7] result = [1*5+3*6 1*7+3*8] = [23 31] + // [2 4] [6 8] [2*5+4*6 2*7+4*8] [34 46] + + ASSERT_FLOAT_EQ(result[0][0], 23.0f); // 1*5 + 3*6 = 23 + ASSERT_FLOAT_EQ(result[0][1], 34.0f); // 2*5 + 4*6 = 34 + ASSERT_FLOAT_EQ(result[1][0], 31.0f); // 1*7 + 3*8 = 31 + ASSERT_FLOAT_EQ(result[1][1], 46.0f); // 2*7 + 4*8 = 46 + + // Test identity property: I * m = m + Matrix identity = Matrix::Eye(); + Matrix identity_result = identity * m1; + + ASSERT_FLOAT_EQ(identity_result[0][0], m1[0][0]); + ASSERT_FLOAT_EQ(identity_result[0][1], m1[0][1]); + ASSERT_FLOAT_EQ(identity_result[1][0], m1[1][0]); + ASSERT_FLOAT_EQ(identity_result[1][1], m1[1][1]); + + // Test with 3x3 matrices + Matrix im1(std::array{1, 0, 0, 0, 1, 0, 0, 0, 1}); // Identity + Matrix im2(std::array{1, 2, 3, 4, 5, 6, 7, 8, 9}); + Matrix iresult = im1 * im2; + + // Identity * matrix = matrix + ASSERT_EQ(iresult[0][0], 1); + ASSERT_EQ(iresult[0][1], 2); + ASSERT_EQ(iresult[0][2], 3); + ASSERT_EQ(iresult[1][0], 4); + ASSERT_EQ(iresult[1][1], 5); + ASSERT_EQ(iresult[1][2], 6); + ASSERT_EQ(iresult[2][0], 7); + ASSERT_EQ(iresult[2][1], 8); + ASSERT_EQ(iresult[2][2], 9); +} + +TEST(Matrix, MatrixVectorMultiplication) { + // Test matrix-vector multiplication + Matrix m1(std::array{1.0f, 2.0f, 3.0f, 4.0f}); + vec v1(2.0f, 3.0f); + + vec result = m1 * v1; + + // m1 = [1 3] v1 = [2] result = [1*2+3*3] = [11] + // [2 4] [3] [2*2+4*3] [16] + + ASSERT_FLOAT_EQ(result[0], 11.0f); // 1*2 + 3*3 = 11 + ASSERT_FLOAT_EQ(result[1], 16.0f); // 2*2 + 4*3 = 16 + + // Test with 3x3 matrix and 3D vector + Matrix im1(std::array{1, 0, 0, 0, 1, 0, 0, 0, 1}); // Identity + vec iv1(5, 10, 15); + + vec iresult = im1 * iv1; + + // Identity * vector = vector + ASSERT_EQ(iresult[0], 5); + ASSERT_EQ(iresult[1], 10); + ASSERT_EQ(iresult[2], 15); + + // Test that original matrix and vector are unchanged + ASSERT_FLOAT_EQ(m1[0][0], 1.0f); + ASSERT_FLOAT_EQ(v1[0], 2.0f); + ASSERT_FLOAT_EQ(v1[1], 3.0f); +} + +TEST(Matrix, EyeIdentityMatrix) { + // Test 2x2 identity matrix + Matrix eye2 = Matrix::Eye(); + + ASSERT_FLOAT_EQ(eye2[0][0], 1.0f); + ASSERT_FLOAT_EQ(eye2[0][1], 0.0f); + ASSERT_FLOAT_EQ(eye2[1][0], 0.0f); + ASSERT_FLOAT_EQ(eye2[1][1], 1.0f); + + // Test 3x3 identity matrix + Matrix eye3 = Matrix::Eye(); + + for (size_t i = 0; i < 3; ++i) { + for (size_t j = 0; j < 3; ++j) { + if (i == j) { + ASSERT_EQ(eye3[i][j], 1); + } else { + ASSERT_EQ(eye3[i][j], 0); + } + } + } + + // Test 4x4 identity matrix + Matrix eye4 = Matrix::Eye(); + + for (size_t i = 0; i < 4; ++i) { + for (size_t j = 0; j < 4; ++j) { + if (i == j) { + ASSERT_DOUBLE_EQ(eye4[i][j], 1.0); + } else { + ASSERT_DOUBLE_EQ(eye4[i][j], 0.0); + } + } + } +} + +TEST(Matrix, LogPrint) { + // Test that logger can print matrices of different types and sizes + Matrix m2(std::array{1.1f, 2.2f, 3.3f, 4.4f}); + Matrix m3(std::array{1, 2, 3, 4, 5, 6, 7, 8, 9}); + Matrix dm2(std::array{1.5, 2.5, 3.5, 4.5}); + + LOG_DEBUG("Matrix ", m2); + LOG_DEBUG("Matrix ", m3); + LOG_DEBUG("Matrix ", dm2); +} + +TEST(Matrix, ChainedOperations) { + // Test chaining matrix operations + Matrix m1(std::array{1.0f, 2.0f, 3.0f, 4.0f}); + Matrix m2(std::array{1.0f, 1.0f, 1.0f, 1.0f}); + Matrix m3(std::array{2.0f, 0.0f, 0.0f, 2.0f}); + + // Test (m1 + m2) * m3 + Matrix result = (m1 + m2) * m3; + + // m1 + m2 = [2 4] m3 = [2 0] result = [4 8] + // [3 5] [0 2] [6 10] + + ASSERT_FLOAT_EQ(result[0][0], 4.0f); + ASSERT_FLOAT_EQ(result[0][1], 6.0f); + ASSERT_FLOAT_EQ(result[1][0], 8.0f); + ASSERT_FLOAT_EQ(result[1][1], 10.0f); + + // Test that original matrices are unchanged + ASSERT_FLOAT_EQ(m1[0][0], 1.0f); + ASSERT_FLOAT_EQ(m2[0][0], 1.0f); + ASSERT_FLOAT_EQ(m3[0][0], 2.0f); +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); From 038ea4f9c2b5030732e7d6dde5bd18a4da8fbf5c Mon Sep 17 00:00:00 2001 From: Jan Mrna Date: Thu, 9 Oct 2025 06:47:57 +0200 Subject: [PATCH 23/23] Fixed generated tests --- cpp/test/test.cpp | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/cpp/test/test.cpp b/cpp/test/test.cpp index 578586e..8b9262b 100644 --- a/cpp/test/test.cpp +++ b/cpp/test/test.cpp @@ -554,15 +554,12 @@ TEST(Matrix, MatrixMultiplication) { Matrix m1(std::array{1.0f, 2.0f, 3.0f, 4.0f}); Matrix m2(std::array{5.0f, 6.0f, 7.0f, 8.0f}); - Matrix result = m1 * m2; + Matrix result = m2 * m1; - // m1 = [1 3] m2 = [5 7] result = [1*5+3*6 1*7+3*8] = [23 31] - // [2 4] [6 8] [2*5+4*6 2*7+4*8] [34 46] - - ASSERT_FLOAT_EQ(result[0][0], 23.0f); // 1*5 + 3*6 = 23 - ASSERT_FLOAT_EQ(result[0][1], 34.0f); // 2*5 + 4*6 = 34 - ASSERT_FLOAT_EQ(result[1][0], 31.0f); // 1*7 + 3*8 = 31 - ASSERT_FLOAT_EQ(result[1][1], 46.0f); // 2*7 + 4*8 = 46 + ASSERT_FLOAT_EQ(result[0][0], 23.0f); + ASSERT_FLOAT_EQ(result[0][1], 34.0f); + ASSERT_FLOAT_EQ(result[1][0], 31.0f); + ASSERT_FLOAT_EQ(result[1][1], 46.0f); // Test identity property: I * m = m Matrix identity = Matrix::Eye(); @@ -597,11 +594,8 @@ TEST(Matrix, MatrixVectorMultiplication) { vec result = m1 * v1; - // m1 = [1 3] v1 = [2] result = [1*2+3*3] = [11] - // [2 4] [3] [2*2+4*3] [16] - - ASSERT_FLOAT_EQ(result[0], 11.0f); // 1*2 + 3*3 = 11 - ASSERT_FLOAT_EQ(result[1], 16.0f); // 2*2 + 4*3 = 16 + ASSERT_FLOAT_EQ(result[0], 8.0f); + ASSERT_FLOAT_EQ(result[1], 18.0f); // Test with 3x3 matrix and 3D vector Matrix im1(std::array{1, 0, 0, 0, 1, 0, 0, 0, 1}); // Identity