refactor(anim): migra AnimatedSprite al format sprite:/frame_ms, animacions en mapa

This commit is contained in:
JailDesigner committed 2026-04-24 21:01:21 +02:00
1 parent ea0f6d98a9
commit 50aae790b1
4 files changed
+158 -138

No files matched your search

+19 -20
View File
@@ -1,7 +1,7 @@
# player animation
tileSetFile: morcus.gif
frameWidth: 16
frameHeight: 32
sprite:
path: morcus.gif
size: [16, 32]
# Geometria de col·lisió del jugador (independent de l'animació):
# size → costat del quadrat de col·lisió (16×16)
@@ -16,30 +16,29 @@ collision:
# els passadissos. Centrada en el mateix punt que la caixa base.
hit_size: 8
# Animacions en mapa (clau = nom). Camps per animació:
# frames → llista d'índexs dins de l'spritesheet.
# frame_ms → durada de cada frame en ms. Escalar = uniforme; llista =
# per-frame (mateixa mida que `frames`).
# loop → true per defecte. `loop: false` = un sol tret (es queda al
# darrer frame i `animationIsCompleted()` passa a true).
# loop_from → rebota a aquest índex en passar del final (implica loop).
animations:
- name: stand_down
frames: [0]
stand_down: { frames: [0] }
stand_up: { frames: [4] }
stand: { frames: [8] }
- name: stand_up
frames: [4]
- name: stand
frames: [8]
- name: walk_down
speed: 0.1
loopFrom: 0
walk_down:
frames: [0, 1, 2, 3]
frame_ms: 100
- name: walk_up
speed: 0.1
loopFrom: 0
walk_up:
frames: [4, 5, 6, 7]
frame_ms: 100
- name: walk
speed: 0.1
loopFrom: 0
walk:
frames: [8, 9, 10, 11]
frame_ms: 100
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
+19 -20
View File
@@ -1,7 +1,7 @@
# player animation
tileSetFile: sam.gif
frameWidth: 16
frameHeight: 32
sprite:
path: sam.gif
size: [16, 32]
# Geometria de col·lisió del jugador (independent de l'animació):
# size → costat del quadrat de col·lisió (16×16)
@@ -16,30 +16,29 @@ collision:
# els passadissos. Centrada en el mateix punt que la caixa base.
hit_size: 8
# Animacions en mapa (clau = nom). Camps per animació:
# frames → llista d'índexs dins de l'spritesheet.
# frame_ms → durada de cada frame en ms. Escalar = uniforme; llista =
# per-frame (mateixa mida que `frames`).
# loop → true per defecte. `loop: false` = un sol tret (es queda al
# darrer frame i `animationIsCompleted()` passa a true).
# loop_from → rebota a aquest índex en passar del final (implica loop).
animations:
- name: stand_down
frames: [1]
stand_down: { frames: [1] }
stand_up: { frames: [5] }
stand: { frames: [9] }
- name: stand_up
frames: [5]
- name: stand
frames: [9]
- name: walk_down
speed: 0.1
loopFrom: 0
walk_down:
frames: [0, 1, 2, 3]
frame_ms: 100
- name: walk_up
speed: 0.1
loopFrom: 0
walk_up:
frames: [4, 5, 6, 7]
frame_ms: 100
- name: walk
speed: 0.1
loopFrom: 0
walk:
frames: [8, 9, 10, 11]
frame_ms: 100
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
+106 -88
View File
@@ -3,6 +3,7 @@
#include <cmath>
#include <iostream>
#include <numeric>
#include <utility>
#include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp"
@@ -24,46 +25,6 @@ namespace {
return frames;
}
auto parseAnimations(const fkyaml::node& yaml, float frame_width, float frame_height, int frames_per_row, int max_tiles) -> std::vector<AnimatedSprite::AnimationData> {
std::vector<AnimatedSprite::AnimationData> animations;
if (!yaml.contains("animations") || !yaml["animations"].is_sequence()) {
return animations;
}
for (const auto& anim_node : yaml["animations"]) {
AnimatedSprite::AnimationData animation;
if (anim_node.contains("name")) {
animation.name = anim_node["name"].get_value<std::string>();
}
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
animation.frames = convertYamlFramesToRects(
anim_node["frames"],
frame_width,
frame_height,
frames_per_row,
max_tiles);
}
if (anim_node.contains("speed")) {
const auto& speed_node = anim_node["speed"];
if (speed_node.is_sequence()) {
for (const auto& s : speed_node) {
animation.speeds.push_back(s.get_value<float>());
}
} else {
auto spd = speed_node.get_value<float>();
if (spd > 0.0F) { animation.speeds.assign(animation.frames.size(), spd); }
}
}
if (anim_node.contains("loopFrom")) {
animation.loop_from = anim_node["loopFrom"].get_value<int>();
}
animations.push_back(animation);
}
return animations;
}
struct SheetParams {
float frame_width{0.0F};
float frame_height{0.0F};
@@ -71,16 +32,22 @@ namespace {
int max_tiles{1};
};
auto parseGlobalConfig(const fkyaml::node& yaml, std::shared_ptr<Surface>& surface) -> SheetParams {
// Llegeix el bloc `sprite: { path, size: [w, h] }` — mateix format que
// les plantilles d'entitat. La surface es resol via Resource::Cache amb
// el basename del fitxer.
auto parseSpriteBlock(const fkyaml::node& yaml, std::shared_ptr<Surface>& surface) -> SheetParams {
SheetParams params;
if (yaml.contains("tileSetFile")) {
surface = Resource::Cache::get().getSurface(yaml["tileSetFile"].get_value<std::string>());
if (!yaml.contains("sprite")) { return params; }
const auto& s = yaml["sprite"];
if (s.contains("path")) {
surface = Resource::Cache::get().getSurface(s["path"].get_value<std::string>());
}
if (yaml.contains("frameWidth")) {
params.frame_width = static_cast<float>(yaml["frameWidth"].get_value<int>());
}
if (yaml.contains("frameHeight")) {
params.frame_height = static_cast<float>(yaml["frameHeight"].get_value<int>());
if (s.contains("size")) {
const auto& sz = s["size"];
if (sz.is_sequence() && sz.size() == 2) {
params.frame_width = static_cast<float>(sz[0].get_value<int>());
params.frame_height = static_cast<float>(sz[1].get_value<int>());
}
}
if (surface && params.frame_width > 0.0F && params.frame_height > 0.0F) {
params.frames_per_row = surface->getWidth() / static_cast<int>(params.frame_width);
@@ -90,21 +57,82 @@ namespace {
return params;
}
// `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;
}
}
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
AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
std::string yaml_content(cached_data.yaml_data.begin(), cached_data.yaml_data.end());
try {
auto yaml = fkyaml::node::deserialize(yaml_content);
auto params = parseGlobalConfig(yaml, surface_);
animations_ = parseAnimations(yaml, params.frame_width, params.frame_height, params.frames_per_row, params.max_tiles);
buildNameIndex();
auto params = parseSpriteBlock(yaml, surface_);
animations_ = parseAnimations(yaml, params);
setWidth(params.frame_width);
setHeight(params.frame_height);
if (!animations_.empty() && !animations_[0].frames.empty()) {
setClip(animations_[0].frames[0]);
// Clip inicial: qualsevol frame del primer element del mapa serveix
// com a placeholder. El consumidor sempre crida `setCurrentAnimation`
// abans del primer `update()`, així que el valor real el posa ell.
if (!animations_.empty()) {
const auto& first = animations_.begin()->second;
if (!first.frames.empty()) { setClip(first.frames[0]); }
}
} catch (const fkyaml::exception& e) {
std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n';
@@ -115,17 +143,12 @@ AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
}
}
void AnimatedSprite::buildNameIndex() {
animation_index_.clear();
for (int i = 0; i < static_cast<int>(animations_.size()); ++i) {
animation_index_[animations_[i].name] = i;
}
}
void AnimatedSprite::animate(float delta_time) {
if (animations_.empty()) { return; }
auto& anim = animations_[current_animation_];
if (anim.speeds.empty()) { return; }
if (current_animation_.empty()) { return; }
auto it = animations_.find(current_animation_);
if (it == animations_.end()) { return; }
auto& anim = it->second;
if (anim.speeds.empty() || anim.frames.empty()) { return; }
anim.accumulated_time += delta_time;
@@ -159,26 +182,25 @@ void AnimatedSprite::animate(float delta_time) {
}
auto AnimatedSprite::animationIsCompleted() -> bool {
return animations_[current_animation_].completed;
if (current_animation_.empty()) { return false; }
auto it = animations_.find(current_animation_);
if (it == animations_.end()) { return false; }
return it->second.completed;
}
void AnimatedSprite::setCurrentAnimation(const std::string& name) {
auto it = animation_index_.find(name);
if (it == animation_index_.end()) {
if (current_animation_ == name) { return; }
auto it = animations_.find(name);
if (it == animations_.end()) {
std::cout << "** Warning: could not find \"" << name << "\" animation" << '\n';
return;
}
setCurrentAnimation(it->second);
}
void AnimatedSprite::setCurrentAnimation(int index) {
if (current_animation_ != index) {
current_animation_ = index;
animations_[current_animation_].current_frame = 0;
animations_[current_animation_].accumulated_time = 0.0F;
animations_[current_animation_].completed = false;
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
}
current_animation_ = name;
auto& anim = it->second;
anim.current_frame = 0;
anim.accumulated_time = 0.0F;
anim.completed = false;
if (!anim.frames.empty()) { setClip(anim.frames[0]); }
}
void AnimatedSprite::update(float delta_time) {
@@ -187,15 +209,11 @@ void AnimatedSprite::update(float delta_time) {
}
void AnimatedSprite::resetAnimation() {
animations_[current_animation_].current_frame = 0;
animations_[current_animation_].accumulated_time = 0.0F;
animations_[current_animation_].completed = false;
}
void AnimatedSprite::setCurrentAnimationFrame(int num) {
if (num < 0 || num >= static_cast<int>(animations_[current_animation_].frames.size())) {
num = 0;
}
animations_[current_animation_].current_frame = num;
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
if (current_animation_.empty()) { return; }
auto it = animations_.find(current_animation_);
if (it == animations_.end()) { return; }
auto& anim = it->second;
anim.current_frame = 0;
anim.accumulated_time = 0.0F;
anim.completed = false;
}
@@ -11,11 +11,19 @@
class AnimatedSprite : public MovingSprite {
public:
// 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
// i l'estat de reproducció.
struct AnimationData {
std::string name;
std::vector<SDL_FRect> frames;
// 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
// la mateixa mida que `frames`.
std::vector<float> speeds;
int loop_from{-1};
// -1 = no loop (es queda al darrer frame i marca `completed`).
// 0 = loop complet des del primer frame.
// N>0 = rebota al frame N en passar del final.
int loop_from{0};
bool completed{false};
int current_frame{0};
float accumulated_time{0.0F};
@@ -28,16 +36,12 @@ class AnimatedSprite : public MovingSprite {
auto animationIsCompleted() -> bool;
void setCurrentAnimation(const std::string& name = "default");
void setCurrentAnimation(int index = 0);
void setCurrentAnimation(const std::string& name);
void resetAnimation();
void setCurrentAnimationFrame(int num);
void animate(float delta_time);
private:
void buildNameIndex();
std::vector<AnimationData> animations_;
std::unordered_map<std::string, int> animation_index_;
int current_animation_{0};
std::unordered_map<std::string, AnimationData> animations_;
// Nom de l'animació activa. Buit fins que algú crida `setCurrentAnimation`.
std::string current_animation_;
};