diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..14eb516 --- /dev/null +++ b/.clang-format @@ -0,0 +1,3 @@ +# Generated by CLion for LLVM +BasedOnStyle: LLVM +IndentWidth: 4 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff9047e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/cmake-build-debug/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 0045d40..47f3b1a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.11) +cmake_minimum_required(VERSION 3.16) project(sandbox2D) set(CMAKE_CXX_STANDARD 20) @@ -6,18 +6,26 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) # Gather sources -file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS src/*.cpp) -file(GLOB_RECURSE HEADER_FILES CONFIGURE_DEPENDS src/*.h src/*.hpp) +file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/src/*.cpp) -add_executable(${PROJECT_NAME} ${SOURCE_FILES} ${HEADER_FILES}) -target_include_directories(${PROJECT_NAME} PRIVATE /opt/homebrew/include) -target_link_directories(${PROJECT_NAME} PRIVATE /opt/homebrew/lib) +add_executable(${PROJECT_NAME} ${SOURCE_FILES}) +target_include_directories(${PROJECT_NAME} + PRIVATE + ${CMAKE_SOURCE_DIR}/include +) + +# SDL2 and extensions find_package(SDL2 REQUIRED) find_package(SDL2_image REQUIRED) find_package(SDL2_ttf REQUIRED) -target_link_libraries(${PROJECT_NAME} PRIVATE SDL2::SDL2 SDL2_image::SDL2_image SDL2_ttf::SDL2_ttf) +target_link_libraries(${PROJECT_NAME} + PRIVATE + SDL2::SDL2 + SDL2_image::SDL2_image + SDL2_ttf::SDL2_ttf +) # Assets include directory set(ASSETS_DIR "${CMAKE_SOURCE_DIR}/assets") diff --git a/include/game/Config.h b/include/game/Config.h new file mode 100644 index 0000000..80a0f9f --- /dev/null +++ b/include/game/Config.h @@ -0,0 +1,31 @@ +// +// Created by Jacopo Uggeri on 15/08/2025. +// +#pragma once + +#include "physics/Vec2.h" +#include +#include + +namespace config { + +inline constexpr std::string_view GAME_NAME {"Sandbox 2D"}; + + namespace resources { + // resources (should move in namespace or a more relevant file later) + inline const std::filesystem::path ASSETS_PATH = "assets"; + inline const std::filesystem::path TEX_PATH = ASSETS_PATH / "textures"; + inline const std::filesystem::path FONT_PATH = ASSETS_PATH / "fonts"; + inline constexpr std::string_view FONT {"Hack-Regular.ttf"}; + inline constexpr std::string_view PLAYER_TEX {"bacon.png"}; + inline constexpr std::string_view WALL_TEX {"wall.png"}; + } + + namespace graphics { + // graphics (should move in namespace or a more relevant file later) + inline constexpr phys::Vec2i WINDOW_SIZE {1280, 720}; + inline constexpr int TILE_SIZE = 16; + inline constexpr float DRAW_SCALE = 4.0; + } + +} \ No newline at end of file diff --git a/include/game/Game.h b/include/game/Game.h new file mode 100644 index 0000000..8f31a4e --- /dev/null +++ b/include/game/Game.h @@ -0,0 +1,31 @@ +// +// Created by Jacopo Uggeri on 15/08/2025. +// +#pragma once + +#include "game/GameState.h" +#include "graphics/Renderer.h" +#include "input/InputManager.h" + +class Game { +public: + bool init(); + void loop(); + +private: + GameState gameState_ {}; + Renderer renderer_ {}; + InputManager inputManager_ {}; + + void step(double deltaSeconds); + +public: + Game() = default; + ~Game() = default; + // Delete copy operations + Game(const Game&) = delete; + Game& operator=(const Game&) = delete; + // Delete move operations + Game(Game&&) = delete; + Game& operator=(Game&&) = delete; +}; diff --git a/include/game/GameState.h b/include/game/GameState.h new file mode 100644 index 0000000..96ffda7 --- /dev/null +++ b/include/game/GameState.h @@ -0,0 +1,17 @@ +// +// Created by Jacopo Uggeri on 20/08/2025. +// +#pragma once +#include "Player.h" +#include "world/World.h" + +struct GameState { + uint64_t gameTicks = 0; + bool running = true; + bool paused = true; + bool debugMode = true; + Player player {}; + World world {}; + + bool unpaused() const { return !paused; } +}; \ No newline at end of file diff --git a/include/game/Player.h b/include/game/Player.h new file mode 100644 index 0000000..0362ce6 --- /dev/null +++ b/include/game/Player.h @@ -0,0 +1,16 @@ +// +// Created by Jacopo Uggeri on 20/08/2025. +// +#pragma once +#include "physics/Vec2.h" +#include "world/World.h" + +struct Player { + constexpr static float SPEED = 10.0f; + phys::Vec2f pos {}; + phys::Vec2f vel {}; + Sprite sprite {std::string(config::resources::PLAYER_TEX)}; + + void set_velocity(phys::Vec2f v); + void move(double deltaSeconds); +}; \ No newline at end of file diff --git a/include/game/graphics/Renderer.h b/include/game/graphics/Renderer.h new file mode 100644 index 0000000..93026b0 --- /dev/null +++ b/include/game/graphics/Renderer.h @@ -0,0 +1,23 @@ +// +// Created by Jacopo Uggeri on 21/08/2025. +// +#pragma once +#include "game/GameState.h" +#include "platform/sdl/GraphicsDevice.h" + +class Renderer { +public: + bool init(int winW, int winH, std::string_view windowTitle); + void render(const GameState& gameState, double deltaSeconds); + +private: + void drawSprite(const Sprite &sprite, phys::Vec2f pos) const; + void drawWorld(const World& world) const; + void cameraSnap(phys::Vec2f pos); + void cameraFollow(phys::Vec2f pos, double deltaSeconds); + phys::Vec2f toScreenCoords(const phys::Vec2f& pos) const; + void drawDebugInfo(const GameState &gameState, double deltaSeconds) const; + + GraphicsDevice graphicsDevice_ {}; + struct Camera { phys::Vec2f pos {0, 0}; } camera_; +}; \ No newline at end of file diff --git a/include/game/input/InputManager.h b/include/game/input/InputManager.h new file mode 100644 index 0000000..9f3ee65 --- /dev/null +++ b/include/game/input/InputManager.h @@ -0,0 +1,15 @@ +// +// Created by Jacopo Uggeri on 21/08/2025. +// + +#pragma once +#include "platform/sdl/InputSource.h" + +class InputManager { +public: + bool init(); + void handleEvents(GameState& state); + +private: + InputSource inputSource_; +}; \ No newline at end of file diff --git a/src/core/physics/Vec2.h b/include/game/physics/Vec2.h similarity index 96% rename from src/core/physics/Vec2.h rename to include/game/physics/Vec2.h index 03639b0..74fb339 100644 --- a/src/core/physics/Vec2.h +++ b/include/game/physics/Vec2.h @@ -1,9 +1,6 @@ // // Created by Jacopo Uggeri on 16/08/2025. // - -#ifndef SANDBOX2D_VEC2_H -#define SANDBOX2D_VEC2_H #pragma once #include #include @@ -69,6 +66,4 @@ namespace phys using Vec2i = Vec2; using Vec2f = Vec2; -} - -#endif //SANDBOX2D_VEC2_H \ No newline at end of file +} \ No newline at end of file diff --git a/include/game/resources/Sprite.h b/include/game/resources/Sprite.h new file mode 100644 index 0000000..d73ca8b --- /dev/null +++ b/include/game/resources/Sprite.h @@ -0,0 +1,12 @@ +// +// Created by Jacopo Uggeri on 21/08/2025. +// + +#pragma once +#include + +struct Sprite { + std::string textureName; + int width {16}; + int height {16}; +}; diff --git a/include/game/world/World.h b/include/game/world/World.h new file mode 100644 index 0000000..29e928c --- /dev/null +++ b/include/game/world/World.h @@ -0,0 +1,53 @@ +// +// Created by Jacopo Uggeri on 19/08/2025. +// +#pragma once + +#include "game/Config.h" +#include "game/resources/Sprite.h" +#include + +namespace world { +constexpr int CHUNK_SIZE = 16; +constexpr int WORLD_WIDTH_CHUNKS = 8; +constexpr int WORLD_HEIGHT_CHUNKS = 4; +constexpr phys::Vec2i WORLD_SIZE {WORLD_WIDTH_CHUNKS * CHUNK_SIZE, WORLD_HEIGHT_CHUNKS * CHUNK_SIZE}; +constexpr int SEA_LEVEL = 0; +constexpr int CHUNK_COUNT = WORLD_WIDTH_CHUNKS * WORLD_HEIGHT_CHUNKS; + +} + + +struct Tile { + Sprite sprite; +}; + +struct Chunk { + std::array tiles; + + Tile& getTile(int tileX, int tileY); +}; + +struct World { + std::array chunks; + + void init(); + + Tile& getTileGlobal(int worldX, int worldY); + + Chunk& getChunk(int chunkX, int chunkY); + + [[nodiscard]] static constexpr phys::Vec2i chunkCoords(int chunkIdx) noexcept { + return { chunkIdx % world::WORLD_WIDTH_CHUNKS, chunkIdx / world::WORLD_WIDTH_CHUNKS }; + } + + [[nodiscard]] static constexpr phys::Vec2i tileCoords(int tileIdx) noexcept { + return { tileIdx % world::CHUNK_SIZE, tileIdx / world::CHUNK_SIZE }; + } + + [[nodiscard]] static constexpr phys::Vec2i worldCoords(int chunkIdx, int tileIdx) noexcept { + const phys::Vec2i chunkPos = chunkCoords(chunkIdx); + const phys::Vec2i tilePos = tileCoords(tileIdx); + return chunkPos * world::CHUNK_SIZE + tilePos - world::WORLD_SIZE / 2; // Center the world + } +}; \ No newline at end of file diff --git a/include/platform/sdl/AssetLoader.h b/include/platform/sdl/AssetLoader.h new file mode 100644 index 0000000..0d35f2e --- /dev/null +++ b/include/platform/sdl/AssetLoader.h @@ -0,0 +1,35 @@ +// +// Created by Jacopo Uggeri on 19/08/2025. +// +#pragma once + +#include "platform/sdl/Resource.h" +#include +#include +#include + +class AssetLoader { +public: + void loadTextures(SDL_Renderer* renderer); + void loadFonts(); + [[nodiscard]] const Texture& getTexture(std::string_view textureName) const noexcept; + [[nodiscard]] const Font& getFont(std::string_view fontName) const noexcept; + void clear() noexcept { + textures.clear(); + fonts.clear(); + } + +private: + void loadTexture(std::string_view textureName, SDL_Renderer* renderer); + void loadFont(std::string_view fontName, int size); + + std::unordered_map textures; + std::unordered_map fonts; + +public: + AssetLoader() = default; + ~AssetLoader() = default; + // Prevent copying + AssetLoader(const AssetLoader&) = delete; + AssetLoader& operator=(const AssetLoader&) = delete; +}; \ No newline at end of file diff --git a/include/platform/sdl/GraphicsDevice.h b/include/platform/sdl/GraphicsDevice.h new file mode 100644 index 0000000..4de02d3 --- /dev/null +++ b/include/platform/sdl/GraphicsDevice.h @@ -0,0 +1,32 @@ +// +// Created by Jacopo Uggeri on 20/08/2025. +// +#pragma once + +#include "game/resources/Sprite.h" +#include "platform/sdl/AssetLoader.h" +#include "platform/sdl/SDLContext.h" +#include +#include +#include + +class GraphicsDevice final { +public: + bool init(int winW, int winH, std::string_view windowTitle); + void beginFrame() const; + void endFrame() const; + void drawTexture(const Sprite &sprite, float screenX, float screenY) const; + void drawText(std::string_view text, float x, float y) const; + void drawOverlay(uint8_t r, uint8_t g, uint8_t b, uint8_t a) const; + std::pair getWindowSize() const; + +private: + SDLContext sdlContext_ {}; + SDL_Window* window_ = nullptr; + SDL_Renderer* renderer_ = nullptr; + AssetLoader assetLoader_ {}; + bool vsyncEnabled_ = true; + +public: + ~GraphicsDevice(); +}; diff --git a/include/platform/sdl/InputSource.h b/include/platform/sdl/InputSource.h new file mode 100644 index 0000000..62aef88 --- /dev/null +++ b/include/platform/sdl/InputSource.h @@ -0,0 +1,15 @@ +// +// Created by Jacopo Uggeri on 20/08/2025. +// +#pragma once + +struct GameState; + +class InputSource final { +public: + bool init(); + void handleEvents(GameState& state); + void handlePlayerInput(GameState& state); + + ~InputSource(); +}; diff --git a/include/platform/sdl/Resource.h b/include/platform/sdl/Resource.h new file mode 100644 index 0000000..b1a89b5 --- /dev/null +++ b/include/platform/sdl/Resource.h @@ -0,0 +1,30 @@ +// +// Created by Jacopo Uggeri on 21/08/2025. +// + +#pragma once + +#include +#include +#include + +template +class Resource { + using ResourcePtr = std::unique_ptr; +public: + explicit Resource(T* resource = nullptr) + : resource_(resource, Deleter) {} + + explicit operator bool() const { return resource_ != nullptr; } + + [[nodiscard]] T* get() const { return resource_.get(); } + + T* operator->() const { return resource_.get(); } + T& operator*() const { return *resource_; } + +private: + ResourcePtr resource_; +}; + +using Texture = Resource; +using Font = Resource; diff --git a/include/platform/sdl/SDLContext.h b/include/platform/sdl/SDLContext.h new file mode 100644 index 0000000..33fa775 --- /dev/null +++ b/include/platform/sdl/SDLContext.h @@ -0,0 +1,46 @@ +// +// Created by Jacopo Uggeri on 21/08/2025. +// + +#pragma once +#include +#include +#include +#include +#include + +class SDLContext { + bool initialized = false; + +public: + SDLContext() { + if (SDL_Init(SDL_INIT_VIDEO) != 0) { + std::cerr << std::format("SDL_Init error: {}\n", SDL_GetError()); + return; + } + if (IMG_Init(IMG_INIT_PNG) == 0) { + std::cerr << std::format("IMG_Init error: {}\n", IMG_GetError()); + return; + } + + if (TTF_Init() != 0) { + std::cerr << std::format("TTF_Init error: {}\n", TTF_GetError()); + return; + } + initialized = true; + } + + ~SDLContext() { + if (initialized) { + TTF_Quit(); + IMG_Quit(); + SDL_Quit(); + } + } + + explicit operator bool() const { return initialized; } + + // Prevent copying + SDLContext(const SDLContext&) = delete; + SDLContext& operator=(const SDLContext&) = delete; +}; \ No newline at end of file diff --git a/src/core/GameConstants.h b/src/core/GameConstants.h deleted file mode 100644 index b35726a..0000000 --- a/src/core/GameConstants.h +++ /dev/null @@ -1,25 +0,0 @@ -// -// Created by Jacopo Uggeri on 15/08/2025. -// - -#ifndef CONST_H -#define CONST_H -#pragma once - -#include "physics/Vec2.h" -#include -#include - -inline constexpr std::string_view GAME_NAME {"Sandbox 2D"}; - -// resources (should move in namespace or a more relevant file later) -inline const std::filesystem::path ASSETS_PATH = "assets"; -inline const std::filesystem::path TEX_PATH = ASSETS_PATH / "textures"; -inline constexpr std::string_view PLAYER_TEX {"bacon.png"}; -inline constexpr std::string_view WALL_TEX {"wall.png"}; - -// graphics (should move in namespace or a more relevant file later) -inline constexpr phys::Vec2i WINDOW_SIZE {1280, 720}; -inline constexpr float DRAW_SCALE = 4.0; - -#endif //CONST_H diff --git a/src/core/GameState.h b/src/core/GameState.h deleted file mode 100644 index af30cb1..0000000 --- a/src/core/GameState.h +++ /dev/null @@ -1,37 +0,0 @@ -// -// Created by Jacopo Uggeri on 15/08/2025. -// - -#ifndef GAMESTATE_H -#define GAMESTATE_H -#pragma once - -#include "GameConstants.h" -#include "resources/TextureManager.h" -#include "physics/Vec2.h" -#include "world/World.h" - - -struct Player { - constexpr static float SPEED = 10.0f; - constexpr static float JUMP_SPEED = 10.0f; - constexpr static phys::Vec2i WORLD_SIZE {WORLD_WIDTH_CHUNKS * CHUNK_SIZE, WORLD_HEIGHT_CHUNKS * CHUNK_SIZE}; - constexpr static phys::Vec2f PLAYER_START_POS {static_cast(WORLD_SIZE) / 2.0f}; - phys::Vec2f pos = PLAYER_START_POS; - phys::Vec2f vel; - Sprite sprite {std::string(PLAYER_TEX)}; - - void set_velocity(phys::Vec2f v); - void move(double deltaSeconds); -}; - -struct GameState { - bool running = true; - bool paused = true; - Player player; - World world; - - void step(double deltaSeconds); -}; - -#endif //GAMESTATE_H diff --git a/src/core/Graphics.cpp b/src/core/Graphics.cpp deleted file mode 100644 index bfaabdb..0000000 --- a/src/core/Graphics.cpp +++ /dev/null @@ -1,171 +0,0 @@ -// -// Created by Jacopo Uggeri on 15/08/2025. -// -#include "GameConstants.h" -#include "GameState.h" -#include "Graphics.h" -#include "resources/TextureManager.h" -#include -#include -#include -#include -#include -#include -#include - -bool Graphics::init(const int winW, const int winH, std::string_view windowTitle) { - if (SDL_Init(SDL_INIT_EVERYTHING) != 0) { - std::cerr << std::format("SDL_Init error: {}\n", SDL_GetError()); - return false; - } - - if (IMG_Init(IMG_INIT_PNG) == 0) { - std::cerr << std::format("IMG_Init error: {}\n", IMG_GetError()); - return false; - } - - if (TTF_Init() != 0) { - std::cerr << std::format("TTF_Init error: {}\n", TTF_GetError()); - return false; - } - - window_ = SDL_CreateWindow(windowTitle.data(), - SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, - winW, winH, - SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE); - - if (!window_) { - std::cerr << std::format("SDL_CreateWindow error: {}\n", SDL_GetError()); - return false; - } - - renderer_ = SDL_CreateRenderer(window_, -1, SDL_RENDERER_ACCELERATED); - if (!renderer_) { - std::cerr << std::format("SDL_CreateRenderer error: {}\n", SDL_GetError()); - return false; - } - - if (SDL_RenderSetVSync(renderer_, 1) != 0){ - std::cerr << std::format("Could not set vsync. Error: {}\n", SDL_GetError()); - vsyncEnabled_ = false; - } - - // Nearest for pixel art - SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "nearest"); - - textureManager_.loadTextures(renderer_); - return true; -} - -void Graphics::cameraFollow(const Player& player, double deltaSeconds) { - const auto dt = static_cast(deltaSeconds); - constexpr float CAMERA_STIFFNESS = 50.0f; - - const phys::Vec2f toPlayer = player.pos - camera_.pos; - const float snapFactor = 1.0f - std::exp(-CAMERA_STIFFNESS * dt); - camera_.pos += toPlayer * snapFactor; -} - - -// Visible fallback (magenta box) if texture missing -static void drawPlaceholder(SDL_Renderer* r, const SDL_FRect& rct) { - const float hw = rct.w/2, hh = rct.h/2; - SDL_SetRenderDrawColor(r, 255, 255, 255, 255); - SDL_RenderFillRectF(r, &rct); - SDL_SetRenderDrawColor(r, 255, 0, 255, 255); - const SDL_FRect q[] = { - {rct.x, rct.y, hw, hh}, {rct.x + hw, rct.y + hh, rct.w - hw, rct.h - hh} - }; - SDL_RenderFillRectsF(r, q, 2); -} - -phys::Vec2f Graphics::toScreenCoords(const phys::Vec2f& v) const { - int windowWidth, windowHeight; - SDL_GetRendererOutputSize(renderer_, &windowWidth, &windowHeight); - const auto windowSize = static_cast(phys::Vec2i{windowWidth, windowHeight}); - return (v - camera_.pos) * TILE_SIZE + (windowSize / 2.0f); -} - -void Graphics::drawSprite(const Sprite& sprite, const phys::Vec2f& pos) const -{ - const SDL_Rect srcRect {0, 0, sprite.width, sprite.height}; - phys::Vec2f screenPos = toScreenCoords(pos); - screenPos -= phys::Vec2f{static_cast(sprite.width) / 2.0f, static_cast(sprite.height) / 2.0f}; - const auto screenX = screenPos.x; - const auto screenY = screenPos.y; - const SDL_FRect destRect {screenX, screenY, (float)sprite.width, (float)sprite.height}; - - if (SDL_Texture* tex = textureManager_.getTexture(sprite.textureName)) { - SDL_RenderCopyF(renderer_, tex, &srcRect, &destRect); - } else { - drawPlaceholder(renderer_, destRect); - } -} - -void Graphics::drawWorld(const GameState& gameState) const { - for (int chunkIdx = 0; chunkIdx < CHUNK_COUNT; ++chunkIdx) { - const auto& chunk = gameState.world.chunks[chunkIdx]; - - for (int tileIdx = 0; tileIdx < CHUNK_SIZE*CHUNK_SIZE; ++tileIdx) { - const auto& tile = chunk.tiles[tileIdx]; - if (tile.sprite.textureName.empty()) continue; - - auto screenPos {static_cast(World::worldCoords(chunkIdx, tileIdx))}; - drawSprite(tile.sprite, screenPos); - } - } -} - -void Graphics::drawText(std::string_view text, float x, float y) const{ - // fps counter - if (framesPerSecond_ > 0) { // Only draw if we have a valid FPS - static TTF_Font* font = nullptr; - if (!font) { - font = TTF_OpenFont("assets/fonts/Hack-Regular.ttf", 16); - if (!font) { - std::cerr << "Failed to load font: " << TTF_GetError() << "\n"; - return; - } - } - - constexpr SDL_Color color {255, 255, 255, 255}; // White color - SDL_Surface* textSurface = TTF_RenderText_Blended(font, std::string(text).c_str(), color); - - if (textSurface) { - if (SDL_Texture* fpsTexture = SDL_CreateTextureFromSurface(renderer_, textSurface)) { - const SDL_FRect fpsRect {x, y, (float)textSurface->w, (float)textSurface->h}; // Position at (10, 10) - SDL_RenderCopyF(renderer_, fpsTexture, nullptr, &fpsRect); - SDL_DestroyTexture(fpsTexture); - } - SDL_FreeSurface(textSurface); - } - } -} - -void Graphics::draw(const GameState& gameState) const { - SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255); // background black - SDL_RenderClear(renderer_); - - drawWorld(gameState); - drawSprite(gameState.player.sprite, gameState.player.pos); - - SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND); - - if (gameState.paused) { - // semi-transparent gray overlay - SDL_SetRenderDrawColor(renderer_, 128, 128, 128, 128); - int w, h; - SDL_GetRendererOutputSize(renderer_, &w, &h); - const SDL_Rect fullscreen = {0, 0, w, h}; - SDL_RenderFillRect(renderer_, &fullscreen); - } - - const std::string fpsStr = std::format("FPS: {:.0f}", framesPerSecond_); - drawText(fpsStr, 10, 10); - drawText(std::format("x: {:.2f}, y: {:.2f}", gameState.player.pos.x, gameState.player.pos.y), 10, 40); - - SDL_RenderPresent(renderer_); - if (!vsyncEnabled_) { - SDL_Delay(5); // small throttle to avoid burning CPU if vsync off - } -} \ No newline at end of file diff --git a/src/core/Graphics.h b/src/core/Graphics.h deleted file mode 100644 index e3559c3..0000000 --- a/src/core/Graphics.h +++ /dev/null @@ -1,60 +0,0 @@ -// -// Created by Jacopo Uggeri on 15/08/2025. -// - -#ifndef WINDOWING_H -#define WINDOWING_H -#pragma once - -#include "GameState.h" -#include "resources/TextureManager.h" -#include -#include -#include -#include - -class Graphics { - SDL_Window* window_ {nullptr}; - SDL_Renderer* renderer_ {nullptr}; - TextureManager textureManager_ {}; - double framesPerSecond_ {}; - bool vsyncEnabled_ {true}; - -public: - Graphics() = default; - ~Graphics() { destroy(); } - - // Delete copy operations - Graphics(const Graphics&) = delete; - Graphics& operator=(const Graphics&) = delete; - - [[nodiscard]] bool init(int winW, int winH, std::string_view windowTitle); - void destroy() noexcept { - if (renderer_) { - SDL_DestroyRenderer(renderer_); - renderer_ = nullptr; - } - if (window_) { - SDL_DestroyWindow(window_); - window_ = nullptr; - } - TTF_Quit(); - IMG_Quit(); - SDL_Quit(); - } - - void draw(const GameState& gameState) const; - void cameraSnap(const Player& player) { camera_.pos = player.pos; } - void cameraFollow(const Player& player, double deltaSeconds); - void setFPS(double fps) { this->framesPerSecond_ = fps; } - -private: - class Camera { public: phys::Vec2f pos {}; } camera_; - - [[nodiscard]] phys::Vec2f toScreenCoords(const phys::Vec2f& v) const; - void drawSprite(const Sprite& sprite, const phys::Vec2f& pos) const; - void drawWorld(const GameState& gameState) const; - void drawText(std::string_view text, float x, float y) const; -}; - -#endif //WINDOWING_H diff --git a/src/core/resources/TextureManager.cpp b/src/core/resources/TextureManager.cpp deleted file mode 100644 index 681e563..0000000 --- a/src/core/resources/TextureManager.cpp +++ /dev/null @@ -1,52 +0,0 @@ -// -// Created by Jacopo Uggeri on 19/08/2025. -// - -#include "../GameConstants.h" -#include "TextureManager.h" -#include -#include -#include -#include -#include - -TextureManager::~TextureManager() { - for (const auto& tex : textures | std::views::values) { - SDL_DestroyTexture(tex); - } -} - -void TextureManager::loadTextures(SDL_Renderer* renderer) -{ - const std::vector essentials = { - PLAYER_TEX, WALL_TEX - }; - for (const auto& tex : essentials) { - loadTexture(tex, renderer); - } -} - -void TextureManager::loadTexture(std::string_view textureName, SDL_Renderer* renderer) { - SDL_Surface* surf = IMG_Load((TEX_PATH / textureName).c_str()); - if (!surf) { - std::cerr << std::format("Failed to load texture with name: {}\n", std::string(textureName)); - return; - } - - SDL_Texture* tex = SDL_CreateTextureFromSurface(renderer, surf); - SDL_FreeSurface(surf); - if (!tex) { - std::cerr << std::format("Failed to create texture with name: {}", std::string(textureName)); - return; - } - - // Cache loaded textures - textures.emplace(textureName, tex); -} - -SDL_Texture* TextureManager::getTexture(std::string_view textureName) const { - if (const auto it = textures.find(std::string(textureName)); it != textures.end()) { - return it->second; - } - return nullptr; -} \ No newline at end of file diff --git a/src/core/resources/TextureManager.h b/src/core/resources/TextureManager.h deleted file mode 100644 index 9e78bd2..0000000 --- a/src/core/resources/TextureManager.h +++ /dev/null @@ -1,40 +0,0 @@ -// -// Created by Jacopo Uggeri on 19/08/2025. -// - -#ifndef SANDBOX2D_SPRITE_H -#define SANDBOX2D_SPRITE_H -#pragma once - -#include -#include -#include -#include - -#include "../GameConstants.h" - -struct Sprite { - std::string textureName; - int width {static_cast(16 * DRAW_SCALE)}; - int height {static_cast(16 * DRAW_SCALE)}; -}; - -class TextureManager { -public: - TextureManager() = default; - ~TextureManager(); - - // Prevent copying - TextureManager(const TextureManager&) = delete; - TextureManager& operator=(const TextureManager&) = delete; - - void loadTextures(SDL_Renderer* renderer); - [[nodiscard]] SDL_Texture* getTexture(std::string_view textureName) const; - -private: - void loadTexture(std::string_view textureName, SDL_Renderer* renderer); - - std::unordered_map textures; -}; - -#endif //SANDBOX2D_SPRITE_H \ No newline at end of file diff --git a/src/core/world/World.cpp b/src/core/world/World.cpp deleted file mode 100644 index 503a6b6..0000000 --- a/src/core/world/World.cpp +++ /dev/null @@ -1,5 +0,0 @@ -// -// Created by Jacopo Uggeri on 19/08/2025. -// - -#include "World.h" \ No newline at end of file diff --git a/src/core/world/World.h b/src/core/world/World.h deleted file mode 100644 index 8ca0d2c..0000000 --- a/src/core/world/World.h +++ /dev/null @@ -1,88 +0,0 @@ -// -// Created by Jacopo Uggeri on 19/08/2025. -// - -#ifndef SANDBOX2D_WORLD_H -#define SANDBOX2D_WORLD_H -#pragma once - -#include "../GameConstants.h" -#include "../resources/TextureManager.h" -#include -#include - -constexpr int TILE_SIZE = 16 * DRAW_SCALE; -constexpr int CHUNK_SIZE = 16; -constexpr int WORLD_WIDTH_CHUNKS = 8; -constexpr int WORLD_HEIGHT_CHUNKS = 4; -constexpr int SEA_LEVEL = WORLD_HEIGHT_CHUNKS * CHUNK_SIZE / 2; -constexpr int CHUNK_COUNT = WORLD_WIDTH_CHUNKS * WORLD_HEIGHT_CHUNKS; - -struct Tile { - Sprite sprite; -}; - -struct Chunk { - std::array tiles; - - Tile& getTile(int tileX, int tileY) { - if (tileX < 0 || tileX >= CHUNK_SIZE || tileY < 0 || tileY >= CHUNK_SIZE) { - std::cerr << "Invalid tile access: (" << tileX << ", " << tileY << ")" << std::endl; - throw std::out_of_range("Invalid tile access"); - } - return tiles[tileY*CHUNK_SIZE + tileX]; - } -}; - -struct World { - std::array chunks; - - void init() { - for (int chunkIdx = 0; chunkIdx < CHUNK_COUNT; ++chunkIdx) { - auto& chunk = chunks[chunkIdx]; - - for (int tileIdx = 0; tileIdx < CHUNK_SIZE*CHUNK_SIZE; ++tileIdx) { - auto& tile = chunk.tiles[tileIdx]; - - auto [worldX, worldY] = worldCoords(chunkIdx, tileIdx); - if (worldY > SEA_LEVEL) { - tile.sprite.textureName = std::string(WALL_TEX); - } - } - } - } - - Tile& getTileGlobal(int worldX, int worldY) { - const int chunkX = worldX / CHUNK_SIZE; - const int chunkY = worldY / CHUNK_SIZE; - const int tileX = worldX % CHUNK_SIZE; - const int tileY = worldY % CHUNK_SIZE; - return getChunk(chunkX, chunkY).getTile(tileX, tileY); - } - - Chunk& getChunk(int chunkX, int chunkY) { - if (chunkX < 0 || chunkX >= WORLD_WIDTH_CHUNKS || chunkY < 0 || chunkY >= WORLD_HEIGHT_CHUNKS) { - std::cerr << "Invalid chunk access: (" << chunkX << ", " << chunkY << ")" << std::endl; - throw std::out_of_range("Invalid chunk access"); - } - return chunks[chunkY*WORLD_WIDTH_CHUNKS + chunkX]; - } - - [[nodiscard]] static constexpr phys::Vec2i chunkCoords(int chunkIdx) noexcept { - return { chunkIdx % WORLD_WIDTH_CHUNKS, chunkIdx / WORLD_WIDTH_CHUNKS }; - } - - [[nodiscard]] static constexpr phys::Vec2i tileCoords(int tileIdx) noexcept { - return { tileIdx % CHUNK_SIZE, tileIdx / CHUNK_SIZE }; - } - - [[nodiscard]] static constexpr phys::Vec2i worldCoords(int chunkIdx, int tileIdx) noexcept { - auto [chunkX, chunkY] = chunkCoords(chunkIdx); - auto [tileX, tileY] = tileCoords(tileIdx); - return { chunkX * CHUNK_SIZE + tileX, - chunkY * CHUNK_SIZE + tileY }; - } -}; - - -#endif //SANDBOX2D_WORLD_H \ No newline at end of file diff --git a/src/game/Game.cpp b/src/game/Game.cpp new file mode 100644 index 0000000..da28ddf --- /dev/null +++ b/src/game/Game.cpp @@ -0,0 +1,62 @@ +// +// Created by Jacopo Uggeri on 15/08/2025. +// + +#include "game/Game.h" +#include "game/GameState.h" + +#include + +bool Game::init() { + if (!renderer_.init( + config::graphics::WINDOW_SIZE.x, + config::graphics::WINDOW_SIZE.y, + config::GAME_NAME)) { + return false; + } + if (!inputManager_.init()) { + return false; + } + gameState_.world.init(); + return true; +} + +void Game::step(double deltaSeconds) { + gameState_.player.move(deltaSeconds); +} + +double getSecondsNow() { + const auto now = std::chrono::steady_clock::now(); + const auto duration = now.time_since_epoch(); + return std::chrono::duration_cast>(duration).count(); +} + +void Game::loop() { + double lastTime = getSecondsNow(); + double accumulator = 0.0; + + while (gameState_.running) { + constexpr double FIXED_TIMESTEP = 1.0 / 60.0; // 60Hz physics + constexpr double MIN_DELTA_SECONDS = 1.0 / 500.0; + constexpr double MAX_DELTA_SECONDS = 0.25; + + const double currentTime = getSecondsNow(); + double deltaSeconds = currentTime - lastTime; + lastTime = currentTime; + + deltaSeconds = std::clamp(deltaSeconds, MIN_DELTA_SECONDS, MAX_DELTA_SECONDS); + accumulator += deltaSeconds; + + inputManager_.handleEvents(gameState_); + + while (accumulator >= FIXED_TIMESTEP) { + if (gameState_.unpaused()) { + step(FIXED_TIMESTEP); // Always pass fixed timestep to physics + ++gameState_.gameTicks; + } + accumulator -= FIXED_TIMESTEP; + } + + renderer_.render(gameState_, deltaSeconds); + } +} \ No newline at end of file diff --git a/src/core/GameState.cpp b/src/game/Player.cpp similarity index 56% rename from src/core/GameState.cpp rename to src/game/Player.cpp index 31f5558..709068c 100644 --- a/src/core/GameState.cpp +++ b/src/game/Player.cpp @@ -1,8 +1,9 @@ // -// Created by Jacopo Uggeri on 15/08/2025. +// Created by Jacopo Uggeri on 20/08/2025. // -#include "GameState.h" +#include "game/Player.h" +#include "game/physics/Vec2.h" void Player::set_velocity(phys::Vec2f v) { vel = v; @@ -11,8 +12,4 @@ void Player::set_velocity(phys::Vec2f v) { void Player::move(double deltaSeconds) { const auto dt = static_cast(deltaSeconds); pos += vel * SPEED * dt; -} - -void GameState::step(double deltaSeconds) { - player.move(deltaSeconds); -} +} \ No newline at end of file diff --git a/src/game/graphics/Renderer.cpp b/src/game/graphics/Renderer.cpp new file mode 100644 index 0000000..623542d --- /dev/null +++ b/src/game/graphics/Renderer.cpp @@ -0,0 +1,71 @@ +// +// Created by Jacopo Uggeri on 21/08/2025. +// + +#include "game/graphics/Renderer.h" + +bool Renderer::init(int winW, int winH, std::string_view windowTitle) { + if (!graphicsDevice_.init(winW, winH, windowTitle)) return false; + return true; +} + +void Renderer::drawWorld(const World& world) const { + for (int chunkIdx = 0; chunkIdx < world::CHUNK_COUNT; ++chunkIdx) { + const auto& chunk = world.chunks[chunkIdx]; + + for (int tileIdx = 0; tileIdx < world::CHUNK_SIZE* world::CHUNK_SIZE; ++tileIdx) { + const auto& tile = chunk.tiles[tileIdx]; + if (tile.sprite.textureName.empty()) continue; + + auto worldPos {static_cast(World::worldCoords(chunkIdx, tileIdx))}; + drawSprite(tile.sprite, worldPos); + } + } +} + +void Renderer::cameraSnap(phys::Vec2f pos) { + camera_.pos = pos; +} + +void Renderer::cameraFollow(phys::Vec2f pos, double deltaSeconds) { + const auto dt = static_cast(deltaSeconds); + constexpr float CAMERA_STIFFNESS = 50.0f; + + const phys::Vec2f toDestination = pos - camera_.pos; + const float snapFactor = 1.0f - std::exp(-CAMERA_STIFFNESS * dt); + camera_.pos += toDestination * snapFactor; +} + +phys::Vec2f Renderer::toScreenCoords(const phys::Vec2f &pos) const { + const auto [windowWidth, windowHeight] = graphicsDevice_.getWindowSize(); + const auto windowSize = static_cast(phys::Vec2i{windowWidth, windowHeight}); + return (pos - camera_.pos) * config::graphics::TILE_SIZE * config::graphics::DRAW_SCALE + (windowSize / 2.0f); +} + +void Renderer::drawDebugInfo(const GameState &gameState, double deltaSeconds) const { + graphicsDevice_.drawText(std::format("FPS: {:.0f}", 1.0 / deltaSeconds), 10, 10); + graphicsDevice_.drawText(std::format("x: {:.2f}, y: {:.2f}", gameState.player.pos.x, gameState.player.pos.y), 10, 40); + graphicsDevice_.drawText(std::format("World Size: {}, {}", world::WORLD_SIZE.x, world::WORLD_SIZE.y), 10, 70); + graphicsDevice_.drawText(std::format("Game ticks: {}", gameState.gameTicks), 10, 100); +} + +void Renderer::render(const GameState& gameState, double deltaSeconds) { + if (gameState.unpaused()) cameraFollow(gameState.player.pos, deltaSeconds); + graphicsDevice_.beginFrame(); + + drawWorld(gameState.world); + drawSprite(gameState.player.sprite, gameState.player.pos); + + // semi-transparent gray overlay + if (gameState.paused) graphicsDevice_.drawOverlay(128, 128, 128, 128); + // Diagnostics + if (gameState.debugMode) { + drawDebugInfo(gameState, deltaSeconds); + } + graphicsDevice_.endFrame(); +} + +void Renderer::drawSprite(const Sprite &sprite, phys::Vec2f pos) const { + auto [screenX, screenY] = toScreenCoords(pos); + graphicsDevice_.drawTexture(sprite, screenX, screenY); +} \ No newline at end of file diff --git a/src/game/input/InputManager.cpp b/src/game/input/InputManager.cpp new file mode 100644 index 0000000..c31a7e9 --- /dev/null +++ b/src/game/input/InputManager.cpp @@ -0,0 +1,14 @@ +// +// Created by Jacopo Uggeri on 21/08/2025. +// + +#include "game/input/InputManager.h" + +bool InputManager::init() { + return inputSource_.init(); +} + +void InputManager::handleEvents(GameState &state) { + inputSource_.handleEvents(state); + inputSource_.handlePlayerInput(state); +} \ No newline at end of file diff --git a/src/game/world/World.cpp b/src/game/world/World.cpp new file mode 100644 index 0000000..ac7dd3e --- /dev/null +++ b/src/game/world/World.cpp @@ -0,0 +1,45 @@ +// +// Created by Jacopo Uggeri on 19/08/2025. +// + +#include "../../../include/game/world/World.h" +#include + +Tile& Chunk::getTile(int tileX, int tileY) { + if (tileX < 0 || tileX >= world::CHUNK_SIZE || tileY < 0 || tileY >= world::CHUNK_SIZE) { + std::cerr << "Invalid tile access: (" << tileX << ", " << tileY << ")" << std::endl; + throw std::out_of_range("Invalid tile access"); + } + return tiles[tileY* world::CHUNK_SIZE + tileX]; +} + +void World::init() { + for (int chunkIdx = 0; chunkIdx < world::CHUNK_COUNT; ++chunkIdx) { + auto& chunk = chunks[chunkIdx]; + + for (int tileIdx = 0; tileIdx < world::CHUNK_SIZE* world::CHUNK_SIZE; ++tileIdx) { + auto& tile = chunk.tiles[tileIdx]; + + auto [worldX, worldY] = worldCoords(chunkIdx, tileIdx); + if (worldY > world::SEA_LEVEL) { + tile.sprite.textureName = std::string(config::resources::WALL_TEX); + } + } + } +} + +Chunk& World::getChunk(int chunkX, int chunkY) { + if (chunkX < 0 || chunkX >= world::WORLD_WIDTH_CHUNKS || chunkY < 0 || chunkY >= world::WORLD_HEIGHT_CHUNKS) { + std::cerr << "Invalid chunk access: (" << chunkX << ", " << chunkY << ")" << std::endl; + throw std::out_of_range("Invalid chunk access"); + } + return chunks[chunkY* world::WORLD_WIDTH_CHUNKS + chunkX]; +} + +Tile& World::getTileGlobal(int worldX, int worldY) { + const int chunkX = worldX / world::CHUNK_SIZE; + const int chunkY = worldY / world::CHUNK_SIZE; + const int tileX = worldX % world::CHUNK_SIZE; + const int tileY = worldY % world::CHUNK_SIZE; + return getChunk(chunkX, chunkY).getTile(tileX, tileY); +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index 591250a..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include "core/GameConstants.h" -#include "core/GameState.h" -#include "core/ioevents.h" -#include "core/Graphics.h" -#include -#include - -void loop(GameState& gameState, Graphics& graphics) { - uint64_t lastStep = SDL_GetPerformanceCounter(); - const uint64_t perfFreq = SDL_GetPerformanceFrequency(); - int frameCounter = 0; - SDL_Event e; - - // Temporary hack to make the camera center on the player from the start - graphics.cameraSnap(gameState.player); - - while (gameState.running) { - const uint64_t frameStart = SDL_GetPerformanceCounter(); - handleEvents(gameState, &e); - - const auto elapsedSeconds = static_cast(frameStart - lastStep); - const double rawDeltaSeconds = elapsedSeconds / static_cast(perfFreq); - const double deltaSeconds = std::clamp(rawDeltaSeconds, 1.0 / 500.0, 1.0); - - if (!gameState.paused) { - handlePlayerInput(gameState); - gameState.step(deltaSeconds); - graphics.cameraFollow(gameState.player, deltaSeconds); - } - lastStep = frameStart; - - const double fps = 1.0 / deltaSeconds; - if (constexpr int FPS_UPDATE_FRAMES = 10; frameCounter % FPS_UPDATE_FRAMES == 0) graphics.setFPS(fps); - graphics.draw(gameState); - frameCounter++; - } -} - -int main() { - GameState gameState {}; - Graphics graphics; - - if (!graphics.init(WINDOW_SIZE.x, WINDOW_SIZE.y, GAME_NAME)) { - return EXIT_FAILURE; - } - - gameState.world.init(); - loop(gameState, graphics); - return EXIT_SUCCESS; -} \ No newline at end of file diff --git a/src/platform/main.cpp b/src/platform/main.cpp new file mode 100644 index 0000000..7213ff7 --- /dev/null +++ b/src/platform/main.cpp @@ -0,0 +1,9 @@ +#include "game/Game.h" + +int main() { + Game game {}; + if (!game.init()) return EXIT_FAILURE; + game.loop(); + SDL_Quit(); + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/src/platform/sdl/AssetLoader.cpp b/src/platform/sdl/AssetLoader.cpp new file mode 100644 index 0000000..5e4dd0d --- /dev/null +++ b/src/platform/sdl/AssetLoader.cpp @@ -0,0 +1,73 @@ +// +// Created by Jacopo Uggeri on 19/08/2025. +// + +#include "platform/sdl/AssetLoader.h" +#include "game/Config.h" + +#include +#include +#include +#include +#include +#include + +void AssetLoader::loadTextures(SDL_Renderer* renderer) +{ + constexpr std::array essentials = { + config::resources::PLAYER_TEX, config::resources::WALL_TEX + }; + for (const auto& tex : essentials) { + loadTexture(tex, renderer); + } +} + +void AssetLoader::loadTexture(std::string_view textureName, SDL_Renderer* renderer) { + SDL_Surface* surf = IMG_Load(std::string(config::resources::TEX_PATH / textureName).c_str()); + if (!surf) { + std::cerr << std::format("Failed to load texture '{}': {}\n", textureName, IMG_GetError()); + return; + } + + SDL_Texture* tex = SDL_CreateTextureFromSurface(renderer, surf); + SDL_FreeSurface(surf); + if (!tex) { + std::cerr << std::format("Failed to create texture '{}': {}\n", textureName, SDL_GetError()); + return; + } + // Cache loaded textures + textures.emplace(textureName, tex); +} + +const Texture& AssetLoader::getTexture(std::string_view textureName) const noexcept { + static Texture emptyTexture; + if (const auto it = textures.find(std::string(textureName)); it != textures.end()) { + return it->second; + } + return emptyTexture; +} + +void AssetLoader::loadFonts() { + constexpr std::array essentials = {config::resources::FONT}; + for (const auto& font : essentials) { + constexpr int SIZE = 16; + loadFont(font, SIZE); + } +} + +void AssetLoader::loadFont(std::string_view fontName, int size) { + TTF_Font* font = TTF_OpenFont(std::string(config::resources::FONT_PATH / fontName).c_str(), size); + if (!font) { + std::cerr << "Failed to load font: " << TTF_GetError() << "\n"; + return; + } + fonts.emplace(fontName, font); +} + +const Font& AssetLoader::getFont(std::string_view fontName) const noexcept { + static Font emptyFont; + if (const auto it = fonts.find(std::string(fontName)); it != fonts.end()) { + return it->second; + } + return emptyFont; +} \ No newline at end of file diff --git a/src/platform/sdl/GraphicsDevice.cpp b/src/platform/sdl/GraphicsDevice.cpp new file mode 100644 index 0000000..643cc7b --- /dev/null +++ b/src/platform/sdl/GraphicsDevice.cpp @@ -0,0 +1,127 @@ +// +// Created by Jacopo Uggeri on 21/08/2025. +// +#include "platform/sdl/GraphicsDevice.h" + +#include "game/Config.h" +#include "game/resources/Sprite.h" + +#include +#include +#include +#include +#include + +bool GraphicsDevice::init(int winW, int winH, std::string_view windowTitle) { + if (!sdlContext_) return false; + window_ = SDL_CreateWindow(std::string(windowTitle).c_str(), + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + winW, winH, + SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE); + + if (!window_) { + std::cerr << std::format("SDL_CreateWindow error: {}\n", SDL_GetError()); + return false; + } + + renderer_ = SDL_CreateRenderer(window_, -1, SDL_RENDERER_ACCELERATED); + if (!renderer_) { + std::cerr << std::format("SDL_CreateRenderer error: {}\n", SDL_GetError()); + return false; + } + + if (SDL_RenderSetVSync(renderer_, 1) != 0){ + std::cerr << std::format("Could not set vsync. Error: {}\n", SDL_GetError()); + vsyncEnabled_ = false; + } + + // Nearest for pixel art + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "nearest"); + assetLoader_.loadTextures(renderer_); + assetLoader_.loadFonts(); + return true; +} + +void GraphicsDevice::beginFrame() const { + SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255); // background black + SDL_RenderClear(renderer_); + SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND); +} + +void GraphicsDevice::endFrame() const { + SDL_RenderPresent(renderer_); + if (!vsyncEnabled_) { + SDL_Delay(5); // small throttle to avoid burning CPU if vsync off + } +} + +// Visible fallback (magenta box) if texture missing +static void drawPlaceholder(SDL_Renderer* r, const SDL_FRect& rct) { + const float hw = rct.w/2, hh = rct.h/2; + SDL_SetRenderDrawColor(r, 255, 255, 255, 255); + SDL_RenderFillRectF(r, &rct); + SDL_SetRenderDrawColor(r, 255, 0, 255, 255); + const SDL_FRect q[] = { + {rct.x, rct.y, hw, hh}, {rct.x + hw, rct.y + hh, rct.w - hw, rct.h - hh} + }; + SDL_RenderFillRectsF(r, q, 2); +} + +void GraphicsDevice::drawText(std::string_view text, float x, float y) const { + constexpr SDL_Color color {255, 255, 255, 255}; + TTF_Font* font = assetLoader_.getFont(config::resources::FONT).get(); + if (!font) { + std::cerr << "Font not loaded: " << std::string(config::resources::FONT) << "\n"; + return; + } + const std::string textStr{text}; + if (const auto textSurface = TTF_RenderText_Blended(font, textStr.c_str(), color)) { + if (SDL_Texture* textTexture = SDL_CreateTextureFromSurface(renderer_, textSurface)) { + const SDL_FRect textRect {x, y, static_cast(textSurface->w), static_cast(textSurface->h)}; + SDL_RenderCopyF(renderer_, textTexture, nullptr, &textRect); + SDL_DestroyTexture(textTexture); + } + SDL_FreeSurface(textSurface); + } +} + +void GraphicsDevice::drawOverlay(uint8_t r, uint8_t g, uint8_t b, uint8_t a) const { + SDL_SetRenderDrawColor(renderer_, r, g, b, a); + int w, h; + SDL_GetRendererOutputSize(renderer_, &w, &h); + const SDL_Rect fullscreen = {0, 0, w, h}; + SDL_RenderFillRect(renderer_, &fullscreen); +} + +std::pair GraphicsDevice::getWindowSize() const { + int windowWidth, windowHeight; + SDL_GetRendererOutputSize(renderer_, &windowWidth, &windowHeight); + return {windowWidth, windowHeight}; +} + +GraphicsDevice::~GraphicsDevice() { + assetLoader_.clear(); + if (renderer_) { + SDL_DestroyRenderer(renderer_); + renderer_ = nullptr; + } + if (window_) { + SDL_DestroyWindow(window_); + window_ = nullptr; + } +} + +void GraphicsDevice::drawTexture(const Sprite &sprite, float screenX, float screenY) const { + const Texture& texture = assetLoader_.getTexture(sprite.textureName); + const SDL_Rect srcRect {0, 0, sprite.width, sprite.height}; + const float scaledWidth = sprite.width * config::graphics::DRAW_SCALE; + const float scaledHeight = sprite.height * config::graphics::DRAW_SCALE; + screenX -= scaledWidth / 2.0f; + screenY -= scaledHeight / 2.0f; + const SDL_FRect destRect {screenX, screenY, scaledWidth, scaledHeight}; + if (SDL_Texture* tex = texture.get()) { + SDL_RenderCopyF(renderer_, tex, &srcRect, &destRect); + } else { + drawPlaceholder(renderer_, destRect); + } +} diff --git a/src/core/ioevents.h b/src/platform/sdl/InputSource.cpp similarity index 60% rename from src/core/ioevents.h rename to src/platform/sdl/InputSource.cpp index 822a33a..e009884 100644 --- a/src/core/ioevents.h +++ b/src/platform/sdl/InputSource.cpp @@ -1,16 +1,24 @@ // -// Created by Jacopo Uggeri on 15/08/2025. +// Created by Jacopo Uggeri on 21/08/2025. // -#ifndef IOEVENTS_H -#define IOEVENTS_H -#pragma once -#include "GameState.h" -#include +#include "platform/sdl/InputSource.h" +#include "game/GameState.h" + +#include #include #include -inline void onKeyDown(GameState& state, const SDL_Keysym* keysym) { +bool InputSource::init() { + if (SDL_InitSubSystem(SDL_INIT_EVENTS) != 0) { + std::cerr << std::format("SDL_INIT_EVENTS error: {}\n", SDL_GetError()); + return false; + } + return true; +} + + +void onKeyDown(GameState &state, const SDL_Keysym *keysym) { switch (keysym->sym) { case SDLK_q: // quit std::cout << std::format("Quit {}\n", state.paused); @@ -24,7 +32,7 @@ inline void onKeyDown(GameState& state, const SDL_Keysym* keysym) { } } -inline void onMouseButtonDown(GameState& gameState, const SDL_MouseButtonEvent* e) { +void onMouseButtonDown(GameState& gameState, const SDL_MouseButtonEvent* e) { if (e->button == SDL_BUTTON_LEFT && e->state == SDL_PRESSED) { const int mx = e->x; const int my = e->y; @@ -32,7 +40,8 @@ inline void onMouseButtonDown(GameState& gameState, const SDL_MouseButtonEvent* } } -inline void onMouseMotion(GameState& gameState, const SDL_MouseMotionEvent* e) { +void onMouseMotion(GameState& gameState, const SDL_MouseMotionEvent* e) +{ if (e->state & SDL_BUTTON_LMASK) { const int mx = e->x; const int my = e->y; @@ -40,27 +49,29 @@ inline void onMouseMotion(GameState& gameState, const SDL_MouseMotionEvent* e) { } } -inline void handleEvents(GameState& state, SDL_Event* e) { - while (SDL_PollEvent(e)) { - switch (e->type) { +void InputSource::handleEvents(GameState &state) { + SDL_Event e; + SDL_Event* ePtr = &e; + while (SDL_PollEvent(ePtr)) { + switch (ePtr->type) { case SDL_QUIT: state.running = false; break; case SDL_KEYDOWN: - onKeyDown(state, &e->key.keysym); + onKeyDown(state, &ePtr->key.keysym); break; case SDL_MOUSEBUTTONDOWN: - onMouseButtonDown(state, &e->button); + onMouseButtonDown(state, &ePtr->button); break; case SDL_MOUSEMOTION: - onMouseMotion(state, &e->motion); + onMouseMotion(state, &ePtr->motion); break; default: ; } } } -inline void handlePlayerInput(GameState& state) { +void InputSource::handlePlayerInput(GameState &state) { const Uint8* keyState = SDL_GetKeyboardState(nullptr); float dx = 0, dy = 0; @@ -78,4 +89,6 @@ inline void handlePlayerInput(GameState& state) { state.player.set_velocity({dx , dy}); } -#endif //IOEVENTS_H +InputSource::~InputSource() { + SDL_QuitSubSystem(SDL_INIT_EVENTS); +} \ No newline at end of file