From 8bdc994b75a52f4f0b2efdd0ab19e2f42f5e4257 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 11 May 2026 17:29:05 +0200 Subject: [PATCH] feat(render): sombra com a sprite 16x16 compartit --- CMakeLists.txt | 1 + data/entities/player/morcus.yaml | 3 -- data/entities/player/sam.yaml | 3 -- data/entities/shadow/shadow.gif | Bin 0 -> 98 bytes source/core/rendering/effects/shadow.cpp | 43 +++++++++++++++++++++++ source/core/rendering/effects/shadow.hpp | 25 +++++++++++++ source/core/system/director.cpp | 6 ++++ source/game/entities/player.cpp | 35 +++++------------- source/game/entities/player.hpp | 1 - 9 files changed, 83 insertions(+), 34 deletions(-) create mode 100644 data/entities/shadow/shadow.gif create mode 100644 source/core/rendering/effects/shadow.cpp create mode 100644 source/core/rendering/effects/shadow.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 98da76f..77dada9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,6 +94,7 @@ set(APP_SOURCES source/core/rendering/sprite/animated_sprite_parser.cpp source/core/rendering/effects/heat_haze.cpp source/core/rendering/effects/scan_reveal.cpp + source/core/rendering/effects/shadow.cpp source/core/rendering/transitions/mosaic.cpp source/core/rendering/transitions/fade_to_black.cpp source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp diff --git a/data/entities/player/morcus.yaml b/data/entities/player/morcus.yaml index d9e2b66..2e64079 100644 --- a/data/entities/player/morcus.yaml +++ b/data/entities/player/morcus.yaml @@ -97,13 +97,10 @@ turn: south_to_north: west # Salt vertical (px de z, no toca la posició al món). -# `shadow_y_offset`: puja la sombra per compensar la franja -# transparent sota els peus del sprite. jump: impulse: 200 gravity: 600 landing_stun_ms: 400 - shadow_y_offset: 3 sound: jump.wav land_sound: land.wav diff --git a/data/entities/player/sam.yaml b/data/entities/player/sam.yaml index a1c20b0..3644177 100644 --- a/data/entities/player/sam.yaml +++ b/data/entities/player/sam.yaml @@ -97,13 +97,10 @@ turn: south_to_north: west # Salt vertical (px de z, no toca la posició al món). -# `shadow_y_offset`: puja la sombra per compensar la franja -# transparent sota els peus del sprite. jump: impulse: 200 gravity: 600 landing_stun_ms: 400 - shadow_y_offset: 3 sound: jump.wav land_sound: land.wav diff --git a/data/entities/shadow/shadow.gif b/data/entities/shadow/shadow.gif new file mode 100644 index 0000000000000000000000000000000000000000..905ac95fb3fd7b8020787f5645aa1a025c57f4ca GIT binary patch literal 98 zcmZ?wbh9u|G++>5XkY*VIWc*~|J;7AA;Hd$0j@@R2F#2=QN^DuoLoR!2c!g~n1M;H vrGMq=xBQFeY`N9F`QD!2{B4gs)DCtUy_lu+Mta5CKYAZxp8Z-U!e9*mbIKoR literal 0 HcmV?d00001 diff --git a/source/core/rendering/effects/shadow.cpp b/source/core/rendering/effects/shadow.cpp new file mode 100644 index 0000000..242b8ce --- /dev/null +++ b/source/core/rendering/effects/shadow.cpp @@ -0,0 +1,43 @@ +#include "core/rendering/effects/shadow.hpp" + +#include + +#include + +#include "core/rendering/surface.hpp" +#include "core/resources/resource_cache.hpp" + +namespace Effects::Shadow { + + void render(int center_x, int feet_y, float height_factor) { + // Lookup únic al primer ús: la Surface viu a Cache durant tota la + // sessió, així evitem la cerca lineal a cada frame. + static const auto SURFACE = Resource::Cache::get().getSurface(CACHE_KEY); + + const int FRAME_COUNT = SURFACE->getWidth() / FRAME_SIZE; + if (FRAME_COUNT <= 0) { return; } + + // Mapping a bandes uniformes: factor 0 → últim (gran), factor 1 → + // primer (petit). Per a 3 fotogrames: [0, 1/3) → 2, [1/3, 2/3) → 1, + // [2/3, 1] → 0. Generalitza a qualsevol nombre de fotogrames per + // si afegim mides intermèdies. + const float CLAMPED = std::clamp(height_factor, 0.0F, 1.0F); + int frame_index = static_cast((1.0F - CLAMPED) * static_cast(FRAME_COUNT)); + frame_index = std::clamp(frame_index, 0, FRAME_COUNT - 1); + + SDL_FRect clip{ + static_cast(frame_index * FRAME_SIZE), + 0.0F, + static_cast(FRAME_SIZE), + static_cast(FRAME_SIZE), + }; + + // Ancoratge: vora inferior del fotograma a `feet_y` — així el + // quadrat 16x16 del sprite encaixa exactament al 16x16 inferior + // d'una entitat 16x16/16x32 centrada en `center_x`. + const int X = center_x - (FRAME_SIZE / 2); + const int Y = feet_y - FRAME_SIZE; + SURFACE->render(X, Y, &clip); + } + +} // namespace Effects::Shadow diff --git a/source/core/rendering/effects/shadow.hpp b/source/core/rendering/effects/shadow.hpp new file mode 100644 index 0000000..33df742 --- /dev/null +++ b/source/core/rendering/effects/shadow.hpp @@ -0,0 +1,25 @@ +#pragma once + +namespace Effects::Shadow { + + // Mida de cada fotograma del sprite compartit de la sombra. + // El sprite és una tira horitzontal de fotogrames de 16x16, ordenats + // del més petit (índex 0) al més gran (últim índex). + constexpr int FRAME_SIZE = 16; + + // Clau amb què `Resource::Cache` té carregada la Surface compartida. + // El precarrega el Director al boot. + constexpr const char* CACHE_KEY = "shadow"; + + // Pinta la sombra al canvas. `height_factor` selecciona el fotograma: + // 0.0 (entitat al terra) → fotograma més gran, 1.0 (pic del salt o + // entitat més alta del terra) → fotograma més petit. La tira es + // divideix en bandes uniformes; valors fora de [0, 1] es clampen. + // + // El fotograma s'ancora al quadrat 16x16 inferior d'una entitat de + // 16x16/16x32 centrada en `center_x` amb peus a `feet_y`: la vora + // inferior del sprite queda exactament a `feet_y`. La posició + // visual de l'oval dins del fotograma la decideix l'artista al GIF. + void render(int center_x, int feet_y, float height_factor); + +} // namespace Effects::Shadow diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 53209a0..b7e46a8 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -18,6 +18,7 @@ #include "core/input/key_config.hpp" #include "core/input/mouse.hpp" #include "core/locale/locale.hpp" +#include "core/rendering/effects/shadow.hpp" #include "core/rendering/notifier.hpp" #include "core/rendering/render_info.hpp" #include "core/rendering/screen.hpp" @@ -162,6 +163,11 @@ Director::Director() { Resource::Cache::get().loadSurface(ROOM_TEXT_FONT_GIF_KEY, UiConfig::get().roomTextFont().gif); Resource::Cache::get().getSurface(ROOM_TEXT_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui")); + // Sprite compartit de la sombra (tira horitzontal de fotogrames de + // 16x16, de petit a gran). El consumeix `Effects::Shadow::render`, + // que el reaprofiten Player i qualsevol entitat futura compatible. + Resource::Cache::get().loadSurface(Effects::Shadow::CACHE_KEY, "data/entities/shadow/shadow.gif"); + // Pistes de música del cicle de sala (intro/playing/outro). Es preloden // ací perquè formen part del preload d'assets, com la font i la paleta; // la lògica de quina sona en cada estat viu a `GameScene`. diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index d20d0b2..6924bc2 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -13,6 +13,7 @@ #include "core/input/gamepad_manager.hpp" #include "core/input/input_state.hpp" #include "core/rendering/canvas.hpp" +#include "core/rendering/effects/shadow.hpp" #include "core/rendering/screen.hpp" #include "core/rendering/surface.hpp" #include "core/resources/palette_loader.hpp" @@ -179,7 +180,6 @@ void Player::readJumpBlock(const fkyaml::node& yaml) { if (j.contains("impulse")) { jump_config.impulse_px_per_s = j["impulse"].get_value(); } if (j.contains("gravity")) { jump_config.gravity_px_per_s2 = j["gravity"].get_value(); } if (j.contains("landing_stun_ms")) { jump_config.landing_stun_ms = j["landing_stun_ms"].get_value(); } - if (j.contains("shadow_y_offset")) { jump_config.shadow_y_offset = j["shadow_y_offset"].get_value(); } // Sons opcionals: el nom del fitxer (relatiu a Defaults::Paths::AUDIO_SOUNDS_DIR) // val tant de clau al Resource::Cache com de basename per a la càrrega. // Si el camp manca, la clau queda buida i `playSound` no es crida. @@ -1435,36 +1435,17 @@ void Player::renderShadow() const { // que `enterDying` netegi el flag. if (falling_through_hole_) { return; } - // Sombra centrada al sprite, recolzada a la línia dels peus reals - // (ignorant `jump_z_`). L'amplada es redueix proporcional a l'altura - // del salt — al pic queda més estreta, donant la sensació de - // "elevació" sense necessitat de coordenades 3D reals. Si el - // jump_config té un divisor zero (cas degenerat), només dibuixem - // la sombra a mida plena per evitar nan. + // `height_factor`: 0 al terra → fotograma més gran, 1 al pic → més + // petit. Si el `jump_config` té un divisor zero (cas degenerat) caem + // a 0 per no propagar nan al renderer. const auto PEAK = (jump_config.gravity_px_per_s2 > 0.0F) ? (jump_config.impulse_px_per_s * jump_config.impulse_px_per_s) / (2.0F * jump_config.gravity_px_per_s2) : 0.0F; - const float NORM_Z = (PEAK > 0.0F) ? std::clamp(jump_z_ / PEAK, 0.0F, 1.0F) : 0.0F; - constexpr float MAX_SHRINK = 0.4F; // Fins a un 40% més estreta al pic. - const float SHRINK = 1.0F - (NORM_Z * MAX_SHRINK); + const float HEIGHT_FACTOR = (PEAK > 0.0F) ? std::clamp(jump_z_ / PEAK, 0.0F, 1.0F) : 0.0F; - constexpr int SHADOW_H = 3; - const auto SHADOW_W = static_cast(static_cast(base_size) * SHRINK); - const float CX = getPosX() + (static_cast(sprite_w) * 0.5F); - const float GROUND_Y = getGroundFeetY(); - const auto SX = static_cast(CX - (static_cast(SHADOW_W) * 0.5F)); - // Pugem la sombra `shadow_y_offset` px per compensar la franja - // transparent que el sprite del jugador té sota els peus reals - // (perquè la silueta camine pel mig del passadís en falsa - // perspectiva). Sense aquest ajust, la sombra apareix per sota - // del passadís. - const auto SY = static_cast(GROUND_Y) - SHADOW_H - jump_config.shadow_y_offset; - - // ARGB negre amb alpha ~40%. `blendRect` mescla sobre el canvas; així - // la sombra fosqueja el sòl (pintat o no) sense tapar-lo del tot. - constexpr std::uint32_t SHADOW_ARGB = 0x00000000U; - constexpr std::uint8_t SHADOW_ALPHA = 100; - Screen::get().blendRect(SX, SY, SHADOW_W, SHADOW_H, SHADOW_ARGB, SHADOW_ALPHA); + const auto CENTER_X = static_cast(getPosX() + (static_cast(sprite_w) * 0.5F)); + const auto FEET_Y = static_cast(getGroundFeetY()); + Effects::Shadow::render(CENTER_X, FEET_Y, HEIGHT_FACTOR); } // Sufix d'animació per cardinal — empat 1:1 amb l'enum `Facing` i el diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index a574873..33a3ce2 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -633,7 +633,6 @@ class Player : public AnimatedSprite { float impulse_px_per_s{0.0F}; // Velocitat vertical inicial. float gravity_px_per_s2{0.0F}; // Acceleració descendent constant. int landing_stun_ms{0}; // Stun en aterrar (no es mou ni salta). - int shadow_y_offset{0}; // Px que puja la sombra (compensa transparències al peu del sprite). std::string sound_key; // Clau al Resource::Cache del so de despegue (buit = silenci). std::string land_sound_key; // Clau del so d'aterratge (buit = silenci). };