From e4004bbfe81f565cbeb975c0f5b38d46d2a57afa Mon Sep 17 00:00:00 2001 From: DcruBro Date: Wed, 18 Mar 2026 08:44:30 +0100 Subject: [PATCH] Removed some agame things; Added VSYNC --- include/game/agame/background.hpp | 6 +- include/game/agame/camcontroller.hpp | 19 ----- include/game/agame/sampletextbox.hpp | 24 ++++++ include/game/gamemanager.hpp | 83 ++++++++++++++++++++- include/object/ui/uitextbox.hpp | 37 ++++++++++ include/renderer/renderer.hpp | 2 + include/utils.hpp | 4 +- include/window/window.hpp | 8 +- src/game/agame/background.cpp | 11 +++ src/game/agame/camcontroller.cpp | 20 ----- src/game/agame/player.cpp | 7 +- src/game/gamemanager.cpp | 37 ++++++---- src/main.cpp | 4 +- src/object/ui/uitextbox.cpp | 105 +++++++++++++++++++++++++++ src/renderer/renderer.cpp | 8 ++ src/window/window.cpp | 56 +++++++++++--- 16 files changed, 354 insertions(+), 77 deletions(-) delete mode 100644 include/game/agame/camcontroller.hpp create mode 100644 include/game/agame/sampletextbox.hpp create mode 100644 include/object/ui/uitextbox.hpp delete mode 100644 src/game/agame/camcontroller.cpp create mode 100644 src/object/ui/uitextbox.cpp diff --git a/include/game/agame/background.hpp b/include/game/agame/background.hpp index 01f5291..2c89631 100644 --- a/include/game/agame/background.hpp +++ b/include/game/agame/background.hpp @@ -7,7 +7,9 @@ namespace Game::AGame { GAME_ENTITY(Background) - private: - Object::Sound mSound; + public: + void onWindowResized(int newWidth, int newHeight) override; + private: + Object::Sound mSound; END_GAME_ENTITY() } \ No newline at end of file diff --git a/include/game/agame/camcontroller.hpp b/include/game/agame/camcontroller.hpp deleted file mode 100644 index 9a7c084..0000000 --- a/include/game/agame/camcontroller.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace Game::AGame { -GAME_ENTITY(CamController) - public: - void onWindowResized(int newWidth, int newHeight) override { - mScreenW = newWidth; - mScreenH = newHeight; - } - - private: - int mScreenW, mScreenH; -END_GAME_ENTITY() -} \ No newline at end of file diff --git a/include/game/agame/sampletextbox.hpp b/include/game/agame/sampletextbox.hpp new file mode 100644 index 0000000..01ea79b --- /dev/null +++ b/include/game/agame/sampletextbox.hpp @@ -0,0 +1,24 @@ +#pragma once +#include + +namespace Game::AGame { + class SampleTextBox : public Object::UITextBox { + using Object::UITextBox::UITextBox; // Inherit constructors + + public: + ~SampleTextBox() override = default; + + void start() override { + // Call the base class start to initialize the text box + mZIndex = 1000; // Ensure it renders on top of most other entities + Object::UITextBox::start(); + setText("Hello, World!"); + + mIsActive = false; + } + void update(float deltaTime) override { + // Call the base class update to handle input and text refreshing + Object::UITextBox::update(deltaTime); + } + }; +} \ No newline at end of file diff --git a/include/game/gamemanager.hpp b/include/game/gamemanager.hpp index b54515d..54fd402 100644 --- a/include/game/gamemanager.hpp +++ b/include/game/gamemanager.hpp @@ -11,9 +11,23 @@ #include #include #include +#include namespace Game { using clock = std::chrono::steady_clock; + + enum class GameStateEnum { + RUNNING, + PAUSED, + STOPPED + }; + + enum class SharedDataType { + STRING, + INT, + FLOAT, + BOOL + }; class GameManager { public: @@ -27,14 +41,77 @@ namespace Game { int getTargetUpdatesPerSecond() { return mTargetUpdatesPerSecond; } float getLastDelta() { return mLastDelta; } - static void setSharedData(const std::string& key, std::string data); - static std::string getSharedData(const std::string& key); - static void removeSharedData(const std::string& key); + template + static void setSharedData(const std::string& key, T data); + template + static T getSharedData(const std::string& key); + static void removeSharedData(const std::string& key, SharedDataType type); + + static GameStateEnum getCurrentGameState() { return mCurrentGameState; } + static void setCurrentGameState(GameStateEnum newState) { mCurrentGameState = newState; } private: int mTargetUpdatesPerSecond = TARGET_UPDATE_RATE; clock::time_point mLastUpdate; static std::unordered_map mSharedStrings; + static std::unordered_map mSharedInts; + static std::unordered_map mSharedFloats; + static std::unordered_map mSharedBools; + static GameStateEnum mCurrentGameState; float mLastDelta = 0.f; }; + + template + void GameManager::setSharedData(const std::string& key, T data) { + if constexpr (std::is_same_v) { + mSharedStrings[key] = data; + } else if constexpr (std::is_same_v) { + mSharedInts[key] = data; + } else if constexpr (std::is_same_v) { + mSharedFloats[key] = data; + } else if constexpr (std::is_same_v) { + mSharedBools[key] = data; + } + } + + template + T GameManager::getSharedData(const std::string& key) { + if constexpr (std::is_same_v) { + auto it = mSharedStrings.find(key); + if (it != mSharedStrings.end()) { + return it->second; + } else { + return ""; + } + } else if constexpr (std::is_same_v) { + auto it = mSharedInts.find(key); + if (it != mSharedInts.end()) { + return it->second; + } else { + return 0; + } + } else if constexpr (std::is_same_v) { + auto it = mSharedFloats.find(key); + if (it != mSharedFloats.end()) { + return it->second; + } else { + return 0.0f; + } + } else if constexpr (std::is_same_v) { + auto it = mSharedBools.find(key); + if (it != mSharedBools.end()) { + return it->second; + } else { + return false; + } + } + + static_assert( + std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v, + "Unsupported type for shared data" + ); + + return T{}; + } } \ No newline at end of file diff --git a/include/object/ui/uitextbox.hpp b/include/object/ui/uitextbox.hpp new file mode 100644 index 0000000..4814789 --- /dev/null +++ b/include/object/ui/uitextbox.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace Game::Object { + class UITextBox : public Entity { + public: + UITextBox(const std::string& name, std::shared_ptr font, const Transform& transform, float x = 0.f, float y = 0.f); + ~UITextBox() override = default; + + void start() override; + void update(float deltaTime) override; + + void setText(const std::string& text); + std::string getText() const; + std::string getValue() const; + bool isFocused() const; + + void setPosition(float x, float y) { mX = x; mY = y; } + std::pair getPosition() const { return {mX, mY}; } + + private: + bool isMouseInsideBox() const; + void refreshVisualText(); + + float mX, mY; + std::string mText; + bool mIsFocused = false; + float mBoxWidth = 0.f; + float mBoxHeight = 0.f; + bool mNeedsTextRefresh = true; + }; +} diff --git a/include/renderer/renderer.hpp b/include/renderer/renderer.hpp index edf55e2..cf5ed34 100644 --- a/include/renderer/renderer.hpp +++ b/include/renderer/renderer.hpp @@ -24,11 +24,13 @@ namespace Game::Renderer { void destroy(); SDL_Renderer* getSDLRenderer() { return mRenderer; } + bool isVSyncEnabled() const { return mVSyncEnabled; } private: void mClear(); void mPresent(); SDL_Renderer* mRenderer; + bool mVSyncEnabled = false; }; } \ No newline at end of file diff --git a/include/utils.hpp b/include/utils.hpp index d8980df..a51c506 100644 --- a/include/utils.hpp +++ b/include/utils.hpp @@ -44,4 +44,6 @@ #define PI 3.14159265358979323846f #define UNIVERSAL_SCALE_COEFFICIENT 0.25f #define TARGET_FPS 60 -#define TARGET_UPDATE_RATE 60 \ No newline at end of file +#define TARGET_UPDATE_RATE 120 +#define ENABLE_LOW_LATENCY_VSYNC 1 +#define VSYNC_FPS_OFFSET 2 \ No newline at end of file diff --git a/include/window/window.hpp b/include/window/window.hpp index 938c1a5..26e6a32 100644 --- a/include/window/window.hpp +++ b/include/window/window.hpp @@ -40,7 +40,11 @@ namespace Game::Window { std::jthread mGameThread; bool mRunning; int mTargetFPS = TARGET_FPS; - size_t mFrameCount = 0; - std::chrono::steady_clock::time_point mLastFPSTime; + int mEffectiveFrameCap = TARGET_FPS; + #if DEBUG + size_t mTelemetryFrameCount = 0; + double mTelemetryFrameTimeMsTotal = 0.0; + #endif + std::chrono::steady_clock::time_point mTelemetryStart = std::chrono::steady_clock::now(); }; } \ No newline at end of file diff --git a/src/game/agame/background.cpp b/src/game/agame/background.cpp index 342cfe2..e84a59e 100644 --- a/src/game/agame/background.cpp +++ b/src/game/agame/background.cpp @@ -17,6 +17,11 @@ namespace Game::AGame { LOG("W: " << w << " H: " << h); + mTransform.x = w / 2.f - (w / 3.f); + mTransform.y = 0.f; + + mTransform.scaleX = 1.f; + mTransform.scaleY = 1.f; mSound.~Sound(); } @@ -38,4 +43,10 @@ namespace Game::AGame { //Object::Camera::getInstance().move(1.f, 0.f); } + + void Background::onWindowResized(int newWidth, int newHeight) { + // Re-center the background on window resize + mTransform.x = newWidth / 2.f - (newWidth / 3.f); + mTransform.y = 0.f; + } } \ No newline at end of file diff --git a/src/game/agame/camcontroller.cpp b/src/game/agame/camcontroller.cpp deleted file mode 100644 index 36cc346..0000000 --- a/src/game/agame/camcontroller.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include -#include -#include -#include -#include -#include - -namespace Game::AGame { - void CamController::start() { - mTex = nullptr; // No texture - SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &mScreenW, &mScreenH); - - mTransform.x = 0.f; - mTransform.y = 0.f; - } - - void CamController::update(float deltaTime) { - if (!mIsActive) return; - } -} \ No newline at end of file diff --git a/src/game/agame/player.cpp b/src/game/agame/player.cpp index a9ef9b0..9a39381 100644 --- a/src/game/agame/player.cpp +++ b/src/game/agame/player.cpp @@ -2,14 +2,16 @@ #include #include #include +#include namespace Game::AGame { void Player::start() { //mSound = Object::Sound("../resources/example.wav", Object::Format::WAV); //mSound.play(); - mZIndex = 100; - + Game::GameManager::setSharedData("gameStage", 1); + Game::GameManager::setSharedData("gameScore", 0); + int w, h; SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &w, &h); @@ -17,7 +19,6 @@ namespace Game::AGame { mTransform.y -= mTex->getHeight() * mTransform.adjustedScaleY() / 2.f; LOG("W: " << w << " H: " << h); - //mSound.~Sound(); } diff --git a/src/game/gamemanager.cpp b/src/game/gamemanager.cpp index 4999bb9..1deb681 100644 --- a/src/game/gamemanager.cpp +++ b/src/game/gamemanager.cpp @@ -2,6 +2,8 @@ #include namespace Game { + GameStateEnum GameManager::mCurrentGameState = GameStateEnum::RUNNING; + void GameManager::run(std::stop_token stopToken) { using namespace std::chrono_literals; LOG("GameManager slave thread started"); @@ -10,6 +12,11 @@ namespace Game { mLastUpdate = clock::now(); // Get the update while (!stopToken.stop_requested()) { + if (mCurrentGameState == GameStateEnum::PAUSED) { + std::this_thread::sleep_for(100ms); + continue; + } + clock::time_point now = clock::now(); std::chrono::duration elapsedDt = now - mLastUpdate; float seconds = elapsedDt.count(); @@ -31,9 +38,12 @@ namespace Game { } mLastDelta = seconds; - mLastUpdate = now; + #ifdef DEBUG + GameManager::setSharedData("lastDelta", mLastDelta); + #endif + const auto elapsed = std::chrono::steady_clock::now() - frameStart; const auto remaining = frameDuration - elapsed; if (remaining > 0s) { @@ -44,20 +54,19 @@ namespace Game { // Statics std::unordered_map GameManager::mSharedStrings; + std::unordered_map GameManager::mSharedInts; + std::unordered_map GameManager::mSharedFloats; + std::unordered_map GameManager::mSharedBools; - void GameManager::setSharedData(const std::string& key, std::string data) { - mSharedStrings[key] = std::move(data); - } - - std::string GameManager::getSharedData(const std::string& key) { - auto it = mSharedStrings.find(key); - if (it != mSharedStrings.end()) { - return it->second; + void GameManager::removeSharedData(const std::string& key, SharedDataType type) { + if (type == SharedDataType::STRING) { + mSharedStrings.erase(key); + } else if (type == SharedDataType::INT) { + mSharedInts.erase(key); + } else if (type == SharedDataType::FLOAT) { + mSharedFloats.erase(key); + } else if (type == SharedDataType::BOOL) { + mSharedBools.erase(key); } - return ""; // Key not found - } - - void GameManager::removeSharedData(const std::string& key) { - mSharedStrings.erase(key); } } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index a53558a..66e4393 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,10 +5,10 @@ #include #include #include -#include #include #include #include +#include using namespace Game; @@ -18,7 +18,7 @@ int main() { Window::Window window = Window::Window(); window.init(1280, 720, "Game Window"); - State::GameState::getInstance().addEntity(std::make_unique("Camera Controller", nullptr, Object::DEFAULT_TRANSFORM)); + State::GameState::getInstance().addEntity(std::make_unique("Sample Text Box", std::make_shared("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 48, "Roboto"), Object::DEFAULT_TRANSFORM, 640.f, 360.f)); //Object::Transform t1{100.f, 100.f, 0.f, 1.f, 1.f}; State::GameState::getInstance().addEntity(std::make_unique("BG", std::make_shared("../resources/bgtest.png", window.getRenderer()->getSDLRenderer()), Object::DEFAULT_TRANSFORM)); diff --git a/src/object/ui/uitextbox.cpp b/src/object/ui/uitextbox.cpp new file mode 100644 index 0000000..ef7b85c --- /dev/null +++ b/src/object/ui/uitextbox.cpp @@ -0,0 +1,105 @@ +#include + +namespace Game::Object { + UITextBox::UITextBox(const std::string& name, std::shared_ptr font, const Transform& transform, float x, float y) + : Entity(name, font, transform), mX(x), mY(y) { } + + void UITextBox::start() { + // Center the text box on the position + mTransform.x -= mTex->getWidth() * mTransform.adjustedScaleX() / 2.f; + mTransform.y -= mTex->getHeight() * mTransform.adjustedScaleY() / 2.f; + + // Keep a stable interaction area even when current text is empty. + mBoxWidth = static_cast(mTex->getWidth()) * mTransform.adjustedScaleX(); + mBoxHeight = static_cast(mTex->getHeight()) * mTransform.adjustedScaleY(); + if (mBoxWidth <= 0.f) mBoxWidth = 180.f; + if (mBoxHeight <= 0.f) mBoxHeight = 36.f; + + refreshVisualText(); + } + + void UITextBox::update(float deltaTime) { + if (!mIsActive) return; + + if (Input::isMouseButtonJustPressed(SDL_BUTTON_LEFT)) { + mIsFocused = isMouseInsideBox(); + mNeedsTextRefresh = true; + } + + if (!mIsFocused) { + if (mNeedsTextRefresh) { + refreshVisualText(); + } + return; + } + + if (Input::isKeyJustPressed(SDL_SCANCODE_BACKSPACE) && !mText.empty()) { + mText.pop_back(); + mNeedsTextRefresh = true; + } + + if (Input::isKeyJustPressed(SDL_SCANCODE_RETURN) || Input::isKeyJustPressed(SDL_SCANCODE_KP_ENTER)) { + mIsFocused = false; + mNeedsTextRefresh = true; + } + + for (int key = 0; key < SDL_SCANCODE_COUNT; ++key) { + SDL_Scancode scancode = static_cast(key); + if (!Input::isKeyJustPressed(scancode)) { + continue; + } + + SDL_Keycode keycode = SDL_GetKeyFromScancode(scancode, SDL_GetModState(), true); + if (keycode >= 32 && keycode <= 126) { + mText.push_back(static_cast(keycode)); + mNeedsTextRefresh = true; + } + } + + if (mNeedsTextRefresh) { + refreshVisualText(); + } + } + + void UITextBox::setText(const std::string& text) { + mText = text; + mNeedsTextRefresh = true; + refreshVisualText(); + } + + std::string UITextBox::getText() const { + return mText; + } + + std::string UITextBox::getValue() const { + return mText; + } + + bool UITextBox::isFocused() const { + return mIsFocused; + } + + bool UITextBox::isMouseInsideBox() const { + const float mouseX = Input::getMouseX(); + const float mouseY = Input::getMouseY(); + + const float left = mTransform.x; + const float right = mTransform.x + mBoxWidth; + const float top = mTransform.y; + const float bottom = mTransform.y + mBoxHeight; + + return mouseX >= left && mouseX <= right && mouseY >= top && mouseY <= bottom; + } + + void UITextBox::refreshVisualText() { + SDL_Color color = mIsFocused ? SDL_Color{255, 240, 180, 255} : SDL_Color{255, 255, 255, 255}; + std::string rendered = mText.empty() ? " " : mText; + if (mIsFocused) { + rendered += "_"; + } + + std::dynamic_pointer_cast(mTex)->build(color, rendered); + mNeedsTextRefresh = false; + } + +} diff --git a/src/renderer/renderer.cpp b/src/renderer/renderer.cpp index 5ae6fc4..7004921 100644 --- a/src/renderer/renderer.cpp +++ b/src/renderer/renderer.cpp @@ -21,12 +21,20 @@ namespace Game::Renderer { } bool Renderer::init(SDL_Window* window) { + // Request VSync before/at renderer setup; some backends honor this hint. + SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1"); + mRenderer = SDL_CreateRenderer(window, nullptr); if (!mRenderer) { std::string errorMsg = "Failed to create renderer: " + std::string(SDL_GetError()); ERROR(errorMsg.c_str()); return false; } + + mVSyncEnabled = SDL_SetRenderVSync(mRenderer, 1); + if (!mVSyncEnabled) { + WARN("VSync could not be enabled, using software frame pacing fallback: " << SDL_GetError()); + } if (!SDL_SetRenderDrawColor(mRenderer, 0, 0, 255, 255)) { ERROR("Failed to set renderer draw color: " << SDL_GetError()); diff --git a/src/window/window.cpp b/src/window/window.cpp index 620f17e..c61ebec 100644 --- a/src/window/window.cpp +++ b/src/window/window.cpp @@ -1,5 +1,7 @@ #include +#include + namespace Game::Window { std::mutex Window::sMutex; @@ -56,6 +58,22 @@ namespace Game::Window { return false; } +#if ENABLE_LOW_LATENCY_VSYNC + if (mRenderer.isVSyncEnabled()) { + const int vsyncCap = std::max(1, mTargetFPS - VSYNC_FPS_OFFSET); + mEffectiveFrameCap = vsyncCap; + LOG("Low-latency VSync mode enabled. Target FPS: " << mTargetFPS << ", cap: " << mEffectiveFrameCap); + } else { + mEffectiveFrameCap = std::max(1, mTargetFPS); + LOG("VSync unavailable, using software cap: " << mEffectiveFrameCap); + } +#else + mEffectiveFrameCap = std::max(1, mTargetFPS); +#endif + + mGameManager.setTargetUpdatesPerSecond(TARGET_UPDATE_RATE); + LOG("Target updates per second: " << mGameManager.getTargetUpdatesPerSecond()); + mGameThread = std::jthread(std::bind_front(&Game::GameManager::run, &mGameManager)); mRunning = true; @@ -66,6 +84,8 @@ namespace Game::Window { void Window::run() { SDL_Event event; while (mRunning) { + const auto frameStart = std::chrono::steady_clock::now(); + SDL_PumpEvents(); while (SDL_PollEvent(&event)) { if (event.type == SDL_EVENT_QUIT) { @@ -90,19 +110,33 @@ namespace Game::Window { } mRenderer.renderFrame(); - SDL_Delay(1000 / mTargetFPS); // Delay to cap the frame rate to the target FPS - // Set the window title to show the current FPS for testing - mFrameCount++; - auto now = std::chrono::steady_clock::now(); - auto elapsed = std::chrono::duration_cast(now - mLastFPSTime).count(); - if (elapsed >= 1) { - int fps = static_cast(mFrameCount / elapsed); - std::string title = "Game Window - FPS: " + std::to_string(fps) + " : Update Time: " + std::to_string(mGameManager.getLastDelta()); - SDL_SetWindowTitle(mWindow, title.c_str()); - mFrameCount = 0; - mLastFPSTime = now; + // Keep rendering slightly below refresh in low-latency mode to reduce queueing/input delay. + const int capFps = std::max(1, mEffectiveFrameCap); + const auto targetFrameDuration = std::chrono::duration(1.0 / static_cast(capFps)); + const auto elapsed = std::chrono::steady_clock::now() - frameStart; + const auto remaining = targetFrameDuration - elapsed; + if (remaining > std::chrono::duration::zero()) { + std::this_thread::sleep_for(remaining); } + + #ifdef DEBUG + const auto frameEnd = std::chrono::steady_clock::now(); + const double frameMs = std::chrono::duration(frameEnd - frameStart).count(); + mTelemetryFrameTimeMsTotal += frameMs; + mTelemetryFrameCount++; + + const auto telemetryElapsed = std::chrono::duration_cast(frameEnd - mTelemetryStart).count(); + if (telemetryElapsed >= 2 && mTelemetryFrameCount > 0) { + const double avgFrameMs = mTelemetryFrameTimeMsTotal / static_cast(mTelemetryFrameCount); + const double avgFps = 1000.0 / std::max(0.001, avgFrameMs); + LOG("Frame telemetry: avg ms=" << avgFrameMs << ", avg fps=" << avgFps << ", cap=" << mEffectiveFrameCap << ", vsync=" << (mRenderer.isVSyncEnabled() ? "on" : "off")); + + mTelemetryFrameTimeMsTotal = 0.0; + mTelemetryFrameCount = 0; + mTelemetryStart = frameEnd; + } + #endif } } } \ No newline at end of file