feat(anim): suport de flip H/V per frame i per animació
This commit is contained in:
1 parent
39eda56c9d
commit
1d43a77800
5 files changed
+438
-87
No files matched your search
@@ -88,6 +88,7 @@ set(APP_SOURCES
|
|||||||
source/core/rendering/sprite/sprite.cpp
|
source/core/rendering/sprite/sprite.cpp
|
||||||
source/core/rendering/sprite/moving_sprite.cpp
|
source/core/rendering/sprite/moving_sprite.cpp
|
||||||
source/core/rendering/sprite/animated_sprite.cpp
|
source/core/rendering/sprite/animated_sprite.cpp
|
||||||
|
source/core/rendering/sprite/animated_sprite_parser.cpp
|
||||||
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
|
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
|
||||||
source/core/rendering/sdl3gpu/viewport.cpp
|
source/core/rendering/sdl3gpu/viewport.cpp
|
||||||
source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp
|
source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp
|
||||||
@@ -444,4 +445,21 @@ if(BUILD_TESTS)
|
|||||||
add_test(NAME game_fsm
|
add_test(NAME game_fsm
|
||||||
COMMAND test_game_fsm
|
COMMAND test_game_fsm
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||||
|
|
||||||
|
# Smoke del parser d'animacions amb suport de flip H/V. Stand-alone:
|
||||||
|
# només enllaça `animated_sprite_parser.cpp`, que és la part del motor
|
||||||
|
# que NO depèn de Resource::Cache ni de Surface — el parser és pure
|
||||||
|
# YAML-in / dades-out, així que no cal arrossegar tot l'stack.
|
||||||
|
add_executable(test_animated_sprite_flip
|
||||||
|
tests/test_animated_sprite_flip.cpp
|
||||||
|
source/core/rendering/sprite/animated_sprite_parser.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(test_animated_sprite_flip PRIVATE
|
||||||
|
"${CMAKE_SOURCE_DIR}/source"
|
||||||
|
)
|
||||||
|
target_link_libraries(test_animated_sprite_flip PRIVATE SDL3::SDL3)
|
||||||
|
target_compile_options(test_animated_sprite_flip PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
add_test(NAME animated_sprite_flip
|
||||||
|
COMMAND test_animated_sprite_flip
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||||
endif()
|
endif()
|
||||||
@@ -7,36 +7,19 @@
|
|||||||
|
|
||||||
#include "core/rendering/surface.hpp"
|
#include "core/rendering/surface.hpp"
|
||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
|
||||||
|
// El parser pure de YAML d'animacions (parseAnimationsFromYaml + helpers)
|
||||||
|
// viu a `animated_sprite_parser.cpp` perquè és testable sense Surface ni
|
||||||
|
// Resource::Cache. Aquesta unitat es queda amb el runtime de la classe i
|
||||||
|
// amb el que sí toca el cache de surfaces (parseSpriteBlock).
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
auto convertYamlFramesToRects(const fkyaml::node& frames_node, float frame_width, float frame_height, int frames_per_row, int max_tiles) -> std::vector<SDL_FRect> {
|
|
||||||
std::vector<SDL_FRect> frames;
|
|
||||||
SDL_FRect rect = {.x = 0.0F, .y = 0.0F, .w = frame_width, .h = frame_height};
|
|
||||||
for (const auto& frame_index_node : frames_node) {
|
|
||||||
const int NUM_TILE = frame_index_node.get_value<int>();
|
|
||||||
if (NUM_TILE <= max_tiles) {
|
|
||||||
rect.x = static_cast<float>(NUM_TILE % frames_per_row) * frame_width;
|
|
||||||
rect.y = static_cast<float>(NUM_TILE / frames_per_row) * frame_height;
|
|
||||||
frames.emplace_back(rect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return frames;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SheetParams {
|
|
||||||
float frame_width{0.0F};
|
|
||||||
float frame_height{0.0F};
|
|
||||||
int frames_per_row{1};
|
|
||||||
int max_tiles{1};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Llegeix el bloc `sprite: { path, size: [w, h] }` — mateix format que
|
// Llegeix el bloc `sprite: { path, size: [w, h] }` — mateix format que
|
||||||
// les plantilles d'entitat. La surface es resol via Resource::Cache amb
|
// les plantilles d'entitat. La surface es resol via Resource::Cache amb
|
||||||
// el basename del fitxer.
|
// el basename del fitxer.
|
||||||
auto parseSpriteBlock(const fkyaml::node& yaml, std::shared_ptr<Surface>& surface) -> SheetParams {
|
auto parseSpriteBlock(const fkyaml::node& yaml, std::shared_ptr<Surface>& surface) -> AnimatedSprite::SheetParams {
|
||||||
SheetParams params;
|
AnimatedSprite::SheetParams params;
|
||||||
if (!yaml.contains("sprite")) { return params; }
|
if (!yaml.contains("sprite")) { return params; }
|
||||||
const auto& s = yaml["sprite"];
|
const auto& s = yaml["sprite"];
|
||||||
if (s.contains("path")) {
|
if (s.contains("path")) {
|
||||||
@@ -57,62 +40,18 @@ namespace {
|
|||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
// `frame_ms` pot ser un escalar (temps uniforme) o una llista (per-frame).
|
// Tradueix els flags lògics flip_h/flip_v al `SDL_FlipMode` que admet
|
||||||
// Convertim sempre a segons perquè `animate()` treballa amb delta_time
|
// `Surface::render`. Important: a SDL3, `SDL_FlipMode` és un enum
|
||||||
// en segons. Si falta o val 0, `speeds` queda buit i l'animació es
|
// exclusiu (NONE/HORIZONTAL/VERTICAL), no un bitmask combinable. Per
|
||||||
// considera estàtica (no avança).
|
// tant, V+H simultani al mateix frame no es pot expressar amb una
|
||||||
void parseFrameTimes(const fkyaml::node& anim_node, AnimatedSprite::AnimationData& animation) {
|
// única crida — prioritzem H (suficient per als casos pràctics
|
||||||
if (!anim_node.contains("frame_ms")) { return; }
|
// d'animació, com `walk_west` o `death_spin`). Si en algun moment fa
|
||||||
const auto& fm = anim_node["frame_ms"];
|
// falta combinar, caldrà ampliar `Surface::render` per a doble-flipar
|
||||||
if (fm.is_sequence()) {
|
// a baix nivell.
|
||||||
for (const auto& v : fm) {
|
auto toFlipMode(bool flip_h, bool flip_v) -> SDL_FlipMode {
|
||||||
animation.speeds.push_back(static_cast<float>(v.get_value<int>()) / 1000.0F);
|
if (flip_h) { return SDL_FLIP_HORIZONTAL; }
|
||||||
}
|
if (flip_v) { return SDL_FLIP_VERTICAL; }
|
||||||
} else {
|
return SDL_FLIP_NONE;
|
||||||
const float SEC = static_cast<float>(fm.get_value<int>()) / 1000.0F;
|
|
||||||
if (SEC > 0.0F) { animation.speeds.assign(animation.frames.size(), SEC); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ordre de prioritat per a la política de loop:
|
|
||||||
// 1) `loop_from: N` → rebota al frame N en passar del final (implica loop).
|
|
||||||
// 2) `loop: false` → animació d'un sol tret (es queda al darrer frame).
|
|
||||||
// 3) Res → loop complet des del frame 0 (default).
|
|
||||||
void parseLoopPolicy(const fkyaml::node& anim_node, AnimatedSprite::AnimationData& animation) {
|
|
||||||
if (anim_node.contains("loop_from")) {
|
|
||||||
animation.loop_from = anim_node["loop_from"].get_value<int>();
|
|
||||||
} else if (anim_node.contains("loop") && !anim_node["loop"].get_value<bool>()) {
|
|
||||||
animation.loop_from = -1;
|
|
||||||
} else {
|
|
||||||
animation.loop_from = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto parseAnimations(const fkyaml::node& yaml, const SheetParams& params) -> std::unordered_map<std::string, AnimatedSprite::AnimationData> {
|
|
||||||
std::unordered_map<std::string, AnimatedSprite::AnimationData> animations;
|
|
||||||
if (!yaml.contains("animations") || !yaml["animations"].is_mapping()) {
|
|
||||||
return animations;
|
|
||||||
}
|
|
||||||
const auto& anims_node = yaml["animations"];
|
|
||||||
for (auto it = anims_node.begin(); it != anims_node.end(); ++it) {
|
|
||||||
const auto NAME = it.key().get_value<std::string>();
|
|
||||||
const auto& anim_node = *it;
|
|
||||||
|
|
||||||
AnimatedSprite::AnimationData animation;
|
|
||||||
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
|
|
||||||
animation.frames = convertYamlFramesToRects(
|
|
||||||
anim_node["frames"],
|
|
||||||
params.frame_width,
|
|
||||||
params.frame_height,
|
|
||||||
params.frames_per_row,
|
|
||||||
params.max_tiles);
|
|
||||||
}
|
|
||||||
parseFrameTimes(anim_node, animation);
|
|
||||||
parseLoopPolicy(anim_node, animation);
|
|
||||||
|
|
||||||
animations.emplace(NAME, std::move(animation));
|
|
||||||
}
|
|
||||||
return animations;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -122,7 +61,7 @@ AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
|
|||||||
try {
|
try {
|
||||||
auto yaml = fkyaml::node::deserialize(yaml_content);
|
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||||
auto params = parseSpriteBlock(yaml, surface_);
|
auto params = parseSpriteBlock(yaml, surface_);
|
||||||
animations_ = parseAnimations(yaml, params);
|
animations_ = parseAnimationsFromYaml(yaml, params);
|
||||||
|
|
||||||
setWidth(params.frame_width);
|
setWidth(params.frame_width);
|
||||||
setHeight(params.frame_height);
|
setHeight(params.frame_height);
|
||||||
@@ -132,7 +71,7 @@ AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
|
|||||||
// abans del primer `update()`, així que el valor real el posa ell.
|
// abans del primer `update()`, així que el valor real el posa ell.
|
||||||
if (!animations_.empty()) {
|
if (!animations_.empty()) {
|
||||||
const auto& first = animations_.begin()->second;
|
const auto& first = animations_.begin()->second;
|
||||||
if (!first.frames.empty()) { setClip(first.frames[0]); }
|
if (!first.frames.empty()) { setClip(first.frames[0].rect); }
|
||||||
}
|
}
|
||||||
} catch (const fkyaml::exception& e) {
|
} catch (const fkyaml::exception& e) {
|
||||||
std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n';
|
std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n';
|
||||||
@@ -154,11 +93,21 @@ void AnimatedSprite::animate(float delta_time) {
|
|||||||
|
|
||||||
const float TOTAL = std::accumulate(anim.speeds.begin(), anim.speeds.end(), 0.0F);
|
const float TOTAL = std::accumulate(anim.speeds.begin(), anim.speeds.end(), 0.0F);
|
||||||
|
|
||||||
|
// Helper local per a propagar el frame actual (clip + opcionalment flip)
|
||||||
|
// al sprite. El flip només s'escriu si l'animació declara `controls_flip`,
|
||||||
|
// per a no trepitjar el flip que el codi del joc encara controla
|
||||||
|
// externament en animacions sense declaració de flip al YAML.
|
||||||
|
auto apply_current_frame = [&](int frame_index) {
|
||||||
|
const auto& frame = anim.frames[frame_index];
|
||||||
|
setClip(frame.rect);
|
||||||
|
if (anim.controls_flip) { setFlip(toFlipMode(frame.flip_h, frame.flip_v)); }
|
||||||
|
};
|
||||||
|
|
||||||
if (anim.accumulated_time >= TOTAL) {
|
if (anim.accumulated_time >= TOTAL) {
|
||||||
if (anim.loop_from < 0) {
|
if (anim.loop_from < 0) {
|
||||||
anim.current_frame = static_cast<int>(anim.frames.size()) - 1;
|
anim.current_frame = static_cast<int>(anim.frames.size()) - 1;
|
||||||
anim.completed = true;
|
anim.completed = true;
|
||||||
setClip(anim.frames[anim.current_frame]);
|
apply_current_frame(anim.current_frame);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
float loop_start = 0.0F;
|
float loop_start = 0.0F;
|
||||||
@@ -172,13 +121,13 @@ void AnimatedSprite::animate(float delta_time) {
|
|||||||
cursor += anim.speeds[i];
|
cursor += anim.speeds[i];
|
||||||
if (anim.accumulated_time < cursor) {
|
if (anim.accumulated_time < cursor) {
|
||||||
anim.current_frame = i;
|
anim.current_frame = i;
|
||||||
setClip(anim.frames[i]);
|
apply_current_frame(i);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
anim.current_frame = static_cast<int>(anim.frames.size()) - 1;
|
anim.current_frame = static_cast<int>(anim.frames.size()) - 1;
|
||||||
setClip(anim.frames[anim.current_frame]);
|
apply_current_frame(anim.current_frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto AnimatedSprite::animationIsCompleted() -> bool {
|
auto AnimatedSprite::animationIsCompleted() -> bool {
|
||||||
@@ -200,7 +149,10 @@ void AnimatedSprite::setCurrentAnimation(const std::string& name) {
|
|||||||
anim.current_frame = 0;
|
anim.current_frame = 0;
|
||||||
anim.accumulated_time = 0.0F;
|
anim.accumulated_time = 0.0F;
|
||||||
anim.completed = false;
|
anim.completed = false;
|
||||||
if (!anim.frames.empty()) { setClip(anim.frames[0]); }
|
if (!anim.frames.empty()) {
|
||||||
|
setClip(anim.frames[0].rect);
|
||||||
|
if (anim.controls_flip) { setFlip(toFlipMode(anim.frames[0].flip_h, anim.frames[0].flip_v)); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AnimatedSprite::update(float delta_time) {
|
void AnimatedSprite::update(float delta_time) {
|
||||||
|
|||||||
@@ -8,14 +8,34 @@
|
|||||||
|
|
||||||
#include "core/rendering/sprite/moving_sprite.hpp"
|
#include "core/rendering/sprite/moving_sprite.hpp"
|
||||||
#include "core/resources/resource_types.hpp"
|
#include "core/resources/resource_types.hpp"
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
|
||||||
class AnimatedSprite : public MovingSprite {
|
class AnimatedSprite : public MovingSprite {
|
||||||
public:
|
public:
|
||||||
|
// Un frame d'animació = rectangle dins el spritesheet + flags de flip
|
||||||
|
// associats. El flip s'aplica al sprite a `animate()` quan l'animació
|
||||||
|
// declara que ella controla el flip (vegeu `AnimationData::controls_flip`).
|
||||||
|
struct Frame {
|
||||||
|
SDL_FRect rect{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
|
||||||
|
bool flip_h{false};
|
||||||
|
bool flip_v{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Paràmetres geomètrics del spritesheet, derivats del bloc `sprite:`
|
||||||
|
// del YAML. Es passen al parser d'animacions per a calcular els
|
||||||
|
// rectangles de cada frame a partir dels seus índexs.
|
||||||
|
struct SheetParams {
|
||||||
|
float frame_width{0.0F};
|
||||||
|
float frame_height{0.0F};
|
||||||
|
int frames_per_row{1};
|
||||||
|
int max_tiles{1};
|
||||||
|
};
|
||||||
|
|
||||||
// Dades d'una animació concreta dins d'un spritesheet. Les claus del
|
// Dades d'una animació concreta dins d'un spritesheet. Les claus del
|
||||||
// mapa (`animations_`) són els noms; aquí només es guarden els fotogrames
|
// mapa (`animations_`) són els noms; aquí només es guarden els fotogrames
|
||||||
// i l'estat de reproducció.
|
// i l'estat de reproducció.
|
||||||
struct AnimationData {
|
struct AnimationData {
|
||||||
std::vector<SDL_FRect> frames;
|
std::vector<Frame> frames;
|
||||||
// Durada de cada frame en segons (convertit des de `frame_ms` al YAML).
|
// Durada de cada frame en segons (convertit des de `frame_ms` al YAML).
|
||||||
// Si té un sol valor, val per a tots els frames; altrament ha de tenir
|
// Si té un sol valor, val per a tots els frames; altrament ha de tenir
|
||||||
// la mateixa mida que `frames`.
|
// la mateixa mida que `frames`.
|
||||||
@@ -24,6 +44,13 @@ class AnimatedSprite : public MovingSprite {
|
|||||||
// 0 = loop complet des del primer frame.
|
// 0 = loop complet des del primer frame.
|
||||||
// N>0 = rebota al frame N en passar del final.
|
// N>0 = rebota al frame N en passar del final.
|
||||||
int loop_from{0};
|
int loop_from{0};
|
||||||
|
// Si true, `animate()` escriu el flip del frame actual al sprite.
|
||||||
|
// Si false (cas backward-compat per a animacions sense cap declaració
|
||||||
|
// de flip), `animate()` no toca el flip — el codi del joc el segueix
|
||||||
|
// controlant externament via `setFlip(...)`. Es marca a true quan
|
||||||
|
// l'animació declara `flip_h`/`flip_v`, o quan algun frame es declara
|
||||||
|
// amb la forma mapping `{ index, flip_h?, flip_v? }`.
|
||||||
|
bool controls_flip{false};
|
||||||
bool completed{false};
|
bool completed{false};
|
||||||
int current_frame{0};
|
int current_frame{0};
|
||||||
float accumulated_time{0.0F};
|
float accumulated_time{0.0F};
|
||||||
@@ -40,6 +67,12 @@ class AnimatedSprite : public MovingSprite {
|
|||||||
void resetAnimation();
|
void resetAnimation();
|
||||||
void animate(float delta_time);
|
void animate(float delta_time);
|
||||||
|
|
||||||
|
// Helper de parsing exposat per a testabilitat. NO toca el cache de
|
||||||
|
// recursos — només el bloc `animations:` del YAML i els paràmetres de
|
||||||
|
// full ja calculats. El ctor d'`AnimatedSprite` el crida internament.
|
||||||
|
static auto parseAnimationsFromYaml(const fkyaml::node& yaml, const SheetParams& params)
|
||||||
|
-> std::unordered_map<std::string, AnimationData>;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unordered_map<std::string, AnimationData> animations_;
|
std::unordered_map<std::string, AnimationData> animations_;
|
||||||
// Nom de l'animació activa. Buit fins que algú crida `setCurrentAnimation`.
|
// Nom de l'animació activa. Buit fins que algú crida `setCurrentAnimation`.
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
#include <algorithm>
|
||||||
|
#include <ranges>
|
||||||
|
|
||||||
|
#include "core/rendering/sprite/animated_sprite.hpp"
|
||||||
|
|
||||||
|
// Implementació del parser pure de YAML d'animacions. Viu en una unitat
|
||||||
|
// separada perquè NO depèn de Resource::Cache ni de Surface — només de
|
||||||
|
// fkyaml — i això permet als tests stand-alone enllaçar només aquest .cpp
|
||||||
|
// per a verificar el parsing sense haver d'arrossegar tot l'stack de
|
||||||
|
// recursos i renderitzat.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Construeix el rectangle d'un frame a partir del seu índex dins del
|
||||||
|
// spritesheet. Centralitzat per a no duplicar la fórmula als dos
|
||||||
|
// formats (escalar / mapping) que admet `frames:` al YAML.
|
||||||
|
auto rectFromIndex(int index, const AnimatedSprite::SheetParams& params) -> SDL_FRect {
|
||||||
|
return SDL_FRect{
|
||||||
|
.x = static_cast<float>(index % params.frames_per_row) * params.frame_width,
|
||||||
|
.y = static_cast<float>(index / params.frames_per_row) * params.frame_height,
|
||||||
|
.w = params.frame_width,
|
||||||
|
.h = params.frame_height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llig un node `frames` (seqüència) i el converteix a la nova
|
||||||
|
// representació `vector<Frame>`. Cada element pot ser:
|
||||||
|
// - escalar (int) → hereta `default_flip_h`/`default_flip_v` de l'animació.
|
||||||
|
// - mapping `{ index: N, flip_h?: bool, flip_v?: bool }` → fully
|
||||||
|
// explícit. Els flags absents defaulten a false (NO hereta de l'anim).
|
||||||
|
auto convertYamlFrames(const fkyaml::node& frames_node, const AnimatedSprite::SheetParams& params, bool default_flip_h, bool default_flip_v) -> std::vector<AnimatedSprite::Frame> {
|
||||||
|
std::vector<AnimatedSprite::Frame> frames;
|
||||||
|
for (const auto& frame_node : frames_node) {
|
||||||
|
if (frame_node.is_mapping()) {
|
||||||
|
if (!frame_node.contains("index")) { continue; }
|
||||||
|
const int NUM_TILE = frame_node["index"].get_value<int>();
|
||||||
|
if (NUM_TILE > params.max_tiles) { continue; }
|
||||||
|
AnimatedSprite::Frame frame;
|
||||||
|
frame.rect = rectFromIndex(NUM_TILE, params);
|
||||||
|
if (frame_node.contains("flip_h")) { frame.flip_h = frame_node["flip_h"].get_value<bool>(); }
|
||||||
|
if (frame_node.contains("flip_v")) { frame.flip_v = frame_node["flip_v"].get_value<bool>(); }
|
||||||
|
frames.emplace_back(frame);
|
||||||
|
} else {
|
||||||
|
const int NUM_TILE = frame_node.get_value<int>();
|
||||||
|
if (NUM_TILE > params.max_tiles) { continue; }
|
||||||
|
frames.emplace_back(AnimatedSprite::Frame{
|
||||||
|
.rect = rectFromIndex(NUM_TILE, params),
|
||||||
|
.flip_h = default_flip_h,
|
||||||
|
.flip_v = default_flip_v,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detecta si dins de `frames_node` hi ha algun element en forma mapping.
|
||||||
|
// Si n'hi ha (encara que els flags interns siguin false), l'animació
|
||||||
|
// passa a controlar el flip — el mapping és senyal explícit que l'autor
|
||||||
|
// del YAML vol governar el flip per frame.
|
||||||
|
auto framesContainMapping(const fkyaml::node& frames_node) -> bool {
|
||||||
|
return std::ranges::any_of(frames_node, [](const auto& frame_node) {
|
||||||
|
return frame_node.is_mapping();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// `frame_ms` pot ser un escalar (temps uniforme) o una llista (per-frame).
|
||||||
|
// Convertim sempre a segons perquè `animate()` treballa amb delta_time
|
||||||
|
// en segons. Si falta o val 0, `speeds` queda buit i l'animació es
|
||||||
|
// considera estàtica (no avança).
|
||||||
|
void parseFrameTimes(const fkyaml::node& anim_node, AnimatedSprite::AnimationData& animation) {
|
||||||
|
if (!anim_node.contains("frame_ms")) { return; }
|
||||||
|
const auto& fm = anim_node["frame_ms"];
|
||||||
|
if (fm.is_sequence()) {
|
||||||
|
for (const auto& v : fm) {
|
||||||
|
animation.speeds.push_back(static_cast<float>(v.get_value<int>()) / 1000.0F);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const float SEC = static_cast<float>(fm.get_value<int>()) / 1000.0F;
|
||||||
|
if (SEC > 0.0F) { animation.speeds.assign(animation.frames.size(), SEC); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordre de prioritat per a la política de loop:
|
||||||
|
// 1) `loop_from: N` → rebota al frame N en passar del final (implica loop).
|
||||||
|
// 2) `loop: false` → animació d'un sol tret (es queda al darrer frame).
|
||||||
|
// 3) Res → loop complet des del frame 0 (default).
|
||||||
|
void parseLoopPolicy(const fkyaml::node& anim_node, AnimatedSprite::AnimationData& animation) {
|
||||||
|
if (anim_node.contains("loop_from")) {
|
||||||
|
animation.loop_from = anim_node["loop_from"].get_value<int>();
|
||||||
|
} else if (anim_node.contains("loop") && !anim_node["loop"].get_value<bool>()) {
|
||||||
|
animation.loop_from = -1;
|
||||||
|
} else {
|
||||||
|
animation.loop_from = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto AnimatedSprite::parseAnimationsFromYaml(const fkyaml::node& yaml, const SheetParams& params)
|
||||||
|
-> std::unordered_map<std::string, AnimationData> {
|
||||||
|
std::unordered_map<std::string, AnimationData> animations;
|
||||||
|
if (!yaml.contains("animations") || !yaml["animations"].is_mapping()) {
|
||||||
|
return animations;
|
||||||
|
}
|
||||||
|
const auto& anims_node = yaml["animations"];
|
||||||
|
for (auto it = anims_node.begin(); it != anims_node.end(); ++it) {
|
||||||
|
const auto NAME = it.key().get_value<std::string>();
|
||||||
|
const auto& anim_node = *it;
|
||||||
|
|
||||||
|
// Flags de flip a nivell d'animació (defaults a false). Els frames
|
||||||
|
// declarats com a int els hereten; els declarats com a mapping no.
|
||||||
|
bool anim_flip_h = false;
|
||||||
|
bool anim_flip_v = false;
|
||||||
|
if (anim_node.contains("flip_h")) { anim_flip_h = anim_node["flip_h"].get_value<bool>(); }
|
||||||
|
if (anim_node.contains("flip_v")) { anim_flip_v = anim_node["flip_v"].get_value<bool>(); }
|
||||||
|
|
||||||
|
AnimationData animation;
|
||||||
|
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
|
||||||
|
const auto& frames_node = anim_node["frames"];
|
||||||
|
animation.frames = convertYamlFrames(frames_node, params, anim_flip_h, anim_flip_v);
|
||||||
|
// L'animació passa a "controlar" el flip si l'autor ha
|
||||||
|
// declarat qualsevol cosa relacionada amb flip — sigui a
|
||||||
|
// nivell d'anim o a través d'un frame en forma mapping.
|
||||||
|
// Sense cap declaració, ens mantenim passius i deixem el
|
||||||
|
// flip al codi del joc (backward-compat amb sam.yaml).
|
||||||
|
animation.controls_flip = anim_flip_h || anim_flip_v || framesContainMapping(frames_node);
|
||||||
|
}
|
||||||
|
parseFrameTimes(anim_node, animation);
|
||||||
|
parseLoopPolicy(anim_node, animation);
|
||||||
|
|
||||||
|
animations.emplace(NAME, std::move(animation));
|
||||||
|
}
|
||||||
|
return animations;
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
// Smoke test del parser d'animacions amb suport de flip H/V. Stand-alone:
|
||||||
|
// només enllaça `animated_sprite_parser.cpp` (la part del motor que NO toca
|
||||||
|
// Resource::Cache ni Surface), perquè la cobertura interessa el contracte
|
||||||
|
// de càrrega del YAML, no el render real.
|
||||||
|
//
|
||||||
|
// Tests coberts:
|
||||||
|
// T1 backward compat — YAML sense cap flip declarat carrega tots els
|
||||||
|
// frames amb (flip_h=false, flip_v=false) i sense
|
||||||
|
// que l'animació prengui control del flip.
|
||||||
|
// T2 flip a nivell d'animació — `flip_h: true` amb frames `int` →
|
||||||
|
// tots els frames hereten flip_h=true.
|
||||||
|
// T3 override per-frame — frame mapping `{ index: N, flip_h: true }`
|
||||||
|
// en una animació sense flip → només aquest frame
|
||||||
|
// té el flag.
|
||||||
|
// T4 mapping no hereta — animació amb `flip_h: true` però un frame
|
||||||
|
// `{ index: N }` (mapping sense flip) → flip_h=false
|
||||||
|
// en aquest frame (el mapping declara el seu propi
|
||||||
|
// estat, sense heretar).
|
||||||
|
// T5 combinat V+H — frame `{ index, flip_h: true, flip_v: true }` →
|
||||||
|
// ambdós flags actius.
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "core/rendering/sprite/animated_sprite.hpp"
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int g_failures = 0;
|
||||||
|
|
||||||
|
void check(bool cond, const char* msg, const char* file, int line) {
|
||||||
|
if (!cond) {
|
||||||
|
std::cerr << " FAIL: " << msg << " (" << file << ":" << line << ")\n";
|
||||||
|
++g_failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#define REQUIRE(cond) check((cond), #cond, __FILE__, __LINE__)
|
||||||
|
|
||||||
|
// Paràmetres comuns per a tots els tests: spritesheet hipotètic 64×64
|
||||||
|
// amb tiles de 16×16 → grid 4×4, max_tiles=16.
|
||||||
|
auto sheetParams() -> AnimatedSprite::SheetParams {
|
||||||
|
return AnimatedSprite::SheetParams{
|
||||||
|
.frame_width = 16.0F,
|
||||||
|
.frame_height = 16.0F,
|
||||||
|
.frames_per_row = 4,
|
||||||
|
.max_tiles = 16,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parse(const std::string& yaml_str) {
|
||||||
|
return AnimatedSprite::parseAnimationsFromYaml(
|
||||||
|
fkyaml::node::deserialize(yaml_str), sheetParams());
|
||||||
|
}
|
||||||
|
|
||||||
|
void testBackwardCompat() {
|
||||||
|
std::cout << "[T1] backward compat: cap flip declarat\n";
|
||||||
|
const std::string YAML = R"(
|
||||||
|
animations:
|
||||||
|
walk: { frames: [0, 1, 2, 3], frame_ms: 100 }
|
||||||
|
stand: { frames: [4] }
|
||||||
|
)";
|
||||||
|
const auto ANIMS = parse(YAML);
|
||||||
|
REQUIRE(ANIMS.size() == 2);
|
||||||
|
|
||||||
|
const auto WALK_IT = ANIMS.find("walk");
|
||||||
|
REQUIRE(WALK_IT != ANIMS.end());
|
||||||
|
if (WALK_IT != ANIMS.end()) {
|
||||||
|
const auto& WALK = WALK_IT->second;
|
||||||
|
REQUIRE(WALK.frames.size() == 4);
|
||||||
|
REQUIRE(!WALK.controls_flip);
|
||||||
|
for (const auto& F : WALK.frames) {
|
||||||
|
REQUIRE(!F.flip_h);
|
||||||
|
REQUIRE(!F.flip_v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto STAND_IT = ANIMS.find("stand");
|
||||||
|
REQUIRE(STAND_IT != ANIMS.end());
|
||||||
|
if (STAND_IT != ANIMS.end()) {
|
||||||
|
const auto& STAND = STAND_IT->second;
|
||||||
|
REQUIRE(STAND.frames.size() == 1);
|
||||||
|
REQUIRE(!STAND.controls_flip);
|
||||||
|
REQUIRE(!STAND.frames[0].flip_h);
|
||||||
|
REQUIRE(!STAND.frames[0].flip_v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void testFlipAtAnimationLevel() {
|
||||||
|
std::cout << "[T2] flip a nivell d'anim hereten frames int\n";
|
||||||
|
const std::string YAML = R"(
|
||||||
|
animations:
|
||||||
|
walk_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
|
||||||
|
)";
|
||||||
|
const auto ANIMS = parse(YAML);
|
||||||
|
const auto IT = ANIMS.find("walk_west");
|
||||||
|
REQUIRE(IT != ANIMS.end());
|
||||||
|
if (IT != ANIMS.end()) {
|
||||||
|
const auto& A = IT->second;
|
||||||
|
REQUIRE(A.frames.size() == 4);
|
||||||
|
REQUIRE(A.controls_flip);
|
||||||
|
for (const auto& F : A.frames) {
|
||||||
|
REQUIRE(F.flip_h);
|
||||||
|
REQUIRE(!F.flip_v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void testOverridePerFrame() {
|
||||||
|
std::cout << "[T3] override per-frame en animació sense flip\n";
|
||||||
|
const std::string YAML = R"(
|
||||||
|
animations:
|
||||||
|
death_spin:
|
||||||
|
frames:
|
||||||
|
- 0
|
||||||
|
- 4
|
||||||
|
- 8
|
||||||
|
- { index: 8, flip_h: true }
|
||||||
|
frame_ms: 100
|
||||||
|
)";
|
||||||
|
const auto ANIMS = parse(YAML);
|
||||||
|
const auto IT = ANIMS.find("death_spin");
|
||||||
|
REQUIRE(IT != ANIMS.end());
|
||||||
|
if (IT != ANIMS.end()) {
|
||||||
|
const auto& A = IT->second;
|
||||||
|
REQUIRE(A.frames.size() == 4);
|
||||||
|
// Hi ha un mapping → l'animació pren el control del flip.
|
||||||
|
REQUIRE(A.controls_flip);
|
||||||
|
|
||||||
|
// Els tres primers (int, sense flip a l'anim) → tot a false.
|
||||||
|
REQUIRE(!A.frames[0].flip_h);
|
||||||
|
REQUIRE(!A.frames[0].flip_v);
|
||||||
|
REQUIRE(!A.frames[1].flip_h);
|
||||||
|
REQUIRE(!A.frames[1].flip_v);
|
||||||
|
REQUIRE(!A.frames[2].flip_h);
|
||||||
|
REQUIRE(!A.frames[2].flip_v);
|
||||||
|
|
||||||
|
// El quart (mapping amb flip_h: true) → només flip_h.
|
||||||
|
REQUIRE(A.frames[3].flip_h);
|
||||||
|
REQUIRE(!A.frames[3].flip_v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void testMappingDoesNotInherit() {
|
||||||
|
std::cout << "[T4] mapping NO hereta el flip de l'animació\n";
|
||||||
|
const std::string YAML = R"(
|
||||||
|
animations:
|
||||||
|
mixed:
|
||||||
|
frames:
|
||||||
|
- 0
|
||||||
|
- { index: 4 }
|
||||||
|
frame_ms: 100
|
||||||
|
flip_h: true
|
||||||
|
)";
|
||||||
|
const auto ANIMS = parse(YAML);
|
||||||
|
const auto IT = ANIMS.find("mixed");
|
||||||
|
REQUIRE(IT != ANIMS.end());
|
||||||
|
if (IT != ANIMS.end()) {
|
||||||
|
const auto& A = IT->second;
|
||||||
|
REQUIRE(A.frames.size() == 2);
|
||||||
|
REQUIRE(A.controls_flip);
|
||||||
|
|
||||||
|
// Frame int hereta el flip_h true de l'animació.
|
||||||
|
REQUIRE(A.frames[0].flip_h);
|
||||||
|
REQUIRE(!A.frames[0].flip_v);
|
||||||
|
|
||||||
|
// Frame mapping declara els seus propis flags (cap declarat
|
||||||
|
// → false) i NO hereta del flip a nivell d'anim.
|
||||||
|
REQUIRE(!A.frames[1].flip_h);
|
||||||
|
REQUIRE(!A.frames[1].flip_v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void testCombinedVerticalAndHorizontal() {
|
||||||
|
std::cout << "[T5] flip combinat V+H per-frame\n";
|
||||||
|
const std::string YAML = R"(
|
||||||
|
animations:
|
||||||
|
upside_down:
|
||||||
|
frames:
|
||||||
|
- { index: 5, flip_h: true, flip_v: true }
|
||||||
|
frame_ms: 100
|
||||||
|
)";
|
||||||
|
const auto ANIMS = parse(YAML);
|
||||||
|
const auto IT = ANIMS.find("upside_down");
|
||||||
|
REQUIRE(IT != ANIMS.end());
|
||||||
|
if (IT != ANIMS.end()) {
|
||||||
|
const auto& A = IT->second;
|
||||||
|
REQUIRE(A.frames.size() == 1);
|
||||||
|
REQUIRE(A.controls_flip);
|
||||||
|
REQUIRE(A.frames[0].flip_h);
|
||||||
|
REQUIRE(A.frames[0].flip_v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto main() -> int {
|
||||||
|
std::cout << "== smoke tests: animation flip parser ==\n";
|
||||||
|
|
||||||
|
testBackwardCompat();
|
||||||
|
testFlipAtAnimationLevel();
|
||||||
|
testOverridePerFrame();
|
||||||
|
testMappingDoesNotInherit();
|
||||||
|
testCombinedVerticalAndHorizontal();
|
||||||
|
|
||||||
|
if (g_failures == 0) {
|
||||||
|
std::cout << "ALL OK\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
std::cerr << g_failures << " smoke check(s) failed\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user