feat(render): sombra com a sprite 16x16 compartit
This commit is contained in:
1 parent
6a70d6de9f
commit
8bdc994b75
9 files changed
+83
-34
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 98 B |
@@ -0,0 +1,43 @@
|
||||
#include "core/rendering/effects/shadow.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#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<int>((1.0F - CLAMPED) * static_cast<float>(FRAME_COUNT));
|
||||
frame_index = std::clamp(frame_index, 0, FRAME_COUNT - 1);
|
||||
|
||||
SDL_FRect clip{
|
||||
static_cast<float>(frame_index * FRAME_SIZE),
|
||||
0.0F,
|
||||
static_cast<float>(FRAME_SIZE),
|
||||
static_cast<float>(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
|
||||
@@ -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
|
||||
@@ -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`.
|
||||
|
||||
@@ -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<float>(); }
|
||||
if (j.contains("gravity")) { jump_config.gravity_px_per_s2 = j["gravity"].get_value<float>(); }
|
||||
if (j.contains("landing_stun_ms")) { jump_config.landing_stun_ms = j["landing_stun_ms"].get_value<int>(); }
|
||||
if (j.contains("shadow_y_offset")) { jump_config.shadow_y_offset = j["shadow_y_offset"].get_value<int>(); }
|
||||
// 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<int>(static_cast<float>(base_size) * SHRINK);
|
||||
const float CX = getPosX() + (static_cast<float>(sprite_w) * 0.5F);
|
||||
const float GROUND_Y = getGroundFeetY();
|
||||
const auto SX = static_cast<int>(CX - (static_cast<float>(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<int>(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<int>(getPosX() + (static_cast<float>(sprite_w) * 0.5F));
|
||||
const auto FEET_Y = static_cast<int>(getGroundFeetY());
|
||||
Effects::Shadow::render(CENTER_X, FEET_Y, HEIGHT_FACTOR);
|
||||
}
|
||||
|
||||
// Sufix d'animació per cardinal — empat 1:1 amb l'enum `Facing` i el
|
||||
|
||||
@@ -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).
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user