Compare commits

...

4 Commits

Author SHA1 Message Date
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
9 changed files with 194 additions and 5 deletions

View File

@ -21,7 +21,7 @@ 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);
} }
} }
@ -52,6 +52,15 @@ void GameLoop::Draw() {
m_Window->DrawCircle(entity_pos_window, entity->GetCollisionRadius()); m_Window->DrawCircle(entity_pos_window, entity->GetCollisionRadius());
} }
} }
// 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, 100);
}
} }
// TODO rethink coupling and dependencies in the game loop class // TODO rethink coupling and dependencies in the game loop class

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
// //
@ -257,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

@ -203,5 +203,56 @@ 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;
}
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::SelectEntitiesInRectangle(WorldPos A, WorldPos B)
{
// TODO use colliders for this
m_SelectedEntities.clear();
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));
}
}
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

@ -14,6 +14,13 @@
using Collision = std::pair<std::weak_ptr<Entity>, std::weak_ptr<Entity>>; 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);
@ -35,6 +42,9 @@ public:
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);
bool IsSelectionBoxActive() const { return m_SelectionBox.active; }
std::pair<WindowPos, WindowSize> GetSelectionBoxPosSize();
private: private:
const std::vector<Collision>& GetEntityCollisions(); const std::vector<Collision>& GetEntityCollisions();
@ -45,4 +55,5 @@ private:
std::vector<std::shared_ptr<Entity>> m_Entities; std::vector<std::shared_ptr<Entity>> m_Entities;
std::unique_ptr<pathfinder::PathFinderBase> m_PathFinder; std::unique_ptr<pathfinder::PathFinderBase> m_PathFinder;
std::vector<std::weak_ptr<Entity>> m_SelectedEntities; 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,21 @@ 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, uint8_t fill_alpha) {
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);
//SDL_RenderFillRect(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());

View File

@ -23,8 +23,10 @@ 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, uint8_t fill_alpha);
void ClearWindow(); void ClearWindow();
void Flush(); void Flush();
void DrawCircle(const WindowPos &position, float radius); void DrawCircle(const WindowPos &position, float radius);

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;