Fade from main menu to gameplay

This commit is contained in:
2025-11-22 20:18:00 +01:00
parent c0bee9296a
commit 3c3a85d6d4
7 changed files with 167 additions and 17 deletions

View File

@ -123,8 +123,11 @@ void GameRenderer::renderPlayingState(
float logicalScale, float logicalScale,
float winW, float winW,
float winH, float winH,
bool showExitConfirmPopup bool showExitConfirmPopup,
int exitPopupSelectedButton,
bool suppressPauseVisuals
) { ) {
(void)exitPopupSelectedButton;
if (!game || !pixelFont) return; if (!game || !pixelFont) return;
// Calculate actual content area (centered within the window) // Calculate actual content area (centered within the window)
@ -236,8 +239,10 @@ void GameRenderer::renderPlayingState(
} }
} }
bool allowActivePieceRender = !game->isPaused() || suppressPauseVisuals;
// Draw ghost piece (where current piece will land) // Draw ghost piece (where current piece will land)
if (!game->isPaused()) { if (allowActivePieceRender) {
Game::Piece ghostPiece = game->current(); Game::Piece ghostPiece = game->current();
// Find landing position // Find landing position
while (true) { while (true) {
@ -270,7 +275,7 @@ void GameRenderer::renderPlayingState(
} }
// Draw the falling piece // Draw the falling piece
if (!game->isPaused()) { if (allowActivePieceRender) {
drawPiece(renderer, blocksTex, game->current(), gridX, gridY, finalBlockSize, false); drawPiece(renderer, blocksTex, game->current(), gridX, gridY, finalBlockSize, false);
} }
@ -412,8 +417,8 @@ void GameRenderer::renderPlayingState(
drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(game->held().type), statsX + 60, statsY + statsH - 80, finalBlockSize * 0.6f); drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(game->held().type), statsX + 60, statsY + statsH - 80, finalBlockSize * 0.6f);
} }
// Pause overlay // Pause overlay (suppressed when requested, e.g., countdown)
if (game->isPaused() && !showExitConfirmPopup) { if (!suppressPauseVisuals && game->isPaused() && !showExitConfirmPopup) {
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180);
SDL_FRect pauseOverlay{0, 0, logicalW, logicalH}; SDL_FRect pauseOverlay{0, 0, logicalW, logicalH};
SDL_RenderFillRect(renderer, &pauseOverlay); SDL_RenderFillRect(renderer, &pauseOverlay);

View File

@ -26,7 +26,9 @@ public:
float logicalScale, float logicalScale,
float winW, float winW,
float winH, float winH,
bool showExitConfirmPopup bool showExitConfirmPopup,
int exitPopupSelectedButton = 1,
bool suppressPauseVisuals = false
); );
private: private:

View File

@ -124,7 +124,8 @@ void GameRenderer::renderPlayingState(
float winW, float winW,
float winH, float winH,
bool showExitConfirmPopup, bool showExitConfirmPopup,
int exitPopupSelectedButton int exitPopupSelectedButton,
bool suppressPauseVisuals
) { ) {
if (!game || !pixelFont) return; if (!game || !pixelFont) return;
@ -237,8 +238,10 @@ void GameRenderer::renderPlayingState(
} }
} }
bool allowActivePieceRender = !game->isPaused() || suppressPauseVisuals;
// Draw ghost piece (where current piece will land) // Draw ghost piece (where current piece will land)
if (!game->isPaused()) { if (allowActivePieceRender) {
Game::Piece ghostPiece = game->current(); Game::Piece ghostPiece = game->current();
// Find landing position // Find landing position
while (true) { while (true) {
@ -271,7 +274,7 @@ void GameRenderer::renderPlayingState(
} }
// Draw the falling piece // Draw the falling piece
if (!game->isPaused()) { if (allowActivePieceRender) {
drawPiece(renderer, blocksTex, game->current(), gridX, gridY, finalBlockSize, false); drawPiece(renderer, blocksTex, game->current(), gridX, gridY, finalBlockSize, false);
} }
@ -413,8 +416,8 @@ void GameRenderer::renderPlayingState(
drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(game->held().type), statsX + 60, statsY + statsH - 80, finalBlockSize * 0.6f); drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(game->held().type), statsX + 60, statsY + statsH - 80, finalBlockSize * 0.6f);
} }
// Pause overlay // Pause overlay (skip when visuals are suppressed, e.g., countdown)
if (game->isPaused() && !showExitConfirmPopup) { if (!suppressPauseVisuals && game->isPaused() && !showExitConfirmPopup) {
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180);
SDL_FRect pauseOverlay{0, 0, logicalW, logicalH}; SDL_FRect pauseOverlay{0, 0, logicalW, logicalH};
SDL_RenderFillRect(renderer, &pauseOverlay); SDL_RenderFillRect(renderer, &pauseOverlay);

View File

@ -27,7 +27,8 @@ public:
float winW, float winW,
float winH, float winH,
bool showExitConfirmPopup, bool showExitConfirmPopup,
int exitPopupSelectedButton = 1 // 0=YES, 1=NO int exitPopupSelectedButton = 1, // 0=YES, 1=NO
bool suppressPauseVisuals = false
); );
private: private:

View File

@ -627,6 +627,19 @@ int main(int, char **)
int currentTrackLoading = 0; int currentTrackLoading = 0;
int totalTracks = 0; // Will be set dynamically based on actual files int totalTracks = 0; // Will be set dynamically based on actual files
enum class MenuFadePhase { None, FadeOut, FadeIn };
MenuFadePhase menuFadePhase = MenuFadePhase::None;
double menuFadeClockMs = 0.0;
float menuFadeAlpha = 0.0f;
const double MENU_PLAY_FADE_DURATION_MS = 450.0;
AppState menuFadeTarget = AppState::Menu;
bool menuPlayCountdownArmed = false;
bool gameplayCountdownActive = false;
double gameplayCountdownElapsed = 0.0;
int gameplayCountdownIndex = 0;
const double GAMEPLAY_COUNTDOWN_STEP_MS = 600.0;
const std::array<const char*, 4> GAMEPLAY_COUNTDOWN_LABELS = { "3", "2", "1", "START" };
// Instantiate state manager // Instantiate state manager
StateManager stateMgr(state); StateManager stateMgr(state);
@ -666,6 +679,29 @@ int main(int, char **)
running = false; running = false;
}; };
auto startMenuPlayTransition = [&]() {
if (!ctx.stateManager) {
return;
}
if (state != AppState::Menu) {
state = AppState::Playing;
ctx.stateManager->setState(state);
return;
}
if (menuFadePhase != MenuFadePhase::None) {
return;
}
menuFadePhase = MenuFadePhase::FadeOut;
menuFadeClockMs = 0.0;
menuFadeAlpha = 0.0f;
menuFadeTarget = AppState::Playing;
menuPlayCountdownArmed = true;
gameplayCountdownActive = false;
gameplayCountdownIndex = 0;
gameplayCountdownElapsed = 0.0;
};
ctx.startPlayTransition = startMenuPlayTransition;
// Instantiate state objects // Instantiate state objects
auto loadingState = std::make_unique<LoadingState>(ctx); auto loadingState = std::make_unique<LoadingState>(ctx);
auto menuState = std::make_unique<MenuState>(ctx); auto menuState = std::make_unique<MenuState>(ctx);
@ -822,9 +858,7 @@ int main(int, char **)
}; };
if (pointInRect(buttonRects[0])) { if (pointInRect(buttonRects[0])) {
game.reset(startLevelSelection); startMenuPlayTransition();
state = AppState::Playing;
stateMgr.setState(state);
} else if (pointInRect(buttonRects[1])) { } else if (pointInRect(buttonRects[1])) {
state = AppState::LevelSelector; state = AppState::LevelSelector;
stateMgr.setState(state); stateMgr.setState(state);
@ -1180,6 +1214,63 @@ int main(int, char **)
break; break;
} }
if (menuFadePhase == MenuFadePhase::FadeOut) {
menuFadeClockMs += frameMs;
menuFadeAlpha = std::min(1.0f, float(menuFadeClockMs / MENU_PLAY_FADE_DURATION_MS));
if (menuFadeClockMs >= MENU_PLAY_FADE_DURATION_MS) {
if (menuFadeTarget == AppState::Playing) {
state = menuFadeTarget;
stateMgr.setState(state);
menuPlayCountdownArmed = true;
gameplayCountdownActive = false;
gameplayCountdownIndex = 0;
gameplayCountdownElapsed = 0.0;
game.setPaused(true);
}
menuFadePhase = MenuFadePhase::FadeIn;
menuFadeClockMs = MENU_PLAY_FADE_DURATION_MS;
menuFadeAlpha = 1.0f;
}
} else if (menuFadePhase == MenuFadePhase::FadeIn) {
menuFadeClockMs -= frameMs;
menuFadeAlpha = std::max(0.0f, float(menuFadeClockMs / MENU_PLAY_FADE_DURATION_MS));
if (menuFadeClockMs <= 0.0) {
menuFadePhase = MenuFadePhase::None;
menuFadeClockMs = 0.0;
menuFadeAlpha = 0.0f;
}
}
if (menuFadePhase == MenuFadePhase::None && menuPlayCountdownArmed && !gameplayCountdownActive && state == AppState::Playing) {
gameplayCountdownActive = true;
menuPlayCountdownArmed = false;
gameplayCountdownElapsed = 0.0;
gameplayCountdownIndex = 0;
game.setPaused(true);
}
if (gameplayCountdownActive && state == AppState::Playing) {
gameplayCountdownElapsed += frameMs;
if (gameplayCountdownElapsed >= GAMEPLAY_COUNTDOWN_STEP_MS) {
gameplayCountdownElapsed -= GAMEPLAY_COUNTDOWN_STEP_MS;
++gameplayCountdownIndex;
if (gameplayCountdownIndex >= static_cast<int>(GAMEPLAY_COUNTDOWN_LABELS.size())) {
gameplayCountdownActive = false;
gameplayCountdownElapsed = 0.0;
gameplayCountdownIndex = 0;
game.setPaused(false);
}
}
}
if (state != AppState::Playing && gameplayCountdownActive) {
gameplayCountdownActive = false;
menuPlayCountdownArmed = false;
gameplayCountdownElapsed = 0.0;
gameplayCountdownIndex = 0;
game.setPaused(false);
}
// --- Render --- // --- Render ---
SDL_SetRenderViewport(renderer, nullptr); SDL_SetRenderViewport(renderer, nullptr);
SDL_SetRenderDrawColor(renderer, 12, 12, 16, 255); SDL_SetRenderDrawColor(renderer, 12, 12, 16, 255);
@ -1387,7 +1478,8 @@ int main(int, char **)
(float)winW, (float)winW,
(float)winH, (float)winH,
showExitConfirmPopup, showExitConfirmPopup,
exitPopupSelectedButton exitPopupSelectedButton,
(gameplayCountdownActive || menuPlayCountdownArmed)
); );
break; break;
case AppState::GameOver: case AppState::GameOver:
@ -1514,6 +1606,44 @@ int main(int, char **)
break; break;
} }
if (menuFadeAlpha > 0.0f) {
SDL_SetRenderViewport(renderer, nullptr);
SDL_SetRenderScale(renderer, 1.f, 1.f);
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
Uint8 alpha = Uint8(std::clamp(menuFadeAlpha, 0.0f, 1.0f) * 255.0f);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, alpha);
SDL_FRect fadeRect{0.f, 0.f, (float)winW, (float)winH};
SDL_RenderFillRect(renderer, &fadeRect);
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
SDL_SetRenderViewport(renderer, &logicalVP);
SDL_SetRenderScale(renderer, logicalScale, logicalScale);
}
if (gameplayCountdownActive && state == AppState::Playing) {
SDL_SetRenderViewport(renderer, nullptr);
SDL_SetRenderScale(renderer, 1.f, 1.f);
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 160);
SDL_FRect dimRect{0.f, 0.f, (float)winW, (float)winH};
SDL_RenderFillRect(renderer, &dimRect);
SDL_SetRenderViewport(renderer, &logicalVP);
SDL_SetRenderScale(renderer, logicalScale, logicalScale);
int cappedIndex = std::min(gameplayCountdownIndex, static_cast<int>(GAMEPLAY_COUNTDOWN_LABELS.size()) - 1);
const char* label = GAMEPLAY_COUNTDOWN_LABELS[cappedIndex];
bool isFinalCue = (cappedIndex == static_cast<int>(GAMEPLAY_COUNTDOWN_LABELS.size()) - 1);
float textScale = isFinalCue ? 2.6f : 3.6f;
int textW = 0, textH = 0;
pixelFont.measure(label, textScale, textW, textH);
float textX = (LOGICAL_W - static_cast<float>(textW)) * 0.5f;
float textY = (LOGICAL_H - static_cast<float>(textH)) * 0.5f;
SDL_Color textColor = isFinalCue ? SDL_Color{255, 230, 90, 255} : SDL_Color{255, 255, 255, 255};
pixelFont.draw(renderer, textX, textY, label, textScale, textColor);
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
}
SDL_RenderPresent(renderer); SDL_RenderPresent(renderer);
SDL_SetRenderScale(renderer, 1.f, 1.f); SDL_SetRenderScale(renderer, 1.f, 1.f);
} }

View File

@ -40,6 +40,14 @@ void MenuState::onExit() {
void MenuState::handleEvent(const SDL_Event& e) { void MenuState::handleEvent(const SDL_Event& e) {
// Keyboard navigation for menu buttons // Keyboard navigation for menu buttons
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
auto triggerPlay = [&]() {
if (ctx.startPlayTransition) {
ctx.startPlayTransition();
} else if (ctx.stateManager) {
ctx.stateManager->setState(AppState::Playing);
}
};
auto setExitSelection = [&](int value) { auto setExitSelection = [&](int value) {
if (ctx.exitPopupSelectedButton) { if (ctx.exitPopupSelectedButton) {
*ctx.exitPopupSelectedButton = value; *ctx.exitPopupSelectedButton = value;
@ -115,7 +123,7 @@ void MenuState::handleEvent(const SDL_Event& e) {
} }
switch (selectedButton) { switch (selectedButton) {
case 0: case 0:
ctx.stateManager->setState(AppState::Playing); triggerPlay();
break; break;
case 1: case 1:
ctx.stateManager->setState(AppState::LevelSelector); ctx.stateManager->setState(AppState::LevelSelector);

View File

@ -55,6 +55,7 @@ struct StateContext {
std::function<void(bool)> applyFullscreen; // Allows states to request fullscreen changes std::function<void(bool)> applyFullscreen; // Allows states to request fullscreen changes
std::function<bool()> queryFullscreen; // Optional callback if fullscreenFlag is not reliable std::function<bool()> queryFullscreen; // Optional callback if fullscreenFlag is not reliable
std::function<void()> requestQuit; // Allows menu/option states to close the app gracefully std::function<void()> requestQuit; // Allows menu/option states to close the app gracefully
std::function<void()> startPlayTransition; // Optional fade hook when transitioning from menu to gameplay
// Pointer to the application's StateManager so states can request transitions // Pointer to the application's StateManager so states can request transitions
StateManager* stateManager = nullptr; StateManager* stateManager = nullptr;
}; };