Merge branch 'feat/enemy-death': muerte d'enemics amb herida prèvia + debris físic
This commit is contained in:
+37
-34
@@ -133,44 +133,47 @@ add_custom_command(
|
||||
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
|
||||
add_dependencies(${PROJECT_NAME} resource_pack)
|
||||
|
||||
# --- COMPILACIÓ DE SHADERS GLSL → SPIR-V ---
|
||||
# Compila tots els shaders .glsl a SPIR-V (Vulkan/Linux/Windows).
|
||||
# macOS necessitarà MSL en el futur (Metal) — es generen amb spirv-cross
|
||||
# o glslang amb target distint en una etapa posterior.
|
||||
# Sortida: build/shaders/*.spv
|
||||
# --- COMPILACIÓ DE SHADERS GLSL → SPIR-V (headers C++ embedits) ---
|
||||
# Compila els shaders .glsl a SPIR-V i els converteix en headers C++ embedits
|
||||
# (source/core/rendering/gpu/spv/*.h). Aquests headers es commiteen al repo:
|
||||
# en macOS no cal glslc (els headers ja existeixen). En Linux/Windows glslc
|
||||
# és obligatori per regenerar els headers en cada canvi del GLSL.
|
||||
#
|
||||
# Per a macOS hi ha a més els headers MSL escrits a mà a source/core/rendering/gpu/msl/.
|
||||
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/shaders")
|
||||
set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/gpu/spv")
|
||||
set(ALL_SHADER_HEADERS
|
||||
"${HEADERS_DIR}/line_vert_spv.h"
|
||||
"${HEADERS_DIR}/line_frag_spv.h"
|
||||
"${HEADERS_DIR}/postfx_vert_spv.h"
|
||||
"${HEADERS_DIR}/postfx_frag_spv.h"
|
||||
)
|
||||
set(ALL_SHADER_SOURCES
|
||||
"${SHADERS_DIR}/line.vert.glsl"
|
||||
"${SHADERS_DIR}/line.frag.glsl"
|
||||
"${SHADERS_DIR}/postfx.vert.glsl"
|
||||
"${SHADERS_DIR}/postfx.frag.glsl"
|
||||
)
|
||||
find_program(GLSLC_EXE NAMES glslc HINTS ${Vulkan_GLSLC_EXECUTABLE})
|
||||
if(GLSLC_EXE)
|
||||
file(GLOB SHADER_SOURCES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/shaders/*.glsl")
|
||||
set(COMPILED_SHADERS "")
|
||||
foreach(SHADER ${SHADER_SOURCES})
|
||||
get_filename_component(SHADER_NAME ${SHADER} NAME)
|
||||
# Detectar stage del nom: line.vert.glsl → vert, line.frag.glsl → frag
|
||||
if(SHADER_NAME MATCHES "\\.vert\\.glsl$")
|
||||
set(SHADER_STAGE "vert")
|
||||
string(REPLACE ".glsl" ".spv" SPV_NAME ${SHADER_NAME})
|
||||
elseif(SHADER_NAME MATCHES "\\.frag\\.glsl$")
|
||||
set(SHADER_STAGE "frag")
|
||||
string(REPLACE ".glsl" ".spv" SPV_NAME ${SHADER_NAME})
|
||||
else()
|
||||
message(WARNING "Shader sense stage detectat: ${SHADER_NAME} (esperat .vert.glsl o .frag.glsl)")
|
||||
continue()
|
||||
endif()
|
||||
set(SPV_OUTPUT "${CMAKE_BINARY_DIR}/shaders/${SPV_NAME}")
|
||||
add_custom_command(
|
||||
OUTPUT ${SPV_OUTPUT}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/shaders"
|
||||
COMMAND ${GLSLC_EXE} -fshader-stage=${SHADER_STAGE} -O ${SHADER} -o ${SPV_OUTPUT}
|
||||
DEPENDS ${SHADER}
|
||||
COMMENT "Compilant shader ${SHADER_NAME} → ${SPV_NAME}"
|
||||
VERBATIM
|
||||
)
|
||||
list(APPEND COMPILED_SHADERS ${SPV_OUTPUT})
|
||||
endforeach()
|
||||
add_custom_target(shaders ALL DEPENDS ${COMPILED_SHADERS})
|
||||
add_custom_command(
|
||||
OUTPUT ${ALL_SHADER_HEADERS}
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-D GLSLC=${GLSLC_EXE}
|
||||
-D SHADERS_DIR=${SHADERS_DIR}
|
||||
-D HEADERS_DIR=${HEADERS_DIR}
|
||||
-P ${CMAKE_SOURCE_DIR}/tools/shaders/compile_spirv.cmake
|
||||
DEPENDS ${ALL_SHADER_SOURCES} ${CMAKE_SOURCE_DIR}/tools/shaders/compile_spirv.cmake
|
||||
COMMENT "Compilant shaders GLSL → headers SPIR-V embedits"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(shaders DEPENDS ${ALL_SHADER_HEADERS})
|
||||
add_dependencies(${PROJECT_NAME} shaders)
|
||||
message(STATUS "Shaders trobats: ${SHADER_SOURCES}")
|
||||
message(STATUS "Shaders: glslc trobat (${GLSLC_EXE}); headers SPV es regeneraran si canvia el GLSL")
|
||||
elseif(APPLE)
|
||||
message(STATUS "Shaders: glslc no trobat en macOS — s'usaran els headers SPV ja commiteats")
|
||||
else()
|
||||
message(FATAL_ERROR "glslc no trobat: instal·la 'shaderc' o 'vulkan-sdk' per compilar shaders SPIR-V")
|
||||
message(FATAL_ERROR "glslc no trobat: instal·la 'shaderc' o 'vulkan-sdk' per compilar shaders SPIR-V (obligatori a Linux/Windows)")
|
||||
endif()
|
||||
|
||||
# --- STATIC ANALYSIS / FORMAT TARGETS ---
|
||||
|
||||
@@ -27,6 +27,13 @@ namespace Config {
|
||||
struct RenderingConfig {
|
||||
int vsync{1}; // 0=disabled, 1=enabled
|
||||
int antialias{1}; // 0=disabled, 1=enabled (AA geomètric a les línies, toggle F5)
|
||||
// Resolució del render target offscreen (independent del tamany lògic
|
||||
// 1280×720 del joc). Aquesta és la resolució real on rasteritzen les
|
||||
// línies abans de l'escala final a la swapchain; pujar-la millora
|
||||
// la nitidesa en finestres grans i fullscreen. Llista tancada de
|
||||
// presets 16:9 — veure Defaults::Rendering::RESOLUTION_PRESETS.
|
||||
int render_width{1280};
|
||||
int render_height{720};
|
||||
};
|
||||
|
||||
struct KeyboardBindings {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
// IWYU pragma: begin_exports
|
||||
#include "core/defaults/audio.hpp"
|
||||
#include "core/defaults/brightness.hpp"
|
||||
#include "core/defaults/controls.hpp"
|
||||
@@ -15,7 +16,9 @@
|
||||
#include "core/defaults/entities.hpp"
|
||||
#include "core/defaults/floating_score.hpp"
|
||||
#include "core/defaults/game.hpp"
|
||||
#include "core/defaults/hud.hpp"
|
||||
#include "core/defaults/math.hpp"
|
||||
#include "core/defaults/notifier.hpp"
|
||||
#include "core/defaults/palette.hpp"
|
||||
#include "core/defaults/physics.hpp"
|
||||
#include "core/defaults/rendering.hpp"
|
||||
@@ -23,3 +26,4 @@
|
||||
#include "core/defaults/title.hpp"
|
||||
#include "core/defaults/window.hpp"
|
||||
#include "core/defaults/zones.hpp"
|
||||
// IWYU pragma: end_exports
|
||||
|
||||
@@ -7,19 +7,30 @@
|
||||
|
||||
namespace Defaults::Enemies {
|
||||
|
||||
// Cuerpo físico común (valores por defecto del constructor)
|
||||
namespace Body {
|
||||
constexpr float DEFAULT_MASS = 5.0F; // Más liviano que la nave (10.0)
|
||||
constexpr float RESTITUTION = 1.0F; // Rebote elástico perfecto contra paredes
|
||||
constexpr float LINEAR_DAMPING = 0.0F; // Sin fricción: mantienen velocidad
|
||||
constexpr float ANGULAR_DAMPING = 0.0F;
|
||||
} // namespace Body
|
||||
|
||||
// Pentagon (esquivador - zigzag evasion)
|
||||
namespace Pentagon {
|
||||
constexpr float VELOCITAT = 35.0F; // px/s (slightly slower)
|
||||
constexpr float CANVI_ANGLE_PROB = 0.20F; // 20% per wall hit (frequent zigzag)
|
||||
constexpr float CANVI_ANGLE_MAX = 1.0F; // Max random angle change (rad)
|
||||
constexpr float DROTACIO_MIN = 0.75F; // Min visual rotation (rad/s) [+50%]
|
||||
constexpr float DROTACIO_MAX = 3.75F; // Max visual rotation (rad/s) [+50%]
|
||||
constexpr float VELOCITAT = 35.0F; // px/s (slightly slower)
|
||||
constexpr float MASS = 5.0F; // Masa estándar
|
||||
constexpr float CANVI_ANGLE_PROB = 0.20F; // 20% per wall hit (frequent zigzag)
|
||||
constexpr float CANVI_ANGLE_MAX = 1.0F; // Max random angle change (rad)
|
||||
constexpr float ZIGZAG_PROB_PER_SECOND = 0.8F; // Probabilidad de zigzag por segundo
|
||||
constexpr float DROTACIO_MIN = 0.75F; // Min visual rotation (rad/s) [+50%]
|
||||
constexpr float DROTACIO_MAX = 3.75F; // Max visual rotation (rad/s) [+50%]
|
||||
constexpr const char* SHAPE_FILE = "enemy_pentagon.shp";
|
||||
} // namespace Pentagon
|
||||
|
||||
// Cuadrado (perseguidor - tracks player)
|
||||
namespace Cuadrado {
|
||||
constexpr float VELOCITAT = 40.0F; // px/s (medium speed)
|
||||
constexpr float MASS = 8.0F; // Más pesado, "tanque"
|
||||
constexpr float TRACKING_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0)
|
||||
constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates
|
||||
constexpr float DROTACIO_MIN = 0.3F; // Slow rotation [+50%]
|
||||
@@ -30,6 +41,7 @@ namespace Defaults::Enemies {
|
||||
// Molinillo (agressiu - fast straight lines, proximity spin-up)
|
||||
namespace Molinillo {
|
||||
constexpr float VELOCITAT = 50.0F; // px/s (fastest)
|
||||
constexpr float MASS = 4.0F; // Más liviano, ágil
|
||||
constexpr float CANVI_ANGLE_PROB = 0.05F; // 5% per wall hit (rare direction change)
|
||||
constexpr float CANVI_ANGLE_MAX = 0.3F; // Small angle adjustments
|
||||
constexpr float DROTACIO_MIN = 3.0F; // Base rotation (rad/s) [+50%]
|
||||
@@ -58,6 +70,12 @@ namespace Defaults::Enemies {
|
||||
constexpr float ROTACIO_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic]
|
||||
} // namespace Animation
|
||||
|
||||
// Wounded state (entre primer impacto y explosión)
|
||||
namespace Wounded {
|
||||
constexpr float DURATION = 1.0F; // Segundos en estado herido antes de explotar
|
||||
constexpr float BLINK_HZ = 10.0F; // Frecuencia de parpadeo color tipo ↔ dorado
|
||||
} // namespace Wounded
|
||||
|
||||
// Spawn safety and invulnerability system
|
||||
namespace Spawn {
|
||||
// Safe spawn distance from player
|
||||
|
||||
@@ -10,9 +10,13 @@ namespace Defaults::Game {
|
||||
constexpr int HEIGHT = 720;
|
||||
|
||||
// Regles de partida
|
||||
constexpr int STARTING_LIVES = 3; // Initial lives
|
||||
constexpr float DEATH_DURATION = 3.0F; // Seconds of death animation
|
||||
constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over
|
||||
constexpr int STARTING_LIVES = 3; // Initial lives
|
||||
constexpr float DEATH_DURATION = 3.0F; // Seconds of death animation
|
||||
constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over
|
||||
|
||||
// Valores centinela del temporitzador de mort per-jugador.
|
||||
constexpr float HIT_TIMER_INACTIVE_PLAYER = 999.0F; // Jugador permanentment inactiu
|
||||
constexpr float HIT_TIMER_TRIGGER_DEATH = 0.001F; // Trigger inicial post-impacte (>0 sense disparar regla)
|
||||
constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 0.80F; // 80% hitbox (generous)
|
||||
constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous)
|
||||
|
||||
@@ -20,6 +24,7 @@ namespace Defaults::Game {
|
||||
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
|
||||
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
|
||||
constexpr float BULLET_GRACE_PERIOD = 0.2F; // Inmunidad post-disparo (s)
|
||||
constexpr float BULLET_SPEED = 700.0F; // Velocidad escalar (px/s). Pascal: 7 px/frame × 20 FPS
|
||||
|
||||
// Transición LEVEL_START (mensajes aleatorios PRE-level)
|
||||
constexpr float LEVEL_START_DURATION = 3.0F; // Duración total
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// hud.hpp - Configuració visual del HUD (marcador, etc.)
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Defaults::Hud {
|
||||
|
||||
// Marcador (scoreboard inferior). Usado por GameScene::drawScoreboard()
|
||||
// y por la animación de entrada en init_hud_animator.
|
||||
constexpr float SCOREBOARD_TEXT_SCALE = 0.85F;
|
||||
constexpr float SCOREBOARD_TEXT_SPACING = 0.0F;
|
||||
|
||||
// Animación de entrada del HUD (init_hud_animator).
|
||||
namespace InitAnim {
|
||||
// Spawn vertical de la nave: 50 px bajo la PLAYAREA (sale desde fuera).
|
||||
constexpr float SHIP_SPAWN_Y_OFFSET = 50.0F;
|
||||
|
||||
// Bordes: ratios de las tres fases (top → laterales → bottom).
|
||||
constexpr float BORDER_PHASE_1_END = 0.33F; // Fin de la fase top
|
||||
constexpr float BORDER_PHASE_2_END = 0.66F; // Fin de la fase laterales
|
||||
} // namespace InitAnim
|
||||
|
||||
// Indicadores ("tips") sobre los enemigos enganchados a la nave.
|
||||
// Offset local al frame de la nave (apunta hacia delante, eje Y negativo).
|
||||
namespace Tips {
|
||||
constexpr float LOCAL_X = 0.0F;
|
||||
constexpr float LOCAL_Y = -12.0F;
|
||||
} // namespace Tips
|
||||
|
||||
// Overlay de debug (FPS, métriques) en coordenades lògiques (1280×720).
|
||||
namespace DebugOverlay {
|
||||
constexpr float X = 12.0F;
|
||||
constexpr float Y_FPS = 12.0F;
|
||||
constexpr float LINE_HEIGHT = 18.0F; // separació entre línies (scale 0.4 → ~16 px alt)
|
||||
constexpr float TEXT_SCALE = 0.4F;
|
||||
constexpr float TEXT_SPACING = 2.0F;
|
||||
constexpr float BRIGHTNESS = 1.0F;
|
||||
constexpr float FPS_UPDATE_INTERVAL = 0.5F; // Cadencia d'actualització del FPS visible
|
||||
} // namespace DebugOverlay
|
||||
|
||||
} // namespace Defaults::Hud
|
||||
@@ -0,0 +1,31 @@
|
||||
// notifier.hpp - Configuració del cuadre de notificacions toast (System::Notifier)
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace Defaults::Notifier {
|
||||
|
||||
// Geometria del cuadre en coordenades lògiques (1280×720).
|
||||
constexpr float CANVAS_WIDTH = 1280.0F;
|
||||
constexpr float MARGIN_TOP = 40.0F;
|
||||
constexpr float PADDING_H = 16.0F;
|
||||
constexpr float PADDING_V = 10.0F;
|
||||
constexpr float BORDER_THICKNESS = 2.0F;
|
||||
constexpr float TEXT_SCALE = 0.55F;
|
||||
constexpr float TEXT_SPACING = 2.0F;
|
||||
constexpr float BORDER_BRIGHTNESS = 1.0F;
|
||||
|
||||
// Cinemàtica del slide.
|
||||
constexpr float SLIDE_DURATION_S = 0.30F;
|
||||
|
||||
// Presets per als atajos semàntics.
|
||||
constexpr SDL_Color COLOR_INFO{.r = 80, .g = 230, .b = 255, .a = 255};
|
||||
constexpr SDL_Color COLOR_WARN{.r = 255, .g = 180, .b = 40, .a = 255};
|
||||
constexpr SDL_Color COLOR_EXIT{.r = 255, .g = 80, .b = 80, .a = 255};
|
||||
constexpr float DURATION_INFO = 2.0F;
|
||||
constexpr float DURATION_WARN = 3.0F;
|
||||
constexpr float DURATION_EXIT = 3.0F;
|
||||
|
||||
} // namespace Defaults::Notifier
|
||||
@@ -15,5 +15,6 @@ namespace Defaults::Palette {
|
||||
constexpr SDL_Color PENTAGON = {.r = 120, .g = 170, .b = 255, .a = 255}; // Azul "esquivador"
|
||||
constexpr SDL_Color QUADRAT = {.r = 255, .g = 110, .b = 110, .a = 255}; // Rojo "tank"
|
||||
constexpr SDL_Color MOLINILLO = {.r = 255, .g = 130, .b = 255, .a = 255}; // Magenta agresivo
|
||||
constexpr SDL_Color WOUNDED = {.r = 255, .g = 215, .b = 0, .a = 255}; // Dorado: enemigo herido
|
||||
|
||||
} // namespace Defaults::Palette
|
||||
|
||||
@@ -10,6 +10,14 @@ namespace Defaults::Physics {
|
||||
constexpr float MAX_VELOCITY = 120.0F; // px/s
|
||||
constexpr float FRICTION = 20.0F; // px/s²
|
||||
|
||||
// Bullet — impacto físico contra enemigo (impulse mass-aware).
|
||||
// Model: el impulse és el moment lineal de la bala (m·v) multiplicat per
|
||||
// un factor de transferència [0..1]. 1.0 = transfereix tot el moment
|
||||
// (col·lisió perfectament inelàstica), 0.5 = transfereix la meitat.
|
||||
namespace Bullet {
|
||||
constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic
|
||||
} // namespace Bullet
|
||||
|
||||
// Explosions (debris physics)
|
||||
namespace Debris {
|
||||
constexpr float VELOCITAT_BASE = 80.0F; // Velocidad inicial (px/s)
|
||||
@@ -17,15 +25,38 @@ namespace Defaults::Physics {
|
||||
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
|
||||
constexpr float ROTACIO_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
|
||||
constexpr float ROTACIO_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s)
|
||||
constexpr float TEMPS_VIDA = 2.0F; // Duració màxima (segons) - enemy/bullet debris
|
||||
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris lifetime (matches DEATH_DURATION)
|
||||
constexpr float SHRINK_RATE = 0.5F; // Reducció de mida (factor/s)
|
||||
constexpr float TEMPS_VIDA = 2.0F; // Vida mínima garantida (s) — després pot morir per velocitat baixa
|
||||
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris min lifetime (matches DEATH_DURATION)
|
||||
constexpr float SHRINK_RATE = 1.0F; // Reducció de mida (1.0 = encoge a 0 al final del min_lifetime)
|
||||
|
||||
// Política de mort: passat el min_lifetime, el fragment mor quan la
|
||||
// seva velocity cau per sota d'aquest llindar. Així els fragments
|
||||
// ràpids no "popen" en moviment.
|
||||
constexpr float MIN_SPEED_TO_DIE = 5.0F; // px/s — al cuadrat per evitar sqrt en update
|
||||
constexpr float MIN_SPEED_TO_DIE_SQ = MIN_SPEED_TO_DIE * MIN_SPEED_TO_DIE;
|
||||
|
||||
// Rebot contra els límits del PLAYAREA (mateix patró que enemics/ship).
|
||||
// 0.7 = 70% de l'energia conservada al rebot.
|
||||
constexpr float RESTITUTION_BOUNDS = 0.7F;
|
||||
|
||||
// Herència de velocity angular (trayectorias curvas)
|
||||
constexpr float FACTOR_HERENCIA_MIN = 0.7F; // Mínimo 70% del drotacio heredat
|
||||
constexpr float FACTOR_HERENCIA_MAX = 1.0F; // Màxim 100% del drotacio heredat
|
||||
constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
|
||||
|
||||
// Velocity heredada de la nau a l'explosió (80% del feel original).
|
||||
constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F;
|
||||
|
||||
// Velocity heredada de l'enemic a l'explosió (palanca per a tuneo).
|
||||
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
|
||||
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
|
||||
|
||||
// Tuneig específic de l'explosió d'enemic (overrides als defaults
|
||||
// que es passen com a paràmetres opcionals a explode()).
|
||||
constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més
|
||||
constexpr float ENEMY_FRICTION = -30.0F; // Fricció més suau perquè s'estenguin més
|
||||
constexpr int ENEMY_SEGMENT_MULTIPLIER = 1; // Sense còpies (5 cares = 5 trossos); >1 produeix grups sincronitzats
|
||||
|
||||
// Angular velocity sin for trajectory inheritance
|
||||
// Excess above this threshold is converted to tangential linear velocity
|
||||
// Prevents "vortex trap" problem with high-rotation enemies
|
||||
|
||||
@@ -3,9 +3,40 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
namespace Defaults::Rendering {
|
||||
|
||||
constexpr int VSYNC_DEFAULT = 1; // 0=disabled, 1=enabled
|
||||
constexpr int ANTIALIAS_DEFAULT = 1; // 0=disabled, 1=enabled (AA geomètric a les línies)
|
||||
|
||||
// Grosor global per defecte de les línies. 1.5 dóna línia visible i crujent;
|
||||
// 1.0 es veu massa fi en pantalles grans. Configurable via setLineThickness.
|
||||
constexpr float LINE_THICKNESS_DEFAULT = 1.5F;
|
||||
|
||||
// Resolució del render target offscreen. El tamany lògic del joc roman a
|
||||
// 1280×720 (coordenades dels objectes); aquesta és la resolució física a
|
||||
// la qual es rasteritzen les línies abans de la composició final.
|
||||
struct ResolutionPreset {
|
||||
int w;
|
||||
int h;
|
||||
};
|
||||
|
||||
constexpr std::array<ResolutionPreset, 5> RESOLUTION_PRESETS{{
|
||||
{.w = 1280, .h = 720}, // HD 720p (default)
|
||||
{.w = 1600, .h = 900}, // HD+ 900p
|
||||
{.w = 1920, .h = 1080}, // Full HD 1080p
|
||||
{.w = 2560, .h = 1440}, // QHD 1440p
|
||||
{.w = 3840, .h = 2160} // 4K UHD 2160p
|
||||
}};
|
||||
|
||||
constexpr int RENDER_WIDTH_DEFAULT = 1280;
|
||||
constexpr int RENDER_HEIGHT_DEFAULT = 720;
|
||||
|
||||
constexpr auto isValidRenderResolution(int w, int h) -> bool {
|
||||
return std::ranges::any_of(RESOLUTION_PRESETS,
|
||||
[w, h](const ResolutionPreset& preset) { return preset.w == w && preset.h == h; });
|
||||
}
|
||||
|
||||
} // namespace Defaults::Rendering
|
||||
|
||||
@@ -13,4 +13,15 @@ namespace Defaults::Ship {
|
||||
constexpr float BLINK_INVISIBLE_TIME = 0.1F; // Tiempo invisible (segundos)
|
||||
// Frecuencia total: 0.2s/ciclo = 5 Hz (~15 parpadeos en 3s)
|
||||
|
||||
// Cuerpo físico
|
||||
constexpr float MASS = 10.0F; // Masa de referencia para choques
|
||||
constexpr float RESTITUTION = 0.6F; // Rebote moderado contra paredes
|
||||
constexpr float LINEAR_DAMPING = 1.5F; // Fricción exponencial (s⁻¹)
|
||||
constexpr float ANGULAR_DAMPING = 0.0F; // Rotación 100% por input (no inercial)
|
||||
|
||||
// Empuje visual: escala proporcional a la velocidad (0..200 px/s → 1.0..1.5)
|
||||
// Mantiene la sensación del Pascal original.
|
||||
constexpr float VISUAL_PUSH_DIVISOR = 33.33F; // SPEED / DIVISOR = empuje visual
|
||||
constexpr float VISUAL_SCALE_DIVISOR = 12.0F; // SCALE = 1 + (PUSH / DIVISOR)
|
||||
|
||||
} // namespace Defaults::Ship
|
||||
|
||||
@@ -50,10 +50,10 @@ Starfield::Starfield(Rendering::Renderer* renderer,
|
||||
estrella.capa = capa_idx;
|
||||
|
||||
// Angle aleatori
|
||||
estrella.angle = (static_cast<float>(rand()) / RAND_MAX) * 2.0F * Defaults::Math::PI;
|
||||
estrella.angle = (static_cast<float>(rand()) / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI;
|
||||
|
||||
// Distancia aleatòria (0.0 a 1.0) per omplir toda la pantalla
|
||||
estrella.distancia_centre = static_cast<float>(rand()) / RAND_MAX;
|
||||
estrella.distancia_centre = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
|
||||
|
||||
// Calcular posición desde la distancia
|
||||
float radi = estrella.distancia_centre * radi_max_;
|
||||
@@ -68,7 +68,7 @@ Starfield::Starfield(Rendering::Renderer* renderer,
|
||||
// Inicialitzar una estrella (nueva o regenerada)
|
||||
void Starfield::initStar(Estrella& estrella) const {
|
||||
// Angle aleatori des del point de fuga hacia fuera
|
||||
estrella.angle = (static_cast<float>(rand()) / RAND_MAX) * 2.0F * Defaults::Math::PI;
|
||||
estrella.angle = (static_cast<float>(rand()) / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI;
|
||||
|
||||
// Distancia inicial pequeña (5% del radi màxim) - neix prop del centro
|
||||
estrella.distancia_centre = 0.05F;
|
||||
|
||||
@@ -10,172 +10,179 @@
|
||||
|
||||
namespace Physics {
|
||||
|
||||
void PhysicsWorld::addBody(RigidBody* body) {
|
||||
if (body == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (std::ranges::find(bodies_, body) == bodies_.end()) {
|
||||
bodies_.push_back(body);
|
||||
}
|
||||
}
|
||||
|
||||
void PhysicsWorld::removeBody(RigidBody* body) {
|
||||
std::erase(bodies_, body);
|
||||
}
|
||||
|
||||
void PhysicsWorld::update(float dt) {
|
||||
integrate(dt);
|
||||
if (has_bounds_) {
|
||||
resolveBoundsCollisions();
|
||||
}
|
||||
resolveBodyCollisions();
|
||||
}
|
||||
|
||||
// Integración semi-implícita de Euler:
|
||||
// v(t+dt) = v(t) + (F/m) * dt
|
||||
// x(t+dt) = x(t) + v(t+dt) * dt
|
||||
// Más estable que Euler explícito para juegos. Damping exponencial.
|
||||
void PhysicsWorld::integrate(float dt) {
|
||||
for (auto* body : bodies_) {
|
||||
if (body == nullptr || body->isStatic()) {
|
||||
continue;
|
||||
void PhysicsWorld::addBody(RigidBody* body) {
|
||||
if (body == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Aplicar fuerzas acumuladas → aceleración
|
||||
const Vec2 ACCELERATION = body->force_accumulator * body->inverse_mass;
|
||||
body->velocity += ACCELERATION * dt;
|
||||
|
||||
// Damping exponencial: equivalente a v *= exp(-damping * dt)
|
||||
// Aproximación lineal cuando damping*dt es pequeño.
|
||||
if (body->linear_damping > 0.0F) {
|
||||
const float DAMP = std::exp(-body->linear_damping * dt);
|
||||
body->velocity *= DAMP;
|
||||
if (std::ranges::find(bodies_, body) == bodies_.end()) {
|
||||
bodies_.push_back(body);
|
||||
}
|
||||
if (body->angular_damping > 0.0F) {
|
||||
const float DAMP = std::exp(-body->angular_damping * dt);
|
||||
body->angular_velocity *= DAMP;
|
||||
}
|
||||
|
||||
// Actualizar posición y rotación
|
||||
body->position += body->velocity * dt;
|
||||
body->angle += body->angular_velocity * dt;
|
||||
|
||||
body->clearAccumulators();
|
||||
}
|
||||
}
|
||||
|
||||
// Rebote contra los 4 bordes del rectángulo bounds_.
|
||||
// Refleja la componente normal de la velocidad por la restitución.
|
||||
void PhysicsWorld::resolveBoundsCollisions() {
|
||||
const float MIN_X = bounds_.x;
|
||||
const float MAX_X = bounds_.x + bounds_.w;
|
||||
const float MIN_Y = bounds_.y;
|
||||
const float MAX_Y = bounds_.y + bounds_.h;
|
||||
void PhysicsWorld::removeBody(RigidBody* body) {
|
||||
std::erase(bodies_, body);
|
||||
}
|
||||
|
||||
for (auto* body : bodies_) {
|
||||
if (body == nullptr || body->isStatic()) {
|
||||
continue;
|
||||
void PhysicsWorld::update(float dt) {
|
||||
integrate(dt);
|
||||
if (has_bounds_) {
|
||||
resolveBoundsCollisions();
|
||||
}
|
||||
const float R = body->radius;
|
||||
resolveBodyCollisions();
|
||||
}
|
||||
|
||||
// Pared izquierda
|
||||
if (body->position.x - R < MIN_X) {
|
||||
body->position.x = MIN_X + R;
|
||||
if (body->velocity.x < 0.0F) {
|
||||
body->velocity.x = -body->velocity.x * body->restitution;
|
||||
// Integración semi-implícita de Euler:
|
||||
// v(t+dt) = v(t) + (F/m) * dt
|
||||
// x(t+dt) = x(t) + v(t+dt) * dt
|
||||
// Más estable que Euler explícito para juegos. Damping exponencial.
|
||||
void PhysicsWorld::integrate(float dt) {
|
||||
for (auto* body : bodies_) {
|
||||
if (body == nullptr || body->isStatic()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Pared derecha
|
||||
if (body->position.x + R > MAX_X) {
|
||||
body->position.x = MAX_X - R;
|
||||
if (body->velocity.x > 0.0F) {
|
||||
body->velocity.x = -body->velocity.x * body->restitution;
|
||||
|
||||
// Aplicar fuerzas acumuladas → aceleración
|
||||
const Vec2 ACCELERATION = body->force_accumulator * body->inverse_mass;
|
||||
body->velocity += ACCELERATION * dt;
|
||||
|
||||
// Damping exponencial: equivalente a v *= exp(-damping * dt)
|
||||
// Aproximación lineal cuando damping*dt es pequeño.
|
||||
if (body->linear_damping > 0.0F) {
|
||||
const float DAMP = std::exp(-body->linear_damping * dt);
|
||||
body->velocity *= DAMP;
|
||||
}
|
||||
}
|
||||
// Pared superior
|
||||
if (body->position.y - R < MIN_Y) {
|
||||
body->position.y = MIN_Y + R;
|
||||
if (body->velocity.y < 0.0F) {
|
||||
body->velocity.y = -body->velocity.y * body->restitution;
|
||||
if (body->angular_damping > 0.0F) {
|
||||
const float DAMP = std::exp(-body->angular_damping * dt);
|
||||
body->angular_velocity *= DAMP;
|
||||
}
|
||||
|
||||
// Actualizar posición y rotación
|
||||
body->position += body->velocity * dt;
|
||||
body->angle += body->angular_velocity * dt;
|
||||
|
||||
body->clearAccumulators();
|
||||
}
|
||||
// Pared inferior
|
||||
if (body->position.y + R > MAX_Y) {
|
||||
body->position.y = MAX_Y - R;
|
||||
if (body->velocity.y > 0.0F) {
|
||||
body->velocity.y = -body->velocity.y * body->restitution;
|
||||
}
|
||||
|
||||
// Rebote contra los 4 bordes del rectángulo bounds_.
|
||||
// Refleja la componente normal de la velocidad por la restitución.
|
||||
void PhysicsWorld::resolveBoundsCollisions() {
|
||||
const float MIN_X = bounds_.x;
|
||||
const float MAX_X = bounds_.x + bounds_.w;
|
||||
const float MIN_Y = bounds_.y;
|
||||
const float MAX_Y = bounds_.y + bounds_.h;
|
||||
|
||||
for (auto* body : bodies_) {
|
||||
if (body == nullptr || body->isStatic()) {
|
||||
continue;
|
||||
}
|
||||
const float R = body->radius;
|
||||
|
||||
// Pared izquierda
|
||||
if (body->position.x - R < MIN_X) {
|
||||
body->position.x = MIN_X + R;
|
||||
if (body->velocity.x < 0.0F) {
|
||||
body->velocity.x = -body->velocity.x * body->restitution;
|
||||
}
|
||||
}
|
||||
// Pared derecha
|
||||
if (body->position.x + R > MAX_X) {
|
||||
body->position.x = MAX_X - R;
|
||||
if (body->velocity.x > 0.0F) {
|
||||
body->velocity.x = -body->velocity.x * body->restitution;
|
||||
}
|
||||
}
|
||||
// Pared superior
|
||||
if (body->position.y - R < MIN_Y) {
|
||||
body->position.y = MIN_Y + R;
|
||||
if (body->velocity.y < 0.0F) {
|
||||
body->velocity.y = -body->velocity.y * body->restitution;
|
||||
}
|
||||
}
|
||||
// Pared inferior
|
||||
if (body->position.y + R > MAX_Y) {
|
||||
body->position.y = MAX_Y - R;
|
||||
if (body->velocity.y > 0.0F) {
|
||||
body->velocity.y = -body->velocity.y * body->restitution;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Colisiones cuerpo-cuerpo: O(n²) círculo-círculo + resolución por impulso.
|
||||
// Para 15 enemigos + 6 balas + 2 naves = ~23 cuerpos → 253 pares. Sobra.
|
||||
//
|
||||
// Fórmula del impulso elástico (referencia: Chris Hecker / Box2D):
|
||||
// j = -(1 + e) * (v_rel · n) / (1/m_a + 1/m_b)
|
||||
// donde n es la normal del contacto (de a hacia b) y v_rel = v_a - v_b.
|
||||
void PhysicsWorld::resolveBodyCollisions() {
|
||||
const std::size_t COUNT = bodies_.size();
|
||||
for (std::size_t i = 0; i < COUNT; ++i) {
|
||||
for (std::size_t j = i + 1; j < COUNT; ++j) {
|
||||
auto* a = bodies_[i];
|
||||
auto* b = bodies_[j];
|
||||
if (a != nullptr && b != nullptr) {
|
||||
resolveBodyPair(*a, *b);
|
||||
// Colisiones cuerpo-cuerpo: O(n²) círculo-círculo + resolución por impulso.
|
||||
// Para 15 enemigos + 6 balas + 2 naves = ~23 cuerpos → 253 pares. Sobra.
|
||||
//
|
||||
// Fórmula del impulso elástico (referencia: Chris Hecker / Box2D):
|
||||
// j = -(1 + e) * (v_rel · n) / (1/m_a + 1/m_b)
|
||||
// donde n es la normal del contacto (de a hacia b) y v_rel = v_a - v_b.
|
||||
void PhysicsWorld::resolveBodyCollisions() {
|
||||
const std::size_t COUNT = bodies_.size();
|
||||
for (std::size_t i = 0; i < COUNT; ++i) {
|
||||
for (std::size_t j = i + 1; j < COUNT; ++j) {
|
||||
auto* a = bodies_[i];
|
||||
auto* b = bodies_[j];
|
||||
if (a != nullptr && b != nullptr) {
|
||||
resolveBodyPair(*a, *b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PhysicsWorld::resolveBodyPair(RigidBody& a, RigidBody& b) {
|
||||
// Dos cuerpos estáticos no necesitan resolución
|
||||
if (a.isStatic() && b.isStatic()) {
|
||||
return;
|
||||
}
|
||||
void PhysicsWorld::resolveBodyPair(RigidBody& a, RigidBody& b) {
|
||||
// Dos cuerpos estáticos no necesitan resolución
|
||||
if (a.isStatic() && b.isStatic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Vec2 DELTA = b.position - a.position;
|
||||
const float DIST_SQ = DELTA.lengthSquared();
|
||||
const float SUM_R = a.radius + b.radius;
|
||||
if (DIST_SQ > SUM_R * SUM_R || DIST_SQ <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
// Un cuerpo con radius=0 es cinemático puro (ej. la bala) y no participa
|
||||
// en body-body. La detecció de gameplay (Physics::checkCollision) usa
|
||||
// el radius de l'entity (no el del body) i s'encarrega d'aquesta parella.
|
||||
if (a.radius <= 0.0F || b.radius <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float DIST = std::sqrt(DIST_SQ);
|
||||
const Vec2 NORMAL = DELTA / DIST; // de A hacia B
|
||||
const Vec2 DELTA = b.position - a.position;
|
||||
const float DIST_SQ = DELTA.lengthSquared();
|
||||
const float SUM_R = a.radius + b.radius;
|
||||
if (DIST_SQ > SUM_R * SUM_R || DIST_SQ <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float DIST = std::sqrt(DIST_SQ);
|
||||
const Vec2 NORMAL = DELTA / DIST; // de A hacia B
|
||||
|
||||
// Corrección posicional (resolver penetración)
|
||||
const float PENETRATION = SUM_R - DIST;
|
||||
const float TOTAL_INV_MASS = a.inverse_mass + b.inverse_mass;
|
||||
if (TOTAL_INV_MASS > 0.0F) {
|
||||
const Vec2 CORRECTION = NORMAL * (PENETRATION / TOTAL_INV_MASS);
|
||||
if (!a.isStatic()) {
|
||||
a.position -= CORRECTION * a.inverse_mass;
|
||||
}
|
||||
if (!b.isStatic()) {
|
||||
b.position += CORRECTION * b.inverse_mass;
|
||||
}
|
||||
}
|
||||
|
||||
// Velocidad relativa proyectada sobre la normal
|
||||
const Vec2 V_REL = b.velocity - a.velocity;
|
||||
const float VEL_ALONG_NORMAL = V_REL.dot(NORMAL);
|
||||
// Si se están separando, no aplicar impulso
|
||||
if (VEL_ALONG_NORMAL > 0.0F) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Restitución promedio (Box2D usa max; promedio es más permisivo)
|
||||
const float E = (a.restitution + b.restitution) * 0.5F;
|
||||
const float J = -(1.0F + E) * VEL_ALONG_NORMAL / TOTAL_INV_MASS;
|
||||
const Vec2 IMPULSE = NORMAL * J;
|
||||
|
||||
// Corrección posicional (resolver penetración)
|
||||
const float PENETRATION = SUM_R - DIST;
|
||||
const float TOTAL_INV_MASS = a.inverse_mass + b.inverse_mass;
|
||||
if (TOTAL_INV_MASS > 0.0F) {
|
||||
const Vec2 CORRECTION = NORMAL * (PENETRATION / TOTAL_INV_MASS);
|
||||
if (!a.isStatic()) {
|
||||
a.position -= CORRECTION * a.inverse_mass;
|
||||
a.velocity -= IMPULSE * a.inverse_mass;
|
||||
}
|
||||
if (!b.isStatic()) {
|
||||
b.position += CORRECTION * b.inverse_mass;
|
||||
b.velocity += IMPULSE * b.inverse_mass;
|
||||
}
|
||||
}
|
||||
|
||||
// Velocidad relativa proyectada sobre la normal
|
||||
const Vec2 V_REL = b.velocity - a.velocity;
|
||||
const float VEL_ALONG_NORMAL = V_REL.dot(NORMAL);
|
||||
// Si se están separando, no aplicar impulso
|
||||
if (VEL_ALONG_NORMAL > 0.0F) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Restitución promedio (Box2D usa max; promedio es más permisivo)
|
||||
const float E = (a.restitution + b.restitution) * 0.5F;
|
||||
const float J = -(1.0F + E) * VEL_ALONG_NORMAL / TOTAL_INV_MASS;
|
||||
const Vec2 IMPULSE = NORMAL * J;
|
||||
|
||||
if (!a.isStatic()) {
|
||||
a.velocity -= IMPULSE * a.inverse_mass;
|
||||
}
|
||||
if (!b.isStatic()) {
|
||||
b.velocity += IMPULSE * b.inverse_mass;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Physics
|
||||
|
||||
@@ -1,105 +1,58 @@
|
||||
// gpu_device.cpp - Implementación del wrapper de SDL_GPUDevice
|
||||
// gpu_device.cpp - Implementació del wrapper de SDL_GPUDevice
|
||||
|
||||
#include "core/rendering/gpu/gpu_device.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/utils/path_utils.hpp"
|
||||
|
||||
namespace Rendering::GPU {
|
||||
|
||||
GpuDevice::~GpuDevice() { destroy(); }
|
||||
GpuDevice::~GpuDevice() { destroy(); }
|
||||
|
||||
auto GpuDevice::init(SDL_Window* window) -> bool {
|
||||
window_ = window;
|
||||
auto GpuDevice::init(SDL_Window* window) -> bool {
|
||||
window_ = window;
|
||||
|
||||
// Solicitar backends en orden de preferencia: Vulkan (Linux/Windows),
|
||||
// Metal (macOS). Sin DirectX según decisión de proyecto.
|
||||
// SDL_GPU_SHADERFORMAT_SPIRV: shaders compilados con glslc.
|
||||
// SDL_GPU_SHADERFORMAT_MSL: pendiente para macOS (Fase futura).
|
||||
constexpr SDL_GPUShaderFormat SHADER_FORMATS =
|
||||
SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL;
|
||||
// Selecció del format de shader per plataforma:
|
||||
// - macOS → MSL (Metal Shading Language), shaders embedits a gpu/msl/*.msl.h
|
||||
// - Resta → SPIR-V (Vulkan), shaders embedits a gpu/spv/*.h
|
||||
#ifdef __APPLE__
|
||||
constexpr SDL_GPUShaderFormat SHADER_FORMATS = SDL_GPU_SHADERFORMAT_MSL;
|
||||
#else
|
||||
constexpr SDL_GPUShaderFormat SHADER_FORMATS = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||
#endif
|
||||
|
||||
device_ = SDL_CreateGPUDevice(SHADER_FORMATS, /*debug=*/true, /*name=*/nullptr);
|
||||
if (device_ == nullptr) {
|
||||
std::cerr << "[GpuDevice] SDL_CreateGPUDevice falló: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* driver = SDL_GetGPUDeviceDriver(device_);
|
||||
std::cout << "[GpuDevice] Backend GPU: " << (driver != nullptr ? driver : "?") << '\n';
|
||||
|
||||
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
|
||||
std::cerr << "[GpuDevice] SDL_ClaimWindowForGPUDevice falló: " << SDL_GetError() << '\n';
|
||||
SDL_DestroyGPUDevice(device_);
|
||||
device_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
swapchain_format_ = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GpuDevice::destroy() {
|
||||
if (device_ != nullptr) {
|
||||
if (window_ != nullptr) {
|
||||
SDL_ReleaseWindowFromGPUDevice(device_, window_);
|
||||
device_ = SDL_CreateGPUDevice(SHADER_FORMATS, /*debug_mode=*/true, /*name=*/nullptr);
|
||||
if (device_ == nullptr) {
|
||||
std::cerr << "[GpuDevice] SDL_CreateGPUDevice falló: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
SDL_DestroyGPUDevice(device_);
|
||||
device_ = nullptr;
|
||||
}
|
||||
window_ = nullptr;
|
||||
swapchain_format_ = SDL_GPU_TEXTUREFORMAT_INVALID;
|
||||
}
|
||||
|
||||
auto GpuDevice::loadShader(const std::string& spv_filename,
|
||||
SDL_GPUShaderStage stage,
|
||||
uint32_t num_uniform_buffers,
|
||||
uint32_t num_samplers) const -> SDL_GPUShader* {
|
||||
if (device_ == nullptr) {
|
||||
return nullptr;
|
||||
const char* driver = SDL_GetGPUDeviceDriver(device_);
|
||||
std::cout << "[GpuDevice] Backend GPU: " << (driver != nullptr ? driver : "?") << '\n';
|
||||
|
||||
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
|
||||
std::cerr << "[GpuDevice] SDL_ClaimWindowForGPUDevice falló: " << SDL_GetError() << '\n';
|
||||
SDL_DestroyGPUDevice(device_);
|
||||
device_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
swapchain_format_ = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Los .spv viven en build/shaders/ junto al binario.
|
||||
const std::string PATH = Utils::getExecutableDirectory() + "/shaders/" + spv_filename;
|
||||
|
||||
std::ifstream file(PATH, std::ios::binary | std::ios::ate);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "[GpuDevice] No s'ha pogut obrir el shader: " << PATH << '\n';
|
||||
return nullptr;
|
||||
void GpuDevice::destroy() {
|
||||
if (device_ != nullptr) {
|
||||
if (window_ != nullptr) {
|
||||
SDL_ReleaseWindowFromGPUDevice(device_, window_);
|
||||
}
|
||||
SDL_DestroyGPUDevice(device_);
|
||||
device_ = nullptr;
|
||||
}
|
||||
window_ = nullptr;
|
||||
swapchain_format_ = SDL_GPU_TEXTUREFORMAT_INVALID;
|
||||
}
|
||||
const std::streamsize SIZE = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
std::vector<uint8_t> buffer(static_cast<size_t>(SIZE));
|
||||
if (!file.read(reinterpret_cast<char*>(buffer.data()), SIZE)) {
|
||||
std::cerr << "[GpuDevice] Error llegint shader: " << PATH << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
SDL_GPUShaderCreateInfo info{};
|
||||
info.code = buffer.data();
|
||||
info.code_size = buffer.size();
|
||||
info.entrypoint = "main";
|
||||
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||
info.stage = stage;
|
||||
info.num_uniform_buffers = num_uniform_buffers;
|
||||
info.num_samplers = num_samplers;
|
||||
info.num_storage_buffers = 0;
|
||||
info.num_storage_textures = 0;
|
||||
|
||||
SDL_GPUShader* shader = SDL_CreateGPUShader(device_, &info);
|
||||
if (shader == nullptr) {
|
||||
std::cerr << "[GpuDevice] SDL_CreateGPUShader (" << spv_filename << "): " << SDL_GetError() << '\n';
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
// gpu_device.hpp - Wrapper de SDL_GPUDevice
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Ownership del SDL_GPUDevice y del claim del window. Backend preferido:
|
||||
// Vulkan (Linux, Windows) y Metal (macOS). Sin DirectX.
|
||||
// Ownership del SDL_GPUDevice i del claim del window. Backend per plataforma:
|
||||
// Vulkan (Linux, Windows) i Metal (macOS). Sense DirectX.
|
||||
//
|
||||
// Uso:
|
||||
// Els shaders s'embedien al binari (no es carreguen de disc): SPIR-V via
|
||||
// headers generats per CMake a `gpu/spv/*.h`, i MSL a mà a `gpu/msl/*.msl.h`.
|
||||
// La creació dels SDL_GPUShader la fan els pipelines via shader_factory.hpp.
|
||||
//
|
||||
// Ús:
|
||||
// GpuDevice device;
|
||||
// if (!device.init(window)) return -1; // claim del window
|
||||
// ... renderer setup ...
|
||||
// ... pipelines setup ...
|
||||
// device.destroy(); // unclaim + destroy device
|
||||
|
||||
#pragma once
|
||||
@@ -15,45 +19,33 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Rendering::GPU {
|
||||
|
||||
class GpuDevice {
|
||||
public:
|
||||
class GpuDevice {
|
||||
public:
|
||||
GpuDevice() = default;
|
||||
~GpuDevice();
|
||||
|
||||
// No copia / move (RAII propietario del device).
|
||||
// No copia / move (RAII propietari del device).
|
||||
GpuDevice(const GpuDevice&) = delete;
|
||||
auto operator=(const GpuDevice&) -> GpuDevice& = delete;
|
||||
GpuDevice(GpuDevice&&) = delete;
|
||||
auto operator=(GpuDevice&&) -> GpuDevice& = delete;
|
||||
|
||||
// Crea el device y claim el window. Devuelve false si no hay backend
|
||||
// soportado o si el driver no permite el claim.
|
||||
// Crea el device i claim el window. Selecciona MSL en macOS, SPIR-V
|
||||
// en la resta. Retorna false si no hi ha backend suportat o si el
|
||||
// driver no permet el claim.
|
||||
[[nodiscard]] auto init(SDL_Window* window) -> bool;
|
||||
void destroy();
|
||||
|
||||
// Carga un shader SPIR-V desde build/shaders/{name}.spv. Devuelve un
|
||||
// SDL_GPUShader* del que ahora es responsable el caller (libera con
|
||||
// SDL_ReleaseGPUShader). Retorna nullptr si falla.
|
||||
//
|
||||
// num_uniform_buffers: nº de uniform buffers que usa el shader (slot 0..N-1).
|
||||
// num_samplers: nº de samplers (combined image+sampler) usados por el shader.
|
||||
[[nodiscard]] auto loadShader(const std::string& spv_filename,
|
||||
SDL_GPUShaderStage stage,
|
||||
uint32_t num_uniform_buffers,
|
||||
uint32_t num_samplers = 0) const -> SDL_GPUShader*;
|
||||
|
||||
[[nodiscard]] auto get() const -> SDL_GPUDevice* { return device_; }
|
||||
[[nodiscard]] auto window() const -> SDL_Window* { return window_; }
|
||||
[[nodiscard]] auto swapchainFormat() const -> SDL_GPUTextureFormat { return swapchain_format_; }
|
||||
|
||||
private:
|
||||
private:
|
||||
SDL_GPUDevice* device_{nullptr};
|
||||
SDL_Window* window_{nullptr};
|
||||
SDL_GPUTextureFormat swapchain_format_{SDL_GPU_TEXTUREFORMAT_INVALID};
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
|
||||
@@ -14,9 +14,11 @@ namespace Rendering::GPU {
|
||||
|
||||
GpuFrameRenderer::~GpuFrameRenderer() { destroy(); }
|
||||
|
||||
auto GpuFrameRenderer::init(SDL_Window* window, float logical_w, float logical_h) -> bool {
|
||||
auto GpuFrameRenderer::init(SDL_Window* window, float logical_w, float logical_h, float render_w, float render_h) -> bool {
|
||||
logical_w_ = logical_w;
|
||||
logical_h_ = logical_h;
|
||||
render_w_ = render_w;
|
||||
render_h_ = render_h;
|
||||
|
||||
if (!device_.init(window)) {
|
||||
return false;
|
||||
@@ -47,13 +49,15 @@ namespace Rendering::GPU {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Textura offscreen del tamaño lógico del juego, COLOR_TARGET + SAMPLER.
|
||||
// Textura offscreen del tamaño físico de render, COLOR_TARGET + SAMPLER.
|
||||
// El tamaño lógico se aplica a los vértices vía UBO; el offscreen puede
|
||||
// ser de mayor resolución para ganar nitidez tras el upscale a la swapchain.
|
||||
SDL_GPUTextureCreateInfo tex_info{};
|
||||
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||
tex_info.format = offscreen_format_;
|
||||
tex_info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||
tex_info.width = static_cast<uint32_t>(logical_w_);
|
||||
tex_info.height = static_cast<uint32_t>(logical_h_);
|
||||
tex_info.width = static_cast<uint32_t>(render_w_);
|
||||
tex_info.height = static_cast<uint32_t>(render_h_);
|
||||
tex_info.layer_count_or_depth = 1;
|
||||
tex_info.num_levels = 1;
|
||||
tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
||||
@@ -107,6 +111,19 @@ namespace Rendering::GPU {
|
||||
indices_.clear();
|
||||
}
|
||||
|
||||
auto GpuFrameRenderer::resizeRenderTarget(float render_w, float render_h) -> bool {
|
||||
// Solo seguro fuera de un frame: si el cmd buffer está vivo y referencia
|
||||
// la textura antigua, recrearla provocaría un cuelgue/UB.
|
||||
if (isInsideFrame()) {
|
||||
std::cerr << "[GpuFrameRenderer] resizeRenderTarget llamado dentro de frame, ignorado\n";
|
||||
return false;
|
||||
}
|
||||
destroyOffscreen();
|
||||
render_w_ = render_w;
|
||||
render_h_ = render_h;
|
||||
return createOffscreen();
|
||||
}
|
||||
|
||||
auto GpuFrameRenderer::beginFrame(float clear_r, float clear_g, float clear_b) -> bool {
|
||||
// Los clear_* se ignoran: el fondo lo pinta el postpro. Mantenemos la
|
||||
// firma para no romper el SDLManager.
|
||||
@@ -438,8 +455,9 @@ namespace Rendering::GPU {
|
||||
ubo.background_max_g = BG_MAX_G;
|
||||
ubo.background_max_b = BG_MAX_B;
|
||||
ubo.background_max_a = 1.0F;
|
||||
ubo.texel_size_x = 1.0F / logical_w_;
|
||||
ubo.texel_size_y = 1.0F / logical_h_;
|
||||
// El sampling del bloom muestrea el offscreen → texel size del tamaño físico.
|
||||
ubo.texel_size_x = 1.0F / render_w_;
|
||||
ubo.texel_size_y = 1.0F / render_h_;
|
||||
ubo.pad_b = 0.0F;
|
||||
ubo.pad_c = 0.0F;
|
||||
|
||||
|
||||
@@ -59,12 +59,24 @@ namespace Rendering::GPU {
|
||||
GpuFrameRenderer(GpuFrameRenderer&&) = delete;
|
||||
auto operator=(GpuFrameRenderer&&) -> GpuFrameRenderer& = delete;
|
||||
|
||||
// Crea device + pipeline + offscreen + sampler. logical_w/h = tamaño
|
||||
// en píxeles lógicos del juego (1280×720), usado como base del
|
||||
// offscreen y de la transformación a NDC del shader de líneas.
|
||||
[[nodiscard]] auto init(SDL_Window* window, float logical_w, float logical_h) -> bool;
|
||||
// Crea device + pipeline + offscreen + sampler.
|
||||
// logical_w/h: tamaño en píxeles lógicos del juego (1280×720). Lo
|
||||
// consume el shader de líneas para transformar a NDC.
|
||||
// render_w/h: tamaño físico del offscreen donde se rasterizan las
|
||||
// líneas. Puede ser > logical para ganar nitidez al
|
||||
// escalar a la swapchain (configurable vía YAML).
|
||||
[[nodiscard]] auto init(SDL_Window* window,
|
||||
float logical_w,
|
||||
float logical_h,
|
||||
float render_w,
|
||||
float render_h) -> bool;
|
||||
void destroy();
|
||||
|
||||
// Recrea el offscreen con un nuevo tamaño físico de render. Solo es
|
||||
// seguro fuera de un frame (isInsideFrame() == false). Devuelve false
|
||||
// si está dentro de frame o si la creación de la textura falla.
|
||||
[[nodiscard]] auto resizeRenderTarget(float render_w, float render_h) -> bool;
|
||||
|
||||
// beginFrame: adquiere swapchain, abre render pass sobre offscreen
|
||||
// con clear a negro. Devuelve false si no hay textura disponible.
|
||||
// Los argumentos clear_r/g/b se ignoran (compatibilidad de API: el
|
||||
@@ -114,10 +126,18 @@ namespace Rendering::GPU {
|
||||
GpuLinePipeline line_pipeline_;
|
||||
GpuPostFxPipeline postfx_pipeline_;
|
||||
|
||||
// Tamaño lógico del juego (= tamaño del offscreen).
|
||||
// Tamaño lógico del juego: espacio de coordenadas de las primitivas
|
||||
// (vértices, UBO del line shader). Fijo a 1280×720.
|
||||
float logical_w_{1280.0F};
|
||||
float logical_h_{720.0F};
|
||||
|
||||
// Tamaño físico del offscreen (configurable). Independiente del lógico:
|
||||
// las coordenadas en NDC son agnósticas a la resolución de salida, así
|
||||
// que rasterizar a mayor render_w_/h_ da líneas más nítidas tras el
|
||||
// upscale lineal a la swapchain.
|
||||
float render_w_{1280.0F};
|
||||
float render_h_{720.0F};
|
||||
|
||||
// Viewport del pase final en píxeles físicos. <0 = full window.
|
||||
float viewport_x_{0.0F};
|
||||
float viewport_y_{0.0F};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// gpu_line_pipeline.cpp - Implementación del pipeline de líneas
|
||||
// gpu_line_pipeline.cpp - Implementació del pipeline de línies
|
||||
|
||||
#include "core/rendering/gpu/gpu_line_pipeline.hpp"
|
||||
|
||||
@@ -8,6 +8,15 @@
|
||||
#include <iostream>
|
||||
|
||||
#include "core/rendering/gpu/gpu_device.hpp"
|
||||
#include "core/rendering/gpu/shader_factory.hpp"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include "core/rendering/gpu/msl/line_frag.msl.h"
|
||||
#include "core/rendering/gpu/msl/line_vert.msl.h"
|
||||
#else
|
||||
#include "core/rendering/gpu/spv/line_frag_spv.h"
|
||||
#include "core/rendering/gpu/spv/line_vert_spv.h"
|
||||
#endif
|
||||
|
||||
namespace Rendering::GPU {
|
||||
|
||||
@@ -20,12 +29,37 @@ namespace Rendering::GPU {
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_GPUShader* vert = device.loadShader("line.vert.spv",
|
||||
// Vertex: 1 UBO (viewport_size). Fragment: cap recurs.
|
||||
#ifdef __APPLE__
|
||||
SDL_GPUShader* vert = createShaderMSL(owner_,
|
||||
Msl::LINE_VERT_MSL,
|
||||
"line_vs",
|
||||
SDL_GPU_SHADERSTAGE_VERTEX,
|
||||
/*num_samplers=*/0,
|
||||
/*num_uniform_buffers=*/1);
|
||||
SDL_GPUShader* frag = device.loadShader("line.frag.spv",
|
||||
SDL_GPUShader* frag = createShaderMSL(owner_,
|
||||
Msl::LINE_FRAG_MSL,
|
||||
"line_fs",
|
||||
SDL_GPU_SHADERSTAGE_FRAGMENT,
|
||||
/*num_samplers=*/0,
|
||||
/*num_uniform_buffers=*/0);
|
||||
#else
|
||||
SDL_GPUShader* vert = createShaderSPIRV(owner_,
|
||||
LINE_VERT_SPV,
|
||||
LINE_VERT_SPV_SIZE,
|
||||
"main",
|
||||
SDL_GPU_SHADERSTAGE_VERTEX,
|
||||
/*num_samplers=*/0,
|
||||
/*num_uniform_buffers=*/1);
|
||||
SDL_GPUShader* frag = createShaderSPIRV(owner_,
|
||||
LINE_FRAG_SPV,
|
||||
LINE_FRAG_SPV_SIZE,
|
||||
"main",
|
||||
SDL_GPU_SHADERSTAGE_FRAGMENT,
|
||||
/*num_samplers=*/0,
|
||||
/*num_uniform_buffers=*/0);
|
||||
#endif
|
||||
|
||||
if ((vert == nullptr) || (frag == nullptr)) {
|
||||
if (vert != nullptr) {
|
||||
SDL_ReleaseGPUShader(owner_, vert);
|
||||
@@ -33,7 +67,7 @@ namespace Rendering::GPU {
|
||||
if (frag != nullptr) {
|
||||
SDL_ReleaseGPUShader(owner_, frag);
|
||||
}
|
||||
std::cerr << "[GpuLinePipeline] Error cargando shaders\n";
|
||||
std::cerr << "[GpuLinePipeline] Error cargando shaders: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -98,8 +132,8 @@ namespace Rendering::GPU {
|
||||
|
||||
pipeline_ = SDL_CreateGPUGraphicsPipeline(owner_, &info);
|
||||
|
||||
// Los shaders se pueden liberar tras crear el pipeline (SDL los retiene
|
||||
// internamente mientras el pipeline esté vivo).
|
||||
// Els shaders es poden alliberar després de crear el pipeline (SDL els
|
||||
// reté internament mentre el pipeline estigui viu).
|
||||
SDL_ReleaseGPUShader(owner_, vert);
|
||||
SDL_ReleaseGPUShader(owner_, frag);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// gpu_postfx_pipeline.cpp - Implementación del pipeline de postprocesado.
|
||||
// gpu_postfx_pipeline.cpp - Implementació del pipeline de postprocesat.
|
||||
|
||||
#include "core/rendering/gpu/gpu_postfx_pipeline.hpp"
|
||||
|
||||
@@ -8,91 +8,122 @@
|
||||
#include <iostream>
|
||||
|
||||
#include "core/rendering/gpu/gpu_device.hpp"
|
||||
#include "core/rendering/gpu/shader_factory.hpp"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include "core/rendering/gpu/msl/postfx_frag.msl.h"
|
||||
#include "core/rendering/gpu/msl/postfx_vert.msl.h"
|
||||
#else
|
||||
#include "core/rendering/gpu/spv/postfx_frag_spv.h"
|
||||
#include "core/rendering/gpu/spv/postfx_vert_spv.h"
|
||||
#endif
|
||||
|
||||
namespace Rendering::GPU {
|
||||
|
||||
GpuPostFxPipeline::~GpuPostFxPipeline() { destroy(); }
|
||||
GpuPostFxPipeline::~GpuPostFxPipeline() { destroy(); }
|
||||
|
||||
auto GpuPostFxPipeline::init(const GpuDevice& device,
|
||||
SDL_GPUTextureFormat target_format) -> bool {
|
||||
owner_ = device.get();
|
||||
if (owner_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// El vertex shader no usa UBO (emite tres vértices hardcodeados).
|
||||
// El fragment shader usa 1 sampler (escena) y 1 UBO (parámetros postpro).
|
||||
SDL_GPUShader* vert = device.loadShader("postfx.vert.spv",
|
||||
SDL_GPU_SHADERSTAGE_VERTEX,
|
||||
/*num_uniform_buffers=*/0,
|
||||
/*num_samplers=*/0);
|
||||
SDL_GPUShader* frag = device.loadShader("postfx.frag.spv",
|
||||
SDL_GPU_SHADERSTAGE_FRAGMENT,
|
||||
/*num_uniform_buffers=*/1,
|
||||
/*num_samplers=*/1);
|
||||
if ((vert == nullptr) || (frag == nullptr)) {
|
||||
if (vert != nullptr) {
|
||||
SDL_ReleaseGPUShader(owner_, vert);
|
||||
auto GpuPostFxPipeline::init(const GpuDevice& device,
|
||||
SDL_GPUTextureFormat target_format) -> bool {
|
||||
owner_ = device.get();
|
||||
if (owner_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (frag != nullptr) {
|
||||
SDL_ReleaseGPUShader(owner_, frag);
|
||||
|
||||
// Vertex shader: sense UBO ni vertex buffer (emet 3 vèrtexs hardcodejats).
|
||||
// Fragment shader: 1 sampler (escena) + 1 UBO (paràmetres postpro).
|
||||
#ifdef __APPLE__
|
||||
SDL_GPUShader* vert = createShaderMSL(owner_,
|
||||
Msl::POSTFX_VERT_MSL,
|
||||
"postfx_vs",
|
||||
SDL_GPU_SHADERSTAGE_VERTEX,
|
||||
/*num_samplers=*/0,
|
||||
/*num_uniform_buffers=*/0);
|
||||
SDL_GPUShader* frag = createShaderMSL(owner_,
|
||||
Msl::POSTFX_FRAG_MSL,
|
||||
"postfx_fs",
|
||||
SDL_GPU_SHADERSTAGE_FRAGMENT,
|
||||
/*num_samplers=*/1,
|
||||
/*num_uniform_buffers=*/1);
|
||||
#else
|
||||
SDL_GPUShader* vert = createShaderSPIRV(owner_,
|
||||
POSTFX_VERT_SPV,
|
||||
POSTFX_VERT_SPV_SIZE,
|
||||
"main",
|
||||
SDL_GPU_SHADERSTAGE_VERTEX,
|
||||
/*num_samplers=*/0,
|
||||
/*num_uniform_buffers=*/0);
|
||||
SDL_GPUShader* frag = createShaderSPIRV(owner_,
|
||||
POSTFX_FRAG_SPV,
|
||||
POSTFX_FRAG_SPV_SIZE,
|
||||
"main",
|
||||
SDL_GPU_SHADERSTAGE_FRAGMENT,
|
||||
/*num_samplers=*/1,
|
||||
/*num_uniform_buffers=*/1);
|
||||
#endif
|
||||
|
||||
if ((vert == nullptr) || (frag == nullptr)) {
|
||||
if (vert != nullptr) {
|
||||
SDL_ReleaseGPUShader(owner_, vert);
|
||||
}
|
||||
if (frag != nullptr) {
|
||||
SDL_ReleaseGPUShader(owner_, frag);
|
||||
}
|
||||
std::cerr << "[GpuPostFxPipeline] Error cargando shaders postfx: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
std::cerr << "[GpuPostFxPipeline] Error cargando shaders postfx\n";
|
||||
return false;
|
||||
|
||||
// Sense vertex input: els tres vèrtexs del triangle es generen al shader.
|
||||
SDL_GPUVertexInputState vertex_input{};
|
||||
vertex_input.vertex_buffer_descriptions = nullptr;
|
||||
vertex_input.num_vertex_buffers = 0;
|
||||
vertex_input.vertex_attributes = nullptr;
|
||||
vertex_input.num_vertex_attributes = 0;
|
||||
|
||||
// Color target del postpro = swapchain. Sense blending: el postpro reescriu
|
||||
// píxels directament (la mescla amb l'escena ja es fa dins del shader).
|
||||
SDL_GPUColorTargetDescription color_target{};
|
||||
color_target.format = target_format;
|
||||
color_target.blend_state.enable_blend = false;
|
||||
color_target.blend_state.color_write_mask =
|
||||
SDL_GPU_COLORCOMPONENT_R | SDL_GPU_COLORCOMPONENT_G |
|
||||
SDL_GPU_COLORCOMPONENT_B | SDL_GPU_COLORCOMPONENT_A;
|
||||
|
||||
SDL_GPUGraphicsPipelineTargetInfo target_info{};
|
||||
target_info.color_target_descriptions = &color_target;
|
||||
target_info.num_color_targets = 1;
|
||||
target_info.has_depth_stencil_target = false;
|
||||
|
||||
SDL_GPUGraphicsPipelineCreateInfo info{};
|
||||
info.vertex_shader = vert;
|
||||
info.fragment_shader = frag;
|
||||
info.vertex_input_state = vertex_input;
|
||||
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
|
||||
info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE;
|
||||
info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
|
||||
info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
||||
info.depth_stencil_state = {};
|
||||
info.target_info = target_info;
|
||||
|
||||
pipeline_ = SDL_CreateGPUGraphicsPipeline(owner_, &info);
|
||||
|
||||
SDL_ReleaseGPUShader(owner_, vert);
|
||||
SDL_ReleaseGPUShader(owner_, frag);
|
||||
|
||||
if (pipeline_ == nullptr) {
|
||||
std::cerr << "[GpuPostFxPipeline] SDL_CreateGPUGraphicsPipeline: "
|
||||
<< SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sin vertex input: los tres vértices del triángulo se generan en el shader.
|
||||
SDL_GPUVertexInputState vertex_input{};
|
||||
vertex_input.vertex_buffer_descriptions = nullptr;
|
||||
vertex_input.num_vertex_buffers = 0;
|
||||
vertex_input.vertex_attributes = nullptr;
|
||||
vertex_input.num_vertex_attributes = 0;
|
||||
|
||||
// Color target del postpro = swapchain. Sin blending: el postpro reescribe
|
||||
// píxeles directamente (la mezcla con la escena ya se hizo dentro del shader).
|
||||
SDL_GPUColorTargetDescription color_target{};
|
||||
color_target.format = target_format;
|
||||
color_target.blend_state.enable_blend = false;
|
||||
color_target.blend_state.color_write_mask =
|
||||
SDL_GPU_COLORCOMPONENT_R | SDL_GPU_COLORCOMPONENT_G |
|
||||
SDL_GPU_COLORCOMPONENT_B | SDL_GPU_COLORCOMPONENT_A;
|
||||
|
||||
SDL_GPUGraphicsPipelineTargetInfo target_info{};
|
||||
target_info.color_target_descriptions = &color_target;
|
||||
target_info.num_color_targets = 1;
|
||||
target_info.has_depth_stencil_target = false;
|
||||
|
||||
SDL_GPUGraphicsPipelineCreateInfo info{};
|
||||
info.vertex_shader = vert;
|
||||
info.fragment_shader = frag;
|
||||
info.vertex_input_state = vertex_input;
|
||||
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
|
||||
info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE;
|
||||
info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
|
||||
info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
||||
info.depth_stencil_state = {};
|
||||
info.target_info = target_info;
|
||||
|
||||
pipeline_ = SDL_CreateGPUGraphicsPipeline(owner_, &info);
|
||||
|
||||
SDL_ReleaseGPUShader(owner_, vert);
|
||||
SDL_ReleaseGPUShader(owner_, frag);
|
||||
|
||||
if (pipeline_ == nullptr) {
|
||||
std::cerr << "[GpuPostFxPipeline] SDL_CreateGPUGraphicsPipeline: "
|
||||
<< SDL_GetError() << '\n';
|
||||
return false;
|
||||
void GpuPostFxPipeline::destroy() {
|
||||
if ((pipeline_ != nullptr) && (owner_ != nullptr)) {
|
||||
SDL_ReleaseGPUGraphicsPipeline(owner_, pipeline_);
|
||||
}
|
||||
pipeline_ = nullptr;
|
||||
owner_ = nullptr;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void GpuPostFxPipeline::destroy() {
|
||||
if ((pipeline_ != nullptr) && (owner_ != nullptr)) {
|
||||
SDL_ReleaseGPUGraphicsPipeline(owner_, pipeline_);
|
||||
}
|
||||
pipeline_ = nullptr;
|
||||
owner_ = nullptr;
|
||||
}
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// line_frag.msl.h - Metal Shading Language del fragment shader de línies
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// IMPORTANT: mantenir sincronitzat a mà amb shaders/line.frag.glsl. SDL3 GPU
|
||||
// compila aquest string MSL en runtime; no hi ha generador automàtic.
|
||||
//
|
||||
// Aplica antialias geomètric via smoothstep sobre edge_dist (±1 als laterals,
|
||||
// 0 al centre del quad). Sense uniforms ni samplers.
|
||||
|
||||
#pragma once
|
||||
#ifdef __APPLE__
|
||||
|
||||
namespace Rendering::GPU::Msl {
|
||||
|
||||
inline constexpr const char* LINE_FRAG_MSL = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct VOut {
|
||||
float4 pos [[position]];
|
||||
float4 color;
|
||||
float edge_dist;
|
||||
};
|
||||
|
||||
fragment float4 line_fs(VOut in [[stage_in]]) {
|
||||
float d = abs(in.edge_dist);
|
||||
float alpha = 1.0 - smoothstep(0.7, 1.0, d);
|
||||
return float4(in.color.rgb, in.color.a * alpha);
|
||||
}
|
||||
)";
|
||||
|
||||
} // namespace Rendering::GPU::Msl
|
||||
|
||||
#endif // __APPLE__
|
||||
@@ -0,0 +1,54 @@
|
||||
// line_vert.msl.h - Metal Shading Language del vertex shader de línies
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// IMPORTANT: mantenir sincronitzat a mà amb shaders/line.vert.glsl. SDL3 GPU
|
||||
// compila aquest string MSL en runtime; no hi ha generador automàtic. Qualsevol
|
||||
// canvi al UBO o al layout de vèrtex cal replicar-lo ací al mateix commit.
|
||||
//
|
||||
// Mapping SDL3 GPU → Metal (segons src/gpu/metal/SDL_gpu_metal.m upstream):
|
||||
// - Vertex buffers d'usuari: [[buffer(14 + slot)]] (METAL_FIRST_VERTEX_BUFFER_SLOT=14).
|
||||
// A través de [[stage_in]] amb [[attribute(N)]], Metal resol automàticament
|
||||
// a partir del MTLVertexDescriptor del pipeline state (transparent al MSL).
|
||||
// - Vertex UBO slot 0 SDL → [[buffer(0)]] MSL (sense offset).
|
||||
|
||||
#pragma once
|
||||
#ifdef __APPLE__
|
||||
|
||||
namespace Rendering::GPU::Msl {
|
||||
|
||||
inline constexpr const char* LINE_VERT_MSL = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct VIn {
|
||||
float2 in_position [[attribute(0)]];
|
||||
float4 in_color [[attribute(1)]];
|
||||
float in_edge_dist[[attribute(2)]];
|
||||
};
|
||||
|
||||
struct VOut {
|
||||
float4 pos [[position]];
|
||||
float4 color;
|
||||
float edge_dist;
|
||||
};
|
||||
|
||||
struct LineUBO {
|
||||
float2 viewport_size;
|
||||
float2 _padding;
|
||||
};
|
||||
|
||||
vertex VOut line_vs(VIn in [[stage_in]],
|
||||
constant LineUBO& ubo [[buffer(0)]]) {
|
||||
float2 ndc = (in.in_position / ubo.viewport_size) * 2.0 - 1.0;
|
||||
ndc.y = -ndc.y;
|
||||
VOut out;
|
||||
out.pos = float4(ndc, 0.0, 1.0);
|
||||
out.color = in.in_color;
|
||||
out.edge_dist = in.in_edge_dist;
|
||||
return out;
|
||||
}
|
||||
)";
|
||||
|
||||
} // namespace Rendering::GPU::Msl
|
||||
|
||||
#endif // __APPLE__
|
||||
@@ -0,0 +1,90 @@
|
||||
// postfx_frag.msl.h - Metal Shading Language del fragment shader del postpro
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// IMPORTANT: mantenir sincronitzat a mà amb shaders/postfx.frag.glsl. SDL3 GPU
|
||||
// compila aquest string MSL en runtime; no hi ha generador automàtic. Qualsevol
|
||||
// canvi al struct PostFxUniforms (gpu_postfx_pipeline.hpp), al GLSL o al MSL
|
||||
// cal replicar-lo a totes tres al mateix commit.
|
||||
//
|
||||
// Composició final: bloom 5×5 amb high-pass, flicker sinusoidal global,
|
||||
// background pulse sumat. Recursos:
|
||||
// - texture2d<float> scene [[texture(0)]] + sampler [[sampler(0)]]
|
||||
// - constant PostFxUBO& ubo [[buffer(0)]] (slot 0 SDL → buffer(0) MSL)
|
||||
//
|
||||
// L'struct PostFxUBO té layout idèntic a PostFxUniforms (5×vec4 = 80 bytes).
|
||||
|
||||
#pragma once
|
||||
#ifdef __APPLE__
|
||||
|
||||
namespace Rendering::GPU::Msl {
|
||||
|
||||
inline constexpr const char* POSTFX_FRAG_MSL = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct PostVOut {
|
||||
float4 pos [[position]];
|
||||
float2 uv;
|
||||
};
|
||||
|
||||
struct PostFxUBO {
|
||||
float time;
|
||||
float bloom_intensity;
|
||||
float bloom_threshold;
|
||||
float bloom_radius_px;
|
||||
|
||||
float flicker_amplitude;
|
||||
float flicker_frequency_hz;
|
||||
float background_pulse_freq_hz;
|
||||
float pad_a;
|
||||
|
||||
float4 background_min;
|
||||
float4 background_max;
|
||||
|
||||
float2 texel_size;
|
||||
float2 pad_b;
|
||||
};
|
||||
|
||||
constant float TAU = 6.28318530718;
|
||||
|
||||
fragment float4 postfx_fs(PostVOut in [[stage_in]],
|
||||
texture2d<float> scene [[texture(0)]],
|
||||
sampler samp [[sampler(0)]],
|
||||
constant PostFxUBO& ubo [[buffer(0)]]) {
|
||||
// === BLOOM ===
|
||||
float3 src = scene.sample(samp, in.uv).rgb;
|
||||
float3 bloom = float3(0.0);
|
||||
float total_weight = 0.0;
|
||||
for (int dy = -2; dy <= 2; ++dy) {
|
||||
for (int dx = -2; dx <= 2; ++dx) {
|
||||
float2 offset = float2(float(dx), float(dy)) * ubo.texel_size * ubo.bloom_radius_px;
|
||||
float3 c = scene.sample(samp, in.uv + offset).rgb;
|
||||
float luma = max(c.r, max(c.g, c.b));
|
||||
float high_pass = max(0.0, luma - ubo.bloom_threshold);
|
||||
float w = exp(-float(dx * dx + dy * dy) / 4.0);
|
||||
bloom += c * high_pass * w;
|
||||
total_weight += w;
|
||||
}
|
||||
}
|
||||
if (total_weight > 0.0) {
|
||||
bloom /= total_weight;
|
||||
}
|
||||
bloom *= ubo.bloom_intensity;
|
||||
|
||||
// === FLICKER ===
|
||||
float pulse = (sin(ubo.time * ubo.flicker_frequency_hz * TAU) * 0.5) + 0.5;
|
||||
float flicker = 1.0 - (ubo.flicker_amplitude * (1.0 - pulse));
|
||||
|
||||
// === BACKGROUND PULSE ===
|
||||
float bg_pulse = (sin(ubo.time * ubo.background_pulse_freq_hz * TAU) * 0.5) + 0.5;
|
||||
float3 background = mix(ubo.background_min.rgb, ubo.background_max.rgb, bg_pulse);
|
||||
|
||||
// === COMPOSICIÓ ===
|
||||
float3 lines_and_glow = (src + bloom) * flicker;
|
||||
return float4(background + lines_and_glow, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
} // namespace Rendering::GPU::Msl
|
||||
|
||||
#endif // __APPLE__
|
||||
@@ -0,0 +1,38 @@
|
||||
// postfx_vert.msl.h - Metal Shading Language del vertex shader del postpro
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// IMPORTANT: mantenir sincronitzat a mà amb shaders/postfx.vert.glsl. SDL3 GPU
|
||||
// compila aquest string MSL en runtime; no hi ha generador automàtic.
|
||||
//
|
||||
// Fullscreen triangle: 3 vèrtexs en (-1,-1), (3,-1), (-1,3) generats per
|
||||
// [[vertex_id]]. UV.y invertida per compensar la diferència de convenció
|
||||
// entre clip-space del line shader (Y-flip) i el mostreig SDL_gpu (origen
|
||||
// top-left). Sense uniforms ni vertex buffers (DrawPrimitives vertex_count=3).
|
||||
|
||||
#pragma once
|
||||
#ifdef __APPLE__
|
||||
|
||||
namespace Rendering::GPU::Msl {
|
||||
|
||||
inline constexpr const char* POSTFX_VERT_MSL = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct PostVOut {
|
||||
float4 pos [[position]];
|
||||
float2 uv;
|
||||
};
|
||||
|
||||
vertex PostVOut postfx_vs(uint vid [[vertex_id]]) {
|
||||
const float2 positions[3] = { {-1.0, -1.0}, { 3.0, -1.0}, {-1.0, 3.0} };
|
||||
const float2 uvs[3] = { { 0.0, 1.0}, { 2.0, 1.0}, { 0.0, -1.0} };
|
||||
PostVOut out;
|
||||
out.pos = float4(positions[vid], 0.0, 1.0);
|
||||
out.uv = uvs[vid];
|
||||
return out;
|
||||
}
|
||||
)";
|
||||
|
||||
} // namespace Rendering::GPU::Msl
|
||||
|
||||
#endif // __APPLE__
|
||||
@@ -0,0 +1,60 @@
|
||||
// shader_factory.hpp - Helpers per crear SDL_GPUShader segons plataforma
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// En __APPLE__ s'utilitza MSL (Metal Shading Language) embedit com a string
|
||||
// literal C++. En la resta de plataformes (Linux/Windows) s'utilitza SPIR-V
|
||||
// embedit com a uint8_t[] en headers generats per CMake. La selecció és
|
||||
// compile-time via #ifdef.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Rendering::GPU {
|
||||
|
||||
#ifdef __APPLE__
|
||||
inline auto createShaderMSL(SDL_GPUDevice* device,
|
||||
const char* msl_source,
|
||||
const char* entrypoint,
|
||||
SDL_GPUShaderStage stage,
|
||||
Uint32 num_samplers,
|
||||
Uint32 num_uniform_buffers) -> SDL_GPUShader* {
|
||||
SDL_GPUShaderCreateInfo info{};
|
||||
info.code = reinterpret_cast<const Uint8*>(msl_source);
|
||||
info.code_size = std::strlen(msl_source) + 1;
|
||||
info.entrypoint = entrypoint;
|
||||
info.format = SDL_GPU_SHADERFORMAT_MSL;
|
||||
info.stage = stage;
|
||||
info.num_samplers = num_samplers;
|
||||
info.num_uniform_buffers = num_uniform_buffers;
|
||||
info.num_storage_buffers = 0;
|
||||
info.num_storage_textures = 0;
|
||||
return SDL_CreateGPUShader(device, &info);
|
||||
}
|
||||
#else
|
||||
inline auto createShaderSPIRV(SDL_GPUDevice* device,
|
||||
const std::uint8_t* spv_code,
|
||||
std::size_t spv_size,
|
||||
const char* entrypoint,
|
||||
SDL_GPUShaderStage stage,
|
||||
Uint32 num_samplers,
|
||||
Uint32 num_uniform_buffers) -> SDL_GPUShader* {
|
||||
SDL_GPUShaderCreateInfo info{};
|
||||
info.code = spv_code;
|
||||
info.code_size = spv_size;
|
||||
info.entrypoint = entrypoint;
|
||||
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||
info.stage = stage;
|
||||
info.num_samplers = num_samplers;
|
||||
info.num_uniform_buffers = num_uniform_buffers;
|
||||
info.num_storage_buffers = 0;
|
||||
info.num_storage_textures = 0;
|
||||
return SDL_CreateGPUShader(device, &info);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
@@ -0,0 +1,670 @@
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
static const uint8_t LINE_FRAG_SPV[] = {
|
||||
0x03,
|
||||
0x02,
|
||||
0x23,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x25,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x53,
|
||||
0x4c,
|
||||
0x2e,
|
||||
0x73,
|
||||
0x74,
|
||||
0x64,
|
||||
0x2e,
|
||||
0x34,
|
||||
0x35,
|
||||
0x30,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0e,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x08,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x6d,
|
||||
0x61,
|
||||
0x69,
|
||||
0x6e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x15,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x17,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x15,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x17,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x16,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x2b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x80,
|
||||
0x3f,
|
||||
0x2b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x33,
|
||||
0x33,
|
||||
0x33,
|
||||
0x3f,
|
||||
0x17,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x14,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x14,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x15,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x16,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x16,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x17,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x15,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x1b,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x2b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x1b,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1c,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x36,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xf8,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3d,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0c,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0c,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0c,
|
||||
0x00,
|
||||
0x08,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x31,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0c,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x83,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x12,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3d,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x19,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x17,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x41,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x17,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1c,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3d,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x85,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x12,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x51,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x19,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x51,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x22,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x19,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x51,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x23,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x19,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x50,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x24,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x22,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x23,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3e,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x15,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x24,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xfd,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x38,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
};
|
||||
static const size_t LINE_FRAG_SPV_SIZE = 664;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,52 +3,56 @@
|
||||
|
||||
#include "core/rendering/line_renderer.hpp"
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
// Color global compartido para líneas sin paleta propia (HUD, debug, texto
|
||||
// genérico). Equivale al "color máximo" de la antigua oscilación CPU: verde
|
||||
// fósforo CRT. El pulso de brillo lo aplica ahora el shader de postpro.
|
||||
SDL_Color g_current_line_color = {100, 255, 100, 255};
|
||||
// Color global compartido para líneas sin paleta propia (HUD, debug, texto
|
||||
// genérico). Equivale al "color máximo" de la antigua oscilación CPU: verde
|
||||
// fósforo CRT. El pulso de brillo lo aplica ahora el shader de postpro.
|
||||
SDL_Color g_current_line_color = {100, 255, 100, 255};
|
||||
|
||||
// Grosor global por defecto. Configurable via setLineThickness.
|
||||
// 1.5 da una línea visible y crujiente; 1.0 se ve demasiado fino en pantallas grandes.
|
||||
float g_current_line_thickness = 1.5F;
|
||||
// Grosor global por defecto. Configurable via setLineThickness.
|
||||
float g_current_line_thickness = Defaults::Rendering::LINE_THICKNESS_DEFAULT;
|
||||
|
||||
void linea(Renderer* renderer,
|
||||
int x1, int y1, int x2, int y2,
|
||||
float brightness,
|
||||
float thickness,
|
||||
SDL_Color color) {
|
||||
if (renderer == nullptr) {
|
||||
return;
|
||||
void linea(Renderer* renderer,
|
||||
int x1,
|
||||
int y1,
|
||||
int x2,
|
||||
int y2,
|
||||
float brightness,
|
||||
float thickness,
|
||||
SDL_Color color) {
|
||||
if (renderer == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Coords lógicas (1280×720). El shader hace el mapeo a NDC; el viewport
|
||||
// del SDLManager hace el letterbox a píxeles físicos.
|
||||
const auto FX1 = static_cast<float>(x1);
|
||||
const auto FY1 = static_cast<float>(y1);
|
||||
const auto FX2 = static_cast<float>(x2);
|
||||
const auto FY2 = static_cast<float>(y2);
|
||||
|
||||
// color.alpha==0 → usar color global (verde fósforo). alpha>0 → color directo.
|
||||
const SDL_Color SOURCE = (color.a > 0) ? color : g_current_line_color;
|
||||
const float R = (static_cast<float>(SOURCE.r) * brightness) / 255.0F;
|
||||
const float G = (static_cast<float>(SOURCE.g) * brightness) / 255.0F;
|
||||
const float B = (static_cast<float>(SOURCE.b) * brightness) / 255.0F;
|
||||
|
||||
const float W = (thickness > 0.0F) ? thickness : g_current_line_thickness;
|
||||
|
||||
renderer->pushLine(FX1, FY1, FX2, FY2, W, R, G, B, 1.0F);
|
||||
}
|
||||
|
||||
// Coords lógicas (1280×720). El shader hace el mapeo a NDC; el viewport
|
||||
// del SDLManager hace el letterbox a píxeles físicos.
|
||||
const auto FX1 = static_cast<float>(x1);
|
||||
const auto FY1 = static_cast<float>(y1);
|
||||
const auto FX2 = static_cast<float>(x2);
|
||||
const auto FY2 = static_cast<float>(y2);
|
||||
void setLineColor(SDL_Color color) { g_current_line_color = color; }
|
||||
|
||||
// color.alpha==0 → usar color global (verde fósforo). alpha>0 → color directo.
|
||||
const SDL_Color SOURCE = (color.a > 0) ? color : g_current_line_color;
|
||||
const float R = (static_cast<float>(SOURCE.r) * brightness) / 255.0F;
|
||||
const float G = (static_cast<float>(SOURCE.g) * brightness) / 255.0F;
|
||||
const float B = (static_cast<float>(SOURCE.b) * brightness) / 255.0F;
|
||||
|
||||
const float W = (thickness > 0.0F) ? thickness : g_current_line_thickness;
|
||||
|
||||
renderer->pushLine(FX1, FY1, FX2, FY2, W, R, G, B, 1.0F);
|
||||
}
|
||||
|
||||
void setLineColor(SDL_Color color) { g_current_line_color = color; }
|
||||
|
||||
void setLineThickness(float thickness) {
|
||||
if (thickness > 0.0F) {
|
||||
g_current_line_thickness = thickness;
|
||||
void setLineThickness(float thickness) {
|
||||
if (thickness > 0.0F) {
|
||||
g_current_line_thickness = thickness;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto getLineThickness() -> float { return g_current_line_thickness; }
|
||||
auto getLineThickness() -> float { return g_current_line_thickness; }
|
||||
|
||||
} // namespace Rendering
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
#include <iostream>
|
||||
|
||||
#include "core/config/postfx_config.hpp"
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/defaults/game.hpp"
|
||||
#include "core/defaults/rendering.hpp"
|
||||
#include "core/defaults/window.hpp"
|
||||
#include "core/input/mouse.hpp"
|
||||
#include "core/rendering/coordinate_transform.hpp"
|
||||
#include "core/system/notifier.hpp"
|
||||
@@ -22,7 +24,9 @@ namespace {
|
||||
int width,
|
||||
int height,
|
||||
bool fullscreen,
|
||||
int initial_vsync) -> bool {
|
||||
int initial_vsync,
|
||||
int render_width,
|
||||
int render_height) -> bool {
|
||||
// Título estático estilo CCAE. El FPS y el estado de VSync los muestra
|
||||
// el DebugOverlay (toggle F11), no la barra de título.
|
||||
const std::string TITLE = std::format("© 2026 {} — JailDesigner",
|
||||
@@ -44,9 +48,13 @@ namespace {
|
||||
}
|
||||
|
||||
// Inicializar el FrameRenderer (claim del window + pipeline de líneas).
|
||||
// logical_*: espacio de coordenadas del juego (fijo 1280×720).
|
||||
// render_*: resolución física del offscreen (configurable vía YAML).
|
||||
if (!gpu_renderer.init(window,
|
||||
static_cast<float>(Defaults::Game::WIDTH),
|
||||
static_cast<float>(Defaults::Game::HEIGHT))) {
|
||||
static_cast<float>(Defaults::Game::HEIGHT),
|
||||
static_cast<float>(render_width),
|
||||
static_cast<float>(render_height))) {
|
||||
std::cerr << "Error inicialitzant GpuFrameRenderer\n";
|
||||
SDL_DestroyWindow(window);
|
||||
return false;
|
||||
@@ -80,7 +88,30 @@ SDLManager::SDLManager(int width, int height, bool fullscreen, Config::EngineCon
|
||||
|
||||
calculateMaxWindowSize();
|
||||
|
||||
if (!initWindowAndGpu(&finestra_, gpu_renderer_, current_width_, current_height_, is_fullscreen_, cfg_->rendering.vsync)) {
|
||||
// Validar la resolució de render del config: si no és un preset 16:9
|
||||
// conegut, fer fallback a 1280×720 i avisar. Això protegeix d'edicions
|
||||
// manuals invàlides al YAML.
|
||||
int effective_render_w = cfg_->rendering.render_width;
|
||||
int effective_render_h = cfg_->rendering.render_height;
|
||||
if (!Defaults::Rendering::isValidRenderResolution(effective_render_w, effective_render_h)) {
|
||||
std::cerr << "Resolució de render invàlida (" << effective_render_w << "x"
|
||||
<< effective_render_h << "), fent fallback a "
|
||||
<< Defaults::Rendering::RENDER_WIDTH_DEFAULT << "x"
|
||||
<< Defaults::Rendering::RENDER_HEIGHT_DEFAULT << '\n';
|
||||
effective_render_w = Defaults::Rendering::RENDER_WIDTH_DEFAULT;
|
||||
effective_render_h = Defaults::Rendering::RENDER_HEIGHT_DEFAULT;
|
||||
cfg_->rendering.render_width = effective_render_w;
|
||||
cfg_->rendering.render_height = effective_render_h;
|
||||
}
|
||||
|
||||
if (!initWindowAndGpu(&finestra_,
|
||||
gpu_renderer_,
|
||||
current_width_,
|
||||
current_height_,
|
||||
is_fullscreen_,
|
||||
cfg_->rendering.vsync,
|
||||
effective_render_w,
|
||||
effective_render_h)) {
|
||||
SDL_Quit();
|
||||
return;
|
||||
}
|
||||
@@ -97,7 +128,9 @@ SDLManager::SDLManager(int width, int height, bool fullscreen, Config::EngineCon
|
||||
|
||||
std::cout << "SDL3 inicialitzat: " << current_width_ << "x" << current_height_
|
||||
<< " (logic: " << Defaults::Game::WIDTH << "x"
|
||||
<< Defaults::Game::HEIGHT << ")";
|
||||
<< Defaults::Game::HEIGHT
|
||||
<< ", render: " << effective_render_w << "x" << effective_render_h
|
||||
<< ")";
|
||||
if (is_fullscreen_) {
|
||||
std::cout << " [FULLSCREEN]";
|
||||
}
|
||||
|
||||
@@ -4,21 +4,13 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/types.hpp"
|
||||
|
||||
namespace System {
|
||||
|
||||
namespace {
|
||||
// Posición y tamaño del overlay en coordenadas lógicas (1280×720).
|
||||
constexpr float OVERLAY_X = 12.0F;
|
||||
constexpr float OVERLAY_Y_FPS = 12.0F;
|
||||
constexpr float OVERLAY_LINE_HEIGHT = 18.0F; // separación entre líneas (scale 0.4 → ~16 px alto)
|
||||
constexpr float OVERLAY_SCALE = 0.4F;
|
||||
constexpr float OVERLAY_SPACING = 2.0F;
|
||||
constexpr float OVERLAY_BRIGHTNESS = 1.0F;
|
||||
|
||||
// Cadencia de actualización del valor de FPS mostrado.
|
||||
constexpr float FPS_UPDATE_INTERVAL = 0.5F;
|
||||
namespace Cfg = Defaults::Hud::DebugOverlay;
|
||||
} // namespace
|
||||
|
||||
DebugOverlay::DebugOverlay(Rendering::Renderer* renderer,
|
||||
@@ -30,7 +22,7 @@ namespace System {
|
||||
fps_accumulator_ += delta_time;
|
||||
fps_frame_count_++;
|
||||
|
||||
if (fps_accumulator_ >= FPS_UPDATE_INTERVAL) {
|
||||
if (fps_accumulator_ >= Cfg::FPS_UPDATE_INTERVAL) {
|
||||
fps_display_ = static_cast<int>(fps_frame_count_ / fps_accumulator_);
|
||||
fps_frame_count_ = 0;
|
||||
fps_accumulator_ = 0.0F;
|
||||
@@ -47,20 +39,20 @@ namespace System {
|
||||
const std::string AA_TEXT = std::string("AA: ") + (rendering_cfg_->antialias == 1 ? "ON" : "OFF");
|
||||
|
||||
text_.render(FPS_TEXT,
|
||||
Vec2{.x = OVERLAY_X, .y = OVERLAY_Y_FPS},
|
||||
OVERLAY_SCALE,
|
||||
OVERLAY_SPACING,
|
||||
OVERLAY_BRIGHTNESS);
|
||||
Vec2{.x = Cfg::X, .y = Cfg::Y_FPS},
|
||||
Cfg::TEXT_SCALE,
|
||||
Cfg::TEXT_SPACING,
|
||||
Cfg::BRIGHTNESS);
|
||||
text_.render(VSYNC_TEXT,
|
||||
Vec2{.x = OVERLAY_X, .y = OVERLAY_Y_FPS + OVERLAY_LINE_HEIGHT},
|
||||
OVERLAY_SCALE,
|
||||
OVERLAY_SPACING,
|
||||
OVERLAY_BRIGHTNESS);
|
||||
Vec2{.x = Cfg::X, .y = Cfg::Y_FPS + Cfg::LINE_HEIGHT},
|
||||
Cfg::TEXT_SCALE,
|
||||
Cfg::TEXT_SPACING,
|
||||
Cfg::BRIGHTNESS);
|
||||
text_.render(AA_TEXT,
|
||||
Vec2{.x = OVERLAY_X, .y = OVERLAY_Y_FPS + (2.0F * OVERLAY_LINE_HEIGHT)},
|
||||
OVERLAY_SCALE,
|
||||
OVERLAY_SPACING,
|
||||
OVERLAY_BRIGHTNESS);
|
||||
Vec2{.x = Cfg::X, .y = Cfg::Y_FPS + (2.0F * Cfg::LINE_HEIGHT)},
|
||||
Cfg::TEXT_SCALE,
|
||||
Cfg::TEXT_SPACING,
|
||||
Cfg::BRIGHTNESS);
|
||||
}
|
||||
|
||||
} // namespace System
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/audio/audio_adapter.hpp"
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/defaults/audio.hpp"
|
||||
#include "core/defaults/window.hpp"
|
||||
#include "core/input/input.hpp"
|
||||
#include "core/input/mouse.hpp"
|
||||
#include "core/rendering/sdl_manager.hpp"
|
||||
|
||||
@@ -60,18 +60,18 @@ namespace GlobalEvents {
|
||||
|
||||
case SDL_SCANCODE_ESCAPE: {
|
||||
// Doble pulsació per confirmar sortida: la primera ESC
|
||||
// dispara un toast d'avís; mentre el toast està entrant
|
||||
// o aguantant (isActiveWindow), la segona ESC tanca el
|
||||
// joc. Si el toast ha començat a sortir o ja ha
|
||||
// desaparegut, ESC torna a obrir la finestra de
|
||||
// confirmació sense tancar.
|
||||
// dispara un toast d'avís; només si aquest toast concret
|
||||
// (isExitPromptActive) segueix visible, la segona ESC
|
||||
// tanca el joc. Si la notificació activa és una altra
|
||||
// (zoom, fullscreen, vsync...), ESC obre el prompt de
|
||||
// sortida en lloc de tancar.
|
||||
auto* notifier = System::Notifier::get();
|
||||
if (notifier != nullptr && !notifier->isActiveWindow()) {
|
||||
if (notifier != nullptr && !notifier->isExitPromptActive()) {
|
||||
notifier->notifyExit("PREMEU ESC UN ALTRE COP PER EIXIR");
|
||||
return true;
|
||||
}
|
||||
// Notifier inexistent (degradació elegant) o segona ESC
|
||||
// dins la finestra activa: tanquem el joc.
|
||||
// sobre el prompt de sortida: tanquem el joc.
|
||||
context.setNextScene(SceneType::EXIT);
|
||||
SceneManager::actual = SceneType::EXIT;
|
||||
return true;
|
||||
|
||||
@@ -2,24 +2,15 @@
|
||||
|
||||
#include "core/system/notifier.hpp"
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/rendering/gpu/gpu_frame_renderer.hpp"
|
||||
#include "core/utils/easing.hpp"
|
||||
|
||||
namespace System {
|
||||
|
||||
namespace {
|
||||
// Geometria del cuadre en coordenades lògiques (1280×720).
|
||||
constexpr float CANVAS_WIDTH = 1280.0F;
|
||||
constexpr float MARGIN_TOP = 40.0F;
|
||||
constexpr float PADDING_H = 16.0F;
|
||||
constexpr float PADDING_V = 10.0F;
|
||||
constexpr float BORDER_THICKNESS = 2.0F;
|
||||
constexpr float TEXT_SCALE = 0.55F;
|
||||
constexpr float TEXT_SPACING = 2.0F;
|
||||
constexpr float BORDER_BRIGHTNESS = 1.0F;
|
||||
|
||||
// Cinemàtica del slide.
|
||||
constexpr float SLIDE_DURATION_S = 0.30F;
|
||||
// Alias d'àmbit local per accedir compactament als defaults del notifier.
|
||||
namespace Cfg = Defaults::Notifier;
|
||||
|
||||
// Conversió color SDL → float [0,1].
|
||||
constexpr auto toUnit(Uint8 v) -> float {
|
||||
@@ -47,14 +38,6 @@ namespace System {
|
||||
.b = toUnit(c.b) * DARKEN,
|
||||
.a = BG_ALPHA};
|
||||
}
|
||||
|
||||
// Presets per als atajos semàntics.
|
||||
constexpr SDL_Color COLOR_INFO{.r = 80, .g = 230, .b = 255, .a = 255};
|
||||
constexpr SDL_Color COLOR_WARN{.r = 255, .g = 180, .b = 40, .a = 255};
|
||||
constexpr SDL_Color COLOR_EXIT{.r = 255, .g = 80, .b = 80, .a = 255};
|
||||
constexpr float DURATION_INFO = 2.0F;
|
||||
constexpr float DURATION_WARN = 3.0F;
|
||||
constexpr float DURATION_EXIT = 3.0F;
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<Notifier> Notifier::instance;
|
||||
@@ -77,16 +60,17 @@ namespace System {
|
||||
current_text_ = text;
|
||||
current_color_ = text_color;
|
||||
hold_remaining_s_ = duration_s;
|
||||
current_is_exit_ = false;
|
||||
|
||||
const float TEXT_W = Graphics::VectorText::getTextWidth(text, TEXT_SCALE, TEXT_SPACING);
|
||||
const float TEXT_H = Graphics::VectorText::getTextHeight(TEXT_SCALE);
|
||||
const float TEXT_W = Graphics::VectorText::getTextWidth(text, Cfg::TEXT_SCALE, Cfg::TEXT_SPACING);
|
||||
const float TEXT_H = Graphics::VectorText::getTextHeight(Cfg::TEXT_SCALE);
|
||||
|
||||
box_w_ = TEXT_W + (PADDING_H * 2.0F);
|
||||
box_h_ = TEXT_H + (PADDING_V * 2.0F);
|
||||
text_x_ = (CANVAS_WIDTH - TEXT_W) * 0.5F;
|
||||
box_w_ = TEXT_W + (Cfg::PADDING_H * 2.0F);
|
||||
box_h_ = TEXT_H + (Cfg::PADDING_V * 2.0F);
|
||||
text_x_ = (Cfg::CANVAS_WIDTH - TEXT_W) * 0.5F;
|
||||
|
||||
y_on_ = MARGIN_TOP;
|
||||
y_off_ = -(box_h_ + BORDER_THICKNESS);
|
||||
y_on_ = Cfg::MARGIN_TOP;
|
||||
y_off_ = -(box_h_ + Cfg::BORDER_THICKNESS);
|
||||
|
||||
// Si ja es veu, reseteja el slide-in des de la posició actual perquè
|
||||
// la transició sembli continua. Si està amagat, arrenc des de fora.
|
||||
@@ -95,23 +79,26 @@ namespace System {
|
||||
}
|
||||
status_ = Status::ENTERING;
|
||||
slide_elapsed_s_ = 0.0F;
|
||||
text_scale_ = TEXT_SCALE;
|
||||
text_scale_ = Cfg::TEXT_SCALE;
|
||||
}
|
||||
|
||||
void Notifier::notifyInfo(const std::string& text) { notify(text, COLOR_INFO, DURATION_INFO); }
|
||||
void Notifier::notifyWarn(const std::string& text) { notify(text, COLOR_WARN, DURATION_WARN); }
|
||||
void Notifier::notifyExit(const std::string& text) { notify(text, COLOR_EXIT, DURATION_EXIT); }
|
||||
void Notifier::notifyInfo(const std::string& text) { notify(text, Cfg::COLOR_INFO, Cfg::DURATION_INFO); }
|
||||
void Notifier::notifyWarn(const std::string& text) { notify(text, Cfg::COLOR_WARN, Cfg::DURATION_WARN); }
|
||||
void Notifier::notifyExit(const std::string& text) {
|
||||
notify(text, Cfg::COLOR_EXIT, Cfg::DURATION_EXIT);
|
||||
current_is_exit_ = true; // notify() ho ha posat a false; restaurem.
|
||||
}
|
||||
|
||||
void Notifier::update(float delta_time) {
|
||||
switch (status_) {
|
||||
case Status::ENTERING: {
|
||||
slide_elapsed_s_ += delta_time;
|
||||
if (slide_elapsed_s_ >= SLIDE_DURATION_S) {
|
||||
if (slide_elapsed_s_ >= Cfg::SLIDE_DURATION_S) {
|
||||
y_current_ = y_on_;
|
||||
status_ = Status::HOLDING;
|
||||
slide_elapsed_s_ = 0.0F;
|
||||
} else {
|
||||
const float T = slide_elapsed_s_ / SLIDE_DURATION_S;
|
||||
const float T = slide_elapsed_s_ / Cfg::SLIDE_DURATION_S;
|
||||
const float K = Utils::Easing::outCubic(T);
|
||||
y_current_ = y_off_ + ((y_on_ - y_off_) * K);
|
||||
}
|
||||
@@ -127,11 +114,11 @@ namespace System {
|
||||
}
|
||||
case Status::EXITING: {
|
||||
slide_elapsed_s_ += delta_time;
|
||||
if (slide_elapsed_s_ >= SLIDE_DURATION_S) {
|
||||
if (slide_elapsed_s_ >= Cfg::SLIDE_DURATION_S) {
|
||||
y_current_ = y_off_;
|
||||
status_ = Status::HIDDEN;
|
||||
} else {
|
||||
const float T = slide_elapsed_s_ / SLIDE_DURATION_S;
|
||||
const float T = slide_elapsed_s_ / Cfg::SLIDE_DURATION_S;
|
||||
const float K = Utils::Easing::inCubic(T);
|
||||
y_current_ = y_on_ + ((y_off_ - y_on_) * K);
|
||||
}
|
||||
@@ -148,7 +135,7 @@ namespace System {
|
||||
return;
|
||||
}
|
||||
|
||||
const float BOX_X = (CANVAS_WIDTH - box_w_) * 0.5F;
|
||||
const float BOX_X = (Cfg::CANVAS_WIDTH - box_w_) * 0.5F;
|
||||
const float BOX_Y = y_current_;
|
||||
const UnitRGBA TC = textColorFloat(current_color_);
|
||||
const UnitRGBA BG = bgColorFloat(current_color_);
|
||||
@@ -163,19 +150,19 @@ namespace System {
|
||||
const float Y1 = BOX_Y;
|
||||
const float X2 = BOX_X + box_w_;
|
||||
const float Y2 = BOX_Y + box_h_;
|
||||
gpu->pushLine(X1, Y1, X2, Y1, BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // top
|
||||
gpu->pushLine(X1, Y2, X2, Y2, BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // bottom
|
||||
gpu->pushLine(X1, Y1, X1, Y2, BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // left
|
||||
gpu->pushLine(X2, Y1, X2, Y2, BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // right
|
||||
gpu->pushLine(X1, Y1, X2, Y1, Cfg::BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // top
|
||||
gpu->pushLine(X1, Y2, X2, Y2, Cfg::BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // bottom
|
||||
gpu->pushLine(X1, Y1, X1, Y2, Cfg::BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // left
|
||||
gpu->pushLine(X2, Y1, X2, Y2, Cfg::BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // right
|
||||
|
||||
// 3. Text centrat dins la caixa, amb color explícit (l'alpha != 0
|
||||
// li diu al renderShape que no agafe l'oscil·lador global de color).
|
||||
const float TEXT_Y = BOX_Y + PADDING_V;
|
||||
const float TEXT_Y = BOX_Y + Cfg::PADDING_V;
|
||||
text_.render(current_text_,
|
||||
Vec2{.x = text_x_, .y = TEXT_Y},
|
||||
text_scale_,
|
||||
TEXT_SPACING,
|
||||
BORDER_BRIGHTNESS,
|
||||
Cfg::TEXT_SPACING,
|
||||
Cfg::BORDER_BRIGHTNESS,
|
||||
current_color_);
|
||||
}
|
||||
|
||||
@@ -183,4 +170,8 @@ namespace System {
|
||||
return status_ == Status::ENTERING || status_ == Status::HOLDING;
|
||||
}
|
||||
|
||||
auto Notifier::isExitPromptActive() const -> bool {
|
||||
return isActiveWindow() && current_is_exit_;
|
||||
}
|
||||
|
||||
} // namespace System
|
||||
|
||||
@@ -47,10 +47,15 @@ namespace System {
|
||||
void draw() const;
|
||||
|
||||
// Activa mentre el toast està entrant o aguantant. Quan està sortint
|
||||
// o ja amagat, retorna false. Útil per a la lògica de doble-pulsació
|
||||
// d'ESC: la segona pulsació només confirma sortida si encara aguanta.
|
||||
// o ja amagat, retorna false.
|
||||
[[nodiscard]] auto isActiveWindow() const -> bool;
|
||||
|
||||
// Cert només si el toast actiu va ser disparat per notifyExit().
|
||||
// Per a la doble-pulsació d'ESC: la segona ESC confirma sortida
|
||||
// únicament si la notificació visible és la de confirmació; si era
|
||||
// de F1/F2/etc., ESC torna a obrir el prompt sense tancar.
|
||||
[[nodiscard]] auto isExitPromptActive() const -> bool;
|
||||
|
||||
private:
|
||||
explicit Notifier(Rendering::Renderer* renderer);
|
||||
|
||||
@@ -74,6 +79,7 @@ namespace System {
|
||||
float box_h_{0.0F};
|
||||
float text_x_{0.0F}; // X esquerre del text dins la caixa
|
||||
float text_scale_{0.4F};
|
||||
bool current_is_exit_{false}; // true només si l'actiu ve de notifyExit()
|
||||
|
||||
static std::unique_ptr<Notifier> instance;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/defaults/rendering.hpp"
|
||||
#include "core/defaults/window.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "project.h"
|
||||
|
||||
@@ -204,6 +205,8 @@ namespace ConfigYaml {
|
||||
|
||||
// Rendering
|
||||
rendering.vsync = Defaults::Rendering::VSYNC_DEFAULT;
|
||||
rendering.render_width = Defaults::Rendering::RENDER_WIDTH_DEFAULT;
|
||||
rendering.render_height = Defaults::Rendering::RENDER_HEIGHT_DEFAULT;
|
||||
|
||||
// Version
|
||||
version = std::string(Project::VERSION);
|
||||
@@ -275,6 +278,28 @@ namespace ConfigYaml {
|
||||
rendering.vsync = Defaults::Rendering::VSYNC_DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolució de render: validem el parell (w, h) contra la llista
|
||||
// tancada de presets 16:9. Si falla l'una o l'altra, fem fallback
|
||||
// dels dos camps al default per mantenir un parell vàlid.
|
||||
int candidate_w = rendering.render_width;
|
||||
int candidate_h = rendering.render_height;
|
||||
readField(rend, "render_width", candidate_w, Defaults::Rendering::RENDER_WIDTH_DEFAULT);
|
||||
readField(rend, "render_height", candidate_h, Defaults::Rendering::RENDER_HEIGHT_DEFAULT);
|
||||
if (Defaults::Rendering::isValidRenderResolution(candidate_w, candidate_h)) {
|
||||
rendering.render_width = candidate_w;
|
||||
rendering.render_height = candidate_h;
|
||||
} else {
|
||||
if (console) {
|
||||
std::cerr << "Resolució de render invàlida al YAML ("
|
||||
<< candidate_w << "x" << candidate_h
|
||||
<< "), fallback a "
|
||||
<< Defaults::Rendering::RENDER_WIDTH_DEFAULT << "x"
|
||||
<< Defaults::Rendering::RENDER_HEIGHT_DEFAULT << '\n';
|
||||
}
|
||||
rendering.render_width = Defaults::Rendering::RENDER_WIDTH_DEFAULT;
|
||||
rendering.render_height = Defaults::Rendering::RENDER_HEIGHT_DEFAULT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,7 +526,11 @@ namespace ConfigYaml {
|
||||
|
||||
file << "# RENDERITZACIÓ\n";
|
||||
file << "rendering:\n";
|
||||
file << " vsync: " << rendering.vsync << " # 0=disabled, 1=enabled\n\n";
|
||||
file << " vsync: " << rendering.vsync << " # 0=disabled, 1=enabled\n";
|
||||
file << " render_width: " << rendering.render_width
|
||||
<< " # Presets 16:9: 1280, 1600, 1920, 2560, 3840 (fallback 1280)\n";
|
||||
file << " render_height: " << rendering.render_height
|
||||
<< " # Parell amb render_width (720, 900, 1080, 1440, 2160)\n\n";
|
||||
|
||||
// Guardar controls de jugadors
|
||||
savePlayer1ControlsToYaml(file);
|
||||
|
||||
@@ -8,26 +8,40 @@
|
||||
|
||||
namespace Effects {
|
||||
|
||||
// Debris: un segment de línia que vola perpendicular a sí mismo
|
||||
// Representa un fragment de una shape destruïda (ship, enemy, bullet)
|
||||
struct Debris {
|
||||
// Geometria del segment (2 points en coordenades mundials)
|
||||
Vec2 p1; // Vec2 inicial del segment
|
||||
Vec2 p2; // Vec2 final del segment
|
||||
// Debris: un segment de línia que vola perpendicular a sí mismo
|
||||
// Representa un fragment de una shape destruïda (ship, enemy, bullet).
|
||||
//
|
||||
// Representació autoritaritzada (font de veritat per a la geometria):
|
||||
// centro + original_angle (orientació spawn) + angle_rotacio (acumulat)
|
||||
// + original_half_length × shrink_factor (mida actual)
|
||||
// p1/p2 es reconstrueixen cada frame en update() des d'aquestes dades, mai
|
||||
// de forma iterativa — evita bugs de rotació quadràtica i shrink exponencial.
|
||||
struct Debris {
|
||||
// Geometria del segment (2 points en coordenades mundials, derivats)
|
||||
Vec2 p1;
|
||||
Vec2 p2;
|
||||
|
||||
// Geometria original (font de veritat, no canvia després del spawn)
|
||||
Vec2 centro; // Centre actual del segment (es mou amb velocity)
|
||||
float original_angle; // Orientació del segment al spawn (rad)
|
||||
float original_half_length; // Mitja-longitud al spawn (px)
|
||||
|
||||
// Física
|
||||
Vec2 velocity; // Velocidad en px/s (components x, y)
|
||||
Vec2 velocity; // Velocidad en px/s (components x, y)
|
||||
float acceleration; // Aceleración negativa (fricció) en px/s²
|
||||
|
||||
// Rotación
|
||||
float angle_rotacio; // Angle de rotación acumulat (radians)
|
||||
float angle_rotacio; // Acumulat de rotació visual des del spawn (radians)
|
||||
float velocitat_rot; // Velocidad de rotación de TRAYECTORIA (rad/s)
|
||||
float velocitat_rot_visual; // Velocidad de rotación VISUAL del segment (rad/s)
|
||||
|
||||
// Estat de vida
|
||||
float temps_vida; // Temps transcorregut (segons)
|
||||
float temps_max; // Temps de vida màxim (segons)
|
||||
bool active; // Está active?
|
||||
// Política: viu sempre durant min_lifetime, després mor quan
|
||||
// |velocity| < MIN_SPEED_TO_DIE (definit en Defaults). Així els
|
||||
// fragments ràpids no "popen" en moviment.
|
||||
float temps_vida; // Temps transcorregut (segons)
|
||||
float min_lifetime; // Temps mínim garantit (segons)
|
||||
bool active; // Està actiu?
|
||||
|
||||
// Shrinking (reducció de distancia entre points)
|
||||
float factor_shrink; // Factor de reducció per segon (0.0-1.0)
|
||||
@@ -35,6 +49,6 @@ struct Debris {
|
||||
// Rendering
|
||||
float brightness; // Factor de brightness (0.0-1.0, heretat de l'objecte original)
|
||||
SDL_Color color{}; // Color heredado del padre. alpha==0 → usa global oscilador
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace Effects
|
||||
|
||||
@@ -14,375 +14,426 @@
|
||||
|
||||
namespace Effects {
|
||||
|
||||
// Helper: transformar point con rotación, scale i traslación
|
||||
// (Copiat de shape_renderer.cpp:12-34)
|
||||
static auto transformPoint(const Vec2& point, const Vec2& shape_centre, const Vec2& position, float angle, float scale) -> Vec2 {
|
||||
// 1. Centrar el point respecte al centro de la shape
|
||||
float centered_x = point.x - shape_centre.x;
|
||||
float centered_y = point.y - shape_centre.y;
|
||||
// Helper: transformar point con rotación, scale i traslación
|
||||
// (Copiat de shape_renderer.cpp:12-34)
|
||||
static auto transformPoint(const Vec2& point, const Vec2& shape_centre, const Vec2& position, float angle, float scale) -> Vec2 {
|
||||
// 1. Centrar el point respecte al centro de la shape
|
||||
float centered_x = point.x - shape_centre.x;
|
||||
float centered_y = point.y - shape_centre.y;
|
||||
|
||||
// 2. Aplicar scale al point centrat
|
||||
float scaled_x = centered_x * scale;
|
||||
float scaled_y = centered_y * scale;
|
||||
// 2. Aplicar scale al point centrat
|
||||
float scaled_x = centered_x * scale;
|
||||
float scaled_y = centered_y * scale;
|
||||
|
||||
// 3. Aplicar rotación
|
||||
float cos_a = std::cos(angle);
|
||||
float sin_a = std::sin(angle);
|
||||
// 3. Aplicar rotación
|
||||
float cos_a = std::cos(angle);
|
||||
float sin_a = std::sin(angle);
|
||||
|
||||
float rotated_x = (scaled_x * cos_a) - (scaled_y * sin_a);
|
||||
float rotated_y = (scaled_x * sin_a) + (scaled_y * cos_a);
|
||||
float rotated_x = (scaled_x * cos_a) - (scaled_y * sin_a);
|
||||
float rotated_y = (scaled_x * sin_a) + (scaled_y * cos_a);
|
||||
|
||||
// 4. Aplicar traslación a posición mundial
|
||||
return {.x = rotated_x + position.x, .y = rotated_y + position.y};
|
||||
}
|
||||
|
||||
DebrisManager::DebrisManager(Rendering::Renderer* renderer)
|
||||
: renderer_(renderer) {
|
||||
// Inicialitzar todos los debris como inactius
|
||||
for (auto& debris : debris_pool_) {
|
||||
debris.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void DebrisManager::explode(const std::shared_ptr<Graphics::Shape>& shape,
|
||||
const Vec2& centro,
|
||||
float angle,
|
||||
float scale,
|
||||
float velocitat_base,
|
||||
float brightness,
|
||||
const Vec2& velocitat_objecte,
|
||||
float velocitat_angular,
|
||||
float factor_herencia_visual,
|
||||
const std::string& sound,
|
||||
SDL_Color color) {
|
||||
if (!shape || !shape->isValid()) {
|
||||
return;
|
||||
// 4. Aplicar traslación a posición mundial
|
||||
return {.x = rotated_x + position.x, .y = rotated_y + position.y};
|
||||
}
|
||||
|
||||
// Reproducir sonido de explosión
|
||||
Audio::get()->playSound(sound, Audio::Group::GAME);
|
||||
DebrisManager::DebrisManager(Rendering::Renderer* renderer)
|
||||
: renderer_(renderer) {
|
||||
// Inicialitzar todos los debris como inactius
|
||||
for (auto& debris : debris_pool_) {
|
||||
debris.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
const Vec2& shape_centre = shape->getCenter();
|
||||
void DebrisManager::explode(const std::shared_ptr<Graphics::Shape>& shape,
|
||||
const Vec2& centro,
|
||||
float angle,
|
||||
float scale,
|
||||
float velocitat_base,
|
||||
float brightness,
|
||||
const Vec2& velocitat_objecte,
|
||||
float velocitat_angular,
|
||||
float factor_herencia_visual,
|
||||
const std::string& sound,
|
||||
SDL_Color color,
|
||||
float lifetime,
|
||||
float friction,
|
||||
int segment_multiplier) {
|
||||
if (!shape || !shape->isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& primitive : shape->getPrimitives()) {
|
||||
for (const auto& [local_p1, local_p2] : extractSegments(primitive)) {
|
||||
// Transformar points locals → coordenades mundials
|
||||
Vec2 world_p1 = transformPoint(local_p1, shape_centre, centro, angle, scale);
|
||||
Vec2 world_p2 = transformPoint(local_p2, shape_centre, centro, angle, scale);
|
||||
// Reproducir sonido de explosión
|
||||
Audio::get()->playSound(sound, Audio::Group::GAME);
|
||||
|
||||
// Si el pool es ple, no té sentit continuar amb la resta de segments
|
||||
if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness,
|
||||
velocitat_objecte, velocitat_angular,
|
||||
factor_herencia_visual, color)) {
|
||||
return;
|
||||
const Vec2& shape_centre = shape->getCenter();
|
||||
|
||||
// Multiplier: cada segment s'emet N vegades amb direccions aleatòries
|
||||
// distintes (la variació ±15° de computeExplosionDirection ho garanteix).
|
||||
const int COPIES = std::max(1, segment_multiplier);
|
||||
|
||||
for (int copy = 0; copy < COPIES; copy++) {
|
||||
for (const auto& primitive : shape->getPrimitives()) {
|
||||
for (const auto& [local_p1, local_p2] : extractSegments(primitive)) {
|
||||
// Transformar points locals → coordenades mundials
|
||||
Vec2 world_p1 = transformPoint(local_p1, shape_centre, centro, angle, scale);
|
||||
Vec2 world_p2 = transformPoint(local_p2, shape_centre, centro, angle, scale);
|
||||
|
||||
// Si el pool es ple, no té sentit continuar amb la resta de segments
|
||||
if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness, velocitat_objecte, velocitat_angular, factor_herencia_visual, color, lifetime, friction)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto DebrisManager::extractSegments(const Graphics::ShapePrimitive& primitive)
|
||||
-> std::vector<std::pair<Vec2, Vec2>> {
|
||||
std::vector<std::pair<Vec2, Vec2>> segments;
|
||||
auto DebrisManager::extractSegments(const Graphics::ShapePrimitive& primitive)
|
||||
-> std::vector<std::pair<Vec2, Vec2>> {
|
||||
std::vector<std::pair<Vec2, Vec2>> segments;
|
||||
|
||||
if (primitive.type == Graphics::PrimitiveType::POLYLINE) {
|
||||
// Polyline: extreure segments consecutius
|
||||
for (size_t i = 0; i + 1 < primitive.points.size(); i++) {
|
||||
segments.emplace_back(primitive.points[i], primitive.points[i + 1]);
|
||||
if (primitive.type == Graphics::PrimitiveType::POLYLINE) {
|
||||
// Polyline: extreure segments consecutius
|
||||
for (size_t i = 0; i + 1 < primitive.points.size(); i++) {
|
||||
segments.emplace_back(primitive.points[i], primitive.points[i + 1]);
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
// PrimitiveType::LINE: un únic segment (si té els 2 punts)
|
||||
if (primitive.points.size() >= 2) {
|
||||
segments.emplace_back(primitive.points[0], primitive.points[1]);
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
// PrimitiveType::LINE: un únic segment (si té els 2 punts)
|
||||
if (primitive.points.size() >= 2) {
|
||||
segments.emplace_back(primitive.points[0], primitive.points[1]);
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
auto DebrisManager::spawnDebris(const Vec2& world_p1, const Vec2& world_p2,
|
||||
const Vec2& centro, float velocitat_base, float brightness,
|
||||
const Vec2& velocitat_objecte, float velocitat_angular,
|
||||
float factor_herencia_visual, SDL_Color color) -> bool {
|
||||
Debris* debris = findFreeSlot();
|
||||
if (debris == nullptr) {
|
||||
std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Geometria
|
||||
debris->p1 = world_p1;
|
||||
debris->p2 = world_p2;
|
||||
|
||||
// Direcció radial (desde el centro hacia el segment)
|
||||
Vec2 direccio = computeExplosionDirection(world_p1, world_p2, centro);
|
||||
|
||||
// Velocidad inicial (base ± variació aleatòria + velocity heretada de l'objecte)
|
||||
float speed =
|
||||
velocitat_base +
|
||||
(((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) *
|
||||
Defaults::Physics::Debris::VARIACIO_VELOCITAT);
|
||||
debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x;
|
||||
debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y;
|
||||
debris->acceleration = Defaults::Physics::Debris::ACCELERACIO;
|
||||
|
||||
// Rotación de trayectoria (con conversió a tangencial si excedeix cap)
|
||||
applyAngularVelocity(*debris, direccio, velocitat_angular);
|
||||
|
||||
// Rotación visual (proporcional o aleatòria)
|
||||
applyVisualRotation(*debris, velocitat_angular, factor_herencia_visual);
|
||||
|
||||
debris->angle_rotacio = 0.0F;
|
||||
|
||||
// Vida i shrinking
|
||||
debris->temps_vida = 0.0F;
|
||||
debris->temps_max = Defaults::Physics::Debris::TEMPS_VIDA;
|
||||
debris->factor_shrink = Defaults::Physics::Debris::SHRINK_RATE;
|
||||
|
||||
// Visuals heretades
|
||||
debris->brightness = brightness;
|
||||
debris->color = color;
|
||||
|
||||
debris->active = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DebrisManager::applyAngularVelocity(Debris& debris, const Vec2& direccio,
|
||||
float velocitat_angular) {
|
||||
if (std::abs(velocitat_angular) <= 0.01F) {
|
||||
debris.velocitat_rot = 0.0F; // Nave: sin curvas
|
||||
return;
|
||||
}
|
||||
|
||||
// FASE 1: Aplicar herència i variació
|
||||
float factor_herencia =
|
||||
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN +
|
||||
((std::rand() / static_cast<float>(RAND_MAX)) *
|
||||
(Defaults::Physics::Debris::FACTOR_HERENCIA_MAX -
|
||||
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN));
|
||||
float velocitat_ang_heretada = velocitat_angular * factor_herencia;
|
||||
float variacio = ((std::rand() / static_cast<float>(RAND_MAX)) * 0.2F) - 0.1F;
|
||||
velocitat_ang_heretada *= (1.0F + variacio);
|
||||
|
||||
// FASE 2: Cap a la velocity màxima; l'excés es converteix en tangencial
|
||||
constexpr float CAP = Defaults::Physics::Debris::VELOCITAT_ROT_MAX;
|
||||
float abs_ang = std::abs(velocitat_ang_heretada);
|
||||
float sign_ang = (velocitat_ang_heretada >= 0.0F) ? 1.0F : -1.0F;
|
||||
|
||||
if (abs_ang <= CAP) {
|
||||
debris.velocitat_rot = velocitat_ang_heretada;
|
||||
return;
|
||||
}
|
||||
|
||||
// Excés: converteix l'excés de velocitat angular en velocitat tangencial lineal
|
||||
float excess = abs_ang - CAP;
|
||||
constexpr float RADIUS = 20.0F; // Radi típic de la shape (enemigos = 20 px)
|
||||
float v_tangential = excess * RADIUS;
|
||||
|
||||
// Direcció tangencial: perpendicular a la radial (90° CCW): tangent = (-dy, dx)
|
||||
debris.velocity.x += -direccio.y * v_tangential;
|
||||
debris.velocity.y += direccio.x * v_tangential;
|
||||
|
||||
// Velocitat angular limitada al cap (preservant el signe)
|
||||
debris.velocitat_rot = sign_ang * CAP;
|
||||
}
|
||||
|
||||
void DebrisManager::applyVisualRotation(Debris& debris, float velocitat_angular,
|
||||
float factor_herencia_visual) {
|
||||
if (factor_herencia_visual > 0.01F && std::abs(velocitat_angular) > 0.01F) {
|
||||
// Heredar rotación visual con factor proporcional + ±5% de variació
|
||||
debris.velocitat_rot_visual = debris.velocitat_rot * factor_herencia_visual;
|
||||
float variacio_visual =
|
||||
((std::rand() / static_cast<float>(RAND_MAX)) * 0.1F) - 0.05F;
|
||||
debris.velocitat_rot_visual *= (1.0F + variacio_visual);
|
||||
return;
|
||||
}
|
||||
|
||||
// Rotación visual aleatòria (factor = 0.0 o sin velocidad angular)
|
||||
debris.velocitat_rot_visual =
|
||||
Defaults::Physics::Debris::ROTACIO_MIN +
|
||||
((std::rand() / static_cast<float>(RAND_MAX)) *
|
||||
(Defaults::Physics::Debris::ROTACIO_MAX -
|
||||
Defaults::Physics::Debris::ROTACIO_MIN));
|
||||
|
||||
// 50% probabilitat de rotación en sentit contrari
|
||||
if (std::rand() % 2 == 0) {
|
||||
debris.velocitat_rot_visual = -debris.velocitat_rot_visual;
|
||||
}
|
||||
}
|
||||
|
||||
void DebrisManager::update(float delta_time) {
|
||||
for (auto& debris : debris_pool_) {
|
||||
if (!debris.active) {
|
||||
continue;
|
||||
auto DebrisManager::spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction) -> bool {
|
||||
Debris* debris = findFreeSlot();
|
||||
if (debris == nullptr) {
|
||||
std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. Actualitzar time de vida
|
||||
debris.temps_vida += delta_time;
|
||||
// Geometria autoritaritzada: centro + original_angle + original_half_length.
|
||||
// p1/p2 es reconstrueixen cada frame en update() des d'aquestes dades.
|
||||
const float DX = world_p2.x - world_p1.x;
|
||||
const float DY = world_p2.y - world_p1.y;
|
||||
debris->centro = {.x = (world_p1.x + world_p2.x) / 2.0F,
|
||||
.y = (world_p1.y + world_p2.y) / 2.0F};
|
||||
debris->original_angle = std::atan2(DY, DX);
|
||||
debris->original_half_length = std::sqrt((DX * DX) + (DY * DY)) / 2.0F;
|
||||
debris->p1 = world_p1;
|
||||
debris->p2 = world_p2;
|
||||
|
||||
// Desactivar si ha superat time màxim
|
||||
if (debris.temps_vida >= debris.temps_max) {
|
||||
debris.active = false;
|
||||
continue;
|
||||
// Direcció radial (desde el centro hacia el segment)
|
||||
Vec2 direccio = computeExplosionDirection(world_p1, world_p2, centro);
|
||||
|
||||
// Velocidad inicial (base ± variació aleatòria + velocity heretada de l'objecte)
|
||||
float speed =
|
||||
velocitat_base +
|
||||
(((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) *
|
||||
Defaults::Physics::Debris::VARIACIO_VELOCITAT);
|
||||
debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x;
|
||||
debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y;
|
||||
debris->acceleration = friction;
|
||||
|
||||
// Rotación de trayectoria (con conversió a tangencial si excedeix cap)
|
||||
applyAngularVelocity(*debris, direccio, velocitat_angular);
|
||||
|
||||
// Rotación visual (proporcional o aleatòria)
|
||||
applyVisualRotation(*debris, velocitat_angular, factor_herencia_visual);
|
||||
|
||||
debris->angle_rotacio = 0.0F;
|
||||
|
||||
// Vida i shrinking — min_lifetime és el temps mínim garantit; després
|
||||
// el fragment mor quan |velocity| < MIN_SPEED_TO_DIE.
|
||||
debris->temps_vida = 0.0F;
|
||||
debris->min_lifetime = lifetime;
|
||||
debris->factor_shrink = Defaults::Physics::Debris::SHRINK_RATE;
|
||||
|
||||
// Visuals heretades
|
||||
debris->brightness = brightness;
|
||||
debris->color = color;
|
||||
|
||||
debris->active = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DebrisManager::applyAngularVelocity(Debris& debris, const Vec2& direccio, float velocitat_angular) {
|
||||
if (std::abs(velocitat_angular) <= 0.01F) {
|
||||
debris.velocitat_rot = 0.0F; // Nave: sin curvas
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Actualitzar velocity (desacceleració)
|
||||
// Aplicar fricció en la direcció del movement
|
||||
float speed = std::sqrt((debris.velocity.x * debris.velocity.x) +
|
||||
(debris.velocity.y * debris.velocity.y));
|
||||
// FASE 1: Aplicar herència i variació
|
||||
float factor_herencia =
|
||||
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN +
|
||||
((std::rand() / static_cast<float>(RAND_MAX)) *
|
||||
(Defaults::Physics::Debris::FACTOR_HERENCIA_MAX -
|
||||
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN));
|
||||
float velocitat_ang_heretada = velocitat_angular * factor_herencia;
|
||||
float variacio = ((std::rand() / static_cast<float>(RAND_MAX)) * 0.2F) - 0.1F;
|
||||
velocitat_ang_heretada *= (1.0F + variacio);
|
||||
|
||||
if (speed > 1.0F) {
|
||||
// Calcular direcció normalitzada
|
||||
float dir_x = debris.velocity.x / speed;
|
||||
float dir_y = debris.velocity.y / speed;
|
||||
// FASE 2: Cap a la velocity màxima; l'excés es converteix en tangencial
|
||||
constexpr float CAP = Defaults::Physics::Debris::VELOCITAT_ROT_MAX;
|
||||
float abs_ang = std::abs(velocitat_ang_heretada);
|
||||
float sign_ang = (velocitat_ang_heretada >= 0.0F) ? 1.0F : -1.0F;
|
||||
|
||||
// Aplicar aceleración negativa (fricció)
|
||||
float nova_speed = speed + (debris.acceleration * delta_time);
|
||||
nova_speed = std::max(nova_speed, 0.0F);
|
||||
|
||||
debris.velocity.x = dir_x * nova_speed;
|
||||
debris.velocity.y = dir_y * nova_speed;
|
||||
} else {
|
||||
// Velocidad mucho baixa, aturar
|
||||
debris.velocity.x = 0.0F;
|
||||
debris.velocity.y = 0.0F;
|
||||
if (abs_ang <= CAP) {
|
||||
debris.velocitat_rot = velocitat_ang_heretada;
|
||||
return;
|
||||
}
|
||||
|
||||
// 2b. Rotar vector de velocity (trayectoria curva)
|
||||
if (std::abs(debris.velocitat_rot) > 0.01F) {
|
||||
// Calcular angle de rotación este frame
|
||||
float dangle = debris.velocitat_rot * delta_time;
|
||||
// Excés: converteix l'excés de velocitat angular en velocitat tangencial lineal
|
||||
float excess = abs_ang - CAP;
|
||||
constexpr float RADIUS = 20.0F; // Radi típic de la shape (enemigos = 20 px)
|
||||
float v_tangential = excess * RADIUS;
|
||||
|
||||
// Rotar vector de velocity usant matriu de rotación 2D
|
||||
float vel_x_old = debris.velocity.x;
|
||||
float vel_y_old = debris.velocity.y;
|
||||
// Direcció tangencial: perpendicular a la radial (90° CCW): tangent = (-dy, dx)
|
||||
debris.velocity.x += -direccio.y * v_tangential;
|
||||
debris.velocity.y += direccio.x * v_tangential;
|
||||
|
||||
float cos_a = std::cos(dangle);
|
||||
float sin_a = std::sin(dangle);
|
||||
// Velocitat angular limitada al cap (preservant el signe)
|
||||
debris.velocitat_rot = sign_ang * CAP;
|
||||
}
|
||||
|
||||
debris.velocity.x = (vel_x_old * cos_a) - (vel_y_old * sin_a);
|
||||
debris.velocity.y = (vel_x_old * sin_a) + (vel_y_old * cos_a);
|
||||
void DebrisManager::applyVisualRotation(Debris& debris, float velocitat_angular, float factor_herencia_visual) {
|
||||
if (factor_herencia_visual > 0.01F && std::abs(velocitat_angular) > 0.01F) {
|
||||
// Heredar rotación visual con factor proporcional + ±5% de variació
|
||||
debris.velocitat_rot_visual = debris.velocitat_rot * factor_herencia_visual;
|
||||
float variacio_visual =
|
||||
((std::rand() / static_cast<float>(RAND_MAX)) * 0.1F) - 0.05F;
|
||||
debris.velocitat_rot_visual *= (1.0F + variacio_visual);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2c. Aplicar fricció angular (desacceleració gradual)
|
||||
if (std::abs(debris.velocitat_rot) > 0.01F) {
|
||||
float sign = (debris.velocitat_rot > 0) ? 1.0F : -1.0F;
|
||||
float reduccion =
|
||||
Defaults::Physics::Debris::FRICCIO_ANGULAR * delta_time;
|
||||
debris.velocitat_rot -= sign * reduccion;
|
||||
// Rotación visual aleatòria (factor = 0.0 o sin velocidad angular)
|
||||
debris.velocitat_rot_visual =
|
||||
Defaults::Physics::Debris::ROTACIO_MIN +
|
||||
((std::rand() / static_cast<float>(RAND_MAX)) *
|
||||
(Defaults::Physics::Debris::ROTACIO_MAX -
|
||||
Defaults::Physics::Debris::ROTACIO_MIN));
|
||||
|
||||
// Evitar canvi de signe (no pot passar de CW a CCW)
|
||||
if ((debris.velocitat_rot > 0) != (sign > 0)) {
|
||||
debris.velocitat_rot = 0.0F;
|
||||
// 50% probabilitat de rotación en sentit contrari
|
||||
if (std::rand() % 2 == 0) {
|
||||
debris.velocitat_rot_visual = -debris.velocitat_rot_visual;
|
||||
}
|
||||
}
|
||||
|
||||
// Rebot del fragment contra els límits del PLAYAREA (mateix patró que
|
||||
// PhysicsWorld::resolveBoundsCollisions per a enemics i ships).
|
||||
static void bounceOffPlayArea(Vec2& centro, Vec2& velocity) {
|
||||
const auto& bounds = Defaults::Zones::PLAYAREA;
|
||||
const float MIN_X = bounds.x;
|
||||
const float MAX_X = bounds.x + bounds.w;
|
||||
const float MIN_Y = bounds.y;
|
||||
const float MAX_Y = bounds.y + bounds.h;
|
||||
constexpr float REST = Defaults::Physics::Debris::RESTITUTION_BOUNDS;
|
||||
if (centro.x < MIN_X) {
|
||||
centro.x = MIN_X;
|
||||
if (velocity.x < 0.0F) {
|
||||
velocity.x = -velocity.x * REST;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Calcular centro del segment
|
||||
Vec2 centro = {.x = (debris.p1.x + debris.p2.x) / 2.0F,
|
||||
.y = (debris.p1.y + debris.p2.y) / 2.0F};
|
||||
|
||||
// 4. Actualitzar posición del centro
|
||||
centro.x += debris.velocity.x * delta_time;
|
||||
centro.y += debris.velocity.y * delta_time;
|
||||
|
||||
// 5. Actualitzar rotación VISUAL
|
||||
debris.angle_rotacio += debris.velocitat_rot_visual * delta_time;
|
||||
|
||||
// 6. Aplicar shrinking (reducció de distancia entre points)
|
||||
float shrink_factor =
|
||||
1.0F - (debris.factor_shrink * debris.temps_vida / debris.temps_max);
|
||||
shrink_factor = std::max(0.0F, shrink_factor); // No negatiu
|
||||
|
||||
// Calcular distancia original entre points
|
||||
float dx = debris.p2.x - debris.p1.x;
|
||||
float dy = debris.p2.y - debris.p1.y;
|
||||
|
||||
// 7. Reconstruir segment con nueva mida i rotación
|
||||
float half_length = std::sqrt((dx * dx) + (dy * dy)) * shrink_factor / 2.0F;
|
||||
float original_angle = std::atan2(dy, dx);
|
||||
float new_angle = original_angle + debris.angle_rotacio;
|
||||
|
||||
debris.p1.x = centro.x - (half_length * std::cos(new_angle));
|
||||
debris.p1.y = centro.y - (half_length * std::sin(new_angle));
|
||||
debris.p2.x = centro.x + (half_length * std::cos(new_angle));
|
||||
debris.p2.y = centro.y + (half_length * std::sin(new_angle));
|
||||
}
|
||||
}
|
||||
|
||||
void DebrisManager::draw() const {
|
||||
for (const auto& debris : debris_pool_) {
|
||||
if (!debris.active) {
|
||||
continue;
|
||||
if (centro.x > MAX_X) {
|
||||
centro.x = MAX_X;
|
||||
if (velocity.x > 0.0F) {
|
||||
velocity.x = -velocity.x * REST;
|
||||
}
|
||||
}
|
||||
|
||||
// Dibujar segmento con brightness y color heredados del padre.
|
||||
Rendering::linea(renderer_,
|
||||
static_cast<int>(debris.p1.x),
|
||||
static_cast<int>(debris.p1.y),
|
||||
static_cast<int>(debris.p2.x),
|
||||
static_cast<int>(debris.p2.y),
|
||||
debris.brightness, 0.0F, debris.color);
|
||||
}
|
||||
}
|
||||
|
||||
auto DebrisManager::findFreeSlot() -> Debris* {
|
||||
for (auto& debris : debris_pool_) {
|
||||
if (!debris.active) {
|
||||
return &debris;
|
||||
if (centro.y < MIN_Y) {
|
||||
centro.y = MIN_Y;
|
||||
if (velocity.y < 0.0F) {
|
||||
velocity.y = -velocity.y * REST;
|
||||
}
|
||||
}
|
||||
if (centro.y > MAX_Y) {
|
||||
centro.y = MAX_Y;
|
||||
if (velocity.y > 0.0F) {
|
||||
velocity.y = -velocity.y * REST;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr; // Pool ple
|
||||
}
|
||||
|
||||
auto DebrisManager::computeExplosionDirection(const Vec2& p1,
|
||||
const Vec2& p2,
|
||||
const Vec2& centre_objecte) -> Vec2 {
|
||||
// 1. Calcular centro del segment
|
||||
float centro_seg_x = (p1.x + p2.x) / 2.0F;
|
||||
float centro_seg_y = (p1.y + p2.y) / 2.0F;
|
||||
void DebrisManager::update(float delta_time) {
|
||||
for (auto& debris : debris_pool_) {
|
||||
if (!debris.active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Calcular vector des del centro de l'objecte hacia el centro del segment
|
||||
// Això garanteix que la direcció siempre apunte hacia fuera (direcció radial)
|
||||
float dx = centro_seg_x - centre_objecte.x;
|
||||
float dy = centro_seg_y - centre_objecte.y;
|
||||
// 1. Actualitzar time de vida
|
||||
debris.temps_vida += delta_time;
|
||||
|
||||
// 3. Normalitzar (obtenir vector unitari)
|
||||
float length = std::sqrt((dx * dx) + (dy * dy));
|
||||
if (length < 0.001F) {
|
||||
// Segment al centro (cas extrem mucho improbable), retornar direcció aleatòria
|
||||
float angle_rand =
|
||||
(std::rand() / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI;
|
||||
return {.x = std::cos(angle_rand), .y = std::sin(angle_rand)};
|
||||
}
|
||||
// Política de mort: viu sí o sí durant min_lifetime; després mor
|
||||
// quan la velocity cau per sota d'un llindar. Així els fragments
|
||||
// ràpids no desapareixen en moviment.
|
||||
if (debris.temps_vida >= debris.min_lifetime) {
|
||||
const float SPEED_SQ = (debris.velocity.x * debris.velocity.x) +
|
||||
(debris.velocity.y * debris.velocity.y);
|
||||
if (SPEED_SQ < Defaults::Physics::Debris::MIN_SPEED_TO_DIE_SQ) {
|
||||
debris.active = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
dx /= length;
|
||||
dy /= length;
|
||||
// 2. Actualitzar velocity (desacceleració)
|
||||
// Aplicar fricció en la direcció del movement
|
||||
float speed = std::sqrt((debris.velocity.x * debris.velocity.x) +
|
||||
(debris.velocity.y * debris.velocity.y));
|
||||
|
||||
// 4. Añadir variació aleatòria pequeña (±15°) per varietat visual
|
||||
float angle_variacio =
|
||||
((std::rand() % 30) - 15) * Defaults::Math::PI / 180.0F;
|
||||
if (speed > 1.0F) {
|
||||
// Calcular direcció normalitzada
|
||||
float dir_x = debris.velocity.x / speed;
|
||||
float dir_y = debris.velocity.y / speed;
|
||||
|
||||
float cos_v = std::cos(angle_variacio);
|
||||
float sin_v = std::sin(angle_variacio);
|
||||
// Aplicar aceleración negativa (fricció)
|
||||
float nova_speed = speed + (debris.acceleration * delta_time);
|
||||
nova_speed = std::max(nova_speed, 0.0F);
|
||||
|
||||
float final_x = (dx * cos_v) - (dy * sin_v);
|
||||
float final_y = (dx * sin_v) + (dy * cos_v);
|
||||
debris.velocity.x = dir_x * nova_speed;
|
||||
debris.velocity.y = dir_y * nova_speed;
|
||||
} else {
|
||||
// Velocidad mucho baixa, aturar
|
||||
debris.velocity.x = 0.0F;
|
||||
debris.velocity.y = 0.0F;
|
||||
}
|
||||
|
||||
return {.x = final_x, .y = final_y};
|
||||
}
|
||||
// 2b. Rotar vector de velocity (trayectoria curva)
|
||||
if (std::abs(debris.velocitat_rot) > 0.01F) {
|
||||
// Calcular angle de rotación este frame
|
||||
float dangle = debris.velocitat_rot * delta_time;
|
||||
|
||||
void DebrisManager::reset() {
|
||||
for (auto& debris : debris_pool_) {
|
||||
debris.active = false;
|
||||
}
|
||||
}
|
||||
// Rotar vector de velocity usant matriu de rotación 2D
|
||||
float vel_x_old = debris.velocity.x;
|
||||
float vel_y_old = debris.velocity.y;
|
||||
|
||||
auto DebrisManager::getActiveCount() const -> int {
|
||||
int count = 0;
|
||||
for (const auto& debris : debris_pool_) {
|
||||
if (debris.active) {
|
||||
count++;
|
||||
float cos_a = std::cos(dangle);
|
||||
float sin_a = std::sin(dangle);
|
||||
|
||||
debris.velocity.x = (vel_x_old * cos_a) - (vel_y_old * sin_a);
|
||||
debris.velocity.y = (vel_x_old * sin_a) + (vel_y_old * cos_a);
|
||||
}
|
||||
|
||||
// 2c. Aplicar fricció angular (desacceleració gradual)
|
||||
if (std::abs(debris.velocitat_rot) > 0.01F) {
|
||||
float sign = (debris.velocitat_rot > 0) ? 1.0F : -1.0F;
|
||||
float reduccion =
|
||||
Defaults::Physics::Debris::FRICCIO_ANGULAR * delta_time;
|
||||
debris.velocitat_rot -= sign * reduccion;
|
||||
|
||||
// Evitar canvi de signe (no pot passar de CW a CCW)
|
||||
if ((debris.velocitat_rot > 0) != (sign > 0)) {
|
||||
debris.velocitat_rot = 0.0F;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Actualitzar posició del centre (integra velocity).
|
||||
debris.centro.x += debris.velocity.x * delta_time;
|
||||
debris.centro.y += debris.velocity.y * delta_time;
|
||||
|
||||
// 4. Rebot contra els límits del PLAYAREA.
|
||||
bounceOffPlayArea(debris.centro, debris.velocity);
|
||||
|
||||
// 5. Actualitzar rotació visual acumulada.
|
||||
debris.angle_rotacio += debris.velocitat_rot_visual * delta_time;
|
||||
|
||||
// 6. Shrink lineal sobre la longitud ORIGINAL (no iteratiu).
|
||||
// SHRINK_T va de 0 a 1 al llarg de min_lifetime; després queda
|
||||
// a 1 i el shrink_factor manté el valor mínim (1 - factor_shrink).
|
||||
const float SHRINK_T = std::min(debris.temps_vida / debris.min_lifetime, 1.0F);
|
||||
const float SHRINK_FACTOR = std::max(0.0F, 1.0F - (debris.factor_shrink * SHRINK_T));
|
||||
|
||||
// 7. Reconstruir p1/p2 des de la geometria autoritaritzada:
|
||||
// centro + (cos/sin(original_angle + angle_rotacio)) × original_half_length × shrink_factor
|
||||
// No iteratiu — evita la rotació quadràtica i el shrink exponencial.
|
||||
const float CURRENT_ANGLE = debris.original_angle + debris.angle_rotacio;
|
||||
const float HALF_LEN = debris.original_half_length * SHRINK_FACTOR;
|
||||
const float COS_A = std::cos(CURRENT_ANGLE);
|
||||
const float SIN_A = std::sin(CURRENT_ANGLE);
|
||||
debris.p1.x = debris.centro.x - (HALF_LEN * COS_A);
|
||||
debris.p1.y = debris.centro.y - (HALF_LEN * SIN_A);
|
||||
debris.p2.x = debris.centro.x + (HALF_LEN * COS_A);
|
||||
debris.p2.y = debris.centro.y + (HALF_LEN * SIN_A);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void DebrisManager::draw() const {
|
||||
for (const auto& debris : debris_pool_) {
|
||||
if (!debris.active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Dibujar segmento con brightness y color heredados del padre.
|
||||
Rendering::linea(renderer_,
|
||||
static_cast<int>(debris.p1.x),
|
||||
static_cast<int>(debris.p1.y),
|
||||
static_cast<int>(debris.p2.x),
|
||||
static_cast<int>(debris.p2.y),
|
||||
debris.brightness,
|
||||
0.0F,
|
||||
debris.color);
|
||||
}
|
||||
}
|
||||
|
||||
auto DebrisManager::findFreeSlot() -> Debris* {
|
||||
for (auto& debris : debris_pool_) {
|
||||
if (!debris.active) {
|
||||
return &debris;
|
||||
}
|
||||
}
|
||||
return nullptr; // Pool ple
|
||||
}
|
||||
|
||||
auto DebrisManager::computeExplosionDirection(const Vec2& p1,
|
||||
const Vec2& p2,
|
||||
const Vec2& centre_objecte) -> Vec2 {
|
||||
// 1. Calcular centro del segment
|
||||
float centro_seg_x = (p1.x + p2.x) / 2.0F;
|
||||
float centro_seg_y = (p1.y + p2.y) / 2.0F;
|
||||
|
||||
// 2. Calcular vector des del centro de l'objecte hacia el centro del segment
|
||||
// Això garanteix que la direcció siempre apunte hacia fuera (direcció radial)
|
||||
float dx = centro_seg_x - centre_objecte.x;
|
||||
float dy = centro_seg_y - centre_objecte.y;
|
||||
|
||||
// 3. Normalitzar (obtenir vector unitari)
|
||||
float length = std::sqrt((dx * dx) + (dy * dy));
|
||||
if (length < 0.001F) {
|
||||
// Segment al centro (cas extrem mucho improbable), retornar direcció aleatòria
|
||||
float angle_rand =
|
||||
(std::rand() / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI;
|
||||
return {.x = std::cos(angle_rand), .y = std::sin(angle_rand)};
|
||||
}
|
||||
|
||||
dx /= length;
|
||||
dy /= length;
|
||||
|
||||
// 4. Añadir variació aleatòria pequeña (±15°) per varietat visual
|
||||
float angle_variacio =
|
||||
((std::rand() % 30) - 15) * Defaults::Math::PI / 180.0F;
|
||||
|
||||
float cos_v = std::cos(angle_variacio);
|
||||
float sin_v = std::sin(angle_variacio);
|
||||
|
||||
float final_x = (dx * cos_v) - (dy * sin_v);
|
||||
float final_y = (dx * sin_v) + (dy * cos_v);
|
||||
|
||||
return {.x = final_x, .y = final_y};
|
||||
}
|
||||
|
||||
void DebrisManager::reset() {
|
||||
for (auto& debris : debris_pool_) {
|
||||
debris.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
auto DebrisManager::getActiveCount() const -> int {
|
||||
int count = 0;
|
||||
for (const auto& debris : debris_pool_) {
|
||||
if (debris.active) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
} // namespace Effects
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/rendering/render_context.hpp"
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array>
|
||||
@@ -13,15 +12,16 @@
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/graphics/shape.hpp"
|
||||
#include "core/rendering/render_context.hpp"
|
||||
#include "core/types.hpp"
|
||||
#include "debris.hpp"
|
||||
|
||||
namespace Effects {
|
||||
|
||||
// Gestor de fragments de explosions
|
||||
// Manté un pool de objectes Debris i gestiona el seu cicle de vida
|
||||
class DebrisManager {
|
||||
public:
|
||||
// Gestor de fragments de explosions
|
||||
// Manté un pool de objectes Debris i gestiona el seu cicle de vida
|
||||
class DebrisManager {
|
||||
public:
|
||||
explicit DebrisManager(Rendering::Renderer* renderer);
|
||||
|
||||
// Crear explosión a partir de una shape
|
||||
@@ -34,6 +34,9 @@ class DebrisManager {
|
||||
// - velocitat_objecte: velocity de l'objecte que explota (px/s, per defecte 0)
|
||||
// - velocitat_angular: velocity angular heretada (rad/s, per defecte 0)
|
||||
// - factor_herencia_visual: factor de herència rotación visual (0.0-1.0, per defecte 0.0)
|
||||
// - lifetime: temps de vida del debris (s, per defecte TEMPS_VIDA = 2s)
|
||||
// - friction: desacceleració del debris (px/s², per defecte ACCELERACIO = -60)
|
||||
// - segment_multiplier: nombre de còpies per segment (per defecte 1 = sense duplicar)
|
||||
void explode(const std::shared_ptr<Graphics::Shape>& shape,
|
||||
const Vec2& centro,
|
||||
float angle,
|
||||
@@ -44,7 +47,10 @@ class DebrisManager {
|
||||
float velocitat_angular = 0.0F,
|
||||
float factor_herencia_visual = 0.0F,
|
||||
const std::string& sound = Defaults::Sound::EXPLOSION,
|
||||
SDL_Color color = {0, 0, 0, 0}); // alpha==0 → fragmentos usan oscilador global
|
||||
SDL_Color color = {0, 0, 0, 0}, // alpha==0 → fragmentos usan oscilador global
|
||||
float lifetime = Defaults::Physics::Debris::TEMPS_VIDA,
|
||||
float friction = Defaults::Physics::Debris::ACCELERACIO,
|
||||
int segment_multiplier = 1);
|
||||
|
||||
// Actualitzar todos los fragments active
|
||||
void update(float delta_time);
|
||||
@@ -58,14 +64,13 @@ class DebrisManager {
|
||||
// Obtenir número de fragments active
|
||||
[[nodiscard]] auto getActiveCount() const -> int;
|
||||
|
||||
private:
|
||||
private:
|
||||
Rendering::Renderer* renderer_;
|
||||
|
||||
// Pool de fragments (màxim concurrent)
|
||||
// Un pentágono té 5 línies, 15 enemigos = 75 línies
|
||||
// + ship (3 línies) + balas (5 línies * 3) = 93 línies màxim
|
||||
// Arrodonit a 100 per seguretat
|
||||
static constexpr int MAX_DEBRIS = 150;
|
||||
// Pentàgon 5 línies × 15 enemics × multiplier 3 = 225 trossos només d'enemics.
|
||||
// + ship (3 línies) + balas (5 línies × 3) = ~243. Arrodonit a 300.
|
||||
static constexpr int MAX_DEBRIS = 300;
|
||||
std::array<Debris, MAX_DEBRIS> debris_pool_;
|
||||
|
||||
// Trobar primer slot inactiu
|
||||
@@ -81,14 +86,9 @@ class DebrisManager {
|
||||
-> std::vector<std::pair<Vec2, Vec2>>;
|
||||
// Inicialitza un debris en un slot lliure i el deixa actiu. Retorna
|
||||
// false si el pool está ple (la cridadora ha d'aturar el bucle).
|
||||
auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2,
|
||||
const Vec2& centro, float velocitat_base, float brightness,
|
||||
const Vec2& velocitat_objecte, float velocitat_angular,
|
||||
float factor_herencia_visual, SDL_Color color) -> bool;
|
||||
static void applyAngularVelocity(Debris& debris, const Vec2& direccio,
|
||||
float velocitat_angular);
|
||||
static void applyVisualRotation(Debris& debris, float velocitat_angular,
|
||||
float factor_herencia_visual);
|
||||
};
|
||||
auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction) -> bool;
|
||||
static void applyAngularVelocity(Debris& debris, const Vec2& direccio, float velocitat_angular);
|
||||
static void applyVisualRotation(Debris& debris, float velocitat_angular, float factor_herencia_visual);
|
||||
};
|
||||
|
||||
} // namespace Effects
|
||||
|
||||
@@ -16,12 +16,6 @@
|
||||
#include "core/types.hpp"
|
||||
#include "game/constants.hpp"
|
||||
|
||||
namespace {
|
||||
// Velocidad escalar de las balas (px/s). Conserva el feel del Pascal original
|
||||
// (7 px/frame × 20 FPS = 140 px/s).
|
||||
constexpr float BULLET_SPEED = 140.0F;
|
||||
} // namespace
|
||||
|
||||
Bullet::Bullet(Rendering::Renderer* renderer)
|
||||
: Entity(renderer) {
|
||||
// Brightness específico para balas
|
||||
@@ -80,7 +74,7 @@ void Bullet::disparar(const Vec2& position, float angle, uint8_t owner_id) {
|
||||
body_.angle = angle;
|
||||
const float DIR_X = std::cos(angle - (Constants::PI / 2.0F));
|
||||
const float DIR_Y = std::sin(angle - (Constants::PI / 2.0F));
|
||||
body_.velocity = Vec2{.x = DIR_X * BULLET_SPEED, .y = DIR_Y * BULLET_SPEED};
|
||||
body_.velocity = Vec2{.x = DIR_X * Defaults::Game::BULLET_SPEED, .y = DIR_Y * Defaults::Game::BULLET_SPEED};
|
||||
body_.angular_velocity = 0.0F;
|
||||
body_.clearAccumulators();
|
||||
|
||||
|
||||
@@ -41,16 +41,16 @@ namespace {
|
||||
Enemy::Enemy(Rendering::Renderer* renderer)
|
||||
: Entity(renderer),
|
||||
|
||||
tracking_strength_(0.5F) {
|
||||
tracking_strength_(Defaults::Enemies::Cuadrado::TRACKING_STRENGTH) {
|
||||
brightness_ = Defaults::Brightness::ENEMIC;
|
||||
|
||||
// Configuración del cuerpo físico — defaults para enemy genérico.
|
||||
// init() ajusta velocidad y masa según el tipo (Pentagon/Quadrat/Molinillo).
|
||||
body_.setMass(5.0F); // Más liviano que la nave (10.0)
|
||||
body_.radius = 0.0F; // 0 hasta spawn (no colisiona inactivo)
|
||||
body_.restitution = 1.0F; // Rebote elástico perfecto contra paredes
|
||||
body_.linear_damping = 0.0F; // Sin fricción: mantienen velocidad
|
||||
body_.angular_damping = 0.0F; // Idem
|
||||
body_.setMass(Defaults::Enemies::Body::DEFAULT_MASS);
|
||||
body_.radius = 0.0F; // 0 hasta spawn (no colisiona inactivo)
|
||||
body_.restitution = Defaults::Enemies::Body::RESTITUTION;
|
||||
body_.linear_damping = Defaults::Enemies::Body::LINEAR_DAMPING;
|
||||
body_.angular_damping = Defaults::Enemies::Body::ANGULAR_DAMPING;
|
||||
}
|
||||
|
||||
void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
||||
@@ -60,7 +60,7 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
||||
float base_speed = 0.0F;
|
||||
float drotacio_min = 0.0F;
|
||||
float drotacio_max = 0.0F;
|
||||
float type_mass = 5.0F;
|
||||
float type_mass = Defaults::Enemies::Body::DEFAULT_MASS;
|
||||
|
||||
switch (type_) {
|
||||
case EnemyType::PENTAGON:
|
||||
@@ -68,7 +68,7 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
||||
base_speed = Defaults::Enemies::Pentagon::VELOCITAT;
|
||||
drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN;
|
||||
drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX;
|
||||
type_mass = 5.0F;
|
||||
type_mass = Defaults::Enemies::Pentagon::MASS;
|
||||
break;
|
||||
|
||||
case EnemyType::QUADRAT:
|
||||
@@ -76,7 +76,7 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
||||
base_speed = Defaults::Enemies::Cuadrado::VELOCITAT;
|
||||
drotacio_min = Defaults::Enemies::Cuadrado::DROTACIO_MIN;
|
||||
drotacio_max = Defaults::Enemies::Cuadrado::DROTACIO_MAX;
|
||||
type_mass = 8.0F; // Más pesado, "tanque"
|
||||
type_mass = Defaults::Enemies::Cuadrado::MASS;
|
||||
tracking_timer_ = 0.0F;
|
||||
break;
|
||||
|
||||
@@ -85,7 +85,7 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
||||
base_speed = Defaults::Enemies::Molinillo::VELOCITAT;
|
||||
drotacio_min = Defaults::Enemies::Molinillo::DROTACIO_MIN;
|
||||
drotacio_max = Defaults::Enemies::Molinillo::DROTACIO_MAX;
|
||||
type_mass = 4.0F; // Más liviano, ágil
|
||||
type_mass = Defaults::Enemies::Molinillo::MASS;
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -153,7 +153,7 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
||||
|
||||
// Rotación visual aleatoria (independiente del body)
|
||||
const float DROTACIO_RANGE = drotacio_max - drotacio_min;
|
||||
drotacio_ = drotacio_min + ((static_cast<float>(std::rand()) / RAND_MAX) * DROTACIO_RANGE);
|
||||
drotacio_ = drotacio_min + ((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DROTACIO_RANGE);
|
||||
rotacio_ = 0.0F;
|
||||
|
||||
// Estado de animación
|
||||
@@ -177,6 +177,17 @@ void Enemy::update(float delta_time) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Decremento de timer "herido"; al cruzar 0 marca expiración para que el
|
||||
// system layer dispare la explosión diferida.
|
||||
wound_expired_this_frame_ = false;
|
||||
if (wounded_timer_ > 0.0F) {
|
||||
wounded_timer_ -= delta_time;
|
||||
if (wounded_timer_ <= 0.0F) {
|
||||
wounded_timer_ = 0.0F;
|
||||
wound_expired_this_frame_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Decremento de invulnerabilidad + LERP de brightness
|
||||
if (timer_invulnerabilitat_ > 0.0F) {
|
||||
timer_invulnerabilitat_ -= delta_time;
|
||||
@@ -190,17 +201,21 @@ void Enemy::update(float delta_time) {
|
||||
brightness_ = START + ((END - START) * SMOOTH_T);
|
||||
}
|
||||
|
||||
// Comportamiento por tipo (ajusta body_.velocity, NO mueve posición)
|
||||
switch (type_) {
|
||||
case EnemyType::PENTAGON:
|
||||
behaviorPentagon(delta_time);
|
||||
break;
|
||||
case EnemyType::QUADRAT:
|
||||
behaviorQuadrat(delta_time);
|
||||
break;
|
||||
case EnemyType::MOLINILLO:
|
||||
behaviorMolinillo(delta_time);
|
||||
break;
|
||||
// Comportamiento por tipo (ajusta body_.velocity, NO mueve posición).
|
||||
// Skip cuando está herido: el enemy és un "cos mort" inert, només
|
||||
// respon a la inèrcia del impulse rebut i a les col·lisions físiques.
|
||||
if (!isWounded()) {
|
||||
switch (type_) {
|
||||
case EnemyType::PENTAGON:
|
||||
behaviorPentagon(delta_time);
|
||||
break;
|
||||
case EnemyType::QUADRAT:
|
||||
behaviorQuadrat(delta_time);
|
||||
break;
|
||||
case EnemyType::MOLINILLO:
|
||||
behaviorMolinillo(delta_time);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Animaciones (palpitación + rotación acelerada)
|
||||
@@ -234,6 +249,17 @@ void Enemy::draw() const {
|
||||
color = Defaults::Palette::MOLINILLO;
|
||||
break;
|
||||
}
|
||||
|
||||
// Parpadeo dorado mientras está herido: alterna color de tipo ↔ dorado
|
||||
// a Wounded::BLINK_HZ usando el timer (fmod sobre el periodo).
|
||||
if (wounded_timer_ > 0.0F) {
|
||||
const float CYCLE = 1.0F / Defaults::Enemies::Wounded::BLINK_HZ;
|
||||
const float T = std::fmod(wounded_timer_, CYCLE);
|
||||
if (T < (CYCLE / 2.0F)) {
|
||||
color = Defaults::Palette::WOUNDED;
|
||||
}
|
||||
}
|
||||
|
||||
Rendering::renderShape(renderer_, shape_, center_, rotacio_, SCALE, 1.0F, brightness_, color);
|
||||
}
|
||||
|
||||
@@ -242,6 +268,18 @@ void Enemy::destruir() {
|
||||
body_.velocity = Vec2{};
|
||||
body_.angular_velocity = 0.0F;
|
||||
body_.radius = 0.0F; // No colisiona mientras está inactivo
|
||||
wounded_timer_ = 0.0F;
|
||||
wound_expired_this_frame_ = false;
|
||||
last_hit_by_ = 0xFF;
|
||||
}
|
||||
|
||||
void Enemy::herir(uint8_t shooter_id) {
|
||||
wounded_timer_ = Defaults::Enemies::Wounded::DURATION;
|
||||
last_hit_by_ = shooter_id;
|
||||
}
|
||||
|
||||
void Enemy::applyImpulse(const Vec2& impulse) {
|
||||
body_.applyImpulse(impulse);
|
||||
}
|
||||
|
||||
void Enemy::setVelocity(float speed) {
|
||||
@@ -268,11 +306,10 @@ void Enemy::behaviorPentagon(float delta_time) {
|
||||
|
||||
// Probabilidad de zigzag por segundo (calibrada para sensación equivalente
|
||||
// a la versión vieja que disparaba en cada toque de pared).
|
||||
constexpr float ZIGZAG_PROB_PER_SECOND = 0.8F;
|
||||
const float RAND_VAL = static_cast<float>(std::rand()) / RAND_MAX;
|
||||
if (RAND_VAL < ZIGZAG_PROB_PER_SECOND * delta_time) {
|
||||
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
||||
if (RAND_VAL < Defaults::Enemies::Pentagon::ZIGZAG_PROB_PER_SECOND * delta_time) {
|
||||
const float CURRENT_ANGLE = velocityToAngle(body_.velocity);
|
||||
const float DELTA = (static_cast<float>(std::rand()) / RAND_MAX) *
|
||||
const float DELTA = (static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) *
|
||||
Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX;
|
||||
const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA);
|
||||
const float SPEED = body_.velocity.length();
|
||||
@@ -339,7 +376,7 @@ void Enemy::updatePalpitation(float delta_time) {
|
||||
animacio_.palpitacio_activa = false;
|
||||
}
|
||||
} else {
|
||||
const float RAND_VAL = static_cast<float>(std::rand()) / RAND_MAX;
|
||||
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
||||
const float TRIGGER_PROB = Defaults::Enemies::Animation::PALPITACIO_TRIGGER_PROB * delta_time;
|
||||
if (RAND_VAL < TRIGGER_PROB) {
|
||||
animacio_.palpitacio_activa = true;
|
||||
@@ -348,17 +385,17 @@ void Enemy::updatePalpitation(float delta_time) {
|
||||
const float FREQ_RANGE = Defaults::Enemies::Animation::PALPITACIO_FREQ_MAX -
|
||||
Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN;
|
||||
animacio_.palpitacio_frequencia = Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN +
|
||||
((static_cast<float>(std::rand()) / RAND_MAX) * FREQ_RANGE);
|
||||
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * FREQ_RANGE);
|
||||
|
||||
const float AMP_RANGE = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MAX -
|
||||
Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN;
|
||||
animacio_.palpitacio_amplitud = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN +
|
||||
((static_cast<float>(std::rand()) / RAND_MAX) * AMP_RANGE);
|
||||
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * AMP_RANGE);
|
||||
|
||||
const float DUR_RANGE = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MAX -
|
||||
Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN;
|
||||
animacio_.palpitacio_temps_restant = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN +
|
||||
((static_cast<float>(std::rand()) / RAND_MAX) * DUR_RANGE);
|
||||
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -378,7 +415,7 @@ void Enemy::updateRotationAcceleration(float delta_time) {
|
||||
drotacio_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T);
|
||||
}
|
||||
} else {
|
||||
const float RAND_VAL = static_cast<float>(std::rand()) / RAND_MAX;
|
||||
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
||||
const float TRIGGER_PROB = Defaults::Enemies::Animation::ROTACIO_ACCEL_TRIGGER_PROB * delta_time;
|
||||
if (RAND_VAL < TRIGGER_PROB) {
|
||||
animacio_.drotacio_t = 0.0F;
|
||||
@@ -386,13 +423,13 @@ void Enemy::updateRotationAcceleration(float delta_time) {
|
||||
const float MULT_RANGE = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MAX -
|
||||
Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN;
|
||||
const float MULTIPLIER = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN +
|
||||
((static_cast<float>(std::rand()) / RAND_MAX) * MULT_RANGE);
|
||||
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * MULT_RANGE);
|
||||
animacio_.drotacio_objetivo = animacio_.drotacio_base * MULTIPLIER;
|
||||
|
||||
const float DUR_RANGE = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MAX -
|
||||
Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN;
|
||||
animacio_.drotacio_duracio = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN +
|
||||
((static_cast<float>(std::rand()) / RAND_MAX) * DUR_RANGE);
|
||||
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,104 +19,121 @@ enum class EnemyType : uint8_t {
|
||||
|
||||
// Estado de animación (palpitación + rotación acelerada)
|
||||
struct EnemyAnimation {
|
||||
// Palpitación (efecto respiración)
|
||||
bool palpitacio_activa = false;
|
||||
float palpitacio_fase = 0.0F;
|
||||
float palpitacio_frequencia = 2.0F;
|
||||
float palpitacio_amplitud = 0.15F;
|
||||
float palpitacio_temps_restant = 0.0F;
|
||||
// Palpitación (efecto respiración)
|
||||
bool palpitacio_activa = false;
|
||||
float palpitacio_fase = 0.0F;
|
||||
float palpitacio_frequencia = 2.0F;
|
||||
float palpitacio_amplitud = 0.15F;
|
||||
float palpitacio_temps_restant = 0.0F;
|
||||
|
||||
// Aceleración de rotación visual (modulación a largo plazo)
|
||||
float drotacio_base = 0.0F;
|
||||
float drotacio_objetivo = 0.0F;
|
||||
float drotacio_t = 0.0F;
|
||||
float drotacio_duracio = 0.0F;
|
||||
// Aceleración de rotación visual (modulación a largo plazo)
|
||||
float drotacio_base = 0.0F;
|
||||
float drotacio_objetivo = 0.0F;
|
||||
float drotacio_t = 0.0F;
|
||||
float drotacio_duracio = 0.0F;
|
||||
};
|
||||
|
||||
class Enemy : public Entities::Entity {
|
||||
public:
|
||||
Enemy()
|
||||
: Entity(nullptr) {}
|
||||
explicit Enemy(Rendering::Renderer* renderer);
|
||||
public:
|
||||
Enemy()
|
||||
: Entity(nullptr) {}
|
||||
explicit Enemy(Rendering::Renderer* renderer);
|
||||
|
||||
void init() override { init(EnemyType::PENTAGON, nullptr); }
|
||||
void init(EnemyType type, const Vec2* ship_pos = nullptr);
|
||||
void update(float delta_time) override;
|
||||
void postUpdate(float delta_time) override;
|
||||
void draw() const override;
|
||||
void init() override { init(EnemyType::PENTAGON, nullptr); }
|
||||
void init(EnemyType type, const Vec2* ship_pos = nullptr);
|
||||
void update(float delta_time) override;
|
||||
void postUpdate(float delta_time) override;
|
||||
void draw() const override;
|
||||
|
||||
// Override: Interfaz de Entity
|
||||
[[nodiscard]] auto isActive() const -> bool override { return esta_; }
|
||||
// Override: Interfaz de Entity
|
||||
[[nodiscard]] auto isActive() const -> bool override { return esta_; }
|
||||
|
||||
// Override: Interfaz de colisión
|
||||
[[nodiscard]] auto getCollisionRadius() const -> float override {
|
||||
return Defaults::Entities::ENEMY_RADIUS;
|
||||
}
|
||||
[[nodiscard]] auto isCollidable() const -> bool override {
|
||||
return esta_ && timer_invulnerabilitat_ <= 0.0F;
|
||||
}
|
||||
// Override: Interfaz de colisión
|
||||
[[nodiscard]] auto getCollisionRadius() const -> float override {
|
||||
return Defaults::Entities::ENEMY_RADIUS;
|
||||
}
|
||||
[[nodiscard]] auto isCollidable() const -> bool override {
|
||||
return esta_ && timer_invulnerabilitat_ <= 0.0F;
|
||||
}
|
||||
|
||||
// Marcar destruido (desactiva el cuerpo físicamente: radius=0)
|
||||
void destruir();
|
||||
// Marcar destruido (desactiva el cuerpo físicamente: radius=0)
|
||||
void destruir();
|
||||
|
||||
// Getters
|
||||
[[nodiscard]] auto getRotationDelta() const -> float { return drotacio_; }
|
||||
[[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; }
|
||||
// Getters
|
||||
[[nodiscard]] auto getRotationDelta() const -> float { return drotacio_; }
|
||||
[[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; }
|
||||
|
||||
// Set ship position reference for tracking behavior
|
||||
void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; }
|
||||
// Set ship position reference for tracking behavior
|
||||
void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; }
|
||||
|
||||
// Stage system API (base stats)
|
||||
[[nodiscard]] auto getBaseVelocity() const -> float;
|
||||
[[nodiscard]] auto getBaseRotation() const -> float;
|
||||
[[nodiscard]] auto getType() const -> EnemyType { return type_; }
|
||||
// Stage system API (base stats)
|
||||
[[nodiscard]] auto getBaseVelocity() const -> float;
|
||||
[[nodiscard]] auto getBaseRotation() const -> float;
|
||||
[[nodiscard]] auto getType() const -> EnemyType { return type_; }
|
||||
|
||||
// Setters para multiplicadores de dificultad (stage system).
|
||||
// Establecen la velocidad escalar deseada manteniendo la dirección
|
||||
// actual del body_.velocity.
|
||||
void setVelocity(float speed);
|
||||
void setRotation(float rot) {
|
||||
drotacio_ = rot;
|
||||
animacio_.drotacio_base = rot;
|
||||
}
|
||||
void setTrackingStrength(float strength);
|
||||
// Setters para multiplicadores de dificultad (stage system).
|
||||
// Establecen la velocidad escalar deseada manteniendo la dirección
|
||||
// actual del body_.velocity.
|
||||
void setVelocity(float speed);
|
||||
void setRotation(float rot) {
|
||||
drotacio_ = rot;
|
||||
animacio_.drotacio_base = rot;
|
||||
}
|
||||
void setTrackingStrength(float strength);
|
||||
|
||||
// Invulnerabilidad
|
||||
[[nodiscard]] auto isInvulnerable() const -> bool { return timer_invulnerabilitat_ > 0.0F; }
|
||||
[[nodiscard]] auto getInvulnerabilityTime() const -> float { return timer_invulnerabilitat_; }
|
||||
// Invulnerabilidad
|
||||
[[nodiscard]] auto isInvulnerable() const -> bool { return timer_invulnerabilitat_ > 0.0F; }
|
||||
[[nodiscard]] auto getInvulnerabilityTime() const -> float { return timer_invulnerabilitat_; }
|
||||
|
||||
private:
|
||||
// Miembros específicos (heredados: renderer_, shape_, center_, angle_, brightness_, body_).
|
||||
// Inicializados en la declaración: el ctor por defecto deja al enemy en estado "inactivo
|
||||
// como pentágono", coherente con lo que harán init() o el ctor con renderer al activarlo.
|
||||
float drotacio_{0.0F}; // Velocidad angular visual (rad/s) — solo decoración, separada de body_.angular_velocity
|
||||
float rotacio_{0.0F}; // Rotación visual acumulada (no afecta movimiento)
|
||||
bool esta_{false};
|
||||
// Estado "herido": entre primer impacto de bala y explosión diferida.
|
||||
// shooter_id: id del jugador que herí; 0xFF = sin atribución (cadena, etc.).
|
||||
void herir(uint8_t shooter_id = 0xFF);
|
||||
[[nodiscard]] auto isWounded() const -> bool { return wounded_timer_ > 0.0F; }
|
||||
[[nodiscard]] auto getWoundedTimer() const -> float { return wounded_timer_; }
|
||||
[[nodiscard]] auto woundExpiredThisFrame() const -> bool { return wound_expired_this_frame_; }
|
||||
void consumeWoundExpired() { wound_expired_this_frame_ = false; }
|
||||
[[nodiscard]] auto getLastHitBy() const -> uint8_t { return last_hit_by_; }
|
||||
|
||||
EnemyType type_{EnemyType::PENTAGON};
|
||||
EnemyAnimation animacio_;
|
||||
// Aplica un impulso (cambio inmediato de velocidad mass-aware) al cuerpo físico.
|
||||
void applyImpulse(const Vec2& impulse);
|
||||
|
||||
// Comportamiento type-specific
|
||||
float tracking_timer_{0.0F}; // Quadrat: tiempo desde último update de dirección
|
||||
const Vec2* ship_position_{nullptr}; // Puntero a posición de la nave (para tracking)
|
||||
float tracking_strength_{0.0F}; // Quadrat: intensidad de tracking (0.0-1.5), default 0.5
|
||||
float direction_change_timer_{0.0F}; // Pentagon: tiempo para próximo cambio de dirección
|
||||
private:
|
||||
// Miembros específicos (heredados: renderer_, shape_, center_, angle_, brightness_, body_).
|
||||
// Inicializados en la declaración: el ctor por defecto deja al enemy en estado "inactivo
|
||||
// como pentágono", coherente con lo que harán init() o el ctor con renderer al activarlo.
|
||||
float drotacio_{0.0F}; // Velocidad angular visual (rad/s) — solo decoración, separada de body_.angular_velocity
|
||||
float rotacio_{0.0F}; // Rotación visual acumulada (no afecta movimiento)
|
||||
bool esta_{false};
|
||||
|
||||
// Invulnerabilidad post-spawn
|
||||
float timer_invulnerabilitat_{0.0F};
|
||||
EnemyType type_{EnemyType::PENTAGON};
|
||||
EnemyAnimation animacio_;
|
||||
|
||||
// Métodos privados
|
||||
void updateAnimation(float delta_time);
|
||||
void updatePalpitation(float delta_time);
|
||||
void updateRotationAcceleration(float delta_time);
|
||||
void behaviorPentagon(float delta_time);
|
||||
void behaviorQuadrat(float delta_time);
|
||||
void behaviorMolinillo(float delta_time);
|
||||
[[nodiscard]] auto computeCurrentScale() const -> float;
|
||||
// Estático: solo opera sobre ship_pos pasado; no consulta estado del enemy.
|
||||
static auto attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool;
|
||||
// Comportamiento type-specific
|
||||
float tracking_timer_{0.0F}; // Quadrat: tiempo desde último update de dirección
|
||||
const Vec2* ship_position_{nullptr}; // Puntero a posición de la nave (para tracking)
|
||||
float tracking_strength_{0.0F}; // Quadrat: intensidad de tracking (0.0-1.5), default 0.5
|
||||
float direction_change_timer_{0.0F}; // Pentagon: tiempo para próximo cambio de dirección
|
||||
|
||||
// Helper: setear body_.velocity desde un ángulo y magnitud.
|
||||
// angle_movement=0 apunta hacia arriba (eje Y negativo SDL).
|
||||
void setVelocityFromAngle(float angle_movement, float speed);
|
||||
// Invulnerabilidad post-spawn
|
||||
float timer_invulnerabilitat_{0.0F};
|
||||
|
||||
// Estado "herido": timer cuenta atrás; al cruzar 0 se marca expiración.
|
||||
float wounded_timer_{0.0F};
|
||||
bool wound_expired_this_frame_{false};
|
||||
uint8_t last_hit_by_{0xFF}; // 0xFF = sin atribución
|
||||
|
||||
// Métodos privados
|
||||
void updateAnimation(float delta_time);
|
||||
void updatePalpitation(float delta_time);
|
||||
void updateRotationAcceleration(float delta_time);
|
||||
void behaviorPentagon(float delta_time);
|
||||
void behaviorQuadrat(float delta_time);
|
||||
void behaviorMolinillo(float delta_time);
|
||||
[[nodiscard]] auto computeCurrentScale() const -> float;
|
||||
// Estático: solo opera sobre ship_pos pasado; no consulta estado del enemy.
|
||||
static auto attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool;
|
||||
|
||||
// Helper: setear body_.velocity desde un ángulo y magnitud.
|
||||
// angle_movement=0 apunta hacia arriba (eje Y negativo SDL).
|
||||
void setVelocityFromAngle(float angle_movement, float speed);
|
||||
};
|
||||
|
||||
@@ -25,11 +25,11 @@ Ship::Ship(Rendering::Renderer* renderer, const char* shape_file)
|
||||
brightness_ = Defaults::Brightness::NAU;
|
||||
|
||||
// Configuración del cuerpo físico
|
||||
body_.setMass(10.0F); // Masa de referencia para choques
|
||||
body_.radius = Defaults::Entities::SHIP_RADIUS; // Radio de colisión
|
||||
body_.restitution = 0.6F; // Rebote moderado contra paredes
|
||||
body_.linear_damping = 1.5F; // Fricción exponencial (s⁻¹)
|
||||
body_.angular_damping = 0.0F; // La rotación es 100% por input, no inercial
|
||||
body_.setMass(Defaults::Ship::MASS);
|
||||
body_.radius = Defaults::Entities::SHIP_RADIUS;
|
||||
body_.restitution = Defaults::Ship::RESTITUTION;
|
||||
body_.linear_damping = Defaults::Ship::LINEAR_DAMPING;
|
||||
body_.angular_damping = Defaults::Ship::ANGULAR_DAMPING;
|
||||
|
||||
// Cargar shape compartida desde archivo
|
||||
shape_ = Graphics::ShapeLoader::load(shape_file);
|
||||
@@ -154,8 +154,8 @@ void Ship::draw() const {
|
||||
// Efecto visual de empuje: escala proporcional a la velocidad.
|
||||
// 0..200 px/s → escala 1.0..1.5 (manteniendo la sensación del Pascal original).
|
||||
const float SPEED = getSpeed();
|
||||
const float VISUAL_PUSH = SPEED / 33.33F;
|
||||
const float SCALE = 1.0F + (VISUAL_PUSH / 12.0F);
|
||||
const float VISUAL_PUSH = SPEED / Defaults::Ship::VISUAL_PUSH_DIVISOR;
|
||||
const float SCALE = 1.0F + (VISUAL_PUSH / Defaults::Ship::VISUAL_SCALE_DIVISOR);
|
||||
|
||||
Rendering::renderShape(renderer_, shape_, center_, angle_, SCALE, 1.0F, brightness_, Defaults::Palette::SHIP);
|
||||
}
|
||||
|
||||
@@ -107,8 +107,8 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
||||
} else {
|
||||
// Jugador inactiu: marcar como a mort permanent
|
||||
ships_[i].markHit();
|
||||
hit_timer_per_player_[i] = 999.0F; // Valor sentinella (permanent inactiu)
|
||||
lives_per_player_[i] = 0; // Sin vides
|
||||
hit_timer_per_player_[i] = Defaults::Game::HIT_TIMER_INACTIVE_PLAYER;
|
||||
lives_per_player_[i] = 0; // Sin vides
|
||||
std::cout << "[GameScene] Jugador " << (i + 1) << " inactiu\n";
|
||||
}
|
||||
}
|
||||
@@ -201,8 +201,8 @@ void GameScene::stepMidGameJoin() {
|
||||
// Solo se permite join si hay al menos un jugador vivo (no se puede
|
||||
// hacer join en pantalla vacía).
|
||||
const bool ALGU_VIU =
|
||||
(match_config_.jugador1_actiu && hit_timer_per_player_[0] != 999.0F) ||
|
||||
(match_config_.jugador2_actiu && hit_timer_per_player_[1] != 999.0F);
|
||||
(match_config_.jugador1_actiu && hit_timer_per_player_[0] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER) ||
|
||||
(match_config_.jugador2_actiu && hit_timer_per_player_[1] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER);
|
||||
if (!ALGU_VIU) {
|
||||
return;
|
||||
}
|
||||
@@ -211,7 +211,7 @@ void GameScene::stepMidGameJoin() {
|
||||
for (uint8_t pid = 0; pid < 2; pid++) {
|
||||
const bool ACTIU = (pid == 0) ? match_config_.jugador1_actiu
|
||||
: match_config_.jugador2_actiu;
|
||||
const bool MUERTO_SIN_VIDAS = hit_timer_per_player_[pid] == 999.0F;
|
||||
const bool MUERTO_SIN_VIDAS = hit_timer_per_player_[pid] == Defaults::Game::HIT_TIMER_INACTIVE_PLAYER;
|
||||
if (ACTIU && !MUERTO_SIN_VIDAS) {
|
||||
continue; // jugador ya está jugando
|
||||
}
|
||||
@@ -284,7 +284,7 @@ auto GameScene::stepGameOver(float delta_time) -> bool {
|
||||
void GameScene::stepDeathSequence(float delta_time) {
|
||||
bool algun_mort = false;
|
||||
for (uint8_t i = 0; i < 2; i++) {
|
||||
if (hit_timer_per_player_[i] <= 0.0F || hit_timer_per_player_[i] >= 999.0F) {
|
||||
if (hit_timer_per_player_[i] <= 0.0F || hit_timer_per_player_[i] >= Defaults::Game::HIT_TIMER_INACTIVE_PLAYER) {
|
||||
continue;
|
||||
}
|
||||
algun_mort = true;
|
||||
@@ -304,7 +304,7 @@ void GameScene::stepDeathSequence(float delta_time) {
|
||||
}
|
||||
|
||||
// Sin vidas: marcar definitivamente muerto y comprobar transición a CONTINUE.
|
||||
hit_timer_per_player_[i] = 999.0F;
|
||||
hit_timer_per_player_[i] = Defaults::Game::HIT_TIMER_INACTIVE_PLAYER;
|
||||
const bool P1_DEAD = !match_config_.jugador1_actiu || lives_per_player_[0] <= 0;
|
||||
const bool P2_DEAD = !match_config_.jugador2_actiu || lives_per_player_[1] <= 0;
|
||||
if (P1_DEAD && P2_DEAD) {
|
||||
@@ -628,8 +628,9 @@ void GameScene::tocado(uint8_t player_id) {
|
||||
const Vec2& ship_pos = ships_[player_id].getCenter();
|
||||
float ship_angle = ships_[player_id].getAngle();
|
||||
Vec2 vel_nau = ships_[player_id].getVelocityVector();
|
||||
// Reduir a 80% la velocity heretada per la ship (més realista)
|
||||
Vec2 vel_nau_80 = {.x = vel_nau.x * 0.8F, .y = vel_nau.y * 0.8F};
|
||||
// Reduir la velocity heretada per la ship segons defaults (més realista)
|
||||
constexpr float INHERIT = Defaults::Physics::Debris::SHIP_VELOCITY_INHERITANCE;
|
||||
Vec2 vel_nau_80 = {.x = vel_nau.x * INHERIT, .y = vel_nau.y * INHERIT};
|
||||
|
||||
debris_manager_.explode(
|
||||
ships_[player_id].getShape(), // Ship shape (3 lines)
|
||||
@@ -646,7 +647,7 @@ void GameScene::tocado(uint8_t player_id) {
|
||||
);
|
||||
|
||||
// Start death timer (non-zero to avoid re-triggering)
|
||||
hit_timer_per_player_[player_id] = 0.001F;
|
||||
hit_timer_per_player_[player_id] = Defaults::Game::HIT_TIMER_TRIGGER_DEATH;
|
||||
}
|
||||
// Phase 2 is automatic (debris updates in update())
|
||||
// Phase 3 is handled in update() when hit_timer_per_player_ >= DEATH_DURATION
|
||||
@@ -674,8 +675,8 @@ void GameScene::drawScoreboard() {
|
||||
std::string text = buildScoreboard();
|
||||
|
||||
// Parámetros de renderització
|
||||
const float SCALE = 0.85F;
|
||||
const float SPACING = 0.0F;
|
||||
const float SCALE = Defaults::Hud::SCOREBOARD_TEXT_SCALE;
|
||||
const float SPACING = Defaults::Hud::SCOREBOARD_TEXT_SPACING;
|
||||
|
||||
// Calcular centro de la zona del marcador
|
||||
const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD;
|
||||
@@ -828,8 +829,8 @@ void GameScene::fireBullet(uint8_t player_id) {
|
||||
const Vec2& ship_centre = ships_[player_id].getCenter();
|
||||
float ship_angle = ships_[player_id].getAngle();
|
||||
|
||||
constexpr float LOCAL_TIP_X = 0.0F;
|
||||
constexpr float LOCAL_TIP_Y = -12.0F;
|
||||
constexpr float LOCAL_TIP_X = Defaults::Hud::Tips::LOCAL_X;
|
||||
constexpr float LOCAL_TIP_Y = Defaults::Hud::Tips::LOCAL_Y;
|
||||
float cos_a = std::cos(ship_angle);
|
||||
float sin_a = std::sin(ship_angle);
|
||||
float tip_x = (LOCAL_TIP_X * cos_a) - (LOCAL_TIP_Y * sin_a) + ship_centre.x;
|
||||
|
||||
@@ -10,9 +10,77 @@
|
||||
|
||||
namespace Systems::Collision {
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t NO_SHOOTER = 0xFF;
|
||||
|
||||
// Lookup tabla puntos / color por tipo de enemy (mantiene la lógica
|
||||
// anterior pero centralizada para reutilizar entre paths).
|
||||
auto scoreForType(EnemyType type) -> int {
|
||||
switch (type) {
|
||||
case EnemyType::PENTAGON:
|
||||
return Defaults::Enemies::Scoring::PENTAGON_SCORE;
|
||||
case EnemyType::QUADRAT:
|
||||
return Defaults::Enemies::Scoring::QUADRAT_SCORE;
|
||||
case EnemyType::MOLINILLO:
|
||||
return Defaults::Enemies::Scoring::MOLINILLO_SCORE;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto colorForType(EnemyType type) -> SDL_Color {
|
||||
switch (type) {
|
||||
case EnemyType::PENTAGON:
|
||||
return Defaults::Palette::PENTAGON;
|
||||
case EnemyType::QUADRAT:
|
||||
return Defaults::Palette::QUADRAT;
|
||||
case EnemyType::MOLINILLO:
|
||||
return Defaults::Palette::MOLINILLO;
|
||||
}
|
||||
return SDL_Color{};
|
||||
}
|
||||
|
||||
// Mata al enemy con explosión: floating score, debris con velocity heredada,
|
||||
// sonido. Si shooter_id ≠ NO_SHOOTER, suma puntos a ese jugador.
|
||||
// CRUCIAL: leer velocity/datos ANTES de destruir() (que zera la velocity).
|
||||
void explodeNow(Context& ctx, Enemy& enemy, uint8_t shooter_id) {
|
||||
const Vec2 ENEMY_POS = enemy.getCenter();
|
||||
const Vec2 ENEMY_VEL = enemy.getVelocityVector();
|
||||
const float BRIGHTNESS = enemy.getBrightness();
|
||||
const auto SHAPE = enemy.getShape();
|
||||
const EnemyType TYPE = enemy.getType();
|
||||
|
||||
const int POINTS = scoreForType(TYPE);
|
||||
const SDL_Color COLOR = colorForType(TYPE);
|
||||
|
||||
if (shooter_id != NO_SHOOTER) {
|
||||
ctx.score_per_player[shooter_id] += POINTS;
|
||||
}
|
||||
ctx.floating_score_manager.crear(POINTS, ENEMY_POS);
|
||||
|
||||
enemy.destruir();
|
||||
|
||||
constexpr float VELOCITAT_EXPLOSIO = 80.0F; // px/s (explosión suave)
|
||||
const Vec2 INHERITED_VEL = ENEMY_VEL * Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE;
|
||||
ctx.debris_manager.explode(
|
||||
SHAPE,
|
||||
ENEMY_POS,
|
||||
0.0F, // angle (rotación interna del enemy)
|
||||
1.0F, // escala
|
||||
VELOCITAT_EXPLOSIO,
|
||||
BRIGHTNESS,
|
||||
INHERITED_VEL,
|
||||
0.0F, // sense herència angular: evita que els 5 trossos curvin en bloc
|
||||
0.0F, // sin herencia visual
|
||||
Defaults::Sound::EXPLOSION,
|
||||
COLOR,
|
||||
Defaults::Physics::Debris::ENEMY_LIFETIME,
|
||||
Defaults::Physics::Debris::ENEMY_FRICTION,
|
||||
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER);
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
void detectBulletEnemy(Context& ctx) {
|
||||
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER;
|
||||
constexpr float VELOCITAT_EXPLOSIO = 80.0F; // px/s (explosión suau)
|
||||
|
||||
for (auto& bullet : ctx.bullets) {
|
||||
for (auto& enemy : ctx.enemies) {
|
||||
@@ -21,57 +89,66 @@ namespace Systems::Collision {
|
||||
}
|
||||
|
||||
// *** COLISIÓN bullet → enemy ***
|
||||
const Vec2& enemy_pos = enemy.getCenter();
|
||||
// Empuje físico cuasi-realista: el impulse és el moment de la bala
|
||||
// (m·v) multiplicat pel factor de transferència. Direcció = vector
|
||||
// velocity de la bala (cap a on viatjava).
|
||||
const Vec2 IMPULSE = bullet.getBody().velocity *
|
||||
(bullet.getBody().mass * Defaults::Physics::Bullet::IMPACT_MOMENTUM_FACTOR);
|
||||
enemy.applyImpulse(IMPULSE);
|
||||
|
||||
// 1. Puntos según tipo
|
||||
int points = 0;
|
||||
switch (enemy.getType()) {
|
||||
case EnemyType::PENTAGON:
|
||||
points = Defaults::Enemies::Scoring::PENTAGON_SCORE;
|
||||
break;
|
||||
case EnemyType::QUADRAT:
|
||||
points = Defaults::Enemies::Scoring::QUADRAT_SCORE;
|
||||
break;
|
||||
case EnemyType::MOLINILLO:
|
||||
points = Defaults::Enemies::Scoring::MOLINILLO_SCORE;
|
||||
break;
|
||||
const uint8_t SHOOTER = bullet.getOwnerId();
|
||||
|
||||
if (enemy.isWounded()) {
|
||||
// Segundo impacto sobre enemy ya herido → muerte instantánea,
|
||||
// puntos al nuevo shooter.
|
||||
explodeNow(ctx, enemy, SHOOTER);
|
||||
} else {
|
||||
// Primer impacto → entra en estado herido (explosión diferida).
|
||||
enemy.herir(SHOOTER);
|
||||
}
|
||||
|
||||
uint8_t owner_id = bullet.getOwnerId();
|
||||
ctx.score_per_player[owner_id] += points;
|
||||
ctx.floating_score_manager.crear(points, enemy_pos);
|
||||
|
||||
// 2. Destruir enemy + crear explosión (debris hereda color del enemy)
|
||||
SDL_Color enemy_color{};
|
||||
switch (enemy.getType()) {
|
||||
case EnemyType::PENTAGON:
|
||||
enemy_color = Defaults::Palette::PENTAGON;
|
||||
break;
|
||||
case EnemyType::QUADRAT:
|
||||
enemy_color = Defaults::Palette::QUADRAT;
|
||||
break;
|
||||
case EnemyType::MOLINILLO:
|
||||
enemy_color = Defaults::Palette::MOLINILLO;
|
||||
break;
|
||||
}
|
||||
enemy.destruir();
|
||||
Vec2 vel_enemic = enemy.getVelocityVector();
|
||||
ctx.debris_manager.explode(
|
||||
enemy.getShape(),
|
||||
enemy_pos,
|
||||
0.0F, // angle (la rotación es interna del enemy)
|
||||
1.0F, // escala
|
||||
VELOCITAT_EXPLOSIO,
|
||||
enemy.getBrightness(),
|
||||
vel_enemic,
|
||||
enemy.getRotationDelta(),
|
||||
0.0F, // sin herencia visual
|
||||
Defaults::Sound::EXPLOSION,
|
||||
enemy_color);
|
||||
|
||||
// 3. Desactivar bullet (solo destruye 1 enemy)
|
||||
bullet.desactivar();
|
||||
break;
|
||||
break; // Una bala impacta a un enemy y muere
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void processWoundedDeaths(Context& ctx) {
|
||||
for (auto& enemy : ctx.enemies) {
|
||||
if (!enemy.woundExpiredThisFrame()) {
|
||||
continue;
|
||||
}
|
||||
enemy.consumeWoundExpired();
|
||||
explodeNow(ctx, enemy, enemy.getLastHitBy());
|
||||
}
|
||||
}
|
||||
|
||||
void detectWoundedChain(Context& ctx) {
|
||||
const std::size_t N = ctx.enemies.size();
|
||||
for (std::size_t i = 0; i < N; i++) {
|
||||
Enemy& a = ctx.enemies[i];
|
||||
if (!a.isCollidable()) {
|
||||
continue;
|
||||
}
|
||||
for (std::size_t j = i + 1; j < N; j++) {
|
||||
Enemy& b = ctx.enemies[j];
|
||||
if (!b.isCollidable()) {
|
||||
continue;
|
||||
}
|
||||
const bool A_WOUNDED = a.isWounded();
|
||||
const bool B_WOUNDED = b.isWounded();
|
||||
if (A_WOUNDED == B_WOUNDED) {
|
||||
continue; // ambos sanos o ambos heridos: nada que propagar
|
||||
}
|
||||
if (!Physics::checkCollision(a, b, 1.0F)) {
|
||||
continue;
|
||||
}
|
||||
// El sano queda herido, propagando el shooter original.
|
||||
if (A_WOUNDED) {
|
||||
b.herir(a.getLastHitBy());
|
||||
} else {
|
||||
a.herir(b.getLastHitBy());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,7 +224,9 @@ namespace Systems::Collision {
|
||||
}
|
||||
|
||||
void detectAll(Context& ctx) {
|
||||
processWoundedDeaths(ctx); // expiran ANTES de ser tocadas por bala este frame
|
||||
detectBulletEnemy(ctx);
|
||||
detectWoundedChain(ctx); // un herit pot ferir a un sa al fregar-lo
|
||||
detectShipEnemy(ctx);
|
||||
detectBulletPlayer(ctx);
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
|
||||
namespace Systems::Collision {
|
||||
|
||||
// Todo lo que las detecciones necesitan leer/modificar. Vive en GameScene;
|
||||
// se le pasa por referencia (no copia, no ownership).
|
||||
struct Context {
|
||||
// Todo lo que las detecciones necesitan leer/modificar. Vive en GameScene;
|
||||
// se le pasa por referencia (no copia, no ownership).
|
||||
struct Context {
|
||||
std::array<Ship, 2>& ships;
|
||||
std::array<Enemy, Defaults::Entities::MAX_ORNIS>& enemies;
|
||||
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BALES) * 2>& bullets;
|
||||
@@ -40,24 +40,32 @@ struct Context {
|
||||
const GameConfig::MatchConfig& match_config;
|
||||
// Trigger de muerte del jugador (GameScene::tocado).
|
||||
std::function<void(uint8_t /*player_id*/)> on_player_hit;
|
||||
};
|
||||
};
|
||||
|
||||
// Detecta colisiones bullet → enemy. Si hit:
|
||||
// - destruye el enemy (radius=0 en physics body)
|
||||
// - crea debris + floating score
|
||||
// - desactiva la bullet
|
||||
// - suma puntos al shooter
|
||||
void detectBulletEnemy(Context& ctx);
|
||||
// Detecta colisiones bullet → enemy. Si hit:
|
||||
// - Primer impacto: aplica impulse, marca al enemy como "herido", desactiva bullet.
|
||||
// - Segundo impacto (enemy ya herido): explosión inmediata + puntos al shooter.
|
||||
void detectBulletEnemy(Context& ctx);
|
||||
|
||||
// Detecta colisiones ship → enemy. Si hit, llama on_player_hit(player_id).
|
||||
void detectShipEnemy(Context& ctx);
|
||||
// Procesa enemigos cuyo wound timer ha expirado este frame: explosión + puntos
|
||||
// al `last_hit_by_` del enemy (si está set).
|
||||
void processWoundedDeaths(Context& ctx);
|
||||
|
||||
// Detecta colisiones bullet → player (friendly fire / self-hit).
|
||||
// Self-hit: el shooter pierde 1 vida. Teammate-hit: la víctima pierde 1, el
|
||||
// atacante gana 1. En ambos casos, llama on_player_hit y desactiva bullet.
|
||||
void detectBulletPlayer(Context& ctx);
|
||||
// Si un enemy herido colisiona con uno sano (ni herido ni invulnerable),
|
||||
// el sano también queda herido (efecto cadena). Propaga `last_hit_by_` para
|
||||
// que el shooter original siga acreditándose la muerte en cascada. El rebote
|
||||
// físico ya lo resuelve PhysicsWorld; aquí solo propagamos el estado.
|
||||
void detectWoundedChain(Context& ctx);
|
||||
|
||||
// Las tres en orden lógico del frame.
|
||||
void detectAll(Context& ctx);
|
||||
// Detecta colisiones ship → enemy. Si hit, llama on_player_hit(player_id).
|
||||
void detectShipEnemy(Context& ctx);
|
||||
|
||||
// Detecta colisiones bullet → player (friendly fire / self-hit).
|
||||
// Self-hit: el shooter pierde 1 vida. Teammate-hit: la víctima pierde 1, el
|
||||
// atacante gana 1. En ambos casos, llama on_player_hit y desactiva bullet.
|
||||
void detectBulletPlayer(Context& ctx);
|
||||
|
||||
// Las tres en orden lógico del frame.
|
||||
void detectAll(Context& ctx);
|
||||
|
||||
} // namespace Systems::Collision
|
||||
|
||||
@@ -13,87 +13,88 @@
|
||||
|
||||
namespace Systems::InitHud {
|
||||
|
||||
auto computeRangeProgress(float global_progress,
|
||||
float ratio_init,
|
||||
float ratio_end) -> float {
|
||||
if (ratio_init >= ratio_end) {
|
||||
return (global_progress >= ratio_end) ? 1.0F : 0.0F;
|
||||
}
|
||||
if (global_progress < ratio_init) {
|
||||
return 0.0F;
|
||||
}
|
||||
if (global_progress > ratio_end) {
|
||||
return 1.0F;
|
||||
}
|
||||
return (global_progress - ratio_init) / (ratio_end - ratio_init);
|
||||
}
|
||||
|
||||
auto computeShipPosition(float progress, const Vec2& final_position) -> Vec2 {
|
||||
const float EASED = Easing::easeOutQuad(progress);
|
||||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||
// Y inicial: 50 px bajo la zona de juego.
|
||||
const float Y_INI = zone.y + zone.h + 50.0F;
|
||||
const float Y_ANIM = Y_INI + ((final_position.y - Y_INI) * EASED);
|
||||
return Vec2{.x = final_position.x, .y = Y_ANIM};
|
||||
}
|
||||
|
||||
void drawBordersAnimated(Rendering::Renderer* renderer, float progress) {
|
||||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||
const float EASED = Easing::easeOutQuad(progress);
|
||||
|
||||
const int X1 = static_cast<int>(zone.x);
|
||||
const int Y1 = static_cast<int>(zone.y);
|
||||
const int X2 = static_cast<int>(zone.x + zone.w);
|
||||
const int Y2 = static_cast<int>(zone.y + zone.h);
|
||||
const int CX = (X1 + X2) / 2;
|
||||
|
||||
constexpr float PHASE_1_END = 0.33F;
|
||||
constexpr float PHASE_2_END = 0.66F;
|
||||
|
||||
// Fase 1: línea superior crece desde el centro hacia los lados.
|
||||
if (EASED > 0.0F) {
|
||||
const float P = std::min(EASED / PHASE_1_END, 1.0F);
|
||||
const int X_LEFT = static_cast<int>(CX - ((CX - X1) * P));
|
||||
const int X_RIGHT = static_cast<int>(CX + ((X2 - CX) * P));
|
||||
Rendering::linea(renderer, CX, Y1, X_LEFT, Y1);
|
||||
Rendering::linea(renderer, CX, Y1, X_RIGHT, Y1);
|
||||
auto computeRangeProgress(float global_progress,
|
||||
float ratio_init,
|
||||
float ratio_end) -> float {
|
||||
if (ratio_init >= ratio_end) {
|
||||
return (global_progress >= ratio_end) ? 1.0F : 0.0F;
|
||||
}
|
||||
if (global_progress < ratio_init) {
|
||||
return 0.0F;
|
||||
}
|
||||
if (global_progress > ratio_end) {
|
||||
return 1.0F;
|
||||
}
|
||||
return (global_progress - ratio_init) / (ratio_end - ratio_init);
|
||||
}
|
||||
|
||||
// Fase 2: laterales bajan.
|
||||
if (EASED > PHASE_1_END) {
|
||||
const float P = std::min((EASED - PHASE_1_END) / (PHASE_2_END - PHASE_1_END), 1.0F);
|
||||
const int Y_BOTTOM = static_cast<int>(Y1 + ((Y2 - Y1) * P));
|
||||
Rendering::linea(renderer, X1, Y1, X1, Y_BOTTOM);
|
||||
Rendering::linea(renderer, X2, Y1, X2, Y_BOTTOM);
|
||||
auto computeShipPosition(float progress, const Vec2& final_position) -> Vec2 {
|
||||
const float EASED = Easing::easeOutQuad(progress);
|
||||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||
// Y inicial: bajo la zona de juego (sale desde fuera).
|
||||
const float Y_INI = zone.y + zone.h + Defaults::Hud::InitAnim::SHIP_SPAWN_Y_OFFSET;
|
||||
const float Y_ANIM = Y_INI + ((final_position.y - Y_INI) * EASED);
|
||||
return Vec2{.x = final_position.x, .y = Y_ANIM};
|
||||
}
|
||||
|
||||
// Fase 3: línea inferior crece desde los lados hacia el centro.
|
||||
if (EASED > PHASE_2_END) {
|
||||
const float P = (EASED - PHASE_2_END) / (1.0F - PHASE_2_END);
|
||||
const int X_LEFT = static_cast<int>(X1 + ((CX - X1) * P));
|
||||
const int X_RIGHT = static_cast<int>(X2 - ((X2 - CX) * P));
|
||||
Rendering::linea(renderer, X1, Y2, X_LEFT, Y2);
|
||||
Rendering::linea(renderer, X2, Y2, X_RIGHT, Y2);
|
||||
void drawBordersAnimated(Rendering::Renderer* renderer, float progress) {
|
||||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||
const float EASED = Easing::easeOutQuad(progress);
|
||||
|
||||
const int X1 = static_cast<int>(zone.x);
|
||||
const int Y1 = static_cast<int>(zone.y);
|
||||
const int X2 = static_cast<int>(zone.x + zone.w);
|
||||
const int Y2 = static_cast<int>(zone.y + zone.h);
|
||||
const int CX = (X1 + X2) / 2;
|
||||
|
||||
constexpr float PHASE_1_END = Defaults::Hud::InitAnim::BORDER_PHASE_1_END;
|
||||
constexpr float PHASE_2_END = Defaults::Hud::InitAnim::BORDER_PHASE_2_END;
|
||||
|
||||
// Fase 1: línea superior crece desde el centro hacia los lados.
|
||||
if (EASED > 0.0F) {
|
||||
const float P = std::min(EASED / PHASE_1_END, 1.0F);
|
||||
const int X_LEFT = static_cast<int>(CX - ((CX - X1) * P));
|
||||
const int X_RIGHT = static_cast<int>(CX + ((X2 - CX) * P));
|
||||
Rendering::linea(renderer, CX, Y1, X_LEFT, Y1);
|
||||
Rendering::linea(renderer, CX, Y1, X_RIGHT, Y1);
|
||||
}
|
||||
|
||||
// Fase 2: laterales bajan.
|
||||
if (EASED > PHASE_1_END) {
|
||||
const float P = std::min((EASED - PHASE_1_END) / (PHASE_2_END - PHASE_1_END), 1.0F);
|
||||
const int Y_BOTTOM = static_cast<int>(Y1 + ((Y2 - Y1) * P));
|
||||
Rendering::linea(renderer, X1, Y1, X1, Y_BOTTOM);
|
||||
Rendering::linea(renderer, X2, Y1, X2, Y_BOTTOM);
|
||||
}
|
||||
|
||||
// Fase 3: línea inferior crece desde los lados hacia el centro.
|
||||
if (EASED > PHASE_2_END) {
|
||||
const float P = (EASED - PHASE_2_END) / (1.0F - PHASE_2_END);
|
||||
const int X_LEFT = static_cast<int>(X1 + ((CX - X1) * P));
|
||||
const int X_RIGHT = static_cast<int>(X2 - ((X2 - CX) * P));
|
||||
Rendering::linea(renderer, X1, Y2, X_LEFT, Y2);
|
||||
Rendering::linea(renderer, X2, Y2, X_RIGHT, Y2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void drawScoreboardAnimated(const Graphics::VectorText& text,
|
||||
const std::string& scoreboard_text,
|
||||
float progress) {
|
||||
const float EASED = Easing::easeOutQuad(progress);
|
||||
void drawScoreboardAnimated(const Graphics::VectorText& text,
|
||||
const std::string& scoreboard_text,
|
||||
float progress) {
|
||||
const float EASED = Easing::easeOutQuad(progress);
|
||||
|
||||
constexpr float SCALE = 0.85F;
|
||||
constexpr float SPACING = 0.0F;
|
||||
const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD;
|
||||
const float CENTRE_X = scoreboard_zone.w / 2.0F;
|
||||
const float Y_FINAL = scoreboard_zone.y + (scoreboard_zone.h / 2.0F);
|
||||
// Posición inicial: fuera de la pantalla por debajo.
|
||||
const auto Y_INI = static_cast<float>(Defaults::Game::HEIGHT);
|
||||
const float Y_ANIM = Y_INI + ((Y_FINAL - Y_INI) * EASED);
|
||||
constexpr float SCALE = Defaults::Hud::SCOREBOARD_TEXT_SCALE;
|
||||
constexpr float SPACING = Defaults::Hud::SCOREBOARD_TEXT_SPACING;
|
||||
const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD;
|
||||
const float CENTRE_X = scoreboard_zone.w / 2.0F;
|
||||
const float Y_FINAL = scoreboard_zone.y + (scoreboard_zone.h / 2.0F);
|
||||
// Posición inicial: fuera de la pantalla por debajo.
|
||||
const auto Y_INI = static_cast<float>(Defaults::Game::HEIGHT);
|
||||
const float Y_ANIM = Y_INI + ((Y_FINAL - Y_INI) * EASED);
|
||||
|
||||
text.renderCentered(scoreboard_text,
|
||||
Vec2{.x = CENTRE_X, .y = Y_ANIM},
|
||||
SCALE, SPACING);
|
||||
}
|
||||
text.renderCentered(scoreboard_text,
|
||||
Vec2{.x = CENTRE_X, .y = Y_ANIM},
|
||||
SCALE,
|
||||
SPACING);
|
||||
}
|
||||
|
||||
} // namespace Systems::InitHud
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# compile_spirv.cmake
|
||||
# Compila shaders GLSL a SPIR-V i genera headers C++ embedibles.
|
||||
# Multiplataforma: Windows, macOS, Linux (no requereix bash ni xxd).
|
||||
#
|
||||
# Invocat per CMakeLists.txt amb:
|
||||
# cmake -D GLSLC=<path> -D SHADERS_DIR=<path> -D HEADERS_DIR=<path> -P compile_spirv.cmake
|
||||
#
|
||||
# També es pot executar manualment des de l'arrel del projecte:
|
||||
# cmake -D GLSLC=glslc -D SHADERS_DIR=shaders \
|
||||
# -D HEADERS_DIR=source/core/rendering/gpu/spv \
|
||||
# -P tools/shaders/compile_spirv.cmake
|
||||
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
cmake_policy(SET CMP0007 NEW)
|
||||
|
||||
# Llista de shaders a compilar: font relativa a SHADERS_DIR
|
||||
set(SHADER_SOURCES
|
||||
"line.vert.glsl"
|
||||
"line.frag.glsl"
|
||||
"postfx.vert.glsl"
|
||||
"postfx.frag.glsl"
|
||||
)
|
||||
|
||||
# Nom de la variable C++ per a cada shader (mateix ordre).
|
||||
# UPPER_CASE perquè són constexpr globals (.clang-tidy ho exigeix).
|
||||
set(SHADER_VARS
|
||||
"LINE_VERT_SPV"
|
||||
"LINE_FRAG_SPV"
|
||||
"POSTFX_VERT_SPV"
|
||||
"POSTFX_FRAG_SPV"
|
||||
)
|
||||
|
||||
# Flags extra per a cada shader (necessaris perquè .vert.glsl/.frag.glsl no s'infereixen)
|
||||
set(SHADER_FLAGS
|
||||
"-fshader-stage=vert"
|
||||
"-fshader-stage=frag"
|
||||
"-fshader-stage=vert"
|
||||
"-fshader-stage=frag"
|
||||
)
|
||||
|
||||
list(LENGTH SHADER_SOURCES NUM_SHADERS)
|
||||
math(EXPR LAST_IDX "${NUM_SHADERS} - 1")
|
||||
|
||||
foreach(IDX RANGE ${LAST_IDX})
|
||||
list(GET SHADER_SOURCES ${IDX} SRC_NAME)
|
||||
list(GET SHADER_VARS ${IDX} VAR)
|
||||
list(GET SHADER_FLAGS ${IDX} EXTRA_FLAG)
|
||||
|
||||
# Derivem el nom del header a partir de la variable: LINE_VERT_SPV → line_vert_spv.h
|
||||
string(TOLOWER "${VAR}" HDR_BASE)
|
||||
set(SRC "${SHADERS_DIR}/${SRC_NAME}")
|
||||
set(SPV "${HEADERS_DIR}/${HDR_BASE}.spv")
|
||||
set(HDR "${HEADERS_DIR}/${HDR_BASE}.h")
|
||||
|
||||
message(STATUS "Compilant ${SRC} ...")
|
||||
|
||||
if(EXTRA_FLAG)
|
||||
execute_process(
|
||||
COMMAND "${GLSLC}" "${EXTRA_FLAG}" -O "${SRC}" -o "${SPV}"
|
||||
RESULT_VARIABLE GLSLC_RESULT
|
||||
ERROR_VARIABLE GLSLC_ERROR
|
||||
)
|
||||
else()
|
||||
execute_process(
|
||||
COMMAND "${GLSLC}" -O "${SRC}" -o "${SPV}"
|
||||
RESULT_VARIABLE GLSLC_RESULT
|
||||
ERROR_VARIABLE GLSLC_ERROR
|
||||
)
|
||||
endif()
|
||||
|
||||
if(NOT GLSLC_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "glslc ha fallat per a ${SRC}:\n${GLSLC_ERROR}")
|
||||
endif()
|
||||
|
||||
# Llegim el binari SPV com a hex (sense separadors) i el dividim en bytes.
|
||||
file(READ "${SPV}" HEX_DATA HEX)
|
||||
string(REGEX MATCHALL ".." BYTES "${HEX_DATA}")
|
||||
list(LENGTH BYTES NUM_BYTES)
|
||||
|
||||
set(ARRAY_BODY "")
|
||||
foreach(BYTE ${BYTES})
|
||||
string(APPEND ARRAY_BODY " 0x${BYTE},\n")
|
||||
endforeach()
|
||||
|
||||
file(WRITE "${HDR}"
|
||||
"#pragma once\n"
|
||||
"#include <cstddef>\n"
|
||||
"#include <cstdint>\n"
|
||||
"static const uint8_t ${VAR}[] = {\n"
|
||||
"${ARRAY_BODY}"
|
||||
"};\n"
|
||||
"static const size_t ${VAR}_SIZE = ${NUM_BYTES};\n"
|
||||
)
|
||||
|
||||
file(REMOVE "${SPV}")
|
||||
message(STATUS " -> ${HDR} (${NUM_BYTES} bytes)")
|
||||
endforeach()
|
||||
|
||||
message(STATUS "Shaders SPIR-V compilats correctament.")
|
||||
Reference in New Issue
Block a user