menu
This commit is contained in:
@@ -4,6 +4,8 @@ project(Letnik3Zadnja)
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
option(FETCH "Fetch SDL dependencies instead of using system-installed packages" OFF)
|
||||
|
||||
# Compile flags
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror -Wno-unused-parameter -O2")
|
||||
|
||||
@@ -17,10 +19,10 @@ add_executable(${PROJECT_NAME} ${SOURCES})
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE include)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Platform-specific dependency handling
|
||||
# Dependency handling
|
||||
# ------------------------------------------------------------
|
||||
|
||||
if(WIN32)
|
||||
if(FETCH OR WIN32)
|
||||
|
||||
# Include FetchContent to download SDL libraries
|
||||
include(FetchContent)
|
||||
|
||||
@@ -12,6 +12,7 @@ GAME_ENTITY(Player)
|
||||
void setGroundTexture(std::shared_ptr<Game::Renderer::Texture> tex);
|
||||
void respawnRandomSea(float landBoundaryX);
|
||||
bool isShipMode() const { return mIsShipMode; }
|
||||
void setShipMode(bool isShip) { mIsShipMode = isShip; } // Set form state for replay
|
||||
void onCollisionEnter(Object::Entity* other) override;
|
||||
private:
|
||||
Object::Sound mSound;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
#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;
|
||||
mIsVisible = false;
|
||||
}
|
||||
void update(float deltaTime) override {
|
||||
// Call the base class update to handle input and text refreshing
|
||||
Object::UITextBox::update(deltaTime);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -19,7 +19,8 @@ namespace Game {
|
||||
enum class GameStateEnum {
|
||||
RUNNING,
|
||||
PAUSED,
|
||||
STOPPED
|
||||
STOPPED,
|
||||
REPLAY
|
||||
};
|
||||
|
||||
enum class SharedDataType {
|
||||
@@ -59,7 +60,14 @@ namespace Game {
|
||||
static void processPendingEntityRemovals();
|
||||
|
||||
static void pushPlayerPosition(Object::Transform transform) { mPlayerTransformHistory.push_back(transform); }
|
||||
static void pushPlayerFormState(bool isShipMode) { mPlayerFormHistory.push_back(isShipMode); }
|
||||
static void getPlayerPositionHistory(std::vector<Object::Transform>& outHistory) { outHistory = mPlayerTransformHistory; }
|
||||
static void getPlayerFormHistory(std::vector<bool>& outHistory) { outHistory = mPlayerFormHistory; }
|
||||
|
||||
// Replay mode API
|
||||
static bool initReplayMode();
|
||||
static bool playReplayFrame();
|
||||
static void stopReplayMode();
|
||||
|
||||
private:
|
||||
int mTargetUpdatesPerSecond = TARGET_UPDATE_RATE;
|
||||
@@ -69,8 +77,17 @@ namespace Game {
|
||||
static std::unordered_map<std::string, float> mSharedFloats;
|
||||
static std::unordered_map<std::string, bool> mSharedBools;
|
||||
static std::vector<Object::Transform> mPlayerTransformHistory;
|
||||
static std::vector<bool> mPlayerFormHistory;
|
||||
static GameStateEnum mCurrentGameState;
|
||||
float mLastDelta = 0.f;
|
||||
|
||||
// Replay data
|
||||
struct ReplayFrame {
|
||||
float x, y, rotation, scaleX, scaleY;
|
||||
bool isShipMode;
|
||||
};
|
||||
static std::vector<ReplayFrame> mReplayFrames;
|
||||
static size_t mCurrentReplayFrame;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <vector>
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
namespace Game {
|
||||
class Input {
|
||||
@@ -16,13 +20,23 @@ namespace Game {
|
||||
static bool isMouseButtonJustReleased(Uint8 button);
|
||||
static float getMouseX();
|
||||
static float getMouseY();
|
||||
|
||||
// Text input from SDL text-input events (pushed by window thread, consumed by game thread)
|
||||
static void pushText(const std::string& utf8);
|
||||
static void consumeText(std::vector<std::string>& out);
|
||||
|
||||
private:
|
||||
static const bool* mCurrentKeyStates;
|
||||
static const bool* mPreviousKeyStates;
|
||||
static std::vector<Uint8> mPreviousKeyStates;
|
||||
static int mNumKeys;
|
||||
static int mPrevNumKeys;
|
||||
static SDL_MouseButtonFlags mCurrentMouseButtonStates;
|
||||
static SDL_MouseButtonFlags mPreviousMouseButtonStates;
|
||||
static float mMouseX;
|
||||
static float mMouseY;
|
||||
|
||||
// Text input queue and mutex (window thread writes via pushText, game thread reads via consumeText)
|
||||
static std::mutex mTextMutex;
|
||||
static std::vector<std::string> mPendingText;
|
||||
};
|
||||
}
|
||||
@@ -25,5 +25,6 @@ namespace Game::Object {
|
||||
void* mClickFunction = nullptr;
|
||||
float mX, mY;
|
||||
std::string mText;
|
||||
bool mIsHovered = false;
|
||||
};
|
||||
}
|
||||
@@ -4,62 +4,54 @@
|
||||
#include <renderer/font.hpp>
|
||||
#include <renderer/texture.hpp>
|
||||
#include <utility>
|
||||
#include <string>
|
||||
#include <game/input.hpp>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <mutex>
|
||||
|
||||
namespace Game::Object {
|
||||
|
||||
struct UITextboxConfig {
|
||||
SDL_Color bgColor = {20, 20, 20, 210};
|
||||
SDL_Color borderColor = {110, 110, 110, 255};
|
||||
SDL_Color focusedBorderColor = {200, 175, 70, 255};
|
||||
SDL_Color textColor = {255, 255, 255, 255};
|
||||
SDL_Color focusedTextColor = {255, 240, 180, 255};
|
||||
SDL_Color placeholderColor = {120, 120, 120, 200};
|
||||
float borderThickness = 2.f;
|
||||
float paddingX = 8.f;
|
||||
float paddingY = 4.f;
|
||||
float minWidth = 160.f;
|
||||
float minHeight = 32.f;
|
||||
int maxLength = 0; // 0 = unlimited
|
||||
std::string placeholder = "";
|
||||
float cursorBlinkRate = 0.53f; // seconds per blink phase
|
||||
};
|
||||
|
||||
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,
|
||||
UITextboxConfig config = {});
|
||||
UITextBox(const std::string& name, std::shared_ptr<Renderer::Texture> background, std::shared_ptr<Renderer::Font> font, const Transform& transform, float x = 0.f, float y = 0.f);
|
||||
~UITextBox() override = default;
|
||||
|
||||
void start() override;
|
||||
void start() override;
|
||||
void update(float deltaTime) override;
|
||||
|
||||
// Custom render to show both background and text
|
||||
void render(Game::Renderer::Renderer* renderer, Game::Renderer::RendererConfig config) override;
|
||||
|
||||
void setText(const std::string& text);
|
||||
std::string getText() const;
|
||||
std::string getValue() const;
|
||||
bool isFocused() const;
|
||||
void setText(const std::string& text);
|
||||
std::string getText() const;
|
||||
void setPlaceholder(const std::string& placeholder) { mPlaceholder = placeholder; mLastRenderedText.clear(); }
|
||||
|
||||
// Insert UTF-8 text at the current cursor position (delivered from Input queue)
|
||||
void insertText(const std::string& utf8);
|
||||
|
||||
void setMaxLength(size_t maxLen) { mMaxLength = maxLen; }
|
||||
void setPasswordMode(bool enable) { mPasswordMode = enable; }
|
||||
void setOnFocus(void* fn) { mOnFocus = fn; }
|
||||
|
||||
void setPosition(float x, float y) { mX = x; mY = y; }
|
||||
std::pair<float, float> getPosition() const { return {mX, mY}; }
|
||||
|
||||
bool isFocused() const { return mIsFocused; }
|
||||
|
||||
private:
|
||||
bool isMouseInsideBox() const;
|
||||
void refreshVisualText();
|
||||
std::shared_ptr<Renderer::Texture> mBackground; // Background texture for the textbox
|
||||
std::shared_ptr<Renderer::Font> mFont; // Font used to render text
|
||||
|
||||
float mX, mY;
|
||||
std::string mText;
|
||||
size_t mCursorIndex = 0;
|
||||
float mCursorBlinkTimer = 0.f;
|
||||
bool mShowCursor = true;
|
||||
size_t mMaxLength = 1024;
|
||||
bool mPasswordMode = false;
|
||||
bool mIsFocused = false;
|
||||
float mBoxWidth = 0.f;
|
||||
float mBoxHeight = 0.f;
|
||||
bool mNeedsTextRefresh = true;
|
||||
UITextboxConfig mConfig;
|
||||
|
||||
float mCursorTimer = 0.f;
|
||||
bool mCursorVisible = true;
|
||||
void* mOnFocus = nullptr; // optional function pointer
|
||||
std::string mLastRenderedText; // to avoid rebuilding font unnecessarily
|
||||
std::string mPlaceholder = "";
|
||||
float mReservedPlaceholderWidth = 0.f; // Reserved pixel width for placeholder to avoid layout shifts
|
||||
std::mutex mRenderMutex; // Protects mLastRenderedText and mReservedPlaceholderWidth from main-thread updates
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ namespace Game::Renderer {
|
||||
|
||||
// Build the texture for the font; Call getSDLTexture() afterwards
|
||||
void build(SDL_Color color, std::string text);
|
||||
// Rebuild GPU-backed texture after a renderer/device reset
|
||||
bool reload(SDL_Renderer* renderer);
|
||||
// Rebuild GPU-backed texture after a renderer/device reset
|
||||
bool reload(SDL_Renderer* renderer);
|
||||
|
||||
SDL_Texture* getSDLTexture();
|
||||
std::string getId();
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace Game::Renderer {
|
||||
float getHeight();
|
||||
bool isTiled() { return mIsTiled; }
|
||||
void setTiled(bool tiled) { mIsTiled = tiled; }
|
||||
void setSDLTexture(SDL_Texture* tex) { mTex = tex; }
|
||||
// Reload GPU-backed texture using a new renderer after device reset
|
||||
virtual bool reload(SDL_Renderer* renderer);
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ namespace Game::Window {
|
||||
|
||||
static SDL_Window* getSDLWindowBackend() { std::scoped_lock lock(sMutex); return sWindowBackend; }
|
||||
Renderer::Renderer* getRenderer() { std::scoped_lock lock(mMutex); return &mRenderer; }
|
||||
// Post a task to be executed on the window/event thread.
|
||||
static void postToMainThread(std::function<void()> fn);
|
||||
|
||||
private:
|
||||
mutable std::mutex mMutex;
|
||||
|
||||
@@ -36,17 +36,23 @@ namespace {
|
||||
file << "Točke: " << score << "\n";
|
||||
file << "Datum: " << std::put_time(&localTime, "%Y-%m-%d %H:%M:%S") << "\n";
|
||||
|
||||
// Replay system
|
||||
// Replay system with form state
|
||||
std::vector<Game::Object::Transform> playerHistory;
|
||||
std::vector<bool> formHistory;
|
||||
Game::GameManager::getPlayerPositionHistory(playerHistory);
|
||||
Game::GameManager::getPlayerFormHistory(formHistory);
|
||||
|
||||
std::ofstream replayFile("replay.txt", std::ios::trunc);
|
||||
if (!replayFile.is_open()) {
|
||||
WARN("Neuspešno odpiranje replay.txt za pisanje");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& transform : playerHistory) {
|
||||
replayFile << transform.x << " " << transform.y << " " << transform.rotation << " " << transform.scaleX << " " << transform.scaleY << "\n";
|
||||
for (size_t i = 0; i < playerHistory.size(); ++i) {
|
||||
const auto& transform = playerHistory[i];
|
||||
bool isShipMode = (i < formHistory.size()) ? formHistory[i] : true;
|
||||
int shipFlag = isShipMode ? 1 : 0;
|
||||
replayFile << transform.x << " " << transform.y << " " << transform.rotation << " " << transform.scaleX << " " << transform.scaleY << " " << shipFlag << "\n";
|
||||
}
|
||||
|
||||
LOG("Zapis končne statistike in replaya igre dokončan");
|
||||
|
||||
@@ -33,13 +33,19 @@ namespace Game::AGame {
|
||||
};
|
||||
|
||||
if (GameManager::getSharedData<bool>("gameLost")) {
|
||||
setText("Umrl si!");
|
||||
if (getText() != "Umrl si!") {
|
||||
const std::string s = "Umrl si!";
|
||||
Window::Window::postToMainThread([this, s]() { setText(s); });
|
||||
}
|
||||
anchorTopRight();
|
||||
return;
|
||||
}
|
||||
|
||||
if (GameManager::getSharedData<bool>("gameWon")) {
|
||||
setText("Zmagal si!");
|
||||
if (getText() != "Zmagal si!") {
|
||||
const std::string s = "Zmagal si!";
|
||||
Window::Window::postToMainThread([this, s]() { setText(s); });
|
||||
}
|
||||
anchorTopRight();
|
||||
return;
|
||||
}
|
||||
@@ -53,7 +59,10 @@ namespace Game::AGame {
|
||||
<< " | Smeti " << GameManager::getSharedData<int>("trashActiveCount")
|
||||
<< " | Sovražniki " << GameManager::getSharedData<int>("enemyActiveCount");
|
||||
|
||||
setText(stream.str());
|
||||
const std::string newHudText = stream.str();
|
||||
if (getText() != newHudText) {
|
||||
Window::Window::postToMainThread([this, newHudText]() { setText(newHudText); });
|
||||
}
|
||||
anchorTopRight();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,8 +162,9 @@ namespace Game::AGame {
|
||||
setTexture(mGroundTex);
|
||||
}
|
||||
|
||||
// Push replay
|
||||
// Push replay (position and form state)
|
||||
GameManager::pushPlayerPosition(mTransform);
|
||||
GameManager::pushPlayerFormState(mIsShipMode);
|
||||
}
|
||||
|
||||
void Player::onCollisionEnter(Object::Entity* other) {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
#include <game/gamemanager.hpp>
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <game/agame/player.hpp>
|
||||
#include <state/gamestate.hpp>
|
||||
|
||||
namespace Game {
|
||||
GameStateEnum GameManager::mCurrentGameState = GameStateEnum::RUNNING;
|
||||
std::vector<GameManager::ReplayFrame> GameManager::mReplayFrames;
|
||||
size_t GameManager::mCurrentReplayFrame = 0;
|
||||
|
||||
void GameManager::run(std::stop_token stopToken) {
|
||||
using namespace std::chrono_literals;
|
||||
@@ -27,19 +33,30 @@ namespace Game {
|
||||
|
||||
try {
|
||||
Input::update(); // Update input states at the start of each frame
|
||||
auto entities = State::GameState::getInstance().getEntitiesSnapshot();
|
||||
for (auto* entity : entities) {
|
||||
if (!entity || !entity->isActive()) {
|
||||
continue;
|
||||
|
||||
// Handle REPLAY state
|
||||
if (mCurrentGameState == GameStateEnum::REPLAY) {
|
||||
if (!playReplayFrame()) {
|
||||
// No more frames - end replay
|
||||
stopReplayMode();
|
||||
mCurrentGameState = GameStateEnum::STOPPED;
|
||||
LOG("Replay finished");
|
||||
}
|
||||
} else {
|
||||
auto entities = State::GameState::getInstance().getEntitiesSnapshot();
|
||||
for (auto* entity : entities) {
|
||||
if (!entity || !entity->isActive()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update components first
|
||||
entity->updateComponents(seconds);
|
||||
if (!entity->isActive()) {
|
||||
continue;
|
||||
// Update components first
|
||||
entity->updateComponents(seconds);
|
||||
if (!entity->isActive()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entity->update(seconds);
|
||||
}
|
||||
|
||||
entity->update(seconds);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
ERROR("Exception in GameManager thread: " << e.what());
|
||||
@@ -68,6 +85,7 @@ namespace Game {
|
||||
std::unordered_map<std::string, float> GameManager::mSharedFloats;
|
||||
std::unordered_map<std::string, bool> GameManager::mSharedBools;
|
||||
std::vector<Object::Transform> GameManager::mPlayerTransformHistory;
|
||||
std::vector<bool> GameManager::mPlayerFormHistory;
|
||||
|
||||
void GameManager::removeSharedData(const std::string& key, SharedDataType type) {
|
||||
if (type == SharedDataType::STRING) {
|
||||
@@ -84,5 +102,65 @@ namespace Game {
|
||||
void GameManager::processPendingEntityRemovals() {
|
||||
State::GameState::getInstance().processPendingRemovals();
|
||||
}
|
||||
|
||||
bool GameManager::initReplayMode() {
|
||||
// Read and parse replay.txt
|
||||
std::ifstream replayFile("replay.txt");
|
||||
if (!replayFile.is_open()) {
|
||||
WARN("Failed to open replay.txt for reading");
|
||||
return false;
|
||||
}
|
||||
|
||||
mReplayFrames.clear();
|
||||
mCurrentReplayFrame = 0;
|
||||
std::string line;
|
||||
|
||||
while (std::getline(replayFile, line)) {
|
||||
if (line.empty()) continue;
|
||||
|
||||
std::istringstream iss(line);
|
||||
ReplayFrame frame;
|
||||
int isShip;
|
||||
|
||||
if (iss >> frame.x >> frame.y >> frame.rotation >> frame.scaleX >> frame.scaleY >> isShip) {
|
||||
frame.isShipMode = (isShip != 0);
|
||||
mReplayFrames.push_back(frame);
|
||||
} else {
|
||||
WARN("Failed to parse replay line: " << line);
|
||||
}
|
||||
}
|
||||
|
||||
replayFile.close();
|
||||
LOG("Loaded " << mReplayFrames.size() << " replay frames");
|
||||
return !mReplayFrames.empty();
|
||||
}
|
||||
|
||||
bool GameManager::playReplayFrame() {
|
||||
if (mReplayFrames.empty() || mCurrentReplayFrame >= mReplayFrames.size()) {
|
||||
return false; // Replay finished
|
||||
}
|
||||
|
||||
const ReplayFrame& frame = mReplayFrames[mCurrentReplayFrame];
|
||||
|
||||
// Apply frame to player
|
||||
auto* player = dynamic_cast<AGame::Player*>(State::GameState::getInstance().getEntityByName("Player"));
|
||||
if (player) {
|
||||
player->getTransform()->x = frame.x;
|
||||
player->getTransform()->y = frame.y;
|
||||
player->getTransform()->rotation = frame.rotation;
|
||||
player->getTransform()->scaleX = frame.scaleX;
|
||||
player->getTransform()->scaleY = frame.scaleY;
|
||||
player->setShipMode(frame.isShipMode);
|
||||
}
|
||||
|
||||
mCurrentReplayFrame++;
|
||||
return mCurrentReplayFrame < mReplayFrames.size(); // Return true if more frames exist
|
||||
}
|
||||
|
||||
void GameManager::stopReplayMode() {
|
||||
mReplayFrames.clear();
|
||||
mCurrentReplayFrame = 0;
|
||||
mCurrentGameState = GameStateEnum::STOPPED;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,35 +1,103 @@
|
||||
#include <game/input.hpp>
|
||||
#include <window/window.hpp>
|
||||
#include <object/ui/uitextbox.hpp>
|
||||
#include <state/gamestate.hpp>
|
||||
|
||||
namespace Game {
|
||||
const bool* Input::mCurrentKeyStates = nullptr;
|
||||
const bool* Input::mPreviousKeyStates = nullptr;
|
||||
std::vector<Uint8> Input::mPreviousKeyStates = {};
|
||||
int Input::mNumKeys = 0;
|
||||
int Input::mPrevNumKeys = 0;
|
||||
SDL_MouseButtonFlags Input::mCurrentMouseButtonStates = 0;
|
||||
SDL_MouseButtonFlags Input::mPreviousMouseButtonStates = 0;
|
||||
float Input::mMouseX = 0.0f;
|
||||
float Input::mMouseY = 0.0f;
|
||||
std::mutex Input::mTextMutex;
|
||||
std::vector<std::string> Input::mPendingText;
|
||||
|
||||
void Input::update() {
|
||||
mPreviousKeyStates = mCurrentKeyStates;
|
||||
// Copy the previous keyboard state (snapshot) so we can detect just-pressed
|
||||
if (mCurrentKeyStates && mNumKeys > 0) {
|
||||
mPreviousKeyStates.assign(mCurrentKeyStates, mCurrentKeyStates + mNumKeys);
|
||||
mPrevNumKeys = mNumKeys;
|
||||
} else {
|
||||
// If we don't have a previous snapshot, initialize previous vector to zeros with current size
|
||||
if (mNumKeys > 0) mPreviousKeyStates.assign(mNumKeys, 0);
|
||||
mPrevNumKeys = mNumKeys;
|
||||
}
|
||||
|
||||
mCurrentKeyStates = SDL_GetKeyboardState(&mNumKeys);
|
||||
|
||||
mPreviousMouseButtonStates = mCurrentMouseButtonStates;
|
||||
mCurrentMouseButtonStates = SDL_GetMouseState(&mMouseX, &mMouseY);
|
||||
float rawMouseX = 0.0f;
|
||||
float rawMouseY = 0.0f;
|
||||
mCurrentMouseButtonStates = SDL_GetMouseState(&rawMouseX, &rawMouseY);
|
||||
|
||||
// Convert mouse coordinates from real display/window pixels into
|
||||
// centered logical game coordinates (1280x720 world space).
|
||||
static constexpr float LOGICAL_WIDTH = 1280.0f;
|
||||
static constexpr float LOGICAL_HEIGHT = 720.0f;
|
||||
|
||||
float displayW = LOGICAL_WIDTH;
|
||||
float displayH = LOGICAL_HEIGHT;
|
||||
|
||||
SDL_Window* sdlWindow = Window::Window::getSDLWindowBackend();
|
||||
if (sdlWindow) {
|
||||
int windowPixelsW = static_cast<int>(LOGICAL_WIDTH);
|
||||
int windowPixelsH = static_cast<int>(LOGICAL_HEIGHT);
|
||||
SDL_GetWindowSizeInPixels(sdlWindow, &windowPixelsW, &windowPixelsH);
|
||||
|
||||
SDL_DisplayID displayId = SDL_GetDisplayForWindow(sdlWindow);
|
||||
const SDL_DisplayMode* displayMode = SDL_GetCurrentDisplayMode(displayId);
|
||||
if (displayMode && displayMode->w > 0 && displayMode->h > 0) {
|
||||
displayW = static_cast<float>(displayMode->w);
|
||||
displayH = static_cast<float>(displayMode->h);
|
||||
} else if (windowPixelsW > 0 && windowPixelsH > 0) {
|
||||
displayW = static_cast<float>(windowPixelsW);
|
||||
displayH = static_cast<float>(windowPixelsH);
|
||||
}
|
||||
}
|
||||
|
||||
const float invScaleX = LOGICAL_WIDTH / displayW;
|
||||
const float invScaleY = LOGICAL_HEIGHT / displayH;
|
||||
mMouseX = rawMouseX * invScaleX - (LOGICAL_WIDTH * 0.5f);
|
||||
mMouseY = rawMouseY * invScaleY - (LOGICAL_HEIGHT * 0.5f);
|
||||
|
||||
// Consume and deliver queued text input from window thread to focused textbox
|
||||
std::vector<std::string> texts;
|
||||
consumeText(texts);
|
||||
if (!texts.empty()) {
|
||||
auto entities = State::GameState::getInstance().getEntitiesSnapshot();
|
||||
for (auto* e : entities) {
|
||||
if (!e) continue;
|
||||
auto* tb = dynamic_cast<Game::Object::UITextBox*>(e);
|
||||
if (tb && tb->isFocused()) {
|
||||
for (auto& s : texts) {
|
||||
tb->insertText(s);
|
||||
}
|
||||
break; // Deliver to first focused textbox only
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Input::isKeyPressed(SDL_Scancode key) {
|
||||
if (key < 0 || key >= mNumKeys) return false;
|
||||
return mCurrentKeyStates[key];
|
||||
return mCurrentKeyStates && mCurrentKeyStates[key];
|
||||
}
|
||||
|
||||
bool Input::isKeyJustPressed(SDL_Scancode key) {
|
||||
if (key < 0 || key >= mNumKeys) return false;
|
||||
return mCurrentKeyStates[key] && (!mPreviousKeyStates || !mPreviousKeyStates[key]);
|
||||
bool cur = mCurrentKeyStates && mCurrentKeyStates[key];
|
||||
bool prev = (key < static_cast<int>(mPreviousKeyStates.size())) ? static_cast<bool>(mPreviousKeyStates[key]) : false;
|
||||
return cur && !prev;
|
||||
}
|
||||
|
||||
bool Input::isKeyJustReleased(SDL_Scancode key) {
|
||||
if (key < 0 || key >= mNumKeys) return false;
|
||||
return (!mCurrentKeyStates[key]) && mPreviousKeyStates && mPreviousKeyStates[key];
|
||||
bool cur = mCurrentKeyStates && mCurrentKeyStates[key];
|
||||
bool prev = (key < static_cast<int>(mPreviousKeyStates.size())) ? static_cast<bool>(mPreviousKeyStates[key]) : false;
|
||||
return !cur && prev;
|
||||
}
|
||||
|
||||
bool Input::isMouseButtonPressed(Uint8 button) {
|
||||
@@ -51,4 +119,14 @@ namespace Game {
|
||||
float Input::getMouseY() {
|
||||
return mMouseY;
|
||||
}
|
||||
|
||||
void Input::pushText(const std::string& utf8) {
|
||||
std::scoped_lock lock(mTextMutex);
|
||||
mPendingText.push_back(utf8);
|
||||
}
|
||||
|
||||
void Input::consumeText(std::vector<std::string>& out) {
|
||||
std::scoped_lock lock(mTextMutex);
|
||||
out.swap(mPendingText);
|
||||
}
|
||||
}
|
||||
255
src/main.cpp
255
src/main.cpp
@@ -11,46 +11,261 @@
|
||||
#include <renderer/texture.hpp>
|
||||
#include <renderer/font.hpp>
|
||||
#include <object/ui/uitextbox.hpp>
|
||||
#include <game/agame/sampletextbox.hpp>
|
||||
#include <object/ui/uibutton.hpp>
|
||||
#include <object/ui/uitext.hpp>
|
||||
#include <object/components/boxcollider.hpp>
|
||||
#include <atomic>
|
||||
|
||||
using namespace Game;
|
||||
|
||||
int main() {
|
||||
PLNIMP("Letnik3Zadnja - Licenca: LGPLv2.1-only, CC BY-SA 4.0");
|
||||
// Prompt for player name before initializing the window/engine
|
||||
std::string playerName;
|
||||
std::cout << "Vnesi uporabniško ime (pusti prazno za 'Igralec'): ";
|
||||
std::getline(std::cin, playerName);
|
||||
if (playerName.empty()) playerName = "Igralec";
|
||||
// Global SDL renderer pointer used by menu callback to create textures/fonts
|
||||
static SDL_Renderer* gSDLRenderer = nullptr;
|
||||
static Uint32 gStartGameEventType = static_cast<Uint32>(-1);
|
||||
static std::atomic_bool gStartGameQueued = false;
|
||||
|
||||
namespace {
|
||||
// Create a solid-color texture (e.g., for textbox backgrounds)
|
||||
std::shared_ptr<Game::Renderer::Texture> createSolidColorTexture(SDL_Renderer* renderer, int width, int height, Uint8 r, Uint8 g, Uint8 b, Uint8 a) {
|
||||
// Create an SDL surface filled with the specified color
|
||||
SDL_Surface* surf = SDL_CreateSurface(width, height, SDL_PIXELFORMAT_ARGB8888);
|
||||
if (!surf) return nullptr;
|
||||
|
||||
Uint32 color = SDL_MapSurfaceRGB(surf, r, g, b);
|
||||
SDL_FillSurfaceRect(surf, nullptr, color);
|
||||
|
||||
// Create texture from surface
|
||||
SDL_Texture* tex = SDL_CreateTextureFromSurface(renderer, surf);
|
||||
SDL_DestroySurface(surf);
|
||||
|
||||
if (!tex) return nullptr;
|
||||
|
||||
// Set the texture to blend with alpha so it composites correctly
|
||||
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetTextureAlphaMod(tex, a);
|
||||
|
||||
// Wrap the SDL_Texture in a Texture object
|
||||
auto texturePtr = std::make_shared<Game::Renderer::Texture>("textbox_background");
|
||||
texturePtr->setSDLTexture(tex); // Set the texture via protected setter
|
||||
return texturePtr;
|
||||
}
|
||||
}
|
||||
|
||||
static void performStartGameTransition() {
|
||||
// Read name from the UI textbox
|
||||
auto* tbEntity = dynamic_cast<Game::Object::UITextBox*>(State::GameState::getInstance().getEntityByName("NameBox"));
|
||||
std::string playerName = "Igralec";
|
||||
if (tbEntity) {
|
||||
auto txt = tbEntity->getText();
|
||||
if (!txt.empty()) playerName = txt;
|
||||
}
|
||||
|
||||
Game::GameManager::setSharedData<std::string>("playerName", playerName);
|
||||
|
||||
Window::Window window = Window::Window();
|
||||
window.init(1280, 720, "Game Window");
|
||||
|
||||
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));
|
||||
// Spawn the main game entities now that we have a player name
|
||||
// Background
|
||||
State::GameState::getInstance().addEntity(std::make_unique<AGame::Background>("BG", std::make_shared<Game::Renderer::Texture>("../resources/bgtest.png", gSDLRenderer), Object::DEFAULT_TRANSFORM));
|
||||
|
||||
// Player
|
||||
auto* player = dynamic_cast<AGame::Player*>(
|
||||
State::GameState::getInstance().addEntity(
|
||||
std::make_unique<AGame::Player>(
|
||||
"Player",
|
||||
std::make_shared<Game::Renderer::Texture>(
|
||||
"../resources/l3ladja.png",
|
||||
window.getRenderer()->getSDLRenderer()
|
||||
),
|
||||
std::make_shared<Game::Renderer::Texture>("../resources/l3ladja.png", gSDLRenderer),
|
||||
Object::DEFAULT_TRANSFORM
|
||||
)
|
||||
)
|
||||
);
|
||||
if (player) {
|
||||
player->addComponent<Object::Components::BoxCollider>();
|
||||
player->setShipTexture(std::make_shared<Game::Renderer::Texture>("../resources/l3ladja.png", window.getRenderer()->getSDLRenderer()));
|
||||
player->setGroundTexture(std::make_shared<Game::Renderer::Texture>("../resources/l3player.png", window.getRenderer()->getSDLRenderer()));
|
||||
player->setShipTexture(std::make_shared<Game::Renderer::Texture>("../resources/l3ladja.png", gSDLRenderer));
|
||||
player->setGroundTexture(std::make_shared<Game::Renderer::Texture>("../resources/l3player.png", gSDLRenderer));
|
||||
}
|
||||
|
||||
State::GameState::getInstance().addEntity(std::make_unique<AGame::HUDText>("HUD", std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 60, "HUDFont"), Object::Transform{0.f, 0.f, 0.f, 1.f, 1.f}, 320.f, 40.f));
|
||||
// HUD
|
||||
State::GameState::getInstance().addEntity(std::make_unique<AGame::HUDText>("HUD", std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", gSDLRenderer, 60, "HUDFont"), Object::Transform{0.f, 0.f, 0.f, 1.f, 1.f}, 320.f, 40.f));
|
||||
|
||||
// Remove menu entities safely at end-of-frame to avoid invalidating
|
||||
// pointers currently being iterated by the game update snapshot.
|
||||
auto deactivateAndQueueRemoval = [](const std::string& entityName) {
|
||||
if (auto* entity = State::GameState::getInstance().getEntityByName(entityName)) {
|
||||
entity->setActive(false);
|
||||
State::GameState::getInstance().queueEntityRemoval(entityName);
|
||||
}
|
||||
};
|
||||
|
||||
deactivateAndQueueRemoval("Title");
|
||||
deactivateAndQueueRemoval("NameBox");
|
||||
deactivateAndQueueRemoval("StartButton");
|
||||
}
|
||||
|
||||
static void performReplayTransition() {
|
||||
// Spawn minimal game entities for replay
|
||||
State::GameState::getInstance().addEntity(std::make_unique<AGame::Background>("BG", std::make_shared<Game::Renderer::Texture>("../resources/bgtest.png", gSDLRenderer), Object::DEFAULT_TRANSFORM));
|
||||
|
||||
auto* player = dynamic_cast<AGame::Player*>(
|
||||
State::GameState::getInstance().addEntity(
|
||||
std::make_unique<AGame::Player>(
|
||||
"Player",
|
||||
std::make_shared<Game::Renderer::Texture>("../resources/l3ladja.png", gSDLRenderer),
|
||||
Object::DEFAULT_TRANSFORM
|
||||
)
|
||||
)
|
||||
);
|
||||
if (player) {
|
||||
player->addComponent<Object::Components::BoxCollider>();
|
||||
player->setShipTexture(std::make_shared<Game::Renderer::Texture>("../resources/l3ladja.png", gSDLRenderer));
|
||||
player->setGroundTexture(std::make_shared<Game::Renderer::Texture>("../resources/l3player.png", gSDLRenderer));
|
||||
}
|
||||
|
||||
// HUD
|
||||
State::GameState::getInstance().addEntity(std::make_unique<AGame::HUDText>("HUD", std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", gSDLRenderer, 60, "HUDFont"), Object::Transform{0.f, 0.f, 0.f, 1.f, 1.f}, 320.f, 40.f));
|
||||
|
||||
// Initialize and start replay
|
||||
if (GameManager::initReplayMode()) {
|
||||
GameManager::setCurrentGameState(GameStateEnum::REPLAY);
|
||||
LOG("Replay mode started");
|
||||
} else {
|
||||
WARN("Failed to initialize replay mode");
|
||||
GameManager::setCurrentGameState(GameStateEnum::STOPPED);
|
||||
}
|
||||
|
||||
// Remove menu entities
|
||||
auto deactivateAndQueueRemoval = [](const std::string& entityName) {
|
||||
if (auto* entity = State::GameState::getInstance().getEntityByName(entityName)) {
|
||||
entity->setActive(false);
|
||||
State::GameState::getInstance().queueEntityRemoval(entityName);
|
||||
}
|
||||
};
|
||||
|
||||
deactivateAndQueueRemoval("Title");
|
||||
deactivateAndQueueRemoval("NameBox");
|
||||
deactivateAndQueueRemoval("StartButton");
|
||||
deactivateAndQueueRemoval("ReplayButton");
|
||||
}
|
||||
|
||||
static void startGameCallback() {
|
||||
// UIButton clicks are handled on the game thread. Queue a custom SDL user
|
||||
// event so entity/texture creation happens on the window/event thread.
|
||||
bool alreadyQueued = gStartGameQueued.exchange(true);
|
||||
if (alreadyQueued) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gStartGameEventType == static_cast<Uint32>(-1)) {
|
||||
// Fallback if custom events are unavailable.
|
||||
performStartGameTransition();
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_Event event{};
|
||||
event.type = gStartGameEventType;
|
||||
event.user.data1 = reinterpret_cast<void*>(&performStartGameTransition);
|
||||
if (!SDL_PushEvent(&event)) {
|
||||
WARN("Failed to push start-game user event: " << SDL_GetError());
|
||||
performStartGameTransition();
|
||||
}
|
||||
}
|
||||
|
||||
static void replayGameCallback() {
|
||||
// UIButton clicks are handled on the game thread. Queue a custom SDL user
|
||||
// event so entity/texture creation happens on the window/event thread.
|
||||
bool alreadyQueued = gStartGameQueued.exchange(true);
|
||||
if (alreadyQueued) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gStartGameEventType == static_cast<Uint32>(-1)) {
|
||||
// Fallback if custom events are unavailable.
|
||||
performReplayTransition();
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_Event event{};
|
||||
event.type = gStartGameEventType;
|
||||
event.user.data1 = reinterpret_cast<void*>(&performReplayTransition);
|
||||
if (!SDL_PushEvent(&event)) {
|
||||
WARN("Failed to push replay user event: " << SDL_GetError());
|
||||
performReplayTransition();
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
PLNIMP("Letnik3Zadnja - Licenca: LGPLv2.1-only, CC BY-SA 4.0");
|
||||
|
||||
Window::Window window = Window::Window();
|
||||
window.init(1280, 720, "Game Window");
|
||||
|
||||
// Make SDL renderer available to callbacks
|
||||
gSDLRenderer = window.getRenderer()->getSDLRenderer();
|
||||
|
||||
// Register a custom event used to perform menu -> game transition on the
|
||||
// window/event thread.
|
||||
gStartGameEventType = SDL_RegisterEvents(1);
|
||||
|
||||
// Separate fonts for each UI element (each font owns its own texture).
|
||||
// Reusing one font object would make all UI labels share one texture.
|
||||
auto titleFont = std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", gSDLRenderer, 72, "TitleFont");
|
||||
auto nameBoxFont = std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", gSDLRenderer, 58, "NameBoxFont");
|
||||
auto startButtonFont = std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", gSDLRenderer, 72, "StartButtonFont");
|
||||
auto replayButtonFont = std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", gSDLRenderer, 60, "ReplayButtonFont");
|
||||
|
||||
// Determine display resolution and convert to centered logical coordinates for UI layout
|
||||
static constexpr float LOGICAL_WIDTH = 1280.0f;
|
||||
static constexpr float LOGICAL_HEIGHT = 720.0f;
|
||||
|
||||
float displayW = LOGICAL_WIDTH;
|
||||
float displayH = LOGICAL_HEIGHT;
|
||||
|
||||
SDL_Window* sdlWin = Window::Window::getSDLWindowBackend();
|
||||
if (sdlWin) {
|
||||
int windowPixelsW = static_cast<int>(LOGICAL_WIDTH);
|
||||
int windowPixelsH = static_cast<int>(LOGICAL_HEIGHT);
|
||||
SDL_GetWindowSizeInPixels(sdlWin, &windowPixelsW, &windowPixelsH);
|
||||
|
||||
SDL_DisplayID displayId = SDL_GetDisplayForWindow(sdlWin);
|
||||
const SDL_DisplayMode* displayMode = SDL_GetCurrentDisplayMode(displayId);
|
||||
if (displayMode && displayMode->w > 0 && displayMode->h > 0) {
|
||||
displayW = static_cast<float>(displayMode->w);
|
||||
displayH = static_cast<float>(displayMode->h);
|
||||
} else if (windowPixelsW > 0 && windowPixelsH > 0) {
|
||||
displayW = static_cast<float>(windowPixelsW);
|
||||
displayH = static_cast<float>(windowPixelsH);
|
||||
}
|
||||
}
|
||||
|
||||
const float centerDisplayX = displayW * 0.5f;
|
||||
const float centerDisplayY = displayH * 0.5f;
|
||||
const float cx = centerDisplayX * (LOGICAL_WIDTH / displayW) - (LOGICAL_WIDTH * 0.5f);
|
||||
const float cy = centerDisplayY * (LOGICAL_HEIGHT / displayH) - (LOGICAL_HEIGHT * 0.5f);
|
||||
|
||||
LOG("cx: " << cx << ", cy: " << cy);
|
||||
|
||||
// Position UI elements relative to center (cx, cy are center offsets in logical space)
|
||||
const float titleY = cy - 160.f; // Title above center
|
||||
const float textboxY = cy; // Textbox at center
|
||||
const float startButtonY = cy + 120.f; // Start button below center
|
||||
const float replayButtonY = cy + 220.f; // Replay button below start button
|
||||
|
||||
// Title text (centered)
|
||||
auto* title = dynamic_cast<Game::Object::UIText*>(State::GameState::getInstance().addEntity(std::make_unique<Game::Object::UIText>("Title", titleFont, Object::DEFAULT_TRANSFORM, cx, titleY)));
|
||||
if (title) title->setText("Game Name");
|
||||
|
||||
// Name input box (larger and more visible) with a light gray background
|
||||
auto textboxBg = createSolidColorTexture(gSDLRenderer, 400, 70, 180, 180, 180, 220); // Larger, slightly darker gray
|
||||
auto* nameBox = dynamic_cast<Game::Object::UITextBox*>(State::GameState::getInstance().addEntity(std::make_unique<Game::Object::UITextBox>("NameBox", textboxBg, nameBoxFont, Object::DEFAULT_TRANSFORM, cx, textboxY)));
|
||||
if (nameBox) nameBox->setPlaceholder("Enter name...");
|
||||
|
||||
// Start button (below center)
|
||||
auto btnTex = std::dynamic_pointer_cast<Game::Renderer::Texture>(startButtonFont);
|
||||
auto* btnEntity = dynamic_cast<Game::Object::UIButton*>(State::GameState::getInstance().addEntity(std::make_unique<Game::Object::UIButton>("StartButton", btnTex, Object::DEFAULT_TRANSFORM, reinterpret_cast<void*>(&startGameCallback), cx, startButtonY)));
|
||||
if (btnEntity) btnEntity->setText("Start Game");
|
||||
|
||||
// Replay button (below start button)
|
||||
auto replayBtnTex = std::dynamic_pointer_cast<Game::Renderer::Texture>(replayButtonFont);
|
||||
auto* replayBtnEntity = dynamic_cast<Game::Object::UIButton*>(State::GameState::getInstance().addEntity(std::make_unique<Game::Object::UIButton>("ReplayButton", replayBtnTex, Object::DEFAULT_TRANSFORM, reinterpret_cast<void*>(&replayGameCallback), cx, replayButtonY)));
|
||||
if (replayBtnEntity) replayBtnEntity->setText("Replay");
|
||||
|
||||
window.run();
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <object/ui/uibutton.hpp>
|
||||
#include <window/window.hpp>
|
||||
|
||||
namespace Game::Object {
|
||||
UIButton::UIButton(const std::string& name, std::shared_ptr<Renderer::Texture> texture, const Transform& transform, void* clickFunction, float x, float y)
|
||||
@@ -19,8 +20,21 @@ namespace Game::Object {
|
||||
float textTop = mTransform.y;
|
||||
float textBottom = mTransform.y + mTex->getHeight() * mTransform.adjustedScaleY();
|
||||
|
||||
if (mouseX >= textLeft && mouseX <= textRight && mouseY >= textTop && mouseY <= textBottom) {
|
||||
std::dynamic_pointer_cast<Renderer::Font>(mTex)->build({200, 200, 200, 255}, mText); // Darken text when hovered
|
||||
const bool isInside = mouseX >= textLeft && mouseX <= textRight && mouseY >= textTop && mouseY <= textBottom;
|
||||
|
||||
if (isInside && !mIsHovered) {
|
||||
mIsHovered = true;
|
||||
auto fontPtr = std::dynamic_pointer_cast<Renderer::Font>(mTex);
|
||||
std::string txt = mText;
|
||||
if (fontPtr) Window::Window::postToMainThread([fontPtr, txt]() { fontPtr->build({200,200,200,255}, txt); });
|
||||
} else if (!isInside && mIsHovered) {
|
||||
mIsHovered = false;
|
||||
auto fontPtr = std::dynamic_pointer_cast<Renderer::Font>(mTex);
|
||||
std::string txt = mText;
|
||||
if (fontPtr) Window::Window::postToMainThread([fontPtr, txt]() { fontPtr->build({255,255,255,255}, txt); });
|
||||
}
|
||||
|
||||
if (isInside) {
|
||||
if (Input::isMouseButtonJustPressed(SDL_BUTTON_LEFT) && mClickFunction) {
|
||||
using ClickFnType = void(*)();
|
||||
ClickFnType clickFn = reinterpret_cast<ClickFnType>(mClickFunction);
|
||||
@@ -32,7 +46,9 @@ namespace Game::Object {
|
||||
|
||||
void UIButton::setText(const std::string& text) {
|
||||
mText = text;
|
||||
std::dynamic_pointer_cast<Renderer::Font>(mTex)->build({255, 255, 255, 255}, text);
|
||||
auto fontPtr = std::dynamic_pointer_cast<Renderer::Font>(mTex);
|
||||
std::string txt = text;
|
||||
if (fontPtr) Window::Window::postToMainThread([fontPtr, txt]() { fontPtr->build({255,255,255,255}, txt); });
|
||||
}
|
||||
|
||||
std::string UIButton::getText() const {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <object/ui/uitext.hpp>
|
||||
#include <window/window.hpp>
|
||||
|
||||
namespace Game::Object {
|
||||
UIText::UIText(const std::string& name, std::shared_ptr<Renderer::Font> font, const Transform& transform, float x, float y)
|
||||
@@ -31,7 +32,15 @@ namespace Game::Object {
|
||||
|
||||
void UIText::setText(const std::string& text) {
|
||||
mText = text;
|
||||
std::dynamic_pointer_cast<Renderer::Font>(mTex)->build({255, 255, 255, 255}, text);
|
||||
// Schedule font texture build on the window thread to avoid
|
||||
// creating/destroying renderer resources from the game thread.
|
||||
auto fontPtr = std::dynamic_pointer_cast<Renderer::Font>(mTex);
|
||||
if (fontPtr) {
|
||||
std::string t = text;
|
||||
Window::Window::postToMainThread([fontPtr, t]() mutable {
|
||||
fontPtr->build({255,255,255,255}, t);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
std::string UIText::getText() const {
|
||||
|
||||
@@ -1,164 +1,278 @@
|
||||
#include <object/ui/uitextbox.hpp>
|
||||
#include <renderer/renderer.hpp>
|
||||
#include <object/camera.hpp>
|
||||
#include <window/window.hpp>
|
||||
|
||||
namespace Game::Object {
|
||||
|
||||
UITextBox::UITextBox(const std::string& name, std::shared_ptr<Renderer::Font> font,
|
||||
const Transform& transform, float x, float y, UITextboxConfig config)
|
||||
: Entity(name, font, transform), mX(x), mY(y), mConfig(config) { }
|
||||
UITextBox::UITextBox(const std::string& name, std::shared_ptr<Renderer::Texture> background, std::shared_ptr<Renderer::Font> font, const Transform& transform, float x, float y)
|
||||
: Entity(name, background, transform), mBackground(background), mFont(font), mX(x), mY(y) {
|
||||
if (!mBackground) {
|
||||
// Fallback to font texture if background is not provided
|
||||
mBackground = std::dynamic_pointer_cast<Renderer::Texture>(mFont);
|
||||
setTexture(mBackground);
|
||||
}
|
||||
}
|
||||
|
||||
void UITextBox::start() {
|
||||
// Compute visual box size first (respecting min sizes and scale), then center the box
|
||||
mBoxWidth = static_cast<float>(mTex ? mTex->getWidth() : 0.f) * mTransform.adjustedScaleX();
|
||||
mBoxHeight = static_cast<float>(mTex ? mTex->getHeight() : 0.f) * mTransform.adjustedScaleY();
|
||||
// Build initial text texture (placeholder if present) so textbox has a
|
||||
// visible indicator and stable dimensions for click hit-testing.
|
||||
const std::string initialDisplay = (mText.empty() && !mPlaceholder.empty()) ? mPlaceholder : (mPasswordMode ? std::string(mText.size(), '*') : mText);
|
||||
if (mFont && !initialDisplay.empty()) {
|
||||
auto fontPtr = mFont;
|
||||
std::string txt = initialDisplay;
|
||||
Window::Window::postToMainThread([fontPtr, txt]() { fontPtr->build({255,255,255,255}, txt); });
|
||||
mLastRenderedText = initialDisplay;
|
||||
}
|
||||
|
||||
if (mBoxWidth < mConfig.minWidth) mBoxWidth = mConfig.minWidth;
|
||||
if (mBoxHeight < mConfig.minHeight) mBoxHeight = mConfig.minHeight;
|
||||
// Center the textbox on the requested position
|
||||
if (mBackground) {
|
||||
mTransform.x = mX - mBackground->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
||||
mTransform.y = mY - mBackground->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
||||
}
|
||||
// Ensure cursor index is valid
|
||||
mCursorIndex = std::min(mCursorIndex, mText.size());
|
||||
}
|
||||
|
||||
// Center using the computed box dimensions rather than raw texture size so padding/min sizes are respected
|
||||
mTransform.x = mX - mBoxWidth / 2.f;
|
||||
mTransform.y = mY - mBoxHeight / 2.f;
|
||||
|
||||
refreshVisualText();
|
||||
static char scancodeToChar(SDL_Scancode scancode, bool shift) {
|
||||
// Basic mapping for letters, digits and common symbols
|
||||
if (scancode >= SDL_SCANCODE_A && scancode <= SDL_SCANCODE_Z) {
|
||||
char c = 'a' + (scancode - SDL_SCANCODE_A);
|
||||
if (shift) c = static_cast<char>(std::toupper(c));
|
||||
return c;
|
||||
}
|
||||
if (scancode >= SDL_SCANCODE_1 && scancode <= SDL_SCANCODE_0) {
|
||||
// Note: SDL_SCANCODE_1..SDL_SCANCODE_0 is not contiguous for '0', handle digits separately
|
||||
}
|
||||
switch (scancode) {
|
||||
case SDL_SCANCODE_SPACE: return ' ';
|
||||
case SDL_SCANCODE_COMMA: return shift ? '<' : ',';
|
||||
case SDL_SCANCODE_PERIOD: return shift ? '>' : '.';
|
||||
case SDL_SCANCODE_MINUS: return shift ? '_' : '-';
|
||||
case SDL_SCANCODE_EQUALS: return shift ? '+' : '=';
|
||||
case SDL_SCANCODE_SEMICOLON: return shift ? ':' : ';';
|
||||
case SDL_SCANCODE_APOSTROPHE: return shift ? '"' : '\'';
|
||||
case SDL_SCANCODE_SLASH: return shift ? '?' : '/';
|
||||
case SDL_SCANCODE_BACKSLASH: return shift ? '|' : '\\';
|
||||
case SDL_SCANCODE_LEFTBRACKET: return shift ? '{' : '[';
|
||||
case SDL_SCANCODE_RIGHTBRACKET: return shift ? '}' : ']';
|
||||
case SDL_SCANCODE_GRAVE: return shift ? '~' : '`';
|
||||
default: break;
|
||||
}
|
||||
// Digits
|
||||
if (scancode >= SDL_SCANCODE_1 && scancode <= SDL_SCANCODE_9) {
|
||||
const char numsShift[] = {')','!','@','#','$','%','^','&','*'};
|
||||
int idx = scancode - SDL_SCANCODE_1;
|
||||
return shift ? numsShift[idx] : ('1' + idx);
|
||||
}
|
||||
if (scancode == SDL_SCANCODE_0) return shift ? ')' : '0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
void UITextBox::update(float deltaTime) {
|
||||
if (!mIsActive) return;
|
||||
|
||||
if (Input::isMouseButtonJustPressed(SDL_BUTTON_LEFT)) {
|
||||
mIsFocused = isMouseInsideBox();
|
||||
mNeedsTextRefresh = true;
|
||||
// Cursor blink
|
||||
mCursorBlinkTimer += deltaTime;
|
||||
if (mCursorBlinkTimer >= 0.5f) {
|
||||
mShowCursor = !mShowCursor;
|
||||
mCursorBlinkTimer = 0.f;
|
||||
}
|
||||
|
||||
float mouseX = Input::getMouseX();
|
||||
float mouseY = Input::getMouseY();
|
||||
float left = mTransform.x;
|
||||
float right = mTransform.x + (mBackground ? mBackground->getWidth() * mTransform.adjustedScaleX() : 0.f);
|
||||
float top = mTransform.y;
|
||||
float bottom = mTransform.y + (mBackground ? mBackground->getHeight() * mTransform.adjustedScaleY() : 0.f);
|
||||
|
||||
bool inside = (mouseX >= left && mouseX <= right && mouseY >= top && mouseY <= bottom);
|
||||
if (inside && Input::isMouseButtonJustPressed(SDL_BUTTON_LEFT)) {
|
||||
mIsFocused = true;
|
||||
// Enable SDL text input on main thread (requires SDL_Window pointer)
|
||||
Window::Window::postToMainThread([]() {
|
||||
SDL_Window* win = Window::Window::getSDLWindowBackend();
|
||||
if (win) SDL_StartTextInput(win);
|
||||
});
|
||||
if (mOnFocus) {
|
||||
using FnType = void(*)();
|
||||
FnType fn = reinterpret_cast<FnType>(mOnFocus);
|
||||
fn();
|
||||
}
|
||||
} else if (Input::isMouseButtonJustPressed(SDL_BUTTON_LEFT) && !inside) {
|
||||
mIsFocused = false;
|
||||
// Disable SDL text input on main thread
|
||||
Window::Window::postToMainThread([]() {
|
||||
SDL_Window* win = Window::Window::getSDLWindowBackend();
|
||||
if (win) SDL_StopTextInput(win);
|
||||
});
|
||||
}
|
||||
|
||||
if (mIsFocused) {
|
||||
// Cursor blink
|
||||
mCursorTimer += deltaTime;
|
||||
if (mCursorTimer >= mConfig.cursorBlinkRate) {
|
||||
mCursorTimer = 0.f;
|
||||
mCursorVisible = !mCursorVisible;
|
||||
mNeedsTextRefresh = true;
|
||||
// Handle special keys
|
||||
if (Input::isKeyJustPressed(SDL_SCANCODE_BACKSPACE)) {
|
||||
if (mCursorIndex > 0 && !mText.empty()) {
|
||||
mText.erase(mCursorIndex - 1, 1);
|
||||
mCursorIndex = std::max<size_t>(0, mCursorIndex - 1);
|
||||
}
|
||||
}
|
||||
if (Input::isKeyJustPressed(SDL_SCANCODE_LEFT)) {
|
||||
if (mCursorIndex > 0) mCursorIndex--;
|
||||
}
|
||||
if (Input::isKeyJustPressed(SDL_SCANCODE_RIGHT)) {
|
||||
if (mCursorIndex < mText.size()) mCursorIndex++;
|
||||
}
|
||||
|
||||
if (Input::isKeyJustPressed(SDL_SCANCODE_BACKSPACE) && !mText.empty()) {
|
||||
mText.pop_back();
|
||||
mNeedsTextRefresh = true;
|
||||
}
|
||||
// Character insertion from scancodes
|
||||
|
||||
if (Input::isKeyJustPressed(SDL_SCANCODE_RETURN) || Input::isKeyJustPressed(SDL_SCANCODE_KP_ENTER)) {
|
||||
mIsFocused = false;
|
||||
mCursorTimer = 0.f;
|
||||
mCursorVisible = true;
|
||||
mNeedsTextRefresh = true;
|
||||
}
|
||||
bool shift = Input::isKeyPressed(SDL_SCANCODE_LSHIFT) || Input::isKeyPressed(SDL_SCANCODE_RSHIFT);
|
||||
|
||||
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) {
|
||||
if (mConfig.maxLength == 0 || static_cast<int>(mText.size()) < mConfig.maxLength) {
|
||||
mText.push_back(static_cast<char>(keycode));
|
||||
mNeedsTextRefresh = true;
|
||||
// Letters A-Z
|
||||
for (int sc = SDL_SCANCODE_A; sc <= SDL_SCANCODE_Z; ++sc) {
|
||||
if (Input::isKeyJustPressed(static_cast<SDL_Scancode>(sc))) {
|
||||
char c = scancodeToChar(static_cast<SDL_Scancode>(sc), shift);
|
||||
if (c && mText.size() < mMaxLength) {
|
||||
mText.insert(mCursorIndex, 1, c);
|
||||
mCursorIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Digits 1-9 and 0
|
||||
for (int sc = SDL_SCANCODE_1; sc <= SDL_SCANCODE_9; ++sc) {
|
||||
if (Input::isKeyJustPressed(static_cast<SDL_Scancode>(sc))) {
|
||||
char c = scancodeToChar(static_cast<SDL_Scancode>(sc), shift);
|
||||
if (c && mText.size() < mMaxLength) {
|
||||
mText.insert(mCursorIndex, 1, c);
|
||||
mCursorIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Input::isKeyJustPressed(SDL_SCANCODE_0)) {
|
||||
char c = scancodeToChar(SDL_SCANCODE_0, shift);
|
||||
if (c && mText.size() < mMaxLength) {
|
||||
mText.insert(mCursorIndex, 1, c);
|
||||
mCursorIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// Space and a few other keys
|
||||
if (Input::isKeyJustPressed(SDL_SCANCODE_SPACE)) {
|
||||
if (mText.size() < mMaxLength) {
|
||||
mText.insert(mCursorIndex, 1, ' ');
|
||||
mCursorIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mNeedsTextRefresh) {
|
||||
refreshVisualText();
|
||||
// Update rendered text only when needed
|
||||
std::string display = mPasswordMode ? std::string(mText.size(), '*') : mText;
|
||||
if (display.empty() && !mIsFocused && !mPlaceholder.empty()) {
|
||||
display = mPlaceholder;
|
||||
}
|
||||
if (display != mLastRenderedText) {
|
||||
if (mFont) {
|
||||
auto fontPtr = mFont;
|
||||
std::string txt = display;
|
||||
Window::Window::postToMainThread([fontPtr, txt]() { fontPtr->build({255,255,255,255}, txt); });
|
||||
mLastRenderedText = display;
|
||||
|
||||
if (mBackground) {
|
||||
mTransform.x = mX - mBackground->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
||||
mTransform.y = mY - mBackground->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When textbox is focused and display is empty, reserve placeholder width by rendering transparent placeholder
|
||||
// This prevents the textbox from shifting when placeholder disappears on focus
|
||||
if (mIsFocused && display.empty() && !mPlaceholder.empty() && mFont && mReservedPlaceholderWidth == 0.f) {
|
||||
auto fontPtr = mFont;
|
||||
std::string placeholder = mPlaceholder;
|
||||
// Post task to build placeholder with full transparency (alpha=0) to reserve width
|
||||
Window::Window::postToMainThread([fontPtr, placeholder, this]() {
|
||||
fontPtr->build({255,255,255,0}, placeholder); // alpha=0 for transparency
|
||||
// Store the width of the placeholder texture for layout stability
|
||||
float w = static_cast<float>(fontPtr->getWidth());
|
||||
{
|
||||
std::scoped_lock lock(mRenderMutex);
|
||||
mReservedPlaceholderWidth = w;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void UITextBox::render(Game::Renderer::Renderer* renderer, Game::Renderer::RendererConfig config) {
|
||||
if (!mIsVisible) return;
|
||||
if (!mIsActive || !mIsVisible) return;
|
||||
|
||||
SDL_Renderer* r = renderer->getSDLRenderer();
|
||||
// Render background texture if available
|
||||
if (mBackground) {
|
||||
SDL_FRect dst;
|
||||
SDL_GetTextureSize(mBackground->getSDLTexture(), &dst.w, &dst.h);
|
||||
dst.w *= mTransform.scaleX * UNIVERSAL_SCALE_COEFFICIENT;
|
||||
dst.h *= mTransform.scaleY * UNIVERSAL_SCALE_COEFFICIENT;
|
||||
dst.x = mTransform.x - config.camX + config.screenW / 2.f;
|
||||
dst.y = mTransform.y - config.camY + config.screenH / 2.f;
|
||||
|
||||
const float bx = mTransform.x - mConfig.paddingX - config.camX + config.screenW / 2.f;
|
||||
const float by = mTransform.y - mConfig.paddingY - config.camY + config.screenH / 2.f;
|
||||
const float bw = mBoxWidth + 2.f * mConfig.paddingX;
|
||||
const float bh = mBoxHeight + 2.f * mConfig.paddingY;
|
||||
const float t = mConfig.borderThickness;
|
||||
SDL_FPoint center;
|
||||
center.x = dst.w / 2.f;
|
||||
center.y = dst.h / 2.f;
|
||||
|
||||
// Background
|
||||
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
|
||||
const SDL_Color& bg = mConfig.bgColor;
|
||||
SDL_SetRenderDrawColor(r, bg.r, bg.g, bg.b, bg.a);
|
||||
const SDL_FRect bgRect = {bx, by, bw, bh};
|
||||
SDL_RenderFillRect(r, &bgRect);
|
||||
SDL_RenderTextureRotated(
|
||||
renderer->getSDLRenderer(),
|
||||
mBackground->getSDLTexture(),
|
||||
nullptr,
|
||||
&dst,
|
||||
mTransform.rotation,
|
||||
¢er,
|
||||
mIsFlipped ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE
|
||||
);
|
||||
}
|
||||
|
||||
// Border (4 filled rects for configurable thickness)
|
||||
const SDL_Color& bc = mIsFocused ? mConfig.focusedBorderColor : mConfig.borderColor;
|
||||
SDL_SetRenderDrawColor(r, bc.r, bc.g, bc.b, bc.a);
|
||||
const SDL_FRect borders[4] = {
|
||||
{bx, by, bw, t }, // top
|
||||
{bx, by + bh - t, bw, t }, // bottom
|
||||
{bx, by, t, bh}, // left
|
||||
{bx + bw - t, by, t, bh}, // right
|
||||
};
|
||||
SDL_RenderFillRects(r, borders, 4);
|
||||
// Render font texture (text) on top of background
|
||||
if (mFont && mFont->getSDLTexture()) {
|
||||
SDL_FRect dst;
|
||||
SDL_GetTextureSize(mFont->getSDLTexture(), &dst.w, &dst.h);
|
||||
dst.w *= mTransform.scaleX * UNIVERSAL_SCALE_COEFFICIENT;
|
||||
dst.h *= mTransform.scaleY * UNIVERSAL_SCALE_COEFFICIENT;
|
||||
dst.x = mTransform.x - config.camX + config.screenW / 2.f;
|
||||
dst.y = mTransform.y - config.camY + config.screenH / 2.f;
|
||||
|
||||
// Text (or placeholder) via base render
|
||||
Entity::render(renderer, config);
|
||||
SDL_FPoint center;
|
||||
center.x = dst.w / 2.f;
|
||||
center.y = dst.h / 2.f;
|
||||
|
||||
SDL_RenderTextureRotated(
|
||||
renderer->getSDLRenderer(),
|
||||
mFont->getSDLTexture(),
|
||||
nullptr,
|
||||
&dst,
|
||||
mTransform.rotation,
|
||||
¢er,
|
||||
mIsFlipped ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void UITextBox::setText(const std::string& text) {
|
||||
mText = text;
|
||||
mNeedsTextRefresh = true;
|
||||
refreshVisualText();
|
||||
mText = text.substr(0, mMaxLength);
|
||||
mCursorIndex = std::min(mText.size(), mCursorIndex);
|
||||
mLastRenderedText.clear(); // force rebuild
|
||||
}
|
||||
|
||||
std::string UITextBox::getText() const { return mText; }
|
||||
std::string UITextBox::getValue() const { return mText; }
|
||||
bool UITextBox::isFocused() const { return mIsFocused; }
|
||||
|
||||
bool UITextBox::isMouseInsideBox() const {
|
||||
// Get screen-space mouse coordinates
|
||||
const float screenMouseX = Input::getMouseX();
|
||||
const float screenMouseY = Input::getMouseY();
|
||||
|
||||
// Get window dimensions
|
||||
int windowW = 0, windowH = 0;
|
||||
SDL_GetWindowSizeInPixels(SDL_GetMouseFocus(), &windowW, &windowH);
|
||||
|
||||
// Get camera position
|
||||
float camX = 0.f, camY = 0.f;
|
||||
Object::Camera::getInstance().getPosition(camX, camY);
|
||||
|
||||
// Convert screen coordinates to world coordinates
|
||||
const float worldMouseX = screenMouseX - windowW / 2.f + camX;
|
||||
const float worldMouseY = screenMouseY - windowH / 2.f + camY;
|
||||
|
||||
// Check bounds in world space
|
||||
const float left = mTransform.x - mConfig.paddingX;
|
||||
const float right = left + mBoxWidth + 2.f * mConfig.paddingX;
|
||||
const float top = mTransform.y - mConfig.paddingY;
|
||||
const float bottom = top + mBoxHeight + 2.f * mConfig.paddingY;
|
||||
|
||||
return worldMouseX >= left && worldMouseX <= right && worldMouseY >= top && worldMouseY <= bottom;
|
||||
void UITextBox::insertText(const std::string& utf8) {
|
||||
// Insert UTF-8 text at cursor position, respecting max length
|
||||
if (utf8.empty() || mText.size() >= mMaxLength) return;
|
||||
|
||||
size_t availableSpace = mMaxLength - mText.size();
|
||||
size_t insertLen = std::min(utf8.size(), availableSpace);
|
||||
mText.insert(mCursorIndex, utf8.substr(0, insertLen));
|
||||
mCursorIndex += insertLen;
|
||||
mLastRenderedText.clear(); // force rebuild on next update
|
||||
mShowCursor = true; // Show cursor immediately when text inserted
|
||||
mCursorBlinkTimer = 0.f; // Reset blink timer
|
||||
}
|
||||
|
||||
void UITextBox::refreshVisualText() {
|
||||
const bool showPlaceholder = mText.empty() && !mIsFocused && !mConfig.placeholder.empty();
|
||||
|
||||
SDL_Color color;
|
||||
std::string rendered;
|
||||
|
||||
if (showPlaceholder) {
|
||||
color = mConfig.placeholderColor;
|
||||
rendered = mConfig.placeholder;
|
||||
} else {
|
||||
color = mIsFocused ? mConfig.focusedTextColor : mConfig.textColor;
|
||||
rendered = mText.empty() ? " " : mText;
|
||||
if (mIsFocused && mCursorVisible) {
|
||||
rendered += "_";
|
||||
}
|
||||
}
|
||||
|
||||
std::dynamic_pointer_cast<Renderer::Font>(mTex)->build(color, rendered);
|
||||
mNeedsTextRefresh = false;
|
||||
std::string UITextBox::getText() const {
|
||||
return mText;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -54,6 +54,12 @@ namespace Game::Renderer {
|
||||
return;
|
||||
}
|
||||
|
||||
// Text surfaces contain transparency around glyphs. If blending is not
|
||||
// enabled, transparent pixels can appear as opaque black boxes.
|
||||
if (!SDL_SetTextureBlendMode(mTex, SDL_BLENDMODE_BLEND)) {
|
||||
WARN("Failed to set blend mode to BLEND for font '" << mId << "': " << SDL_GetError());
|
||||
}
|
||||
|
||||
// Fonts look better with linear filtering than with nearest-neighbor scaling.
|
||||
if (!SDL_SetTextureScaleMode(mTex, SDL_SCALEMODE_LINEAR)) {
|
||||
WARN("Failed to set texture scale mode to LINEAR for font '" << mId << "': " << SDL_GetError());
|
||||
|
||||
@@ -2,9 +2,18 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <renderer/texture.hpp>
|
||||
#include <game/input.hpp>
|
||||
|
||||
namespace Game::Window {
|
||||
std::mutex Window::sMutex;
|
||||
// Tasks posted from other threads to run on the window thread
|
||||
std::mutex sTasksMutex;
|
||||
std::vector<std::function<void()>> sPostedTasks;
|
||||
|
||||
void Window::postToMainThread(std::function<void()> fn) {
|
||||
std::scoped_lock lock(sTasksMutex);
|
||||
sPostedTasks.push_back(std::move(fn));
|
||||
}
|
||||
|
||||
Window::Window() : mWindow(nullptr), mRenderer(), mGameManager(), mRunning(false) { }
|
||||
|
||||
@@ -88,9 +97,34 @@ namespace Game::Window {
|
||||
SDL_Event event;
|
||||
while (mRunning) {
|
||||
const auto frameStart = std::chrono::steady_clock::now();
|
||||
// Execute any tasks posted to the main/window thread
|
||||
std::vector<std::function<void()>> tasks;
|
||||
{
|
||||
std::scoped_lock lock(sTasksMutex);
|
||||
tasks.swap(sPostedTasks);
|
||||
}
|
||||
for (auto& t : tasks) {
|
||||
try { t(); } catch (...) { WARN("Exception in posted main-thread task"); }
|
||||
}
|
||||
|
||||
SDL_PumpEvents();
|
||||
while (SDL_PollEvent(&event)) {
|
||||
// Forward SDL text input events to the Input queue
|
||||
if (event.type == SDL_EVENT_TEXT_INPUT) {
|
||||
if (event.text.text && event.text.text[0]) {
|
||||
Game::Input::pushText(std::string(event.text.text));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Run custom callbacks on the window/event thread.
|
||||
if (event.type >= SDL_EVENT_USER && event.user.data1) {
|
||||
using UserEventFn = void(*)();
|
||||
UserEventFn fn = reinterpret_cast<UserEventFn>(event.user.data1);
|
||||
fn();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_QUIT) {
|
||||
mRunning = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user