diff --git a/CMakeLists.txt b/CMakeLists.txt index 18317c9..51a7af8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,13 @@ FetchContent_Declare( GIT_TAG release-3.2.4 ) +# Download SDL3_ttf from source +FetchContent_Declare( + SDL3_ttf + GIT_REPOSITORY https://github.com/libsdl-org/SDL_ttf.git + GIT_TAG release-3.2.2 +) + # Work around PipeWire API mismatch on some Linux distributions. # Disable PipeWire backend in SDL; PulseAudio/ALSA backends remain available. set(SDL_PIPEWIRE OFF CACHE BOOL "Disable SDL PipeWire backend" FORCE) @@ -29,6 +36,7 @@ set(SDL_PIPEWIRE_SHARED OFF CACHE BOOL "Disable dynamic PipeWire loading in SDL" # Make SDL libraries available FetchContent_MakeAvailable(SDL3) FetchContent_MakeAvailable(SDL3_image) +FetchContent_MakeAvailable(SDL3_ttf) # Collect all source files from src/ and nested directories file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp") @@ -43,4 +51,4 @@ add_executable(${PROJECT_NAME} ${SOURCES}) target_include_directories(${PROJECT_NAME} PRIVATE include) # Link SDL3 and SDL3_image to the executable -target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3 SDL3_image::SDL3_image) +target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3 SDL3_image::SDL3_image SDL3_ttf::SDL3_ttf) diff --git a/README.md b/README.md index 6c8fa67..4dcc038 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,21 @@ make -j (Za Windows uporabite MSYS2 MINGW64 terminal) +## Delovanje + +### Master Thread (Rendering Thread) +Ta nit/thread ustvari glavno okno in kliče metode za renderiranje. Prav tako obdeluje dogodke (events) in posodablja stanje igre. + +### Game Thread (Slave Thread) +Ta nit/thread izvaja glavno zanko igre, posodablja stanje igre in entitete. Odgovarja za logiko igre, medtem ko master thread skrbi za renderiranje in dogodke. + +### Sinhronizacija +Med tema dvema nitoma se uporablja `std::shared_mutex` za sinhronizacijo dostopa do skupnih virov, kot so entitete v `GameState`. Master thread uporablja `std::shared_lock` za branje entitet med renderiranjem, medtem ko game thread uporablja `std::unique_lock` za posodabljanje entitet. + ## Licenca Vsa izvorna koda (razen kadar je drugače navedeno ali uporabljeno) je licencirana pod "Lesser General Public License v2.1" edino (okrajšano na "LGPL v2.1-only"). Več informacij o licenci najdete v datoteki LICENSE. Vse slike (v direktorijo resources/) so podane pod "Creative Commons Attribution-ShareAlike" (CC BY-SA) licenco. +Font "Roboto" je licenciran pod "SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007" (Na voljo na spletu). ## Avtorske pravice Vse avtorske pravice (copyright) so rezervirane k avtorju te izvorne kode/slik. \ No newline at end of file diff --git a/include/game/player.hpp b/include/game/agame/player.hpp similarity index 70% rename from include/game/player.hpp rename to include/game/agame/player.hpp index 0bfe828..3142239 100644 --- a/include/game/player.hpp +++ b/include/game/agame/player.hpp @@ -1,12 +1,15 @@ #pragma once #include +#include +#include namespace Game::AGame { class Player : public Object::Entity { using Object::Entity::Entity; public: + ~Player() override = default; void start() override; void update() override; }; diff --git a/include/game/gamemanager.hpp b/include/game/gamemanager.hpp new file mode 100644 index 0000000..6351e29 --- /dev/null +++ b/include/game/gamemanager.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace Game { + class GameManager { + public: + GameManager() { LOG("Created GameManager"); }; + DISABLE_COPY_AND_MOVE(GameManager); + ~GameManager() = default; + + // Start the game logic slave thread, which will update the gamestate and entities; Run as jthread + void run(std::stop_token stopToken); + void setTargetUpdatesPerSecond(int target) { mTargetUpdatesPerSecond = target; } + int getTargetUpdatesPerSecond() { return mTargetUpdatesPerSecond; } + private: + int mTargetUpdatesPerSecond = 60; + }; +} \ No newline at end of file diff --git a/include/object/entity.hpp b/include/object/entity.hpp index 2ae1e79..135a507 100644 --- a/include/object/entity.hpp +++ b/include/object/entity.hpp @@ -22,7 +22,7 @@ namespace Game::Object { Entity& operator=(const Entity&); Entity(Entity&&) noexcept; Entity& operator=(Entity&&) noexcept; - ~Entity(); + virtual ~Entity() = 0; virtual void start() = 0; virtual void update() = 0; diff --git a/include/renderer/font.hpp b/include/renderer/font.hpp new file mode 100644 index 0000000..70f6cc0 --- /dev/null +++ b/include/renderer/font.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace Game::Renderer { + class Font : public Texture { + public: + Font(const std::string& path, SDL_Renderer* renderer, int ptSize, std::string id = "noname"); + Font(const Font&); + Font& operator=(const Font&); + DISABLE_MOVE(Font); + ~Font(); + + // Build the texture for the font; Call getSDLTexture() afterwards + void build(SDL_Color color, std::string text); + + SDL_Texture* getSDLTexture(); + std::string getId(); + private: + TTF_Font* mFont; + SDL_Renderer* mRenderer; + }; +} \ No newline at end of file diff --git a/include/renderer/texture.hpp b/include/renderer/texture.hpp index 9a3b9f4..99cd8d7 100644 --- a/include/renderer/texture.hpp +++ b/include/renderer/texture.hpp @@ -8,15 +8,16 @@ namespace Game::Renderer { class Texture { public: + Texture(std::string id = "noname"); Texture(const std::string& path, SDL_Renderer* renderer, std::string id = "noname"); Texture(const Texture&); Texture& operator=(const Texture&); DISABLE_MOVE(Texture); - ~Texture(); + virtual ~Texture(); SDL_Texture* getSDLTexture(); std::string getId(); - private: + protected: SDL_Texture* mTex; std::string mId; }; diff --git a/include/state/gamestate.hpp b/include/state/gamestate.hpp index a5fbb41..af491dd 100644 --- a/include/state/gamestate.hpp +++ b/include/state/gamestate.hpp @@ -4,20 +4,27 @@ #include #include #include +#include namespace Game::State { class GameState { public: static GameState& getInstance() { static GameState instance; return instance; } - // Retrieve a REFERENCE of the entities; DANGEROUS! - std::vector>* getEntitiesRef(); - // Update entity at index, by REFERENCE - Object::Entity* getAtIndex(size_t at); + // Execute work while holding the GameState mutex to keep access thread-safe. + template + void withEntitiesLocked(Fn&& fn) { + std::scoped_lock lock(mMutex); + fn(mEntities); + } + // Update entity at index, by REFERENCE. + Object::Entity* getAtIndex(size_t at); + // Add an entity to the gamestate. void addEntity(std::unique_ptr entity); private: + mutable std::mutex mMutex; // Shared mutex for thread safety std::vector> mEntities; }; } \ No newline at end of file diff --git a/include/window/window.hpp b/include/window/window.hpp index fa0e0f6..3ad98f6 100644 --- a/include/window/window.hpp +++ b/include/window/window.hpp @@ -2,11 +2,13 @@ #include #include +#include #include - +#include #include #include #include +#include namespace Game::Window { class Window { @@ -18,11 +20,17 @@ namespace Game::Window { bool init(int width, int height, const std::string& title); void run(); + void setTargetFPS(int fps) { mTargetFPS = fps; } + int getTargetFPS() { return mTargetFPS; } + Renderer::Renderer* getRenderer() { return &mRenderer; } private: SDL_Window* mWindow; Renderer::Renderer mRenderer; + Game::GameManager mGameManager; + std::jthread mGameThread; bool mRunning; + int mTargetFPS = 60; }; } \ No newline at end of file diff --git a/resources/roboto.ttf b/resources/roboto.ttf new file mode 100644 index 0000000..01656a3 Binary files /dev/null and b/resources/roboto.ttf differ diff --git a/src/game/agame/player.cpp b/src/game/agame/player.cpp new file mode 100644 index 0000000..a5615b9 --- /dev/null +++ b/src/game/agame/player.cpp @@ -0,0 +1,25 @@ +#include + +namespace Game::AGame { + void Player::start() { + LOG("Created the Player"); + + if (mTex && mTex->getId() == "Arial") { + LOG("Player texture is a font"); + // Treat as Font and build it + std::shared_ptr font = std::dynamic_pointer_cast(mTex); + if (font) { + font->build({255, 255, 255, 255}, "Hello, World!"); + } else { + ERROR("Failed to cast texture to font"); + } + } + } + + void Player::update() { + if (!mIsActive) return; + //LOG("Updated Player"); + mTransform.x += 0.5f; // Move right at a constant speed for testing + mTransform.rotation += 1.f; // Rotate clockwise for testing + } +} \ No newline at end of file diff --git a/src/game/gamemanager.cpp b/src/game/gamemanager.cpp new file mode 100644 index 0000000..ab97f0b --- /dev/null +++ b/src/game/gamemanager.cpp @@ -0,0 +1,31 @@ +#include +#include + +namespace Game { + void GameManager::run(std::stop_token stopToken) { + using namespace std::chrono_literals; + LOG("GameManager thread started"); + + while (!stopToken.stop_requested()) { + const int updatesPerSecond = std::max(1, mTargetUpdatesPerSecond); + const auto frameDuration = std::chrono::duration(1.0 / static_cast(updatesPerSecond)); + const auto frameStart = std::chrono::steady_clock::now(); + + try { + State::GameState::getInstance().withEntitiesLocked([](auto& entities) { + for (auto& entity : entities) { + entity->update(); + } + }); + } catch (const std::exception& e) { + ERROR("Exception in GameManager thread: " << e.what()); + } + + const auto elapsed = std::chrono::steady_clock::now() - frameStart; + const auto remaining = frameDuration - elapsed; + if (remaining > 0s) { + std::this_thread::sleep_for(remaining); + } + } + } +} \ No newline at end of file diff --git a/src/game/player.cpp b/src/game/player.cpp deleted file mode 100644 index d487e58..0000000 --- a/src/game/player.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include - -namespace Game::AGame { - void Player::start() { - LOG("Created the Player"); - } - - void Player::update() { - if (!mIsActive) return; - //LOG("Updated Player"); - mTransform.x += 0.5f; // Move right at a constant speed for testing - mTransform.rotation += 1.f; // Rotate clockwise for testing - } -} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 6a2c04c..b86f0f2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,9 +3,10 @@ #include #include #include -#include +#include #include #include +#include using namespace Game; @@ -13,7 +14,9 @@ int main() { Window::Window window = Window::Window(); window.init(1280, 720, "Game Window"); + Object::Transform t1{100.f, 100.f, 0.f, 1.f, 1.f}; State::GameState::getInstance().addEntity(std::make_unique("Player", std::make_shared("../resources/missing_texture.png", window.getRenderer()->getSDLRenderer()), Object::DEFAULT_TRANSFORM)); + State::GameState::getInstance().addEntity(std::make_unique("Player2", std::make_shared("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 128, "Arial"), t1)); window.run(); diff --git a/src/object/entity.cpp b/src/object/entity.cpp index baad982..7e824a3 100644 --- a/src/object/entity.cpp +++ b/src/object/entity.cpp @@ -3,10 +3,8 @@ #include namespace Game::Object { - Entity::~Entity() { - LOG("Destroyed Entity: " << mName); - } - + Entity::~Entity() = default; + Entity::Entity(const Entity& other) : mName(other.mName), mTex(other.mTex), mTransform(other.mTransform), mIsActive(other.mIsActive) { LOG("Copied Entity: " << mName); } diff --git a/src/renderer/font.cpp b/src/renderer/font.cpp new file mode 100644 index 0000000..e378782 --- /dev/null +++ b/src/renderer/font.cpp @@ -0,0 +1,55 @@ +#include + +namespace Game::Renderer { + Font::Font(const std::string& path, SDL_Renderer* renderer, int ptSize, std::string id) + : Texture(id), mFont(nullptr), mRenderer(renderer) { + if (!mRenderer) { + ERROR("You must specify a renderer!"); + return; + } + + mFont = TTF_OpenFont(path.c_str(), ptSize); + if (!mFont) { + ERROR("Error opening font: " << SDL_GetError()); + return; + } + } + + Font::Font(const Font& other) { + this->mTex = other.mTex; + this->mFont = other.mFont; + } + + Font& Font::operator=(const Font& other) { + this->mTex = other.mTex; + this->mFont = other.mFont; + return *this; + } + + Font::~Font() { + if (mFont) + TTF_CloseFont(mFont); + } + + void Font::build(SDL_Color color, std::string text) { + if (!mFont) { return; } + + SDL_Surface* surf = TTF_RenderText_Blended(mFont, text.c_str(), text.size(), color); + if (!surf) { + ERROR("TTF_RenderText_Blended Error: " << SDL_GetError() << " (This object may be unusuable)"); + return; + } + + // Convert to texture + mTex = SDL_CreateTextureFromSurface(mRenderer, surf); + SDL_DestroySurface(surf); + if (!mTex) { + ERROR("SDL_CreateTextureFromSurface Error: " << SDL_GetError() << " (This object may be unusuable)"); + return; + } + + } + + SDL_Texture* Font::getSDLTexture() { return mTex; } + std::string Font::getId() { return mId; } +} \ No newline at end of file diff --git a/src/renderer/renderer.cpp b/src/renderer/renderer.cpp index f7f5628..adb4a9e 100644 --- a/src/renderer/renderer.cpp +++ b/src/renderer/renderer.cpp @@ -39,12 +39,14 @@ namespace Game::Renderer { void Renderer::renderFrame() { mClear(); - // Get gamestate and render the objects here; GameState::getState().objects or something, idk - auto entities = Game::State::GameState::getInstance().getEntitiesRef(); - //LOG("Entity count: " << entities->size()); - - for (auto& entity : *entities) { - entity->render(this); + try { + Game::State::GameState::getInstance().withEntitiesLocked([this](auto& entities) { + for (auto& entity : entities) { + entity->render(this); + } + }); + } catch (const std::exception& e) { + ERROR("Exception while rendering frame: " << e.what()); } mPresent(); diff --git a/src/renderer/texture.cpp b/src/renderer/texture.cpp index 30f660b..3955a50 100644 --- a/src/renderer/texture.cpp +++ b/src/renderer/texture.cpp @@ -1,4 +1,8 @@ #include +#include + +Game::Renderer::Texture::Texture(std::string id) + : mTex(nullptr), mId(std::move(id)) {} Game::Renderer::Texture::Texture(const std::string& path, SDL_Renderer* renderer, std::string id) : mTex(nullptr), mId(id) { diff --git a/src/state/gamestate.cpp b/src/state/gamestate.cpp index 19f38e8..4504fd5 100644 --- a/src/state/gamestate.cpp +++ b/src/state/gamestate.cpp @@ -2,11 +2,8 @@ #include namespace Game::State { - std::vector>* GameState::getEntitiesRef() { - return &mEntities; - } - Object::Entity* GameState::getAtIndex(size_t at) { + std::lock_guard lock(mMutex); // Lock the mutex for thread safety try { return mEntities.at(at).get(); } catch (const std::out_of_range& e) { @@ -16,8 +13,11 @@ namespace Game::State { } void GameState::addEntity(std::unique_ptr entity) { + std::lock_guard lock(mMutex); // Lock the mutex for thread safety mEntities.push_back(std::move(entity)); - GameState::getInstance().getAtIndex(mEntities.size() - 1)->start(); // Call start() on the newly added entity - LOG("Added entity '" << GameState::getInstance().getAtIndex(mEntities.size() - 1)->getName() << "' to GameState"); + + Object::Entity* addedEntity = mEntities.back().get(); + addedEntity->start(); // Initialize the entity immediately after insertion. + LOG("Added entity '" << addedEntity->getName() << "' to GameState"); } } \ No newline at end of file diff --git a/src/window/window.cpp b/src/window/window.cpp index 4623983..5e52a6e 100644 --- a/src/window/window.cpp +++ b/src/window/window.cpp @@ -1,11 +1,18 @@ #include namespace Game::Window { - Window::Window() : mWindow(nullptr), mRenderer(), mRunning(false) { } + Window::Window() : mWindow(nullptr), mRenderer(), mGameManager(), mRunning(false) { } Window::~Window() { mRenderer.destroy(); + // Try to kill the game slave thread + if (mGameThread.joinable()) { + mGameThread.request_stop(); + mGameThread.join(); + LOG("Game thread stopped successfully"); + } + if (mWindow) { SDL_DestroyWindow(mWindow); mWindow = nullptr; @@ -20,6 +27,12 @@ namespace Game::Window { return false; } + if (!TTF_Init()) { + ERROR("Failed to initialize SDL_ttf: " << SDL_GetError()); + SDL_Quit(); + return false; + } + mWindow = SDL_CreateWindow(title.c_str(), width, height, SDL_WINDOW_RESIZABLE); if (!mWindow) { ERROR("Failed to create window: " << SDL_GetError()); @@ -36,6 +49,8 @@ namespace Game::Window { return false; } + mGameThread = std::jthread(std::bind_front(&Game::GameManager::run, &mGameManager)); + mRunning = true; return true; @@ -52,13 +67,14 @@ namespace Game::Window { // Handle other events (e.g., keyboard, mouse) here } + /* auto entities = State::GameState::getInstance().getEntitiesRef(); for (auto& entity : *entities) { entity->update(); - } + }*/ mRenderer.renderFrame(); - SDL_Delay(16); // ~60 FPS target, maybe make dynamic based on avg. frame time - TODO + SDL_Delay(1000 / mTargetFPS); // Delay to cap the frame rate to the target FPS } } } \ No newline at end of file