diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..54c7f64 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,90 @@ +cmake_minimum_required(VERSION 3.20) +project(PathfindingDemo) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Find required packages +find_package(PkgConfig REQUIRED) +pkg_check_modules(SDL3 REQUIRED sdl3) +pkg_check_modules(SDL3_image REQUIRED sdl3-image) +find_package(OpenGL REQUIRED) +find_package(GLEW REQUIRED) + +# Add Google Test +find_package(GTest REQUIRED) + +# Include directories +include_directories(cpp/src) + +# Source files for the main executable +set(MAIN_SOURCES + cpp/src/main.cpp + cpp/src/camera.cpp + cpp/src/entities.cpp + cpp/src/gameloop.cpp + cpp/src/map.cpp + cpp/src/pathfinder/base.cpp + cpp/src/pathfinder/bfs.cpp + cpp/src/pathfinder/dijkstra.cpp + cpp/src/pathfinder/gbfs.cpp + cpp/src/pathfinder/utils.cpp + cpp/src/pathfindingdemo.cpp + cpp/src/sprite.cpp + cpp/src/tile.cpp + cpp/src/user_input.cpp + cpp/src/window.cpp +) + +# Header files (for IDE support) +set(HEADERS + cpp/src/array.hpp + cpp/src/camera.hpp + cpp/src/entities.hpp + cpp/src/gameloop.hpp + cpp/src/log.hpp + cpp/src/map.hpp + cpp/src/math.hpp + cpp/src/pathfinder/base.hpp + cpp/src/pathfinder/bfs.hpp + cpp/src/pathfinder/dijkstra.hpp + cpp/src/pathfinder/gbfs.hpp + cpp/src/pathfinder/utils.hpp + cpp/src/pathfindingdemo.hpp + cpp/src/sprite.hpp + cpp/src/tile.hpp + cpp/src/user_input.hpp + cpp/src/window.hpp +) + +# Create main executable +add_executable(pathfinding_demo ${MAIN_SOURCES} ${HEADERS}) + +# Link libraries for main executable +target_link_libraries(pathfinding_demo + ${SDL3_LIBRARIES} + ${SDL3_image_LIBRARIES} + OpenGL::GL + GLEW::GLEW +) + +# Add compile flags +target_compile_options(pathfinding_demo PRIVATE ${SDL3_CFLAGS_OTHER}) + +# Test executable +add_executable(tests cpp/test/test.cpp) +target_link_libraries(tests GTest::gtest GTest::gtest_main) + +# Enable testing +enable_testing() +add_test(NAME unit_tests COMMAND tests) + +# Compiler-specific options +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + target_compile_options(pathfinding_demo PRIVATE -Wall -Wextra -Wpedantic) + target_compile_options(tests PRIVATE -Wall -Wextra -Wpedantic) +endif() + +# Debug/Release configurations +set(CMAKE_CXX_FLAGS_DEBUG "-g -O0") +set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG") diff --git a/cpp/src/camera.hpp b/cpp/src/camera.hpp index f57b441..b321baa 100644 --- a/cpp/src/camera.hpp +++ b/cpp/src/camera.hpp @@ -15,6 +15,18 @@ public: WorldPos WindowToWorld(WindowPos) const; WindowSize WorldToWindowSize(WorldSize) const; WorldSize WindowToWorldSize(WindowSize) const; + + template + requires std::floating_point + T WindowToWorldSize(T window_size) const { + return window_size / static_cast(m_Zoom); + } + + template + requires std::floating_point + T WorldToWindowSize(T world_size) const { + return world_size * static_cast(m_Zoom); + } private: // TODO this should be replaced with a matrix diff --git a/cpp/src/entities.cpp b/cpp/src/entities.cpp index d8e1ad2..453b003 100644 --- a/cpp/src/entities.cpp +++ b/cpp/src/entities.cpp @@ -46,6 +46,47 @@ void Entity::Update(float time_delta) { m_Position += m_ActualVelocity * time_delta; } +std::optional Entity::GetMoveTarget() +{ + auto& path = GetPath(); + if (path.empty()) { + return {}; + } + + WorldPos current_pos = GetPosition(); + WorldPos next_pos = path.front(); + + if (current_pos.DistanceTo(next_pos) > 1.0) { + // target not reached yet + return next_pos; + } + // target reached, pop it + //m_MoveQueue.pop(); + path.erase(path.begin()); + // return nothing - if there's next point in the queue, + // we'll get it in the next iteration + return {}; +} + +bool Entity::CollidesWith(const Entity& other) const +{ + const auto& A = *this; + const auto& B = other; + + auto position_A = A.GetPosition(); + auto position_B = B.GetPosition(); + auto distance_sq = position_A.DistanceSquared(position_B); + auto collision_distance_sq = + A.GetCollisionRadiusSquared() + + B.GetCollisionRadiusSquared() + + 2 * A.GetCollisionRadius() * B.GetCollisionRadius(); + if (distance_sq < collision_distance_sq) + { + return true; + } + return false; +} + Player::Player() { LOG_DEBUG("."); if (m_Sprite == nullptr) { diff --git a/cpp/src/entities.hpp b/cpp/src/entities.hpp index bed7204..6ad8521 100644 --- a/cpp/src/entities.hpp +++ b/cpp/src/entities.hpp @@ -5,10 +5,12 @@ #include #include #include +#include #include "log.hpp" #include "math.hpp" #include "sprite.hpp" +#include "pathfinder/base.hpp" class Entity { public: @@ -56,14 +58,29 @@ public: void ZeroActualVelocityInDirection(WorldPos direction); + const pathfinder::Path& GetPath() const { return m_Path; } + pathfinder::Path& GetPath() { return m_Path; } + void SetPath(pathfinder::Path& path) { m_Path = path; } + std::optional GetMoveTarget(); + + bool CollidesWith(const Entity& other) const; + + bool IsCollisionBoxVisible() const { return m_CollisionBoxVisible; } + + void Select() { m_Selected = true; } + void Deselect() { m_Selected = false; } + bool IsSelected() const { return m_Selected; } + protected: WorldPos m_Position; WorldPos m_ActualVelocity; WorldPos m_RequestedVelocity; + pathfinder::Path m_Path; private: bool m_FlagExpired = false; - static constexpr float m_CollisionRadiusSq = 1000.0f; + bool m_CollisionBoxVisible = true; + bool m_Selected = false; }; class Player final : public Entity { @@ -75,7 +92,7 @@ public: constexpr Entity::Type GetType() const override { return Entity::Type::PLAYER; } - constexpr float GetCollisionRadius() const override { return 50.0f; } + constexpr float GetCollisionRadius() const override { return 25.0f; } bool IsMovable() const override { return true; } bool IsCollidable() const override { return true; } diff --git a/cpp/src/gameloop.cpp b/cpp/src/gameloop.cpp index 5e35aa4..7944493 100644 --- a/cpp/src/gameloop.cpp +++ b/cpp/src/gameloop.cpp @@ -21,26 +21,52 @@ void GameLoop::Draw() { TilePos{static_cast(row), static_cast(col)})); const auto &size = camera.WorldToWindowSize(map.GetTileSize()); // LOG_DEBUG("Drawing rect (", row, ", ", col, ")"); - m_Window->DrawRect(position, size, tiles[row][col]->R, tiles[row][col]->G, + m_Window->DrawFilledRect(position, size, tiles[row][col]->R, tiles[row][col]->G, tiles[row][col]->B, tiles[row][col]->A); } } // draw the path, if it exists - WorldPos start_pos = m_Game->GetPlayer()->GetPosition(); - for (const auto &next_pos : m_Game->GetPath()) { - const auto &camera = m_Game->GetCamera(); - m_Window->DrawLine(camera.WorldToWindow(start_pos), - camera.WorldToWindow(next_pos)); - start_pos = next_pos; + + for (const auto& entity : m_Game->GetEntities()) + { + WorldPos start_pos = entity->GetPosition(); + for (const auto &next_pos : entity->GetPath()) + { + 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()) { const auto &camera = m_Game->GetCamera(); - m_Window->DrawSprite(camera.WorldToWindow(entity->GetPosition()), - entity->GetSprite(), camera.GetZoom()); + auto entity_pos = camera.WorldToWindow(entity->GetPosition()); + m_Window->DrawSprite(entity_pos, + entity->GetSprite(), + camera.GetZoom()); + if (entity->IsCollisionBoxVisible()) + { + float collision_radius = camera.WorldToWindowSize(entity->GetCollisionRadius()); + m_Window->DrawCircle(entity_pos, collision_radius, 255, 0, 0); + } + if (entity->IsSelected()) + { + float collision_radius = camera.WorldToWindowSize(entity->GetCollisionRadius()); + m_Window->DrawCircle(entity_pos, collision_radius, 0, 255, 0); + } } + + // draw the selection box, if present + if (m_Game->IsSelectionBoxActive()) + { + const auto& [corner_pos, size] = m_Game->GetSelectionBoxPosSize(); + m_Window->DrawRect(corner_pos, size, 200, 20, 20); + } + + } // TODO rethink coupling and dependencies in the game loop class @@ -49,9 +75,10 @@ void GameLoop::Run() { LOG_INFO("Running the game"); while (!m_Game->IsExitRequested()) { m_Game->HandleActions(m_UserInput->GetActions()); - m_Game->UpdatePlayerVelocity(); + m_Game->UpdateWorld(); // TODO measure fps, draw only if delay for target fps was reached + // or create a separate thread for drawing m_Window->ClearWindow(); Draw(); m_Window->Flush(); diff --git a/cpp/src/math.hpp b/cpp/src/math.hpp index fbd0682..be6e290 100644 --- a/cpp/src/math.hpp +++ b/cpp/src/math.hpp @@ -44,6 +44,8 @@ public: requires(std::same_as && ...) && (sizeof...(ArgsT) == N) vec(ArgsT... args) : m_Array{args...} {} + vec(std::array array) : m_Array{array} {} + // // Access to elements & data // @@ -165,6 +167,12 @@ public: return (a - b).Length(); } + T DistanceSquared(const vec &b) const { + const vec &a = *this; + return (a - b).LengthSquared(); + } + + // // In-place vector operations // @@ -251,6 +259,12 @@ public: return m_Array[2]; } + template + vec ChangeTag() + { + return vec(m_Array); + } + private: std::array m_Array; }; @@ -394,4 +408,4 @@ public: private: std::array m_Array; -}; \ No newline at end of file +}; diff --git a/cpp/src/pathfindingdemo.cpp b/cpp/src/pathfindingdemo.cpp index eef2382..7d8cd50 100644 --- a/cpp/src/pathfindingdemo.cpp +++ b/cpp/src/pathfindingdemo.cpp @@ -55,54 +55,108 @@ void PathFindingDemo::CreateMap() { m_Map.PaintLine(TilePos{84,81}, TilePos{84,96}, 1.0, TileType::WALL); m_Map.PaintLine(TilePos{78,87}, TilePos{78,100}, 1.0, TileType::WALL); - // add player + // add some controllable entities m_Entities.clear(); - m_Player = std::make_shared(); - m_Player->SetPosition(m_Map.TileToWorld(TilePos{25, 20})); - m_Entities.push_back(m_Player); + auto player = std::make_shared(); + player->SetPosition(m_Map.TileToWorld(TilePos{25, 20})); + AddEntity(player); + + auto player2 = std::make_shared(); + player2->SetPosition(m_Map.TileToWorld(TilePos{50, 20})); + AddEntity(player2); + + for (int i = 0; i < 1; i++) + { + for (int j = 0; j < 10; j++) + { + auto p = std::make_shared(); + p->SetPosition(m_Map.TileToWorld(TilePos{10+5*i, 40+5*j})); + AddEntity(p); + } + } + + // select everything - TODO this is just temporary for testing + for (const auto& entity : m_Entities) + { + m_SelectedEntities.push_back(entity); + } } WorldPos PathFindingDemo::GetRandomPosition() const { return WorldPos{0.0f, 0.0f}; // totally random! } -std::optional PathFindingDemo::GetMoveTarget() { - WorldPos current_player_pos = GetPlayer()->GetPosition(); - if (m_Path.empty()) { - return {}; + +const std::vector& PathFindingDemo::GetEntityCollisions() +{ + static std::vector m_Collisions; + m_Collisions.clear(); + + for (const auto &entity_A : m_Entities) + { + for (const auto &entity_B : m_Entities) + { + if (entity_A == entity_B) + continue; + if (!entity_A->IsCollidable() || !entity_B->IsCollidable()) + continue; + if (entity_A->CollidesWith(*entity_B)) + { + // handle collision logic + m_Collisions.emplace_back(Collision(entity_A, entity_B)); + } + } } - - WorldPos next_player_pos = m_Path.front(); - - if (current_player_pos.DistanceTo(next_player_pos) > 1.0) { - // target not reached yet - return next_player_pos; - } - // target reached, pop it - //m_MoveQueue.pop(); - m_Path.erase(m_Path.begin()); - // return nothing - if there's next point in the queue, - // we'll get it in the next iteration - return {}; + return m_Collisions; } -void PathFindingDemo::UpdatePlayerVelocity() { - auto player = GetPlayer(); - auto current_pos = player->GetPosition(); - double tile_velocity_coeff = m_Map.GetTileVelocityCoeff(current_pos); - auto next_pos = GetMoveTarget(); - WorldPos velocity = WorldPos{}; - if (next_pos) - { - velocity = next_pos.value() - current_pos; - velocity.Normalize(); - //LOG_DEBUG("I want to move to: ", next_pos.value(), - // ", velocity: ", velocity); - } - player->SetActualVelocity(velocity * tile_velocity_coeff); + +// Update entity positions, handle collisions +void PathFindingDemo::UpdateWorld() { + float time_delta = 1.0f; - player->Update(time_delta); + + for (auto& entity : m_Entities) + { + // calculate the velocity + auto current_pos = entity->GetPosition(); + double tile_velocity_coeff = m_Map.GetTileVelocityCoeff(current_pos); + auto next_pos = entity->GetMoveTarget(); + WorldPos velocity = WorldPos{}; + if (next_pos) + { + velocity = next_pos.value() - current_pos; + velocity.Normalize(); + //LOG_DEBUG("I want to move to: ", next_pos.value(), + // ", velocity: ", velocity); + } + entity->SetActualVelocity(velocity * tile_velocity_coeff); + + for (const auto& collision : GetEntityCollisions()) + { + // TODO this loop is quite "hot", is it good idea to use weak_ptr and promote it? + auto weak_A = std::get<0>(collision); + auto weak_B = std::get<1>(collision); + auto A = weak_A.lock(); + auto B = weak_B.lock(); + if (A == nullptr || B == nullptr) + { + continue; + } + if (!A->IsMovable()) + continue; + // modify actual speed + // LOG_DEBUG("Collision: A is ", A, ", B is ", B); + auto AB = B->GetPosition() - A->GetPosition(); + A->ZeroActualVelocityInDirection(AB); + // handle logic + // TODO + } + + // update the position + entity->Update(time_delta); + } } void PathFindingDemo::HandleActions(const std::vector &actions) @@ -116,10 +170,19 @@ void PathFindingDemo::HandleActions(const std::vector &actions) } 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()); + WorldPos target_pos = m_Camera.WindowToWorld(action.Argument.position); + for (auto& selected_entity : m_SelectedEntities) + { + LOG_INFO("Calculating path to target: ", target_pos); + if (auto sp = selected_entity.lock()) + { + auto path = m_PathFinder->CalculatePath(sp->GetPosition(), target_pos); + sp->SetPath(path); + LOG_INFO("Done, path node count: ", path.size()); + } else { + LOG_INFO("Cannot calculate path for destroyed entity (weak_ptr.lock() failed)"); + } + } } else if (action.type == UserAction::Type::SELECT_PATHFINDER) { @@ -140,5 +203,69 @@ void PathFindingDemo::HandleActions(const std::vector &actions) m_Camera.Zoom(action.Argument.float_number); LOG_INFO("Camera zoom: ", action.Argument.float_number); } + else if (action.type == UserAction::Type::SELECTION_START) + { + m_SelectionBox.active = true; + m_SelectionBox.start = action.Argument.position; + m_SelectionBox.end = action.Argument.position; + } + else if (action.type == UserAction::Type::SELECTION_END) + { + m_SelectionBox.end = action.Argument.position; + m_SelectionBox.active = false; + auto diff = m_SelectionBox.end - m_SelectionBox.start; + // here we explicitly change the vector type from WindowPos to WindowSize + m_SelectionBox.size = diff.ChangeTag(); + WorldPos start = m_Camera.WindowToWorld(m_SelectionBox.start); + WorldPos end = m_Camera.WindowToWorld(m_SelectionBox.end); + SelectEntitiesInRectangle(start, end); + } + else if (action.type == UserAction::Type::SELECTION_CHANGE) + { + m_SelectionBox.end = action.Argument.position; + auto diff = m_SelectionBox.end - m_SelectionBox.start; + m_SelectionBox.size = diff.ChangeTag(); + } }; } + +void PathFindingDemo::DeselectEntities() +{ + std::for_each(m_SelectedEntities.begin(), m_SelectedEntities.end(), [](auto& x) + { + if (auto entity = x.lock()) + entity->Deselect(); + } + ); + m_SelectedEntities.clear(); +} + +void PathFindingDemo::SelectEntitiesInRectangle(WorldPos A, WorldPos B) +{ + DeselectEntities(); + // TODO use colliders for this + auto [x_min, x_max] = std::minmax(A.x(), B.x()); + auto [y_min, y_max] = std::minmax(A.y(), B.y()); + for (const auto& entity : m_Entities) + { + const auto& pos = entity->GetPosition(); + bool x_in_range = x_min < pos.x() && pos.x() < x_max; + bool y_in_range = y_min < pos.y() && pos.y() < y_max; + if (x_in_range && y_in_range) + { + m_SelectedEntities.push_back(std::weak_ptr(entity)); + entity->Select(); + } + } + LOG_INFO("Selected ", m_SelectedEntities.size(), " entities"); +} + +std::pair PathFindingDemo::GetSelectionBoxPosSize() +{ + const auto& pos = m_SelectionBox.start; + WindowPos size_pos = m_SelectionBox.end - m_SelectionBox.start; + WindowSize size = size_pos.ChangeTag(); + return std::pair(pos, size); + +} + diff --git a/cpp/src/pathfindingdemo.hpp b/cpp/src/pathfindingdemo.hpp index 9743c86..2f3eb4c 100644 --- a/cpp/src/pathfindingdemo.hpp +++ b/cpp/src/pathfindingdemo.hpp @@ -12,6 +12,15 @@ #include "pathfinder/base.hpp" #include "camera.hpp" +using Collision = std::pair, std::weak_ptr>; + +struct SelectionBox +{ + WindowPos start, end; + WindowSize size; + bool active; +}; + class PathFindingDemo { public: PathFindingDemo(int width, int height); @@ -22,26 +31,31 @@ public: PathFindingDemo &operator=(const PathFindingDemo &) = delete; PathFindingDemo &operator=(PathFindingDemo &&) = delete; - 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; } void AddEntity(std::shared_ptr e); void CreateMap(); - std::optional GetMoveTarget(); - void UpdatePlayerVelocity(); + void UpdateWorld(); void HandleActions(const std::vector &actions); WorldPos GetRandomPosition() const; + void SelectEntitiesInRectangle(WorldPos A, WorldPos B); + void DeselectEntities(); + bool IsSelectionBoxActive() const { return m_SelectionBox.active; } + std::pair GetSelectionBoxPosSize(); + std::vector> GetSelectedEntities() { return m_SelectedEntities; } + private: + const std::vector& GetEntityCollisions(); + bool m_ExitRequested = false; Map m_Map; Camera m_Camera; std::vector> m_Entities; - std::shared_ptr m_Player; - pathfinder::Path m_Path; std::unique_ptr m_PathFinder; + std::vector> m_SelectedEntities; + SelectionBox m_SelectionBox; }; diff --git a/cpp/src/user_input.cpp b/cpp/src/user_input.cpp index 1604f94..ae9895f 100644 --- a/cpp/src/user_input.cpp +++ b/cpp/src/user_input.cpp @@ -31,7 +31,14 @@ void UserInput::GetActions_mouse(const SDL_Event& event) { if (button == MouseButton::LEFT) { - LOG_DEBUG("Mouse down: ", mouse_event.x, ", ", mouse_event.y); + LOG_DEBUG("Selection start at ", mouse_event.x, ", ", mouse_event.y); + m_SelectionActive = true; + m_Actions.emplace_back(UserAction::Type::SELECTION_START, + WindowPos{mouse_event.x, mouse_event.y}); + } + else if (button == MouseButton::RIGHT) + { + LOG_DEBUG("Set move target to: ", mouse_event.x, ", ", mouse_event.y); m_Actions.emplace_back(UserAction::Type::SET_MOVE_TARGET, WindowPos{mouse_event.x, mouse_event.y}); } @@ -42,6 +49,13 @@ void UserInput::GetActions_mouse(const SDL_Event& event) } else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP) { + if (button == MouseButton::LEFT) + { + LOG_DEBUG("Selection end at ", mouse_event.x, ", ", mouse_event.y); + m_SelectionActive = false; + m_Actions.emplace_back(UserAction::Type::SELECTION_END, + WindowPos{mouse_event.x, mouse_event.y}); + } if (button == MouseButton::MIDDLE) { mouse_pan = false; @@ -55,6 +69,11 @@ void UserInput::GetActions_mouse(const SDL_Event& event) m_Actions.emplace_back(UserAction::Type::CAMERA_PAN, WindowPos{motion_event.xrel, motion_event.yrel}); } + if (m_SelectionActive) + { + m_Actions.emplace_back(UserAction::Type::SELECTION_CHANGE, + WindowPos{mouse_event.x, mouse_event.y}); + } } else if(event.type == SDL_EVENT_MOUSE_WHEEL) { diff --git a/cpp/src/user_input.hpp b/cpp/src/user_input.hpp index c32b071..dee9231 100644 --- a/cpp/src/user_input.hpp +++ b/cpp/src/user_input.hpp @@ -12,7 +12,19 @@ enum class MouseButton { LEFT = 1, MIDDLE, RIGHT }; class UserAction { public: - enum class Type { NONE, EXIT, SET_MOVE_TARGET, SELECT_PATHFINDER, CAMERA_PAN, CAMERA_ZOOM }; + + enum class Type + { + NONE, + EXIT, + SET_MOVE_TARGET, + SELECT_PATHFINDER, + CAMERA_PAN, + CAMERA_ZOOM, + SELECTION_START, + SELECTION_CHANGE, + SELECTION_END + }; UserAction() : type(Type::NONE), Argument{.number = 0} {} UserAction(Type t) : type(t), Argument{.number = 0} {} @@ -51,6 +63,7 @@ public: private: std::vector m_Actions; + bool m_SelectionActive = false; void GetActions_keyboard(const SDL_Event&); void GetActions_mouse(const SDL_Event&); diff --git a/cpp/src/window.cpp b/cpp/src/window.cpp index 91b9ab6..f92bfcc 100644 --- a/cpp/src/window.cpp +++ b/cpp/src/window.cpp @@ -84,13 +84,19 @@ void Window::DrawSprite(const WindowPos &position, Sprite &s, float scale) { SDL_RenderTexture(m_Renderer.get(), s.GetTexture(), nullptr, &rect); } -void Window::DrawRect(const WindowPos &position, const WindowSize size, uint8_t R, +void Window::DrawFilledRect(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); SDL_RenderFillRect(m_Renderer.get(), &rect); } +void Window::DrawRect(const WindowPos &position, const WindowSize size, uint8_t R, uint8_t G, uint8_t B) { + SDL_FRect rect = {position.x(), position.y(), size.x(), size.y()}; + SDL_SetRenderDrawColor(m_Renderer.get(), R, G, B, 255); + SDL_RenderRect(m_Renderer.get(), &rect); +} + void Window::ClearWindow() { SDL_SetRenderDrawColor(m_Renderer.get(), 50, 50, 50, 255); SDL_RenderClear(m_Renderer.get()); @@ -98,10 +104,11 @@ void Window::ClearWindow() { void Window::Flush() { SDL_RenderPresent(m_Renderer.get()); } -void Window::DrawCircle(const WindowPos &position, float radius) { +// TODO use some struct for color +void Window::DrawCircle(const WindowPos &position, float radius, uint8_t R, uint8_t G, uint8_t B) { int cx = static_cast(position.x()); int cy = static_cast(position.y()); - SDL_SetRenderDrawColor(m_Renderer.get(), 255, 0, 0, 255); + SDL_SetRenderDrawColor(m_Renderer.get(), R, G, B, 255); for (int i = 0; i < 360; ++i) { double a = i * M_PI / 180.0; SDL_RenderPoint(m_Renderer.get(), diff --git a/cpp/src/window.hpp b/cpp/src/window.hpp index 8453d67..0729bd4 100644 --- a/cpp/src/window.hpp +++ b/cpp/src/window.hpp @@ -23,11 +23,12 @@ public: std::expected Init(); void DrawSprite(const WindowPos &position, Sprite &s, float scale = 1.0f); - void DrawRect(const WindowPos &position, const WindowSize size, uint8_t R, + void DrawFilledRect(const WindowPos &position, const WindowSize size, uint8_t R, uint8_t G, uint8_t B, uint8_t A); + void DrawRect(const WindowPos &position, const WindowSize size, uint8_t R, uint8_t G, uint8_t B); void ClearWindow(); void Flush(); - void DrawCircle(const WindowPos &position, float radius); + void DrawCircle(const WindowPos &position, float radius, uint8_t R, uint8_t G, uint8_t B); void DrawLine(const WindowPos &A, const WindowPos &B); private: diff --git a/cpp/test/test.cpp b/cpp/test/test.cpp index 8b9262b..b94b846 100644 --- a/cpp/test/test.cpp +++ b/cpp/test/test.cpp @@ -26,6 +26,37 @@ TEST(vec, GetElements) { ASSERT_EQ(v1[2], 56); } +TEST(vec, ArrayConstruction) { + // Test construction from std::array + std::array arr3{1.5f, 2.5f, 3.5f}; + vec3 v1(arr3); + + ASSERT_FLOAT_EQ(v1[0], 1.5f); + ASSERT_FLOAT_EQ(v1[1], 2.5f); + ASSERT_FLOAT_EQ(v1[2], 3.5f); + + // Test with 2D vector + std::array arr2{10.0f, 20.0f}; + vec2 v2(arr2); + + ASSERT_FLOAT_EQ(v2[0], 10.0f); + ASSERT_FLOAT_EQ(v2[1], 20.0f); + + // Test with integer vector + std::array arr4{1, 2, 3, 4}; + ivec4 v4(arr4); + + ASSERT_EQ(v4[0], 1); + ASSERT_EQ(v4[1], 2); + ASSERT_EQ(v4[2], 3); + ASSERT_EQ(v4[3], 4); + + // Test that original array is unchanged + ASSERT_FLOAT_EQ(arr3[0], 1.5f); + ASSERT_FLOAT_EQ(arr3[1], 2.5f); + ASSERT_FLOAT_EQ(arr3[2], 3.5f); +} + TEST(vec, equalEpsilon) { // Test equalEpsilon @@ -430,6 +461,43 @@ TEST(vec, ChainedOperations) { ASSERT_FLOAT_EQ(a[1], 3.0f); } +TEST(vec, ChangeTag) { + // Test changing tag from WorldPos to WindowPos + WorldPos world_pos{100.0f, 200.0f}; + WindowPos window_pos = world_pos.ChangeTag(); + + // Data should be preserved + ASSERT_FLOAT_EQ(window_pos[0], 100.0f); + ASSERT_FLOAT_EQ(window_pos[1], 200.0f); + + // Original should be unchanged + ASSERT_FLOAT_EQ(world_pos[0], 100.0f); + ASSERT_FLOAT_EQ(world_pos[1], 200.0f); + + // Test changing tag from TilePos to another tag + TilePos tile_pos{5, 10}; + auto generic_pos = tile_pos.ChangeTag(); + + ASSERT_EQ(generic_pos[0], 5); + ASSERT_EQ(generic_pos[1], 10); + + // Test with 3D vector and custom tag + struct CustomTag {}; + vec3 original{1.5f, 2.5f, 3.5f}; + vec custom_tagged = original.ChangeTag(); + + ASSERT_FLOAT_EQ(custom_tagged[0], 1.5f); + ASSERT_FLOAT_EQ(custom_tagged[1], 2.5f); + ASSERT_FLOAT_EQ(custom_tagged[2], 3.5f); + + // Test that we can change back + vec3 back_to_original = custom_tagged.ChangeTag(); + + ASSERT_FLOAT_EQ(back_to_original[0], 1.5f); + ASSERT_FLOAT_EQ(back_to_original[1], 2.5f); + ASSERT_FLOAT_EQ(back_to_original[2], 3.5f); +} + TEST(Matrix, DefaultConstruction) { // Test that default-constructed matrix has all elements equal to zero Matrix m1;