Removed some agame things; Added VSYNC

This commit is contained in:
2026-03-18 08:44:30 +01:00
parent def737316c
commit e4004bbfe8
16 changed files with 354 additions and 77 deletions

View File

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

View File

@@ -1,19 +0,0 @@
#pragma once
#include <object/entity.hpp>
#include <renderer/texture.hpp>
#include <renderer/font.hpp>
#include <object/sound.hpp>
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()
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include <object/ui/uitextbox.hpp>
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);
}
};
}

View File

@@ -11,9 +11,23 @@
#include <game/input.hpp>
#include <unordered_map>
#include <memory>
#include <type_traits>
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<typename T>
static void setSharedData(const std::string& key, T data);
template<typename T>
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<std::string, std::string> mSharedStrings;
static std::unordered_map<std::string, int> mSharedInts;
static std::unordered_map<std::string, float> mSharedFloats;
static std::unordered_map<std::string, bool> mSharedBools;
static GameStateEnum mCurrentGameState;
float mLastDelta = 0.f;
};
template<typename T>
void GameManager::setSharedData(const std::string& key, T data) {
if constexpr (std::is_same_v<T, std::string>) {
mSharedStrings[key] = data;
} else if constexpr (std::is_same_v<T, int>) {
mSharedInts[key] = data;
} else if constexpr (std::is_same_v<T, float>) {
mSharedFloats[key] = data;
} else if constexpr (std::is_same_v<T, bool>) {
mSharedBools[key] = data;
}
}
template<typename T>
T GameManager::getSharedData(const std::string& key) {
if constexpr (std::is_same_v<T, std::string>) {
auto it = mSharedStrings.find(key);
if (it != mSharedStrings.end()) {
return it->second;
} else {
return "";
}
} else if constexpr (std::is_same_v<T, int>) {
auto it = mSharedInts.find(key);
if (it != mSharedInts.end()) {
return it->second;
} else {
return 0;
}
} else if constexpr (std::is_same_v<T, float>) {
auto it = mSharedFloats.find(key);
if (it != mSharedFloats.end()) {
return it->second;
} else {
return 0.0f;
}
} else if constexpr (std::is_same_v<T, bool>) {
auto it = mSharedBools.find(key);
if (it != mSharedBools.end()) {
return it->second;
} else {
return false;
}
}
static_assert(
std::is_same_v<T, std::string> || std::is_same_v<T, int> ||
std::is_same_v<T, float> || std::is_same_v<T, bool>,
"Unsupported type for shared data"
);
return T{};
}
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include <object/entity.hpp>
#include <renderer/font.hpp>
#include <renderer/texture.hpp>
#include <utility>
#include <game/input.hpp>
namespace Game::Object {
class UITextBox : public Entity {
public:
UITextBox(const std::string& name, std::shared_ptr<Renderer::Font> 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<float, float> 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;
};
}

View File

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

View File

@@ -44,4 +44,6 @@
#define PI 3.14159265358979323846f
#define UNIVERSAL_SCALE_COEFFICIENT 0.25f
#define TARGET_FPS 60
#define TARGET_UPDATE_RATE 60
#define TARGET_UPDATE_RATE 120
#define ENABLE_LOW_LATENCY_VSYNC 1
#define VSYNC_FPS_OFFSET 2

View File

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

View File

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

View File

@@ -1,20 +0,0 @@
#include <game/agame/camcontroller.hpp>
#include <window/window.hpp>
#include <cmath>
#include <object/camera.hpp>
#include <state/gamestate.hpp>
#include <game/input.hpp>
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;
}
}

View File

@@ -2,14 +2,16 @@
#include <window/window.hpp>
#include <cmath>
#include <game/input.hpp>
#include <game/gamemanager.hpp>
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();
}

View File

@@ -2,6 +2,8 @@
#include <algorithm>
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<float> 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<std::string, std::string> GameManager::mSharedStrings;
std::unordered_map<std::string, int> GameManager::mSharedInts;
std::unordered_map<std::string, float> GameManager::mSharedFloats;
std::unordered_map<std::string, bool> 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);
}
}

View File

@@ -5,10 +5,10 @@
#include <object/transform.hpp>
#include <game/agame/player.hpp>
#include <game/agame/background.hpp>
#include <game/agame/camcontroller.hpp>
#include <renderer/renderer.hpp>
#include <renderer/texture.hpp>
#include <renderer/font.hpp>
#include <game/agame/sampletextbox.hpp>
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<AGame::CamController>("Camera Controller", nullptr, Object::DEFAULT_TRANSFORM));
State::GameState::getInstance().addEntity(std::make_unique<AGame::SampleTextBox>("Sample Text Box", std::make_shared<Game::Renderer::Font>("../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<AGame::Background>("BG", std::make_shared<Game::Renderer::Texture>("../resources/bgtest.png", window.getRenderer()->getSDLRenderer()), Object::DEFAULT_TRANSFORM));

105
src/object/ui/uitextbox.cpp Normal file
View File

@@ -0,0 +1,105 @@
#include <object/ui/uitextbox.hpp>
namespace Game::Object {
UITextBox::UITextBox(const std::string& name, std::shared_ptr<Renderer::Font> 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<float>(mTex->getWidth()) * mTransform.adjustedScaleX();
mBoxHeight = static_cast<float>(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<SDL_Scancode>(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<char>(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<Renderer::Font>(mTex)->build(color, rendered);
mNeedsTextRefresh = false;
}
}

View File

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

View File

@@ -1,5 +1,7 @@
#include <window/window.hpp>
#include <algorithm>
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<std::chrono::seconds>(now - mLastFPSTime).count();
if (elapsed >= 1) {
int fps = static_cast<int>(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<double>(1.0 / static_cast<double>(capFps));
const auto elapsed = std::chrono::steady_clock::now() - frameStart;
const auto remaining = targetFrameDuration - elapsed;
if (remaining > std::chrono::duration<double>::zero()) {
std::this_thread::sleep_for(remaining);
}
#ifdef DEBUG
const auto frameEnd = std::chrono::steady_clock::now();
const double frameMs = std::chrono::duration<double, std::milli>(frameEnd - frameStart).count();
mTelemetryFrameTimeMsTotal += frameMs;
mTelemetryFrameCount++;
const auto telemetryElapsed = std::chrono::duration_cast<std::chrono::seconds>(frameEnd - mTelemetryStart).count();
if (telemetryElapsed >= 2 && mTelemetryFrameCount > 0) {
const double avgFrameMs = mTelemetryFrameTimeMsTotal / static_cast<double>(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
}
}
}