feat(starfield): capa parallax al fons del playfield amb tint blanc-cyan

This commit is contained in:
2026-05-22 19:46:57 +02:00
parent 88b002b277
commit 1db7368c9f
6 changed files with 248 additions and 0 deletions
+1
View File
@@ -26,6 +26,7 @@
#include "core/defaults/playfield.hpp"
#include "core/defaults/rendering.hpp"
#include "core/defaults/ship.hpp"
#include "core/defaults/starfield_parallax.hpp"
#include "core/defaults/title.hpp"
#include "core/defaults/trail.hpp"
#include "core/defaults/window.hpp"
@@ -0,0 +1,36 @@
// starfield_parallax.hpp - Capa de fons del playfield: estrelles 2D amb parallax
// © 2026 JailDesigner
//
// 3 capes de profunditat. Cada capa té estrelles amb brillantor, mida i
// factor parallax propis. Les més properes són més brillants i grans i es
// mouen més ràpid quan el món es desplaça; les més llunyanes són tènues i
// petites i amb prou feines es mouen.
#pragma once
namespace Defaults::StarfieldParallax {
namespace Far {
constexpr int COUNT = 60;
constexpr float BRIGHTNESS = 0.15F;
constexpr float PARALLAX_FACTOR = 0.15F; // multiplicador sobre world_velocity
constexpr int SIZE_PX = 1; // 1 px (punt)
} // namespace Far
namespace Mid {
constexpr int COUNT = 50;
constexpr float BRIGHTNESS = 0.30F;
constexpr float PARALLAX_FACTOR = 0.35F;
constexpr int SIZE_PX = 2; // creu de 3x3 (extensió ±1)
} // namespace Mid
namespace Near {
constexpr int COUNT = 40;
constexpr float BRIGHTNESS = 0.55F;
constexpr float PARALLAX_FACTOR = 0.70F;
constexpr int SIZE_PX = 3; // creu de 5x5 (extensió ±2)
} // namespace Near
constexpr int TOTAL_COUNT = Far::COUNT + Mid::COUNT + Near::COUNT;
} // namespace Defaults::StarfieldParallax
+145
View File
@@ -0,0 +1,145 @@
// starfield_parallax.cpp - Implementació del starfield 2D amb parallax
// © 2026 JailDesigner
#include "core/graphics/starfield_parallax.hpp"
#include <cstdlib>
#include "core/defaults.hpp"
#include "core/rendering/line_renderer.hpp"
namespace Graphics {
namespace {
auto randUniform(float min_v, float max_v) -> float {
const float NORM = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
return min_v + (NORM * (max_v - min_v));
}
} // namespace
StarfieldParallax::StarfieldParallax(Rendering::Renderer* renderer)
: renderer_(renderer) {
buildStars();
}
void StarfieldParallax::buildStars() {
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
const float MIN_X = zona.x;
const float MAX_X = zona.x + zona.w;
const float MIN_Y = zona.y;
const float MAX_Y = zona.y + zona.h;
// Tint aleatori entre blanc (255,255,255) i cyan (0,255,255) per estrella.
// T ∈ [0,1]: 0 → blanc; 1 → cyan. R = 255·(1-T), G=B=255.
const auto FILL_LAYER = [&](int layer, int count, int& idx) {
for (int i = 0; i < count; i++) {
const float T = randUniform(0.0F, 1.0F);
stars_[idx++] = Star{
.x = randUniform(MIN_X, MAX_X),
.y = randUniform(MIN_Y, MAX_Y),
.layer = layer,
.color = SDL_Color{
.r = static_cast<Uint8>(255.0F * (1.0F - T)),
.g = 255,
.b = 255,
.a = 255}};
}
};
int idx = 0;
FILL_LAYER(0, Defaults::StarfieldParallax::Far::COUNT, idx);
FILL_LAYER(1, Defaults::StarfieldParallax::Mid::COUNT, idx);
FILL_LAYER(2, Defaults::StarfieldParallax::Near::COUNT, idx);
}
auto StarfieldParallax::layerBrightness(int layer) -> float {
switch (layer) {
case 0:
return Defaults::StarfieldParallax::Far::BRIGHTNESS;
case 1:
return Defaults::StarfieldParallax::Mid::BRIGHTNESS;
case 2:
return Defaults::StarfieldParallax::Near::BRIGHTNESS;
default:
return 0.0F;
}
}
auto StarfieldParallax::layerParallax(int layer) -> float {
switch (layer) {
case 0:
return Defaults::StarfieldParallax::Far::PARALLAX_FACTOR;
case 1:
return Defaults::StarfieldParallax::Mid::PARALLAX_FACTOR;
case 2:
return Defaults::StarfieldParallax::Near::PARALLAX_FACTOR;
default:
return 0.0F;
}
}
auto StarfieldParallax::layerSize(int layer) -> int {
switch (layer) {
case 0:
return Defaults::StarfieldParallax::Far::SIZE_PX;
case 1:
return Defaults::StarfieldParallax::Mid::SIZE_PX;
case 2:
return Defaults::StarfieldParallax::Near::SIZE_PX;
default:
return 1;
}
}
void StarfieldParallax::update(float delta_time, Vec2 world_velocity) {
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
const float MIN_X = zona.x;
const float MAX_X = zona.x + zona.w;
const float MIN_Y = zona.y;
const float MAX_Y = zona.y + zona.h;
const float W = zona.w;
const float H = zona.h;
for (auto& star : stars_) {
const float FACTOR = layerParallax(star.layer);
star.x += world_velocity.x * FACTOR * delta_time;
star.y += world_velocity.y * FACTOR * delta_time;
// Wraparound (PLAYAREA torica).
while (star.x < MIN_X) {
star.x += W;
}
while (star.x > MAX_X) {
star.x -= W;
}
while (star.y < MIN_Y) {
star.y += H;
}
while (star.y > MAX_Y) {
star.y -= H;
}
}
}
void StarfieldParallax::draw() const {
for (const auto& star : stars_) {
const float B = layerBrightness(star.layer);
const int SIZE = layerSize(star.layer);
const int X = static_cast<int>(star.x);
const int Y = static_cast<int>(star.y);
if (SIZE <= 1) {
// Punt d'1 px: línia degenerada horitzontal de 1 px.
Rendering::linea(renderer_, X, Y, X + 1, Y, B, 0.0F, star.color);
} else {
// Creu "+" amb extensió HALF des del centre en cada direcció.
const int HALF = SIZE - 1; // SIZE=2 → ±1 (creu 3x3); SIZE=3 → ±2 (creu 5x5)
Rendering::linea(renderer_, X - HALF, Y, X + HALF + 1, Y, B, 0.0F, star.color);
Rendering::linea(renderer_, X, Y - HALF, X, Y + HALF + 1, B, 0.0F, star.color);
}
}
}
} // namespace Graphics
@@ -0,0 +1,51 @@
// starfield_parallax.hpp - Capa més profunda del fons: estrelles 2D amb parallax
// © 2026 JailDesigner
//
// Estrelles 2D distribuïdes en 3 capes de profunditat. Cada capa té el seu
// factor parallax: el "món" es desplaça amb world_velocity i les estrelles
// d'una capa es mouen amb world_velocity * parallax_factor. Les més
// properes es mouen més (factor alt) → sensació de profunditat.
// Quan una estrella surt de PLAYAREA, reapareix per la banda oposada
// (wraparound).
#pragma once
#include <SDL3/SDL.h>
#include <array>
#include "core/defaults/starfield_parallax.hpp"
#include "core/rendering/render_context.hpp"
#include "core/types.hpp"
namespace Graphics {
class StarfieldParallax {
public:
explicit StarfieldParallax(Rendering::Renderer* renderer);
// Avança el desplaçament de les estrelles segons world_velocity (vector
// del moviment del món en px/s; típicament = -ship_velocity).
// world_velocity == {0, 0} → estrelles quietes.
void update(float delta_time, Vec2 world_velocity);
void draw() const;
private:
struct Star {
float x{0.0F};
float y{0.0F};
int layer{0}; // 0=Far, 1=Mid, 2=Near
SDL_Color color{}; // tint precomputat entre blanc i cyan
};
void buildStars();
static auto layerBrightness(int layer) -> float;
static auto layerParallax(int layer) -> float;
static auto layerSize(int layer) -> int;
Rendering::Renderer* renderer_;
std::array<Star, Defaults::StarfieldParallax::TOTAL_COUNT> stars_{};
};
} // namespace Graphics
+11
View File
@@ -30,6 +30,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
floating_score_manager_(sdl.getRenderer()),
trail_manager_(sdl.getRenderer()),
text_(sdl.getRenderer()),
starfield_parallax_(sdl.getRenderer()),
playfield_(sdl.getRenderer()),
border_(sdl.getRenderer()) {
// Recuperar configuración de match des del context
@@ -213,6 +214,9 @@ void GameScene::stepPhysics(float delta_time) {
bullet.postUpdate(delta_time);
}
trail_manager_.update(delta_time, ships_);
// De moment world_velocity = {0, 0} → estrelles quietes; al ser-hi l'estat
// del wraparound es resol normalment quan activem el moviment.
starfield_parallax_.update(delta_time, Vec2{.x = 0.0F, .y = 0.0F});
playfield_.update(delta_time);
border_.update(delta_time);
@@ -569,6 +573,7 @@ void GameScene::drawActiveShipsAlive() const {
}
void GameScene::drawContinueState() {
starfield_parallax_.draw();
border_.draw();
drawEnemies();
drawBullets();
@@ -580,6 +585,7 @@ void GameScene::drawContinueState() {
}
void GameScene::drawGameOverState() {
starfield_parallax_.draw();
border_.draw();
drawEnemies();
drawBullets();
@@ -625,6 +631,8 @@ void GameScene::drawInitHudState() {
Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT,
Defaults::Game::INIT_HUD_SHIP2_RATIO_END);
// Capa de fons més profunda: estrelles 2D (apareixen senceres des del frame 0).
starfield_parallax_.draw();
// Graella de fons al darrere (timer intern propi, cobreix tot l'INIT_HUD).
playfield_.draw();
@@ -650,6 +658,7 @@ void GameScene::drawInitHudState() {
}
void GameScene::drawLevelStartState() {
starfield_parallax_.draw();
playfield_.draw();
border_.draw();
trail_manager_.draw();
@@ -663,6 +672,7 @@ void GameScene::drawLevelStartState() {
}
void GameScene::drawPlayingState() {
starfield_parallax_.draw();
playfield_.draw();
border_.draw();
trail_manager_.draw();
@@ -676,6 +686,7 @@ void GameScene::drawPlayingState() {
}
void GameScene::drawLevelCompletedState() {
starfield_parallax_.draw();
playfield_.draw();
border_.draw();
trail_manager_.draw();
+4
View File
@@ -10,6 +10,7 @@
#include "core/graphics/border.hpp"
#include "core/graphics/playfield.hpp"
#include "core/graphics/starfield_parallax.hpp"
#include "core/graphics/vector_text.hpp"
#include "core/physics/physics_world.hpp"
#include "core/rendering/sdl_manager.hpp"
@@ -82,6 +83,9 @@ class GameScene final : public Scene {
// Text vectorial
Graphics::VectorText text_;
// Capa més profunda del fons: estrelles 2D amb parallax (estàtiques de moment).
Graphics::StarfieldParallax starfield_parallax_;
// Fons del playfield (graella + futures capes)
Graphics::Playfield playfield_;