Compare commits

...

11 Commits

Author SHA1 Message Date
Jan Mrna
b4fcd56f10 Added CMake build for Linux 2025-10-12 19:53:24 +02:00
Jan Mrna
2f346c11c3 Deselect all entities on re-selection 2025-10-12 16:44:59 +02:00
Jan Mrna
df6d323e42 Fix collision radius size when zooming 2025-10-12 16:19:38 +02:00
Jan Mrna
1ce793c6e8 Fixed selection rectangle glitch 2025-10-12 16:09:12 +02:00
Jan Mrna
d3af793092 Show rectangle when selecting entities 2025-10-12 16:04:40 +02:00
Jan Mrna
4b3a4c53e8 Fix entity selection when zoomed in/out 2025-10-12 15:46:40 +02:00
Jan Mrna
370df129a8 ChangeTag for vec and construct from std::array 2025-10-12 15:44:56 +02:00
Jan Mrna
08b4b10113 Selection rectangle 2025-10-10 19:32:58 +02:00
Jan Mrna
250f0963c8 Basic collisions 2025-10-10 19:00:08 +02:00
Jan Mrna
3d34b68133 Multiple entities + pathfinding 2025-10-10 10:45:27 +02:00
Jan Mrna
2f80129dce Name refactor 2025-10-10 07:49:30 +02:00
13 changed files with 516 additions and 66 deletions

90
CMakeLists.txt Normal file
View File

@ -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")

View File

@ -16,6 +16,18 @@ public:
WindowSize WorldToWindowSize(WorldSize) const; WindowSize WorldToWindowSize(WorldSize) const;
WorldSize WindowToWorldSize(WindowSize) const; WorldSize WindowToWorldSize(WindowSize) const;
template <typename T>
requires std::floating_point<T>
T WindowToWorldSize(T window_size) const {
return window_size / static_cast<T>(m_Zoom);
}
template <typename T>
requires std::floating_point<T>
T WorldToWindowSize(T world_size) const {
return world_size * static_cast<T>(m_Zoom);
}
private: private:
// TODO this should be replaced with a matrix // TODO this should be replaced with a matrix
float m_Zoom = 1.0f; float m_Zoom = 1.0f;

View File

@ -46,6 +46,47 @@ void Entity::Update(float time_delta) {
m_Position += m_ActualVelocity * time_delta; m_Position += m_ActualVelocity * time_delta;
} }
std::optional<WorldPos> 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() { Player::Player() {
LOG_DEBUG("."); LOG_DEBUG(".");
if (m_Sprite == nullptr) { if (m_Sprite == nullptr) {

View File

@ -5,10 +5,12 @@
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include <string_view> #include <string_view>
#include <optional>
#include "log.hpp" #include "log.hpp"
#include "math.hpp" #include "math.hpp"
#include "sprite.hpp" #include "sprite.hpp"
#include "pathfinder/base.hpp"
class Entity { class Entity {
public: public:
@ -56,14 +58,29 @@ public:
void ZeroActualVelocityInDirection(WorldPos direction); 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<WorldPos> 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: protected:
WorldPos m_Position; WorldPos m_Position;
WorldPos m_ActualVelocity; WorldPos m_ActualVelocity;
WorldPos m_RequestedVelocity; WorldPos m_RequestedVelocity;
pathfinder::Path m_Path;
private: private:
bool m_FlagExpired = false; bool m_FlagExpired = false;
static constexpr float m_CollisionRadiusSq = 1000.0f; bool m_CollisionBoxVisible = true;
bool m_Selected = false;
}; };
class Player final : public Entity { class Player final : public Entity {
@ -75,7 +92,7 @@ public:
constexpr Entity::Type GetType() const override { constexpr Entity::Type GetType() const override {
return Entity::Type::PLAYER; 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 IsMovable() const override { return true; }
bool IsCollidable() const override { return true; } bool IsCollidable() const override { return true; }

View File

@ -21,26 +21,52 @@ void GameLoop::Draw() {
TilePos{static_cast<int32_t>(row), static_cast<int32_t>(col)})); TilePos{static_cast<int32_t>(row), static_cast<int32_t>(col)}));
const auto &size = camera.WorldToWindowSize(map.GetTileSize()); const auto &size = camera.WorldToWindowSize(map.GetTileSize());
// LOG_DEBUG("Drawing rect (", row, ", ", col, ")"); // 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); tiles[row][col]->B, tiles[row][col]->A);
} }
} }
// draw the path, if it exists // draw the path, if it exists
WorldPos start_pos = m_Game->GetPlayer()->GetPosition();
for (const auto &next_pos : m_Game->GetPath()) { 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(); const auto &camera = m_Game->GetCamera();
m_Window->DrawLine(camera.WorldToWindow(start_pos), m_Window->DrawLine(camera.WorldToWindow(start_pos),
camera.WorldToWindow(next_pos)); camera.WorldToWindow(next_pos));
start_pos = next_pos; start_pos = next_pos;
} }
}
// draw all the entities (player etc) // draw all the entities (player etc)
for (auto &entity : m_Game->GetEntities()) { for (auto &entity : m_Game->GetEntities()) {
const auto &camera = m_Game->GetCamera(); const auto &camera = m_Game->GetCamera();
m_Window->DrawSprite(camera.WorldToWindow(entity->GetPosition()), auto entity_pos = camera.WorldToWindow(entity->GetPosition());
entity->GetSprite(), camera.GetZoom()); 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 // TODO rethink coupling and dependencies in the game loop class
@ -49,9 +75,10 @@ void GameLoop::Run() {
LOG_INFO("Running the game"); LOG_INFO("Running the game");
while (!m_Game->IsExitRequested()) { while (!m_Game->IsExitRequested()) {
m_Game->HandleActions(m_UserInput->GetActions()); m_Game->HandleActions(m_UserInput->GetActions());
m_Game->UpdatePlayerVelocity(); m_Game->UpdateWorld();
// TODO measure fps, draw only if delay for target fps was reached // TODO measure fps, draw only if delay for target fps was reached
// or create a separate thread for drawing
m_Window->ClearWindow(); m_Window->ClearWindow();
Draw(); Draw();
m_Window->Flush(); m_Window->Flush();

View File

@ -44,6 +44,8 @@ public:
requires(std::same_as<ArgsT, T> && ...) && (sizeof...(ArgsT) == N) requires(std::same_as<ArgsT, T> && ...) && (sizeof...(ArgsT) == N)
vec(ArgsT... args) : m_Array{args...} {} vec(ArgsT... args) : m_Array{args...} {}
vec(std::array<T, N> array) : m_Array{array} {}
// //
// Access to elements & data // Access to elements & data
// //
@ -165,6 +167,12 @@ public:
return (a - b).Length(); return (a - b).Length();
} }
T DistanceSquared(const vec &b) const {
const vec &a = *this;
return (a - b).LengthSquared();
}
// //
// In-place vector operations // In-place vector operations
// //
@ -251,6 +259,12 @@ public:
return m_Array[2]; return m_Array[2];
} }
template <typename TargetTag>
vec<T,N,TargetTag> ChangeTag()
{
return vec<T,N,TargetTag>(m_Array);
}
private: private:
std::array<T, N> m_Array; std::array<T, N> m_Array;
}; };

View File

@ -55,43 +55,74 @@ void PathFindingDemo::CreateMap() {
m_Map.PaintLine(TilePos{84,81}, TilePos{84,96}, 1.0, TileType::WALL); 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); m_Map.PaintLine(TilePos{78,87}, TilePos{78,100}, 1.0, TileType::WALL);
// add player // add some controllable entities
m_Entities.clear(); m_Entities.clear();
m_Player = std::make_shared<Player>(); auto player = std::make_shared<Player>();
m_Player->SetPosition(m_Map.TileToWorld(TilePos{25, 20})); player->SetPosition(m_Map.TileToWorld(TilePos{25, 20}));
m_Entities.push_back(m_Player); AddEntity(player);
auto player2 = std::make_shared<Player>();
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<Player>();
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 { WorldPos PathFindingDemo::GetRandomPosition() const {
return WorldPos{0.0f, 0.0f}; // totally random! return WorldPos{0.0f, 0.0f}; // totally random!
} }
std::optional<WorldPos> PathFindingDemo::GetMoveTarget() {
WorldPos current_player_pos = GetPlayer()->GetPosition();
if (m_Path.empty()) {
return {}; const std::vector<Collision>& PathFindingDemo::GetEntityCollisions()
{
static std::vector<Collision> 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));
}
}
}
return m_Collisions;
} }
WorldPos next_player_pos = m_Path.front();
if (current_player_pos.DistanceTo(next_player_pos) > 1.0) { // Update entity positions, handle collisions
// target not reached yet void PathFindingDemo::UpdateWorld() {
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 {};
}
void PathFindingDemo::UpdatePlayerVelocity() { float time_delta = 1.0f;
auto player = GetPlayer();
auto current_pos = player->GetPosition(); for (auto& entity : m_Entities)
{
// calculate the velocity
auto current_pos = entity->GetPosition();
double tile_velocity_coeff = m_Map.GetTileVelocityCoeff(current_pos); double tile_velocity_coeff = m_Map.GetTileVelocityCoeff(current_pos);
auto next_pos = GetMoveTarget(); auto next_pos = entity->GetMoveTarget();
WorldPos velocity = WorldPos{}; WorldPos velocity = WorldPos{};
if (next_pos) if (next_pos)
{ {
@ -100,9 +131,32 @@ void PathFindingDemo::UpdatePlayerVelocity() {
//LOG_DEBUG("I want to move to: ", next_pos.value(), //LOG_DEBUG("I want to move to: ", next_pos.value(),
// ", velocity: ", velocity); // ", velocity: ", velocity);
} }
player->SetActualVelocity(velocity * tile_velocity_coeff); entity->SetActualVelocity(velocity * tile_velocity_coeff);
float time_delta = 1.0f;
player->Update(time_delta); 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<UserAction> &actions) void PathFindingDemo::HandleActions(const std::vector<UserAction> &actions)
@ -116,10 +170,19 @@ void PathFindingDemo::HandleActions(const std::vector<UserAction> &actions)
} }
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); WorldPos target_pos = m_Camera.WindowToWorld(action.Argument.position);
LOG_INFO("Calculating path to target: ", wp); for (auto& selected_entity : m_SelectedEntities)
m_Path = m_PathFinder->CalculatePath(m_Player->GetPosition(), wp); {
LOG_INFO("Done, path node count: ", m_Path.size()); 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) else if (action.type == UserAction::Type::SELECT_PATHFINDER)
{ {
@ -140,5 +203,69 @@ void PathFindingDemo::HandleActions(const std::vector<UserAction> &actions)
m_Camera.Zoom(action.Argument.float_number); m_Camera.Zoom(action.Argument.float_number);
LOG_INFO("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<WindowSizeTag>();
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<WindowSizeTag>();
}
}; };
} }
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<WindowPos, WindowSize> PathFindingDemo::GetSelectionBoxPosSize()
{
const auto& pos = m_SelectionBox.start;
WindowPos size_pos = m_SelectionBox.end - m_SelectionBox.start;
WindowSize size = size_pos.ChangeTag<WindowSizeTag>();
return std::pair(pos, size);
}

View File

@ -12,6 +12,15 @@
#include "pathfinder/base.hpp" #include "pathfinder/base.hpp"
#include "camera.hpp" #include "camera.hpp"
using Collision = std::pair<std::weak_ptr<Entity>, std::weak_ptr<Entity>>;
struct SelectionBox
{
WindowPos start, end;
WindowSize size;
bool active;
};
class PathFindingDemo { class PathFindingDemo {
public: public:
PathFindingDemo(int width, int height); PathFindingDemo(int width, int height);
@ -22,26 +31,31 @@ public:
PathFindingDemo &operator=(const PathFindingDemo &) = delete; PathFindingDemo &operator=(const PathFindingDemo &) = delete;
PathFindingDemo &operator=(PathFindingDemo &&) = delete; PathFindingDemo &operator=(PathFindingDemo &&) = delete;
std::shared_ptr<Player> GetPlayer() { return m_Player; }
std::vector<std::shared_ptr<Entity>>& GetEntities() { return m_Entities; } std::vector<std::shared_ptr<Entity>>& GetEntities() { return m_Entities; }
const Map& GetMap() const { return m_Map; } const Map& GetMap() const { return m_Map; }
const Camera& GetCamera() const { return m_Camera; } const Camera& GetCamera() const { return m_Camera; }
const pathfinder::Path& GetPath() const { return m_Path; }
bool IsExitRequested() const { return m_ExitRequested; } bool IsExitRequested() const { return m_ExitRequested; }
void AddEntity(std::shared_ptr<Entity> e); void AddEntity(std::shared_ptr<Entity> e);
void CreateMap(); void CreateMap();
std::optional<WorldPos> GetMoveTarget(); void UpdateWorld();
void UpdatePlayerVelocity();
void HandleActions(const std::vector<UserAction> &actions); void HandleActions(const std::vector<UserAction> &actions);
WorldPos GetRandomPosition() const; WorldPos GetRandomPosition() const;
void SelectEntitiesInRectangle(WorldPos A, WorldPos B);
void DeselectEntities();
bool IsSelectionBoxActive() const { return m_SelectionBox.active; }
std::pair<WindowPos, WindowSize> GetSelectionBoxPosSize();
std::vector<std::weak_ptr<Entity>> GetSelectedEntities() { return m_SelectedEntities; }
private: private:
const std::vector<Collision>& GetEntityCollisions();
bool m_ExitRequested = false; bool m_ExitRequested = false;
Map m_Map; Map m_Map;
Camera m_Camera; Camera m_Camera;
std::vector<std::shared_ptr<Entity>> m_Entities; std::vector<std::shared_ptr<Entity>> m_Entities;
std::shared_ptr<Player> m_Player;
pathfinder::Path m_Path;
std::unique_ptr<pathfinder::PathFinderBase> m_PathFinder; std::unique_ptr<pathfinder::PathFinderBase> m_PathFinder;
std::vector<std::weak_ptr<Entity>> m_SelectedEntities;
SelectionBox m_SelectionBox;
}; };

View File

@ -31,7 +31,14 @@ void UserInput::GetActions_mouse(const SDL_Event& event)
{ {
if (button == MouseButton::LEFT) 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, m_Actions.emplace_back(UserAction::Type::SET_MOVE_TARGET,
WindowPos{mouse_event.x, mouse_event.y}); 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) 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) if (button == MouseButton::MIDDLE)
{ {
mouse_pan = false; mouse_pan = false;
@ -55,6 +69,11 @@ void UserInput::GetActions_mouse(const SDL_Event& event)
m_Actions.emplace_back(UserAction::Type::CAMERA_PAN, m_Actions.emplace_back(UserAction::Type::CAMERA_PAN,
WindowPos{motion_event.xrel, motion_event.yrel}); 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) else if(event.type == SDL_EVENT_MOUSE_WHEEL)
{ {

View File

@ -12,7 +12,19 @@ enum class MouseButton { LEFT = 1, MIDDLE, RIGHT };
class UserAction { class UserAction {
public: 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(Type::NONE), Argument{.number = 0} {}
UserAction(Type t) : type(t), Argument{.number = 0} {} UserAction(Type t) : type(t), Argument{.number = 0} {}
@ -51,6 +63,7 @@ public:
private: private:
std::vector<UserAction> m_Actions; std::vector<UserAction> m_Actions;
bool m_SelectionActive = false;
void GetActions_keyboard(const SDL_Event&); void GetActions_keyboard(const SDL_Event&);
void GetActions_mouse(const SDL_Event&); void GetActions_mouse(const SDL_Event&);

View File

@ -84,13 +84,19 @@ void Window::DrawSprite(const WindowPos &position, Sprite &s, float scale) {
SDL_RenderTexture(m_Renderer.get(), s.GetTexture(), nullptr, &rect); 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) { 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_SetRenderDrawColor(m_Renderer.get(), R, G, B, A);
SDL_RenderFillRect(m_Renderer.get(), &rect); 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() { void Window::ClearWindow() {
SDL_SetRenderDrawColor(m_Renderer.get(), 50, 50, 50, 255); SDL_SetRenderDrawColor(m_Renderer.get(), 50, 50, 50, 255);
SDL_RenderClear(m_Renderer.get()); SDL_RenderClear(m_Renderer.get());
@ -98,10 +104,11 @@ void Window::ClearWindow() {
void Window::Flush() { SDL_RenderPresent(m_Renderer.get()); } 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<int>(position.x()); int cx = static_cast<int>(position.x());
int cy = static_cast<int>(position.y()); int cy = static_cast<int>(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) { for (int i = 0; i < 360; ++i) {
double a = i * M_PI / 180.0; double a = i * M_PI / 180.0;
SDL_RenderPoint(m_Renderer.get(), SDL_RenderPoint(m_Renderer.get(),

View File

@ -23,11 +23,12 @@ public:
std::expected<void, std::string> Init(); std::expected<void, std::string> Init();
void DrawSprite(const WindowPos &position, Sprite &s, float scale = 1.0f); 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); 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 ClearWindow();
void Flush(); 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); void DrawLine(const WindowPos &A, const WindowPos &B);
private: private:

View File

@ -26,6 +26,37 @@ TEST(vec, GetElements) {
ASSERT_EQ(v1[2], 56); ASSERT_EQ(v1[2], 56);
} }
TEST(vec, ArrayConstruction) {
// Test construction from std::array
std::array<float, 3> 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<float, 2> 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<int32_t, 4> 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(vec, equalEpsilon) {
// Test equalEpsilon // Test equalEpsilon
@ -430,6 +461,43 @@ TEST(vec, ChainedOperations) {
ASSERT_FLOAT_EQ(a[1], 3.0f); 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<WindowPosTag>();
// 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<Any>();
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<float, 3, CustomTag> custom_tagged = original.ChangeTag<CustomTag>();
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<Any>();
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(Matrix, DefaultConstruction) {
// Test that default-constructed matrix has all elements equal to zero // Test that default-constructed matrix has all elements equal to zero
Matrix<float, 2> m1; Matrix<float, 2> m1;