Add map pan

This commit is contained in:
Jan Mrna 2025-10-05 12:11:06 +02:00 committed by Mrna
parent 75eeac06df
commit f458468644
11 changed files with 219 additions and 33 deletions

View File

@ -1,16 +1,43 @@
#include "camera.hpp" #include "camera.hpp"
#include "math.hpp" #include "math.hpp"
#include "log.hpp"
// for now only pass-through placeholder functions, // for now only pass-through placeholder functions,
// since we draw the whole map // 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 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 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]};
} }

View File

@ -5,12 +5,16 @@
class Camera class Camera
{ {
public: public:
WindowPos WorldToWindow(WorldPos) const; void Pan(const WorldPos& delta);
WorldPos WindowToWorld(WindowPos) const; void Zoom(const WorldPos& delta);
WindowPos WorldToWindow(WorldPos) const;
WorldPos WindowToWorld(WindowPos) const;
WindowSize WorldToWindowSize(WorldSize) const;
WorldSize WindowToWorldSize(WindowSize) const;
private: private:
// TODO this should be replaced with a matrix // TODO this should be replaced with a matrix
float m_Zoom; float m_Zoom;
WorldPos m_RectCorner; // upper left corner (0,0) of the drawn regios WorldPos m_Pan;
}; };

View File

@ -27,14 +27,19 @@ void GameLoop::Run() {
for (size_t row = 0; row < tiles.size(); row++) { for (size_t row = 0; row < tiles.size(); row++) {
for (size_t col = 0; col < tiles[row].size(); col++) { for (size_t col = 0; col < tiles[row].size(); col++) {
const auto& camera = m_Game->GetCamera(); const auto& camera = m_Game->GetCamera();
const auto& position = camera.WorldToWindow(
map.TileEdgeToWorld(
TilePos{static_cast<int32_t>(row), static_cast<int32_t>(col)}
)
);
const auto& size = camera.WorldToWindowSize(
map.GetTileSize()
);
// LOG_DEBUG("Drawing rect (", row, ", ", col, ")"); // LOG_DEBUG("Drawing rect (", row, ", ", col, ")");
m_Window->DrawRect( m_Window->DrawRect(
camera.WorldToWindow( position,
map.TileEdgeToWorld( size,
TilePos{static_cast<int32_t>(row), static_cast<int32_t>(col)} tiles[row][col]->R, tiles[row][col]->G,
)
),
camera.WorldToWindow(map.GetTileSize()), tiles[row][col]->R, tiles[row][col]->G,
tiles[row][col]->B, tiles[row][col]->A); tiles[row][col]->B, tiles[row][col]->A);
} }
} }

View File

@ -28,8 +28,7 @@ TilePos Map::WorldToTile(WorldPos p) const {
return TilePos{static_cast<int32_t>(p.x() / TILE_SIZE), static_cast<int32_t>(p.y() / TILE_SIZE)}; return TilePos{static_cast<int32_t>(p.x() / TILE_SIZE), static_cast<int32_t>(p.y() / TILE_SIZE)};
} }
// TODO this should probably use something like WorldSize or WorldVec to make the distinction clear WorldSize Map::GetTileSize() const { return WorldSize{TILE_SIZE, TILE_SIZE}; }
WorldPos Map::GetTileSize() const { return WorldPos{TILE_SIZE, TILE_SIZE}; }
const Tile *Map::GetTileAt(TilePos p) const { const Tile *Map::GetTileAt(TilePos p) const {
assert(IsTilePosValid(p)); assert(IsTilePosValid(p));

View File

@ -25,8 +25,8 @@ public:
WorldPos TileToWorld(TilePos p) const; WorldPos TileToWorld(TilePos p) const;
WorldPos TileEdgeToWorld(TilePos p) const; WorldPos TileEdgeToWorld(TilePos p) const;
TilePos WorldToTile(WorldPos p) const; TilePos WorldToTile(WorldPos p) const;
WorldPos GetTileSize() const; WorldSize GetTileSize() const;
const Tile *GetTileAt(TilePos p) const; const Tile *GetTileAt(TilePos p) const;
const Tile *GetTileAt(WorldPos p) const; const Tile *GetTileAt(WorldPos p) const;

View File

@ -238,14 +238,22 @@ using uvec3 = vec<std::uint32_t, 3>;
using uvec4 = vec<std::uint32_t, 4>; using uvec4 = vec<std::uint32_t, 4>;
// tags for differentiating between domains // tags for differentiating between domains
struct WorldTag {}; struct WorldPosTag {};
struct WindowTag {}; struct WorldSizeTag {};
struct TileTag {}; struct WindowPosTag {};
struct WindowSizeTag {};
struct TilePosTag {};
struct TileSizeTag {};
// types for each domain // types for each domain
using WorldPos = vec<float, 2, WorldTag>; using WorldPos = vec<float, 2, WorldPosTag>;
using WindowPos = vec<float, 2, WindowTag>; using WindowPos = vec<float, 2, WindowPosTag>;
using TilePos = vec<int32_t, 2, TileTag>; using TilePos = vec<int32_t, 2, TilePosTag>;
// Size
using WorldSize = vec<float, 2, WorldSizeTag>;
using WindowSize = vec<float, 2, WindowSizeTag>;
using TileSize = vec<int32_t, 2, TileSizeTag>;
// //
// Utils // Utils
@ -259,3 +267,121 @@ struct TilePosHash {
} }
}; };
// old stuff - TODO delete
// constexpr double EQUALITY_LIMIT = 1e-6;
// template <typename T> struct Vec2D {
// public:
// Vec2D() = default;
// ~Vec2D() = default;
//
// template <typename U>
// Vec2D(Vec2D<U> other) {
// this->x = static_cast<T>(other.x);
// this->y = static_cast<T>(other.y);
// }
//
// Vec2D& operator+=(const Vec2D &other) {
// x += other.x;
// y += other.y;
// return *this;
// }
//
// template <typename U>
// requires std::is_arithmetic_v<U>
// Vec2D& operator/=(U k) {
// this->x /= static_cast<T>(k);
// this->y /= static_cast<T>(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 <typename U>
// requires std::is_arithmetic_v<U>
// friend Vec2D operator*(U k, const Vec2D &v)
// {
// return Vec2D{k * v.x, k * v.y};
// }
//
// template <typename U>
// requires std::is_arithmetic_v<U>
// 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<T>) {
// return a.x == b.x && a.y == b.y;
// } else if constexpr (std::is_floating_point_v<T>) {
// 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<T>
// {
// return sqrt(distance_squared(other));
// }
//
// void normalize()
// requires std::floating_point<T>
// {
// 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<T>
// {
// 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 <typename U> Vec2D(std::initializer_list<U> list) {
// assert(list.size() == 2);
// auto first_element = *list.begin();
// auto second_element = *(list.begin() + 1);
// x = static_cast<T>(first_element);
// y = static_cast<T>(second_element);
// }
//
// T x, y;
//
// friend std::ostream &operator<<(std::ostream &os, const Vec2D &obj) {
// os << "( " << obj.x << ", " << obj.y << ")";
// return os;
// }
// };

View File

@ -110,7 +110,7 @@ void PathFindingDemo::HandleActions(const std::vector<UserAction> &actions) {
LOG_INFO("Exit requested"); LOG_INFO("Exit requested");
m_ExitRequested = true; m_ExitRequested = true;
} else if (action.type == UserAction::Type::SET_MOVE_TARGET) { } 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); LOG_INFO("Calculating path to target: ", wp);
m_Path = m_PathFinder->CalculatePath(m_Player->GetPosition(), wp); m_Path = m_PathFinder->CalculatePath(m_Player->GetPosition(), wp);
LOG_INFO("Done, path node count: ", m_Path.size()); LOG_INFO("Done, path node count: ", m_Path.size());
@ -119,6 +119,11 @@ void PathFindingDemo::HandleActions(const std::vector<UserAction> &actions) {
PathFinderType type = static_cast<PathFinderType>(action.Argument.number); PathFinderType type = static_cast<PathFinderType>(action.Argument.number);
m_PathFinder = pathfinder::utils::create(type, (const Map*)&m_Map); m_PathFinder = pathfinder::utils::create(type, (const Map*)&m_Map);
LOG_INFO("Switched to path finding method: ", m_PathFinder->GetName()); 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);
} }
}; };
} }

View File

@ -22,8 +22,12 @@ std::expected<void, std::string> UserInput::Init() { return {}; }
const std::vector<UserAction> &UserInput::GetActions() { const std::vector<UserAction> &UserInput::GetActions() {
m_Actions.clear(); m_Actions.clear();
SDL_Event event; SDL_Event event;
static WindowPos mouse_pan_start;
static bool mouse_pan = false;
while (SDL_PollEvent(&event)) { 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_KEY_DOWN || event.type == SDL_EVENT_KEY_UP) {
bool key_down = event.type == SDL_EVENT_KEY_DOWN ? true : false; bool key_down = event.type == SDL_EVENT_KEY_DOWN ? true : false;
SDL_KeyboardEvent kbd_event = event.key; SDL_KeyboardEvent kbd_event = event.key;
@ -55,9 +59,25 @@ const std::vector<UserAction> &UserInput::GetActions() {
} }
} else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { } else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
SDL_MouseButtonEvent mouse_event = event.button; SDL_MouseButtonEvent mouse_event = event.button;
LOG_DEBUG("Mouse down: ", mouse_event.x, ", ", mouse_event.y); if (mouse_event.button == 1) {
m_Actions.emplace_back(UserAction::Type::SET_MOVE_TARGET, LOG_DEBUG("Mouse down: ", mouse_event.x, ", ", mouse_event.y);
WorldPos{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 { } else {
// TODO uncomment, for now too much noise // TODO uncomment, for now too much noise
// LOG_WARNING("Action not processed"); // LOG_WARNING("Action not processed");

View File

@ -9,25 +9,25 @@
class UserAction { class UserAction {
public: 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(Type::NONE), Argument{.number = 0} {}
UserAction(Type t) : type(t), Argument{.number = 0} {} UserAction(Type t) : type(t), Argument{.number = 0} {}
UserAction(Type t, char key) : type(t), Argument{.key = key} {} 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(Type t, int arg) : type(t), Argument{.number = arg} {}
~UserAction() = default; ~UserAction() = default;
Type type; Type type;
union { union {
WorldPos position; WindowPos position;
char key; char key;
int number; int number;
} Argument; } Argument;
// TODO use std::variant // TODO use std::variant
//std::variant<WorldPos, char, int> Argument; //std::variant<WindowPos, char, int> Argument;
}; };
class UserInput { class UserInput {

View File

@ -84,7 +84,7 @@ void Window::DrawSprite(const WindowPos &position, Sprite &s) {
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 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) { 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);

View File

@ -23,7 +23,7 @@ public:
std::expected<void, std::string> Init(); std::expected<void, std::string> Init();
void DrawSprite(const WindowPos &position, Sprite &s); 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); uint8_t G, uint8_t B, uint8_t A);
void ClearWindow(); void ClearWindow();
void Flush(); void Flush();