Nazaj na multithreadanje - fonti

This commit is contained in:
2026-03-12 16:18:51 +01:00
parent 834f0b29c3
commit 74159a6fda
20 changed files with 254 additions and 43 deletions

View File

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

View File

@@ -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.

View File

@@ -1,12 +1,15 @@
#pragma once
#include <object/entity.hpp>
#include <renderer/texture.hpp>
#include <renderer/font.hpp>
namespace Game::AGame {
class Player : public Object::Entity {
using Object::Entity::Entity;
public:
~Player() override = default;
void start() override;
void update() override;
};

View File

@@ -0,0 +1,25 @@
#pragma once
#include <state/gamestate.hpp>
#include <object/entity.hpp>
#include <utils.hpp>
#include <thread>
#include <mutex>
#include <chrono>
#include <functional>
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;
};
}

View File

@@ -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;

27
include/renderer/font.hpp Normal file
View File

@@ -0,0 +1,27 @@
#pragma once
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <utils.hpp>
#include <string>
#include <renderer/texture.hpp>
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;
};
}

View File

@@ -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;
};

View File

@@ -4,20 +4,27 @@
#include <memory>
#include <utils.hpp>
#include <object/entity.hpp>
#include <mutex>
namespace Game::State {
class GameState {
public:
static GameState& getInstance() { static GameState instance; return instance; }
// Retrieve a REFERENCE of the entities; DANGEROUS!
std::vector<std::unique_ptr<Object::Entity>>* 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<typename Fn>
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<Object::Entity> entity);
private:
mutable std::mutex mMutex; // Shared mutex for thread safety
std::vector<std::unique_ptr<Object::Entity>> mEntities;
};
}

View File

@@ -2,11 +2,13 @@
#include <string>
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <iostream>
#include <thread>
#include <utils.hpp>
#include <renderer/renderer.hpp>
#include <state/gamestate.hpp>
#include <game/gamemanager.hpp>
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;
};
}

BIN
resources/roboto.ttf Normal file

Binary file not shown.

25
src/game/agame/player.cpp Normal file
View File

@@ -0,0 +1,25 @@
#include <game/agame/player.hpp>
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<Renderer::Font> font = std::dynamic_pointer_cast<Renderer::Font>(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
}
}

31
src/game/gamemanager.cpp Normal file
View File

@@ -0,0 +1,31 @@
#include <game/gamemanager.hpp>
#include <algorithm>
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<double>(1.0 / static_cast<double>(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);
}
}
}
}

View File

@@ -1,14 +0,0 @@
#include <game/player.hpp>
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
}
}

View File

@@ -3,9 +3,10 @@
#include <state/gamestate.hpp>
#include <object/entity.hpp>
#include <object/transform.hpp>
#include <game/player.hpp>
#include <game/agame/player.hpp>
#include <renderer/renderer.hpp>
#include <renderer/texture.hpp>
#include <renderer/font.hpp>
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<AGame::Player>("Player", std::make_shared<Game::Renderer::Texture>("../resources/missing_texture.png", window.getRenderer()->getSDLRenderer()), Object::DEFAULT_TRANSFORM));
State::GameState::getInstance().addEntity(std::make_unique<AGame::Player>("Player2", std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 128, "Arial"), t1));
window.run();

View File

@@ -3,9 +3,7 @@
#include <renderer/texture.hpp>
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);

55
src/renderer/font.cpp Normal file
View File

@@ -0,0 +1,55 @@
#include <renderer/font.hpp>
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; }
}

View File

@@ -39,13 +39,15 @@ 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) {
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();
}

View File

@@ -1,4 +1,8 @@
#include <renderer/texture.hpp>
#include <utility>
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) {

View File

@@ -2,11 +2,8 @@
#include <iostream>
namespace Game::State {
std::vector<std::unique_ptr<Object::Entity>>* GameState::getEntitiesRef() {
return &mEntities;
}
Object::Entity* GameState::getAtIndex(size_t at) {
std::lock_guard<std::mutex> 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<Object::Entity> entity) {
std::lock_guard<std::mutex> 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");
}
}

View File

@@ -1,11 +1,18 @@
#include <window/window.hpp>
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
}
}
}