Nazaj na multithreadanje - fonti
This commit is contained in:
@@ -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)
|
||||
|
||||
12
README.md
12
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.
|
||||
@@ -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;
|
||||
};
|
||||
25
include/game/gamemanager.hpp
Normal file
25
include/game/gamemanager.hpp
Normal 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;
|
||||
};
|
||||
}
|
||||
@@ -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
27
include/renderer/font.hpp
Normal 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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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
BIN
resources/roboto.ttf
Normal file
Binary file not shown.
25
src/game/agame/player.cpp
Normal file
25
src/game/agame/player.cpp
Normal 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
31
src/game/gamemanager.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
#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
55
src/renderer/font.cpp
Normal 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; }
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user