feat: implement textured line clear effects and refine UI alignment
- **Visual Effects**: Upgraded line clear particles to use the game's block texture instead of simple circles, matching the reference web game's aesthetic. - **Particle Physics**: Tuned particle velocity, gravity, and fade rates for a more dynamic explosion effect. - **Rendering Integration**: Updated [main.cpp](cci:7://file:///d:/Sites/Work/tetris/src/main.cpp:0:0-0:0) and `GameRenderer` to pass the block texture to the effect system and correctly trigger animations upon line completion. - **Menu UI**: Fixed [MenuState](cci:1://file:///d:/Sites/Work/tetris/src/states/MenuState.cpp:19:0-19:55) layout calculations to use fixed logical dimensions (1200x1000), ensuring consistent centering and alignment of the logo, buttons, and settings icon across different window sizes. - **Code Cleanup**: Refactored `PlayingState` to delegate effect triggering to the rendering layer where correct screen coordinates are available.
This commit is contained in:
41
src/graphics/effects/Starfield.cpp
Normal file
41
src/graphics/effects/Starfield.cpp
Normal file
@ -0,0 +1,41 @@
|
||||
// Starfield.cpp - implementation (copied into src/graphics)
|
||||
#include "graphics/Starfield.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <random>
|
||||
|
||||
void Starfield::init(int count, int w, int h)
|
||||
{
|
||||
stars.clear();
|
||||
stars.reserve(count);
|
||||
std::mt19937 rng{std::random_device{}()};
|
||||
std::uniform_real_distribution<float> dx(0.f, (float)w), dy(0.f, (float)h), dz(0.2f, 1.f);
|
||||
for (int i = 0; i < count; ++i)
|
||||
stars.push_back({dx(rng), dy(rng), dz(rng), 15.f + 35.f * dz(rng)});
|
||||
lastW = w;
|
||||
lastH = h;
|
||||
}
|
||||
|
||||
void Starfield::update(float dt, int w, int h)
|
||||
{
|
||||
if (w != lastW || h != lastH || stars.empty())
|
||||
init((w * h) / 8000 + 120, w, h);
|
||||
for (auto &s : stars)
|
||||
{
|
||||
s.y += s.speed * dt;
|
||||
if (s.y > h)
|
||||
{
|
||||
s.y -= h;
|
||||
s.x = (float)(rand() % w);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Starfield::draw(SDL_Renderer *r) const
|
||||
{
|
||||
SDL_SetRenderDrawColor(r, 255, 255, 255, 255);
|
||||
for (auto &s : stars)
|
||||
{
|
||||
SDL_FRect fr{s.x, s.y, 1.f * s.z, 1.f * s.z};
|
||||
SDL_RenderFillRect(r, &fr);
|
||||
}
|
||||
}
|
||||
15
src/graphics/effects/Starfield.h
Normal file
15
src/graphics/effects/Starfield.h
Normal file
@ -0,0 +1,15 @@
|
||||
// Starfield.h - Procedural starfield background effect
|
||||
#pragma once
|
||||
#include <vector>
|
||||
struct SDL_Renderer; // fwd
|
||||
|
||||
class Starfield {
|
||||
public:
|
||||
void init(int count, int w, int h);
|
||||
void update(float dt, int w, int h);
|
||||
void draw(SDL_Renderer* r) const;
|
||||
private:
|
||||
struct Star { float x,y,z,speed; };
|
||||
std::vector<Star> stars;
|
||||
int lastW{0}, lastH{0};
|
||||
};
|
||||
164
src/graphics/effects/Starfield3D.cpp
Normal file
164
src/graphics/effects/Starfield3D.cpp
Normal file
@ -0,0 +1,164 @@
|
||||
// Starfield3D.cpp - 3D Parallax Starfield Implementation
|
||||
#include "Starfield3D.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
Starfield3D::Starfield3D() : rng(std::random_device{}()), width(800), height(600), centerX(400), centerY(300) {
|
||||
}
|
||||
|
||||
void Starfield3D::init(int w, int h, int starCount) {
|
||||
width = w;
|
||||
height = h;
|
||||
centerX = width * 0.5f;
|
||||
centerY = height * 0.5f;
|
||||
|
||||
stars.resize(starCount);
|
||||
createStarfield();
|
||||
}
|
||||
|
||||
void Starfield3D::resize(int w, int h) {
|
||||
width = w;
|
||||
height = h;
|
||||
centerX = width * 0.5f;
|
||||
centerY = height * 0.5f;
|
||||
}
|
||||
|
||||
float Starfield3D::randomFloat(float min, float max) {
|
||||
std::uniform_real_distribution<float> dist(min, max);
|
||||
return dist(rng);
|
||||
}
|
||||
|
||||
int Starfield3D::randomRange(int min, int max) {
|
||||
std::uniform_int_distribution<int> dist(min, max - 1);
|
||||
return dist(rng);
|
||||
}
|
||||
|
||||
void Starfield3D::setRandomDirection(Star3D& star) {
|
||||
star.targetVx = randomFloat(-MAX_VELOCITY, MAX_VELOCITY);
|
||||
star.targetVy = randomFloat(-MAX_VELOCITY, MAX_VELOCITY);
|
||||
|
||||
// Allow stars to move both toward and away from viewer
|
||||
if (randomFloat(0.0f, 1.0f) < REVERSE_PROBABILITY) {
|
||||
// Move away from viewer (positive Z)
|
||||
star.targetVz = STAR_SPEED * randomFloat(0.5f, 1.0f);
|
||||
} else {
|
||||
// Move toward viewer (negative Z)
|
||||
star.targetVz = -STAR_SPEED * randomFloat(0.7f, 1.3f);
|
||||
}
|
||||
|
||||
star.changing = true;
|
||||
star.changeTimer = randomFloat(30.0f, 120.0f); // Direction change lasts 30-120 frames
|
||||
}
|
||||
|
||||
void Starfield3D::updateStar(int index) {
|
||||
Star3D& star = stars[index];
|
||||
|
||||
star.x = randomFloat(-25.0f, 25.0f);
|
||||
star.y = randomFloat(-25.0f, 25.0f);
|
||||
star.z = randomFloat(1.0f, MAX_DEPTH);
|
||||
|
||||
// Give stars initial velocities in all possible directions
|
||||
if (randomFloat(0.0f, 1.0f) < 0.5f) {
|
||||
// Half stars start moving toward viewer
|
||||
star.vx = randomFloat(-0.1f, 0.1f);
|
||||
star.vy = randomFloat(-0.1f, 0.1f);
|
||||
star.vz = -STAR_SPEED * randomFloat(0.8f, 1.2f);
|
||||
} else {
|
||||
// Half stars start moving in random directions
|
||||
star.vx = randomFloat(-0.2f, 0.2f);
|
||||
star.vy = randomFloat(-0.2f, 0.2f);
|
||||
|
||||
// 30% chance to start moving away
|
||||
if (randomFloat(0.0f, 1.0f) < 0.3f) {
|
||||
star.vz = STAR_SPEED * randomFloat(0.5f, 0.8f);
|
||||
} else {
|
||||
star.vz = -STAR_SPEED * randomFloat(0.8f, 1.2f);
|
||||
}
|
||||
}
|
||||
|
||||
star.targetVx = star.vx;
|
||||
star.targetVy = star.vy;
|
||||
star.targetVz = star.vz;
|
||||
star.changing = false;
|
||||
star.changeTimer = 0.0f;
|
||||
star.type = randomRange(0, COLOR_COUNT);
|
||||
|
||||
// Give some stars initial direction variations
|
||||
if (randomFloat(0.0f, 1.0f) < 0.4f) {
|
||||
setRandomDirection(star);
|
||||
}
|
||||
}
|
||||
|
||||
void Starfield3D::createStarfield() {
|
||||
for (size_t i = 0; i < stars.size(); ++i) {
|
||||
updateStar(static_cast<int>(i));
|
||||
}
|
||||
}
|
||||
|
||||
void Starfield3D::update(float deltaTime) {
|
||||
const float frameRate = 60.0f; // Target 60 FPS for consistency
|
||||
const float frameMultiplier = deltaTime * frameRate;
|
||||
|
||||
for (size_t i = 0; i < stars.size(); ++i) {
|
||||
Star3D& star = stars[i];
|
||||
|
||||
// Randomly change direction occasionally
|
||||
if (!star.changing && randomFloat(0.0f, 1.0f) < DIRECTION_CHANGE_PROBABILITY * frameMultiplier) {
|
||||
setRandomDirection(star);
|
||||
}
|
||||
|
||||
// Update velocities to approach target values
|
||||
if (star.changing) {
|
||||
// Smoothly transition to target velocities
|
||||
const float change = VELOCITY_CHANGE * frameMultiplier;
|
||||
star.vx += (star.targetVx - star.vx) * change;
|
||||
star.vy += (star.targetVy - star.vy) * change;
|
||||
star.vz += (star.targetVz - star.vz) * change;
|
||||
|
||||
// Decrement change timer
|
||||
star.changeTimer -= frameMultiplier;
|
||||
if (star.changeTimer <= 0.0f) {
|
||||
star.changing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Update position using current velocity
|
||||
star.x += star.vx * frameMultiplier;
|
||||
star.y += star.vy * frameMultiplier;
|
||||
star.z += star.vz * frameMultiplier;
|
||||
|
||||
// Handle boundaries - reset star if it moves out of bounds, too close, or too far
|
||||
if (star.z <= MIN_Z ||
|
||||
star.z >= MAX_Z ||
|
||||
std::abs(star.x) > 50.0f ||
|
||||
std::abs(star.y) > 50.0f) {
|
||||
updateStar(static_cast<int>(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Starfield3D::drawStar(SDL_Renderer* renderer, float x, float y, int type) {
|
||||
const SDL_Color& color = STAR_COLORS[type % COLOR_COUNT];
|
||||
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
|
||||
|
||||
// Draw star as a small rectangle (1x1 pixel)
|
||||
SDL_FRect rect{x, y, 1.0f, 1.0f};
|
||||
SDL_RenderFillRect(renderer, &rect);
|
||||
}
|
||||
|
||||
void Starfield3D::draw(SDL_Renderer* renderer) {
|
||||
for (const Star3D& star : stars) {
|
||||
// Calculate perspective projection factor
|
||||
const float k = DEPTH_FACTOR / star.z;
|
||||
|
||||
// Calculate screen position with perspective
|
||||
const float px = star.x * k + centerX;
|
||||
const float py = star.y * k + centerY;
|
||||
|
||||
// Only draw stars that are within the viewport
|
||||
if (px >= 0.0f && px <= static_cast<float>(width) &&
|
||||
py >= 0.0f && py <= static_cast<float>(height)) {
|
||||
drawStar(renderer, px, py, star.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/graphics/effects/Starfield3D.h
Normal file
63
src/graphics/effects/Starfield3D.h
Normal file
@ -0,0 +1,63 @@
|
||||
// Starfield3D.h - 3D Parallax Starfield Effect (canonical)
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <vector>
|
||||
#include <random>
|
||||
#include <array>
|
||||
|
||||
class Starfield3D {
|
||||
public:
|
||||
Starfield3D();
|
||||
~Starfield3D() = default;
|
||||
|
||||
void init(int width, int height, int starCount = 160);
|
||||
void update(float deltaTime);
|
||||
void draw(SDL_Renderer* renderer);
|
||||
void resize(int width, int height);
|
||||
|
||||
private:
|
||||
struct Star3D {
|
||||
float x, y, z;
|
||||
float vx, vy, vz;
|
||||
float targetVx, targetVy, targetVz;
|
||||
float changeTimer;
|
||||
bool changing;
|
||||
int type;
|
||||
};
|
||||
|
||||
// Helpers used by the implementation
|
||||
void createStarfield();
|
||||
void updateStar(int index);
|
||||
void setRandomDirection(Star3D& star);
|
||||
float randomFloat(float min, float max);
|
||||
int randomRange(int min, int max);
|
||||
void drawStar(SDL_Renderer* renderer, float x, float y, int type);
|
||||
|
||||
std::vector<Star3D> stars;
|
||||
int width{0}, height{0};
|
||||
float centerX{0}, centerY{0};
|
||||
|
||||
// Random number generator
|
||||
std::mt19937 rng;
|
||||
|
||||
// Visual / behavioral constants (tweakable)
|
||||
inline static constexpr float MAX_VELOCITY = 0.5f;
|
||||
inline static constexpr float REVERSE_PROBABILITY = 0.12f;
|
||||
inline static constexpr float STAR_SPEED = 0.6f;
|
||||
inline static constexpr float MAX_DEPTH = 120.0f;
|
||||
inline static constexpr float DIRECTION_CHANGE_PROBABILITY = 0.002f;
|
||||
inline static constexpr float VELOCITY_CHANGE = 0.02f;
|
||||
inline static constexpr float MIN_Z = 0.1f;
|
||||
inline static constexpr float MAX_Z = MAX_DEPTH;
|
||||
inline static constexpr float DEPTH_FACTOR = 320.0f;
|
||||
|
||||
inline static constexpr int COLOR_COUNT = 5;
|
||||
inline static const std::array<SDL_Color, COLOR_COUNT> STAR_COLORS = {
|
||||
SDL_Color{255,255,255,255},
|
||||
SDL_Color{200,200,255,255},
|
||||
SDL_Color{255,220,180,255},
|
||||
SDL_Color{180,220,255,255},
|
||||
SDL_Color{255,180,200,255}
|
||||
};
|
||||
};
|
||||
480
src/graphics/renderers/GameRenderer.cpp
Normal file
480
src/graphics/renderers/GameRenderer.cpp
Normal file
@ -0,0 +1,480 @@
|
||||
#include "GameRenderer.h"
|
||||
#include "../../gameplay/core/Game.h"
|
||||
#include "../ui/Font.h"
|
||||
#include "../../gameplay/effects/LineEffect.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
// Color constants (copied from main.cpp)
|
||||
static const SDL_Color COLORS[] = {
|
||||
{0, 0, 0, 255}, // 0: BLACK (empty)
|
||||
{0, 255, 255, 255}, // 1: I-piece - cyan
|
||||
{255, 255, 0, 255}, // 2: O-piece - yellow
|
||||
{128, 0, 128, 255}, // 3: T-piece - purple
|
||||
{0, 255, 0, 255}, // 4: S-piece - green
|
||||
{255, 0, 0, 255}, // 5: Z-piece - red
|
||||
{0, 0, 255, 255}, // 6: J-piece - blue
|
||||
{255, 165, 0, 255} // 7: L-piece - orange
|
||||
};
|
||||
|
||||
void GameRenderer::drawRect(SDL_Renderer* renderer, float x, float y, float w, float h, SDL_Color c) {
|
||||
SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a);
|
||||
SDL_FRect fr{x, y, w, h};
|
||||
SDL_RenderFillRect(renderer, &fr);
|
||||
}
|
||||
|
||||
void GameRenderer::drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType) {
|
||||
if (!blocksTex || blockType < 0 || blockType >= PIECE_COUNT) {
|
||||
// Fallback to colored rectangle if texture isn't available
|
||||
SDL_Color color = (blockType >= 0 && blockType < PIECE_COUNT) ? COLORS[blockType + 1] : SDL_Color{128, 128, 128, 255};
|
||||
drawRect(renderer, x, y, size-1, size-1, color);
|
||||
return;
|
||||
}
|
||||
|
||||
// JavaScript uses: sx = type * spriteSize, sy = 0, with 2px padding
|
||||
// Each sprite is 90px wide in the horizontal sprite sheet
|
||||
const int SPRITE_SIZE = 90;
|
||||
float srcX = blockType * SPRITE_SIZE + 2; // Add 2px padding like JS
|
||||
float srcY = 2; // Add 2px padding from top like JS
|
||||
float srcW = SPRITE_SIZE - 4; // Subtract 4px total padding like JS
|
||||
float srcH = SPRITE_SIZE - 4; // Subtract 4px total padding like JS
|
||||
|
||||
SDL_FRect srcRect = {srcX, srcY, srcW, srcH};
|
||||
SDL_FRect dstRect = {x, y, size, size};
|
||||
SDL_RenderTexture(renderer, blocksTex, &srcRect, &dstRect);
|
||||
}
|
||||
|
||||
void GameRenderer::drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost) {
|
||||
if (piece.type >= PIECE_COUNT) return;
|
||||
|
||||
for (int cy = 0; cy < 4; ++cy) {
|
||||
for (int cx = 0; cx < 4; ++cx) {
|
||||
if (Game::cellFilled(piece, cx, cy)) {
|
||||
float px = ox + (piece.x + cx) * tileSize;
|
||||
float py = oy + (piece.y + cy) * tileSize;
|
||||
|
||||
if (isGhost) {
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
|
||||
// Draw ghost piece as barely visible gray outline
|
||||
SDL_SetRenderDrawColor(renderer, 180, 180, 180, 20); // Very faint gray
|
||||
SDL_FRect rect = {px + 2, py + 2, tileSize - 4, tileSize - 4};
|
||||
SDL_RenderFillRect(renderer, &rect);
|
||||
|
||||
// Draw thin gray border
|
||||
SDL_SetRenderDrawColor(renderer, 180, 180, 180, 30);
|
||||
SDL_FRect border = {px + 1, py + 1, tileSize - 2, tileSize - 2};
|
||||
SDL_RenderRect(renderer, &border);
|
||||
} else {
|
||||
drawBlockTexture(renderer, blocksTex, px, py, tileSize, piece.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameRenderer::drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize) {
|
||||
if (pieceType >= PIECE_COUNT) return;
|
||||
|
||||
// Use the first rotation (index 0) for preview
|
||||
Game::Piece previewPiece;
|
||||
previewPiece.type = pieceType;
|
||||
previewPiece.rot = 0;
|
||||
previewPiece.x = 0;
|
||||
previewPiece.y = 0;
|
||||
|
||||
// Center the piece in the preview area
|
||||
float offsetX = 0, offsetY = 0;
|
||||
if (pieceType == 0) { offsetX = tileSize * 0.5f; } // I-piece centering (assuming I = 0)
|
||||
else if (pieceType == 1) { offsetX = tileSize * 0.5f; } // O-piece centering (assuming O = 1)
|
||||
|
||||
// Use semi-transparent alpha for preview blocks
|
||||
Uint8 previewAlpha = 180;
|
||||
if (blocksTex) {
|
||||
SDL_SetTextureAlphaMod(blocksTex, previewAlpha);
|
||||
}
|
||||
|
||||
for (int cy = 0; cy < 4; ++cy) {
|
||||
for (int cx = 0; cx < 4; ++cx) {
|
||||
if (Game::cellFilled(previewPiece, cx, cy)) {
|
||||
float px = x + offsetX + cx * tileSize;
|
||||
float py = y + offsetY + cy * tileSize;
|
||||
drawBlockTexture(renderer, blocksTex, px, py, tileSize, pieceType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset alpha
|
||||
if (blocksTex) {
|
||||
SDL_SetTextureAlphaMod(blocksTex, 255);
|
||||
}
|
||||
}
|
||||
|
||||
void GameRenderer::renderPlayingState(
|
||||
SDL_Renderer* renderer,
|
||||
Game* game,
|
||||
FontAtlas* pixelFont,
|
||||
LineEffect* lineEffect,
|
||||
SDL_Texture* blocksTex,
|
||||
float logicalW,
|
||||
float logicalH,
|
||||
float logicalScale,
|
||||
float winW,
|
||||
float winH,
|
||||
bool showExitConfirmPopup
|
||||
) {
|
||||
if (!game || !pixelFont) return;
|
||||
|
||||
// Calculate actual content area (centered within the window)
|
||||
float contentScale = logicalScale;
|
||||
float contentW = logicalW * contentScale;
|
||||
float contentH = logicalH * contentScale;
|
||||
float contentOffsetX = (winW - contentW) * 0.5f / contentScale;
|
||||
float contentOffsetY = (winH - contentH) * 0.5f / contentScale;
|
||||
|
||||
// Helper lambda for drawing rectangles with content offset
|
||||
auto drawRectWithOffset = [&](float x, float y, float w, float h, SDL_Color c) {
|
||||
SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a);
|
||||
SDL_FRect fr{x + contentOffsetX, y + contentOffsetY, w, h};
|
||||
SDL_RenderFillRect(renderer, &fr);
|
||||
};
|
||||
|
||||
// Responsive layout that scales with window size while maintaining margins
|
||||
const float MIN_MARGIN = 40.0f;
|
||||
const float TOP_MARGIN = 60.0f;
|
||||
const float PANEL_WIDTH = 180.0f;
|
||||
const float PANEL_SPACING = 30.0f;
|
||||
const float NEXT_PIECE_HEIGHT = 120.0f;
|
||||
const float BOTTOM_MARGIN = 60.0f;
|
||||
|
||||
// Calculate layout dimensions
|
||||
const float availableWidth = logicalW - (MIN_MARGIN * 2) - (PANEL_WIDTH * 2) - (PANEL_SPACING * 2);
|
||||
const float availableHeight = logicalH - TOP_MARGIN - BOTTOM_MARGIN - NEXT_PIECE_HEIGHT;
|
||||
|
||||
const float maxBlockSizeW = availableWidth / Game::COLS;
|
||||
const float maxBlockSizeH = availableHeight / Game::ROWS;
|
||||
const float BLOCK_SIZE = std::min(maxBlockSizeW, maxBlockSizeH);
|
||||
const float finalBlockSize = std::max(20.0f, std::min(BLOCK_SIZE, 40.0f));
|
||||
|
||||
const float GRID_W = Game::COLS * finalBlockSize;
|
||||
const float GRID_H = Game::ROWS * finalBlockSize;
|
||||
|
||||
// Calculate positions
|
||||
const float totalContentHeight = NEXT_PIECE_HEIGHT + GRID_H;
|
||||
const float availableVerticalSpace = logicalH - TOP_MARGIN - BOTTOM_MARGIN;
|
||||
const float verticalCenterOffset = (availableVerticalSpace - totalContentHeight) * 0.5f;
|
||||
const float contentStartY = TOP_MARGIN + verticalCenterOffset;
|
||||
|
||||
const float totalLayoutWidth = PANEL_WIDTH + PANEL_SPACING + GRID_W + PANEL_SPACING + PANEL_WIDTH;
|
||||
const float layoutStartX = (logicalW - totalLayoutWidth) * 0.5f;
|
||||
|
||||
const float statsX = layoutStartX + contentOffsetX;
|
||||
const float gridX = layoutStartX + PANEL_WIDTH + PANEL_SPACING + contentOffsetX;
|
||||
const float scoreX = layoutStartX + PANEL_WIDTH + PANEL_SPACING + GRID_W + PANEL_SPACING + contentOffsetX;
|
||||
const float gridY = contentStartY + NEXT_PIECE_HEIGHT + contentOffsetY;
|
||||
|
||||
const float statsY = gridY;
|
||||
const float statsW = PANEL_WIDTH;
|
||||
const float statsH = GRID_H;
|
||||
|
||||
// Next piece preview position
|
||||
const float nextW = finalBlockSize * 4 + 20;
|
||||
const float nextH = finalBlockSize * 2 + 20;
|
||||
const float nextX = gridX + (GRID_W - nextW) * 0.5f;
|
||||
const float nextY = contentStartY + contentOffsetY;
|
||||
|
||||
// Handle line clearing effects
|
||||
if (game->hasCompletedLines() && lineEffect && !lineEffect->isActive()) {
|
||||
auto completedLines = game->getCompletedLines();
|
||||
lineEffect->startLineClear(completedLines, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize));
|
||||
}
|
||||
|
||||
// Draw game grid border
|
||||
drawRectWithOffset(gridX - 3 - contentOffsetX, gridY - 3 - contentOffsetY, GRID_W + 6, GRID_H + 6, {100, 120, 200, 255});
|
||||
drawRectWithOffset(gridX - 1 - contentOffsetX, gridY - 1 - contentOffsetY, GRID_W + 2, GRID_H + 2, {60, 80, 160, 255});
|
||||
drawRectWithOffset(gridX - contentOffsetX, gridY - contentOffsetY, GRID_W, GRID_H, {20, 25, 35, 255});
|
||||
|
||||
// Draw panel backgrounds
|
||||
SDL_SetRenderDrawColor(renderer, 10, 15, 25, 160);
|
||||
SDL_FRect lbg{statsX - 16, gridY - 10, statsW + 32, GRID_H + 20};
|
||||
SDL_RenderFillRect(renderer, &lbg);
|
||||
|
||||
SDL_FRect rbg{scoreX - 16, gridY - 16, statsW + 32, GRID_H + 32};
|
||||
SDL_RenderFillRect(renderer, &rbg);
|
||||
|
||||
// Draw grid lines
|
||||
SDL_SetRenderDrawColor(renderer, 40, 45, 60, 255);
|
||||
for (int x = 1; x < Game::COLS; ++x) {
|
||||
float lineX = gridX + x * finalBlockSize;
|
||||
SDL_RenderLine(renderer, lineX, gridY, lineX, gridY + GRID_H);
|
||||
}
|
||||
for (int y = 1; y < Game::ROWS; ++y) {
|
||||
float lineY = gridY + y * finalBlockSize;
|
||||
SDL_RenderLine(renderer, gridX, lineY, gridX + GRID_W, lineY);
|
||||
}
|
||||
|
||||
// Draw block statistics panel border
|
||||
drawRectWithOffset(statsX - 3 - contentOffsetX, statsY - 3 - contentOffsetY, statsW + 6, statsH + 6, {100, 120, 200, 255});
|
||||
drawRectWithOffset(statsX - contentOffsetX, statsY - contentOffsetY, statsW, statsH, {30, 35, 50, 255});
|
||||
|
||||
// Draw next piece preview panel border
|
||||
drawRectWithOffset(nextX - 3 - contentOffsetX, nextY - 3 - contentOffsetY, nextW + 6, nextH + 6, {100, 120, 200, 255});
|
||||
drawRectWithOffset(nextX - contentOffsetX, nextY - contentOffsetY, nextW, nextH, {30, 35, 50, 255});
|
||||
|
||||
// Draw the game board
|
||||
const auto &board = game->boardRef();
|
||||
for (int y = 0; y < Game::ROWS; ++y) {
|
||||
for (int x = 0; x < Game::COLS; ++x) {
|
||||
int v = board[y * Game::COLS + x];
|
||||
if (v > 0) {
|
||||
float bx = gridX + x * finalBlockSize;
|
||||
float by = gridY + y * finalBlockSize;
|
||||
drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw ghost piece (where current piece will land)
|
||||
if (!game->isPaused()) {
|
||||
Game::Piece ghostPiece = game->current();
|
||||
// Find landing position
|
||||
while (true) {
|
||||
Game::Piece testPiece = ghostPiece;
|
||||
testPiece.y++;
|
||||
bool collision = false;
|
||||
|
||||
// Simple collision check
|
||||
for (int cy = 0; cy < 4; ++cy) {
|
||||
for (int cx = 0; cx < 4; ++cx) {
|
||||
if (Game::cellFilled(testPiece, cx, cy)) {
|
||||
int gx = testPiece.x + cx;
|
||||
int gy = testPiece.y + cy;
|
||||
if (gy >= Game::ROWS || gx < 0 || gx >= Game::COLS ||
|
||||
(gy >= 0 && board[gy * Game::COLS + gx] != 0)) {
|
||||
collision = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (collision) break;
|
||||
}
|
||||
|
||||
if (collision) break;
|
||||
ghostPiece = testPiece;
|
||||
}
|
||||
|
||||
// Draw ghost piece
|
||||
drawPiece(renderer, blocksTex, ghostPiece, gridX, gridY, finalBlockSize, true);
|
||||
}
|
||||
|
||||
// Draw the falling piece
|
||||
if (!game->isPaused()) {
|
||||
drawPiece(renderer, blocksTex, game->current(), gridX, gridY, finalBlockSize, false);
|
||||
}
|
||||
|
||||
// Draw line clearing effects
|
||||
if (lineEffect && lineEffect->isActive()) {
|
||||
lineEffect->render(renderer, blocksTex, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize));
|
||||
}
|
||||
|
||||
// Draw next piece preview
|
||||
pixelFont->draw(renderer, nextX + 10, nextY - 20, "NEXT", 1.0f, {255, 220, 0, 255});
|
||||
if (game->next().type < PIECE_COUNT) {
|
||||
drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(game->next().type), nextX + 10, nextY + 10, finalBlockSize * 0.6f);
|
||||
}
|
||||
|
||||
// Draw block statistics (left panel)
|
||||
pixelFont->draw(renderer, statsX + 10, statsY + 10, "BLOCKS", 1.0f, {255, 220, 0, 255});
|
||||
|
||||
const auto& blockCounts = game->getBlockCounts();
|
||||
int totalBlocks = 0;
|
||||
for (int i = 0; i < PIECE_COUNT; ++i) totalBlocks += blockCounts[i];
|
||||
|
||||
const char* pieceNames[] = {"I", "O", "T", "S", "Z", "J", "L"};
|
||||
float yCursor = statsY + 52;
|
||||
|
||||
for (int i = 0; i < PIECE_COUNT; ++i) {
|
||||
float py = yCursor;
|
||||
|
||||
// Draw small piece icon
|
||||
float previewSize = finalBlockSize * 0.55f;
|
||||
drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(i), statsX + 18, py, previewSize);
|
||||
|
||||
// Compute preview height
|
||||
int maxCy = -1;
|
||||
Game::Piece prev;
|
||||
prev.type = static_cast<PieceType>(i);
|
||||
prev.rot = 0;
|
||||
prev.x = 0;
|
||||
prev.y = 0;
|
||||
for (int cy = 0; cy < 4; ++cy) {
|
||||
for (int cx = 0; cx < 4; ++cx) {
|
||||
if (Game::cellFilled(prev, cx, cy)) maxCy = std::max(maxCy, cy);
|
||||
}
|
||||
}
|
||||
int tilesHigh = (maxCy >= 0 ? maxCy + 1 : 1);
|
||||
float previewHeight = tilesHigh * previewSize;
|
||||
|
||||
// Count display
|
||||
int count = blockCounts[i];
|
||||
char countStr[16];
|
||||
snprintf(countStr, sizeof(countStr), "%d", count);
|
||||
pixelFont->draw(renderer, statsX + statsW - 20, py + 6, countStr, 1.1f, {240, 240, 245, 255});
|
||||
|
||||
// Percentage bar
|
||||
int perc = (totalBlocks > 0) ? int(std::round(100.0 * double(count) / double(totalBlocks))) : 0;
|
||||
char percStr[16];
|
||||
snprintf(percStr, sizeof(percStr), "%d%%", perc);
|
||||
|
||||
float barX = statsX + 12;
|
||||
float barY = py + previewHeight + 18.0f;
|
||||
float barW = statsW - 24;
|
||||
float barH = 6;
|
||||
|
||||
pixelFont->draw(renderer, barX, barY - 16, percStr, 0.8f, {230, 230, 235, 255});
|
||||
|
||||
// Progress bar
|
||||
SDL_SetRenderDrawColor(renderer, 170, 170, 175, 200);
|
||||
SDL_FRect track{barX, barY, barW, barH};
|
||||
SDL_RenderFillRect(renderer, &track);
|
||||
|
||||
SDL_Color pc = COLORS[i + 1];
|
||||
SDL_SetRenderDrawColor(renderer, pc.r, pc.g, pc.b, 230);
|
||||
float fillW = barW * (perc / 100.0f);
|
||||
if (fillW < 0) fillW = 0;
|
||||
if (fillW > barW) fillW = barW;
|
||||
SDL_FRect fill{barX, barY, fillW, barH};
|
||||
SDL_RenderFillRect(renderer, &fill);
|
||||
|
||||
yCursor = barY + barH + 18.0f;
|
||||
}
|
||||
|
||||
// Draw score panel (right side)
|
||||
const float contentTopOffset = 0.0f;
|
||||
const float contentBottomOffset = 290.0f;
|
||||
const float contentPad = 36.0f;
|
||||
float scoreContentH = (contentBottomOffset - contentTopOffset) + contentPad;
|
||||
float baseY = gridY + (GRID_H - scoreContentH) * 0.5f;
|
||||
|
||||
pixelFont->draw(renderer, scoreX, baseY + 0, "SCORE", 1.0f, {255, 220, 0, 255});
|
||||
char scoreStr[32];
|
||||
snprintf(scoreStr, sizeof(scoreStr), "%d", game->score());
|
||||
pixelFont->draw(renderer, scoreX, baseY + 25, scoreStr, 0.9f, {255, 255, 255, 255});
|
||||
|
||||
pixelFont->draw(renderer, scoreX, baseY + 70, "LINES", 1.0f, {255, 220, 0, 255});
|
||||
char linesStr[16];
|
||||
snprintf(linesStr, sizeof(linesStr), "%03d", game->lines());
|
||||
pixelFont->draw(renderer, scoreX, baseY + 95, linesStr, 0.9f, {255, 255, 255, 255});
|
||||
|
||||
pixelFont->draw(renderer, scoreX, baseY + 140, "LEVEL", 1.0f, {255, 220, 0, 255});
|
||||
char levelStr[16];
|
||||
snprintf(levelStr, sizeof(levelStr), "%02d", game->level());
|
||||
pixelFont->draw(renderer, scoreX, baseY + 165, levelStr, 0.9f, {255, 255, 255, 255});
|
||||
|
||||
// Next level progress
|
||||
int startLv = game->startLevelBase();
|
||||
int firstThreshold = (startLv + 1) * 10;
|
||||
int linesDone = game->lines();
|
||||
int nextThreshold = 0;
|
||||
if (linesDone < firstThreshold) {
|
||||
nextThreshold = firstThreshold;
|
||||
} else {
|
||||
int blocksPast = linesDone - firstThreshold;
|
||||
nextThreshold = firstThreshold + ((blocksPast / 10) + 1) * 10;
|
||||
}
|
||||
int linesForNext = std::max(0, nextThreshold - linesDone);
|
||||
pixelFont->draw(renderer, scoreX, baseY + 200, "NEXT LVL", 1.0f, {255, 220, 0, 255});
|
||||
char nextStr[32];
|
||||
snprintf(nextStr, sizeof(nextStr), "%d LINES", linesForNext);
|
||||
pixelFont->draw(renderer, scoreX, baseY + 225, nextStr, 0.9f, {80, 255, 120, 255});
|
||||
|
||||
// Time display
|
||||
pixelFont->draw(renderer, scoreX, baseY + 265, "TIME", 1.0f, {255, 220, 0, 255});
|
||||
int totalSecs = static_cast<int>(game->elapsed());
|
||||
int mins = totalSecs / 60;
|
||||
int secs = totalSecs % 60;
|
||||
char timeStr[16];
|
||||
snprintf(timeStr, sizeof(timeStr), "%02d:%02d", mins, secs);
|
||||
pixelFont->draw(renderer, scoreX, baseY + 290, timeStr, 0.9f, {255, 255, 255, 255});
|
||||
|
||||
// Gravity HUD
|
||||
char gms[64];
|
||||
double gms_val = game->getGravityMs();
|
||||
double gfps = gms_val > 0.0 ? (1000.0 / gms_val) : 0.0;
|
||||
snprintf(gms, sizeof(gms), "GRAV: %.0f ms (%.2f fps)", gms_val, gfps);
|
||||
pixelFont->draw(renderer, logicalW - 260, 10, gms, 0.9f, {200, 200, 220, 255});
|
||||
|
||||
// Hold piece (if implemented)
|
||||
if (game->held().type < PIECE_COUNT) {
|
||||
pixelFont->draw(renderer, statsX + 10, statsY + statsH - 80, "HOLD", 1.0f, {255, 220, 0, 255});
|
||||
drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(game->held().type), statsX + 60, statsY + statsH - 80, finalBlockSize * 0.6f);
|
||||
}
|
||||
|
||||
// Pause overlay
|
||||
if (game->isPaused() && !showExitConfirmPopup) {
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180);
|
||||
SDL_FRect pauseOverlay{0, 0, logicalW, logicalH};
|
||||
SDL_RenderFillRect(renderer, &pauseOverlay);
|
||||
|
||||
pixelFont->draw(renderer, logicalW * 0.5f - 80, logicalH * 0.5f - 20, "PAUSED", 2.0f, {255, 255, 255, 255});
|
||||
pixelFont->draw(renderer, logicalW * 0.5f - 120, logicalH * 0.5f + 30, "Press P to resume", 0.8f, {200, 200, 220, 255});
|
||||
}
|
||||
|
||||
// Exit confirmation popup
|
||||
if (showExitConfirmPopup) {
|
||||
float popupW = 420.0f, popupH = 180.0f;
|
||||
float popupX = (logicalW - popupW) * 0.5f;
|
||||
float popupY = (logicalH - popupH) * 0.5f;
|
||||
|
||||
// Dim entire window (do not change viewport/scales here)
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 200);
|
||||
SDL_FRect fullWin{0.f, 0.f, winW, winH};
|
||||
SDL_RenderFillRect(renderer, &fullWin);
|
||||
|
||||
// Draw popup box in logical coords with content offsets
|
||||
drawRectWithOffset(popupX - 4.0f, popupY - 4.0f, popupW + 8.0f, popupH + 8.0f, {60, 70, 90, 255});
|
||||
drawRectWithOffset(popupX, popupY, popupW, popupH, {20, 22, 28, 240});
|
||||
|
||||
// Text content (measure to perfectly center)
|
||||
const std::string title = "Exit game?";
|
||||
const std::string line1 = "Are you sure you want to";
|
||||
const std::string line2 = "leave the current game?";
|
||||
|
||||
int wTitle=0,hTitle=0; pixelFont->measure(title, 1.6f, wTitle, hTitle);
|
||||
int wL1=0,hL1=0; pixelFont->measure(line1, 0.9f, wL1, hL1);
|
||||
int wL2=0,hL2=0; pixelFont->measure(line2, 0.9f, wL2, hL2);
|
||||
|
||||
float titleX = popupX + (popupW - (float)wTitle) * 0.5f + contentOffsetX;
|
||||
float l1X = popupX + (popupW - (float)wL1) * 0.5f + contentOffsetX;
|
||||
float l2X = popupX + (popupW - (float)wL2) * 0.5f + contentOffsetX;
|
||||
|
||||
pixelFont->draw(renderer, titleX, popupY + contentOffsetY + 20.0f, title, 1.6f, {255, 220, 0, 255});
|
||||
pixelFont->draw(renderer, l1X, popupY + contentOffsetY + 60.0f, line1, 0.9f, {220, 220, 230, 255});
|
||||
pixelFont->draw(renderer, l2X, popupY + contentOffsetY + 84.0f, line2, 0.9f, {220, 220, 230, 255});
|
||||
|
||||
// Buttons
|
||||
float btnW = 140.0f, btnH = 46.0f;
|
||||
float yesX = popupX + popupW * 0.25f - btnW * 0.5f;
|
||||
float noX = popupX + popupW * 0.75f - btnW * 0.5f;
|
||||
float btnY = popupY + popupH - 60.0f;
|
||||
|
||||
// YES button
|
||||
drawRectWithOffset(yesX - 2.0f, btnY - 2.0f, btnW + 4.0f, btnH + 4.0f, {100, 120, 140, 255});
|
||||
drawRectWithOffset(yesX, btnY, btnW, btnH, {200, 60, 60, 255});
|
||||
const std::string yes = "YES";
|
||||
int wYes=0,hYes=0; pixelFont->measure(yes, 1.0f, wYes, hYes);
|
||||
pixelFont->draw(renderer, yesX + (btnW - (float)wYes) * 0.5f + contentOffsetX,
|
||||
btnY + (btnH - (float)hYes) * 0.5f + contentOffsetY,
|
||||
yes, 1.0f, {255, 255, 255, 255});
|
||||
|
||||
// NO button
|
||||
drawRectWithOffset(noX - 2.0f, btnY - 2.0f, btnW + 4.0f, btnH + 4.0f, {100, 120, 140, 255});
|
||||
drawRectWithOffset(noX, btnY, btnW, btnH, {80, 140, 80, 255});
|
||||
const std::string no = "NO";
|
||||
int wNo=0,hNo=0; pixelFont->measure(no, 1.0f, wNo, hNo);
|
||||
pixelFont->draw(renderer, noX + (btnW - (float)wNo) * 0.5f + contentOffsetX,
|
||||
btnY + (btnH - (float)hNo) * 0.5f + contentOffsetY,
|
||||
no, 1.0f, {255, 255, 255, 255});
|
||||
}
|
||||
}
|
||||
40
src/graphics/renderers/GameRenderer.h
Normal file
40
src/graphics/renderers/GameRenderer.h
Normal file
@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
#include "../../gameplay/core/Game.h"
|
||||
|
||||
// Forward declarations
|
||||
class FontAtlas;
|
||||
class LineEffect;
|
||||
|
||||
/**
|
||||
* GameRenderer - Utility class for rendering the Tetris game board and HUD.
|
||||
*
|
||||
* This class encapsulates all the game-specific rendering logic that was
|
||||
* previously in main.cpp, making it reusable across different contexts.
|
||||
*/
|
||||
class GameRenderer {
|
||||
public:
|
||||
// Render the complete playing state including game board, HUD, and effects
|
||||
static void renderPlayingState(
|
||||
SDL_Renderer* renderer,
|
||||
Game* game,
|
||||
FontAtlas* pixelFont,
|
||||
LineEffect* lineEffect,
|
||||
SDL_Texture* blocksTex,
|
||||
float logicalW,
|
||||
float logicalH,
|
||||
float logicalScale,
|
||||
float winW,
|
||||
float winH,
|
||||
bool showExitConfirmPopup
|
||||
);
|
||||
|
||||
private:
|
||||
// Helper functions for drawing game elements
|
||||
static void drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType);
|
||||
static void drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost = false);
|
||||
static void drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize);
|
||||
|
||||
// Helper function for drawing rectangles
|
||||
static void drawRect(SDL_Renderer* renderer, float x, float y, float w, float h, SDL_Color c);
|
||||
};
|
||||
328
src/graphics/renderers/RenderManager.cpp
Normal file
328
src/graphics/renderers/RenderManager.cpp
Normal file
@ -0,0 +1,328 @@
|
||||
#include "RenderManager.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
|
||||
RenderManager::RenderManager() = default;
|
||||
|
||||
RenderManager::~RenderManager() {
|
||||
if (m_initialized) {
|
||||
shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
bool RenderManager::initialize(int width, int height, const std::string& title) {
|
||||
if (m_initialized) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_RENDER, "RenderManager already initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_RENDER, "Initializing RenderManager (%dx%d)", width, height);
|
||||
|
||||
// Create window
|
||||
m_window = SDL_CreateWindow(
|
||||
title.c_str(),
|
||||
width, height,
|
||||
SDL_WINDOW_RESIZABLE
|
||||
);
|
||||
|
||||
if (!m_window) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_RENDER, "Failed to create window: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create renderer
|
||||
m_renderer = SDL_CreateRenderer(m_window, nullptr);
|
||||
if (!m_renderer) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_RENDER, "Failed to create renderer: %s", SDL_GetError());
|
||||
SDL_DestroyWindow(m_window);
|
||||
m_window = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enable VSync
|
||||
SDL_SetRenderVSync(m_renderer, 1);
|
||||
|
||||
// Store window dimensions
|
||||
m_windowWidth = width;
|
||||
m_windowHeight = height;
|
||||
m_logicalWidth = width;
|
||||
m_logicalHeight = height;
|
||||
|
||||
// Set initial logical size
|
||||
setLogicalSize(width, height);
|
||||
|
||||
m_initialized = true;
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_RENDER, "RenderManager initialized successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderManager::shutdown() {
|
||||
if (!m_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_RENDER, "Shutting down RenderManager");
|
||||
|
||||
if (m_renderer) {
|
||||
SDL_DestroyRenderer(m_renderer);
|
||||
m_renderer = nullptr;
|
||||
}
|
||||
|
||||
if (m_window) {
|
||||
SDL_DestroyWindow(m_window);
|
||||
m_window = nullptr;
|
||||
}
|
||||
|
||||
m_initialized = false;
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_RENDER, "RenderManager shutdown complete");
|
||||
}
|
||||
|
||||
void RenderManager::beginFrame() {
|
||||
if (!m_initialized || !m_renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Trace beginFrame entry
|
||||
{
|
||||
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::beginFrame entry\n"); fclose(f); }
|
||||
}
|
||||
|
||||
// Clear the screen (wrapped with trace)
|
||||
clear(12, 12, 16, 255); // Dark background similar to original
|
||||
|
||||
// Trace after clear
|
||||
{
|
||||
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::beginFrame after clear\n"); fclose(f); }
|
||||
}
|
||||
}
|
||||
|
||||
void RenderManager::endFrame() {
|
||||
if (!m_initialized || !m_renderer) {
|
||||
return;
|
||||
}
|
||||
// Trace before present
|
||||
{
|
||||
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::endFrame before present\n"); fclose(f); }
|
||||
}
|
||||
|
||||
SDL_RenderPresent(m_renderer);
|
||||
|
||||
// Trace after present
|
||||
{
|
||||
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::endFrame after present\n"); fclose(f); }
|
||||
}
|
||||
}
|
||||
|
||||
void RenderManager::setLogicalSize(int width, int height) {
|
||||
if (!m_initialized || !m_renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_logicalWidth = width;
|
||||
m_logicalHeight = height;
|
||||
updateLogicalScale();
|
||||
}
|
||||
|
||||
void RenderManager::setViewport(int x, int y, int width, int height) {
|
||||
if (!m_initialized || !m_renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_Rect viewport = { x, y, width, height };
|
||||
SDL_SetRenderViewport(m_renderer, &viewport);
|
||||
// Keep cached logical viewport in sync if this matches our computed logical scale
|
||||
m_logicalVP = viewport;
|
||||
}
|
||||
|
||||
void RenderManager::setScale(float scaleX, float scaleY) {
|
||||
if (!m_initialized || !m_renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_scaleX = scaleX;
|
||||
m_scaleY = scaleY;
|
||||
SDL_SetRenderScale(m_renderer, scaleX, scaleY);
|
||||
}
|
||||
|
||||
void RenderManager::resetViewport() {
|
||||
if (!m_initialized || !m_renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset to full window viewport and recompute logical scale/viewport
|
||||
SDL_SetRenderViewport(m_renderer, nullptr);
|
||||
updateLogicalScale();
|
||||
}
|
||||
|
||||
// IRenderer interface implementation
|
||||
void RenderManager::clearScreen(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
|
||||
clear(static_cast<Uint8>(r), static_cast<Uint8>(g), static_cast<Uint8>(b), static_cast<Uint8>(a));
|
||||
}
|
||||
|
||||
void RenderManager::present() {
|
||||
if (!m_initialized || !m_renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_RenderPresent(m_renderer);
|
||||
}
|
||||
|
||||
SDL_Renderer* RenderManager::getSDLRenderer() {
|
||||
return m_renderer;
|
||||
}
|
||||
|
||||
void RenderManager::getWindowDimensions(int& width, int& height) const {
|
||||
getWindowSize(width, height);
|
||||
}
|
||||
|
||||
void RenderManager::setViewport(const SDL_Rect* viewport) {
|
||||
if (!m_initialized || !m_renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_SetRenderViewport(m_renderer, viewport);
|
||||
}
|
||||
|
||||
// Legacy clear method
|
||||
void RenderManager::clear(Uint8 r, Uint8 g, Uint8 b, Uint8 a) {
|
||||
if (!m_initialized || !m_renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_SetRenderDrawColor(m_renderer, r, g, b, a);
|
||||
SDL_RenderClear(m_renderer);
|
||||
}
|
||||
|
||||
void RenderManager::renderTexture(SDL_Texture* texture, const SDL_FRect* src, const SDL_FRect* dst) {
|
||||
if (!m_initialized || !m_renderer || !texture) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Trace renderTexture usage
|
||||
{
|
||||
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::renderTexture entry tex=%llu src=%p dst=%p\n", (unsigned long long)(uintptr_t)texture, (void*)src, (void*)dst); fclose(f); }
|
||||
}
|
||||
SDL_RenderTexture(m_renderer, texture, src, dst);
|
||||
{
|
||||
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::renderTexture after SDL_RenderTexture tex=%llu\n", (unsigned long long)(uintptr_t)texture); fclose(f); }
|
||||
}
|
||||
}
|
||||
|
||||
void RenderManager::renderTexture(SDL_Texture* texture, float x, float y) {
|
||||
if (!texture) {
|
||||
return;
|
||||
}
|
||||
|
||||
float w, h;
|
||||
SDL_GetTextureSize(texture, &w, &h);
|
||||
SDL_FRect dst = { x, y, w, h };
|
||||
renderTexture(texture, nullptr, &dst);
|
||||
}
|
||||
|
||||
void RenderManager::renderTexture(SDL_Texture* texture, float x, float y, float w, float h) {
|
||||
if (!texture) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_FRect dst = { x, y, w, h };
|
||||
renderTexture(texture, nullptr, &dst);
|
||||
}
|
||||
|
||||
void RenderManager::renderRect(const SDL_FRect& rect, Uint8 r, Uint8 g, Uint8 b, Uint8 a) {
|
||||
if (!m_initialized || !m_renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_SetRenderDrawColor(m_renderer, r, g, b, a);
|
||||
SDL_RenderFillRect(m_renderer, &rect);
|
||||
}
|
||||
|
||||
void RenderManager::renderLine(float x1, float y1, float x2, float y2, Uint8 r, Uint8 g, Uint8 b, Uint8 a) {
|
||||
if (!m_initialized || !m_renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_SetRenderDrawColor(m_renderer, r, g, b, a);
|
||||
SDL_RenderLine(m_renderer, x1, y1, x2, y2);
|
||||
}
|
||||
|
||||
void RenderManager::renderPoint(float x, float y, Uint8 r, Uint8 g, Uint8 b, Uint8 a) {
|
||||
if (!m_initialized || !m_renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_SetRenderDrawColor(m_renderer, r, g, b, a);
|
||||
SDL_RenderPoint(m_renderer, x, y);
|
||||
}
|
||||
|
||||
void RenderManager::handleWindowResize(int newWidth, int newHeight) {
|
||||
if (!m_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_RENDER, "Window resized to %dx%d", newWidth, newHeight);
|
||||
|
||||
m_windowWidth = newWidth;
|
||||
m_windowHeight = newHeight;
|
||||
updateLogicalScale();
|
||||
}
|
||||
|
||||
void RenderManager::setFullscreen(bool fullscreen) {
|
||||
if (!m_initialized || !m_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_isFullscreen == fullscreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_SetWindowFullscreen(m_window, fullscreen ? SDL_WINDOW_FULLSCREEN : 0);
|
||||
m_isFullscreen = fullscreen;
|
||||
|
||||
// Update window size after fullscreen change
|
||||
SDL_GetWindowSize(m_window, &m_windowWidth, &m_windowHeight);
|
||||
updateLogicalScale();
|
||||
}
|
||||
|
||||
void RenderManager::getWindowSize(int& width, int& height) const {
|
||||
width = m_windowWidth;
|
||||
height = m_windowHeight;
|
||||
}
|
||||
|
||||
void RenderManager::getTextureSize(SDL_Texture* tex, int& w, int& h) const {
|
||||
if (!tex) { w = 0; h = 0; return; }
|
||||
// SDL3 provides SDL_GetTextureSize which accepts float or int pointers depending on overloads
|
||||
float fw = 0.0f, fh = 0.0f;
|
||||
SDL_GetTextureSize(tex, &fw, &fh);
|
||||
w = int(fw + 0.5f);
|
||||
h = int(fh + 0.5f);
|
||||
}
|
||||
|
||||
void RenderManager::updateLogicalScale() {
|
||||
if (!m_initialized || !m_renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate scale to fit logical size into window
|
||||
float scaleX = static_cast<float>(m_windowWidth) / static_cast<float>(m_logicalWidth);
|
||||
float scaleY = static_cast<float>(m_windowHeight) / static_cast<float>(m_logicalHeight);
|
||||
|
||||
// Use uniform scaling to maintain aspect ratio
|
||||
float scale = std::min(scaleX, scaleY);
|
||||
if (scale <= 0.0f) {
|
||||
scale = 1.0f;
|
||||
}
|
||||
|
||||
setScale(scale, scale);
|
||||
// Compute centered logical viewport that preserves aspect ratio and is centered in window
|
||||
int vpW = static_cast<int>(m_logicalWidth * scale);
|
||||
int vpH = static_cast<int>(m_logicalHeight * scale);
|
||||
int vpX = (m_windowWidth - vpW) / 2;
|
||||
int vpY = (m_windowHeight - vpH) / 2;
|
||||
SDL_Rect vp{ vpX, vpY, vpW, vpH };
|
||||
SDL_SetRenderViewport(m_renderer, &vp);
|
||||
|
||||
// Cache logical viewport and scale for callers
|
||||
m_logicalVP = vp;
|
||||
m_logicalScale = scale;
|
||||
}
|
||||
95
src/graphics/renderers/RenderManager.h
Normal file
95
src/graphics/renderers/RenderManager.h
Normal file
@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <string>
|
||||
#include "../../core/interfaces/IRenderer.h"
|
||||
|
||||
/**
|
||||
* RenderManager - Abstracts SDL rendering functionality
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Manage SDL_Window and SDL_Renderer lifecycle
|
||||
* - Provide high-level rendering interface
|
||||
* - Handle viewport and scaling logic
|
||||
* - Abstract SDL-specific details from game code
|
||||
*/
|
||||
class RenderManager : public IRenderer {
|
||||
public:
|
||||
RenderManager();
|
||||
~RenderManager();
|
||||
|
||||
// Initialization and cleanup
|
||||
bool initialize(int width, int height, const std::string& title);
|
||||
void shutdown();
|
||||
|
||||
// Frame management
|
||||
void beginFrame();
|
||||
void endFrame();
|
||||
|
||||
// IRenderer interface implementation
|
||||
void clearScreen(uint8_t r, uint8_t g, uint8_t b, uint8_t a) override;
|
||||
void present() override;
|
||||
SDL_Renderer* getSDLRenderer() override;
|
||||
void getWindowDimensions(int& width, int& height) const override;
|
||||
void setViewport(const SDL_Rect* viewport) override;
|
||||
void setScale(float scaleX, float scaleY) override;
|
||||
|
||||
// Additional RenderManager-specific methods
|
||||
void setLogicalSize(int width, int height);
|
||||
void setViewport(int x, int y, int width, int height);
|
||||
void resetViewport();
|
||||
|
||||
// Query the computed logical viewport and scale (useful for consistent input mapping)
|
||||
SDL_Rect getLogicalViewport() const { return m_logicalVP; }
|
||||
float getLogicalScale() const { return m_logicalScale; }
|
||||
|
||||
// Basic rendering operations (legacy method signature)
|
||||
void clear(Uint8 r = 0, Uint8 g = 0, Uint8 b = 0, Uint8 a = 255);
|
||||
|
||||
// Texture rendering
|
||||
void renderTexture(SDL_Texture* texture, const SDL_FRect* src, const SDL_FRect* dst);
|
||||
void renderTexture(SDL_Texture* texture, float x, float y);
|
||||
void renderTexture(SDL_Texture* texture, float x, float y, float w, float h);
|
||||
|
||||
// Primitive rendering
|
||||
void renderRect(const SDL_FRect& rect, Uint8 r, Uint8 g, Uint8 b, Uint8 a = 255);
|
||||
void renderLine(float x1, float y1, float x2, float y2, Uint8 r, Uint8 g, Uint8 b, Uint8 a = 255);
|
||||
void renderPoint(float x, float y, Uint8 r, Uint8 g, Uint8 b, Uint8 a = 255);
|
||||
|
||||
// Window management
|
||||
void handleWindowResize(int newWidth, int newHeight);
|
||||
void setFullscreen(bool fullscreen);
|
||||
void getWindowSize(int& width, int& height) const;
|
||||
|
||||
// Direct access to SDL objects (temporary, will be removed later)
|
||||
SDL_Window* getSDLWindow() const { return m_window; }
|
||||
// Texture queries
|
||||
void getTextureSize(SDL_Texture* tex, int& w, int& h) const;
|
||||
|
||||
// State queries
|
||||
bool isInitialized() const { return m_initialized; }
|
||||
bool isFullscreen() const { return m_isFullscreen; }
|
||||
|
||||
private:
|
||||
// SDL objects
|
||||
SDL_Window* m_window = nullptr;
|
||||
SDL_Renderer* m_renderer = nullptr;
|
||||
|
||||
// Window properties
|
||||
int m_windowWidth = 0;
|
||||
int m_windowHeight = 0;
|
||||
int m_logicalWidth = 0;
|
||||
int m_logicalHeight = 0;
|
||||
float m_scaleX = 1.0f;
|
||||
float m_scaleY = 1.0f;
|
||||
|
||||
// State
|
||||
bool m_initialized = false;
|
||||
bool m_isFullscreen = false;
|
||||
// Cached logical viewport and scale (centered within window)
|
||||
SDL_Rect m_logicalVP{0,0,0,0};
|
||||
float m_logicalScale = 1.0f;
|
||||
|
||||
// Helper methods
|
||||
void updateLogicalScale();
|
||||
};
|
||||
36
src/graphics/ui/Font.cpp
Normal file
36
src/graphics/ui/Font.cpp
Normal file
@ -0,0 +1,36 @@
|
||||
// Font.cpp - implementation of FontAtlas (copied into src/graphics)
|
||||
#include "graphics/Font.h"
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
bool FontAtlas::init(const std::string& path, int basePt) { fontPath = path; baseSize = basePt; return true; }
|
||||
|
||||
void FontAtlas::shutdown() { for (auto &kv : cache) if (kv.second) TTF_CloseFont(kv.second); cache.clear(); }
|
||||
|
||||
TTF_Font* FontAtlas::getSized(int ptSize) {
|
||||
auto it = cache.find(ptSize); if (it!=cache.end()) return it->second;
|
||||
TTF_Font* f = TTF_OpenFont(fontPath.c_str(), ptSize);
|
||||
if (!f) return nullptr; cache[ptSize] = f; return f;
|
||||
}
|
||||
|
||||
void FontAtlas::draw(SDL_Renderer* r, float x, float y, const std::string& text, float scale, SDL_Color color) {
|
||||
if (scale <= 0) return; int pt = int(baseSize * scale); if (pt < 8) pt = 8; TTF_Font* f = getSized(pt); if (!f) return;
|
||||
SDL_Surface* surf = TTF_RenderText_Blended(f, text.c_str(), text.length(), color); if (!surf) return;
|
||||
SDL_Texture* tex = SDL_CreateTextureFromSurface(r, surf);
|
||||
if (tex) { SDL_FRect dst{ x, y, (float)surf->w, (float)surf->h }; SDL_RenderTexture(r, tex, nullptr, &dst); SDL_DestroyTexture(tex); }
|
||||
SDL_DestroySurface(surf);
|
||||
}
|
||||
|
||||
void FontAtlas::measure(const std::string& text, float scale, int& outW, int& outH) {
|
||||
outW = 0; outH = 0;
|
||||
if (scale <= 0) return;
|
||||
int pt = int(baseSize * scale);
|
||||
if (pt < 1) pt = 1;
|
||||
TTF_Font* f = getSized(pt);
|
||||
if (!f) return;
|
||||
// Use render-to-surface measurement to avoid dependency on specific TTF_* measurement API variants
|
||||
SDL_Color dummy = {255,255,255,255};
|
||||
SDL_Surface* surf = TTF_RenderText_Blended(f, text.c_str(), text.length(), dummy);
|
||||
if (!surf) return;
|
||||
outW = surf->w; outH = surf->h;
|
||||
SDL_DestroySurface(surf);
|
||||
}
|
||||
20
src/graphics/ui/Font.h
Normal file
20
src/graphics/ui/Font.h
Normal file
@ -0,0 +1,20 @@
|
||||
// Font.h - Font rendering abstraction with simple size cache
|
||||
#pragma once
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
struct SDL_Renderer;
|
||||
|
||||
class FontAtlas {
|
||||
public:
|
||||
bool init(const std::string& path, int basePt);
|
||||
void shutdown();
|
||||
void draw(SDL_Renderer* r, float x, float y, const std::string& text, float scale, SDL_Color color);
|
||||
// Measure rendered text size in pixels for a given scale
|
||||
void measure(const std::string& text, float scale, int& outW, int& outH);
|
||||
private:
|
||||
std::string fontPath;
|
||||
int baseSize{24};
|
||||
std::unordered_map<int, TTF_Font*> cache; // point size -> font*
|
||||
TTF_Font* getSized(int ptSize);
|
||||
};
|
||||
Reference in New Issue
Block a user