feat(effects): polvet transitori al despegue i a l'aterratge
This commit is contained in:
1 parent
c38c80753a
commit
3e379ce129
11 files changed
+209
-1
No files matched your search
@@ -47,6 +47,7 @@ set(ENTITY_SOURCES
|
|||||||
source/game/entities/concrete/rolling_rock.cpp
|
source/game/entities/concrete/rolling_rock.cpp
|
||||||
source/game/entities/concrete/wall_trap.cpp
|
source/game/entities/concrete/wall_trap.cpp
|
||||||
source/game/entities/concrete/floor_trap.cpp
|
source/game/entities/concrete/floor_trap.cpp
|
||||||
|
source/game/entities/concrete/dust.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set(APP_SOURCES
|
set(APP_SOURCES
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 307 B |
@@ -0,0 +1,22 @@
|
|||||||
|
name: dust
|
||||||
|
type: effect
|
||||||
|
sprite:
|
||||||
|
path: dust.gif
|
||||||
|
size: [40, 16]
|
||||||
|
|
||||||
|
# El polvet és purament visual: no col·lisiona, no interactua, no fa
|
||||||
|
# dany. Només viu mentre dura l'animació triada (`jump` o `land`) i
|
||||||
|
# desapareix per `markDead` quan acaba.
|
||||||
|
flags:
|
||||||
|
collides: false
|
||||||
|
interactable: false
|
||||||
|
|
||||||
|
# Dues animacions, no-loop: una per a cada moment del salt. El spawner
|
||||||
|
# (GameScene, en resposta a events del bus) instancia la variant
|
||||||
|
# apropiada via `setVariant` abans del primer tick.
|
||||||
|
# jump: frames 0..6 (7 frames) → reproduït al despegue.
|
||||||
|
# land: frames 7..12 (6 frames) → reproduït a l'aterratge.
|
||||||
|
# fps: 10 = 100 ms per frame.
|
||||||
|
animations:
|
||||||
|
jump: { frames: [0, 1, 2, 3, 4, 5, 6], fps: 10, loop: false }
|
||||||
|
land: { frames: [7, 8, 9, 10, 11, 12], fps: 10, loop: false }
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#include "game/entities/concrete/dust.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "game/entities/entity_factory.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Llig una animació opcional del bloc `animations` del YAML, amb
|
||||||
|
// valors per defecte segurs (1 frame, fps=1, sense loop). Si la
|
||||||
|
// clau no existeix, retornem una animació "ja completada" — així,
|
||||||
|
// un Dust spawnejat amb una variant inexistent al YAML mor al
|
||||||
|
// pròxim frame sense petar.
|
||||||
|
auto buildAnim(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
|
||||||
|
std::vector<int> frames = {0};
|
||||||
|
float fps = 1.0F;
|
||||||
|
bool loop = false;
|
||||||
|
if (yaml.contains("animations") && yaml["animations"].contains(key)) {
|
||||||
|
const auto& anim = yaml["animations"][key];
|
||||||
|
if (anim.contains("frames")) {
|
||||||
|
frames.clear();
|
||||||
|
for (const auto& f : anim["frames"]) {
|
||||||
|
// cppcheck-suppress useStlAlgorithm
|
||||||
|
frames.push_back(f.get_value<int>());
|
||||||
|
}
|
||||||
|
if (frames.empty()) { frames.push_back(0); }
|
||||||
|
}
|
||||||
|
if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); }
|
||||||
|
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
|
||||||
|
}
|
||||||
|
return {frames, fps, loop};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Dust::Dust(const fkyaml::node& yaml)
|
||||||
|
: anim_jump_(buildAnim(yaml, "jump")),
|
||||||
|
anim_land_(buildAnim(yaml, "land")) {
|
||||||
|
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("dust");
|
||||||
|
collides_ = false;
|
||||||
|
interactable_ = false;
|
||||||
|
// Per defecte JUMP; el spawner pot canviar-la abans del primer tick.
|
||||||
|
anim_ = anim_jump_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Dust::setVariant(Variant v) {
|
||||||
|
anim_ = (v == Variant::JUMP) ? anim_jump_ : anim_land_;
|
||||||
|
if (anim_.has_value()) { anim_->reset(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void Dust::update(float delta_time) {
|
||||||
|
Entity::update(delta_time);
|
||||||
|
if (anim_.has_value() && anim_->completed()) { markDead(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
REGISTER_ENTITY("dust", Dust)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "game/entities/animation_component.hpp"
|
||||||
|
#include "game/entities/entity.hpp"
|
||||||
|
|
||||||
|
// Polvet transitori: efecte purament visual que viu una animació i
|
||||||
|
// s'autoelimina (`markDead`) quan acaba. Spawneja el `GameScene` com a
|
||||||
|
// reacció a events `player_jumped` i `player_landed` del bus. Les dues
|
||||||
|
// variants (despegue / aterratge) viuen al mateix YAML amb dues
|
||||||
|
// animacions distintes (`jump` i `land`); l'instància tria una via
|
||||||
|
// `setVariant` just després de construir-se.
|
||||||
|
class Dust : public Entity {
|
||||||
|
public:
|
||||||
|
enum class Variant : std::uint8_t {
|
||||||
|
JUMP,
|
||||||
|
LAND,
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit Dust(const fkyaml::node& yaml);
|
||||||
|
|
||||||
|
// Tria quina animació es reproduirà. Cal cridar-lo abans del primer
|
||||||
|
// `update`; per defecte la variant és JUMP.
|
||||||
|
void setVariant(Variant v);
|
||||||
|
|
||||||
|
// Tickeja l'animació activa. Quan arriba a `completed()`, marquem
|
||||||
|
// l'entitat morta perquè l'`EntityManager` la purgue al final del
|
||||||
|
// frame.
|
||||||
|
void update(float delta_time) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
AnimationComponent anim_jump_;
|
||||||
|
AnimationComponent anim_land_;
|
||||||
|
};
|
||||||
@@ -88,6 +88,14 @@ class Entity {
|
|||||||
[[nodiscard]] auto collides() const -> bool { return collides_; }
|
[[nodiscard]] auto collides() const -> bool { return collides_; }
|
||||||
[[nodiscard]] auto interactable() const -> bool { return interactable_; }
|
[[nodiscard]] auto interactable() const -> bool { return interactable_; }
|
||||||
|
|
||||||
|
// Marca per a entitats transitòries: efectes (polvet, espurnes,
|
||||||
|
// explosions...) que viuen una sola animació i s'autoeliminen quan
|
||||||
|
// acaba. L'`EntityManager` purga totes les entitats amb `dead_=true`
|
||||||
|
// al final de cada `update`. Les entitats persistents (sarcs, momies,
|
||||||
|
// portes, jugadors) mai no la posen i hi viuen sempre.
|
||||||
|
[[nodiscard]] auto isDead() const -> bool { return dead_; }
|
||||||
|
void markDead() { dead_ = true; }
|
||||||
|
|
||||||
// Associar el bus una sola vegada (propietat externa, p.ex.
|
// Associar el bus una sola vegada (propietat externa, p.ex.
|
||||||
// `EntityManager`). Entitats que no publiquen events (Torch, trampes
|
// `EntityManager`). Entitats que no publiquen events (Torch, trampes
|
||||||
// estàtiques de moment) el poden ignorar.
|
// estàtiques de moment) el poden ignorar.
|
||||||
@@ -162,6 +170,7 @@ class Entity {
|
|||||||
bool collides_{false};
|
bool collides_{false};
|
||||||
bool interactable_{false};
|
bool interactable_{false};
|
||||||
int height_{0};
|
int height_{0};
|
||||||
|
bool dead_{false};
|
||||||
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
||||||
EventBus* bus_{nullptr};
|
EventBus* bus_{nullptr};
|
||||||
};
|
};
|
||||||
@@ -64,7 +64,16 @@ auto EntityManager::addSarcophagus(std::unique_ptr<Sarcophagus> sarc) -> Sarcoph
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EntityManager::update(float delta_time) {
|
void EntityManager::update(float delta_time) {
|
||||||
|
// ATENCIÓ: si en el futur una entitat publica al bus events que
|
||||||
|
// resulten en `entity_manager_.add()` (push_back a `entities_`), açò
|
||||||
|
// invalidaria l'iterador de la ranged-for. Avui no passa: els spawns
|
||||||
|
// runtime venen de fora del cicle d'`update` (p.ex. el polvet del
|
||||||
|
// saltador es spawneja durant `Player::tick`, abans d'aquest mètode).
|
||||||
for (auto& e : entities_) { e->update(delta_time); }
|
for (auto& e : entities_) { e->update(delta_time); }
|
||||||
|
// Purga d'entitats marcades mortes aquest frame (efectes acabats,
|
||||||
|
// p.ex. polvet d'aterratge). Les persistents (sarcs, momies, etc.)
|
||||||
|
// mai no es marquen.
|
||||||
|
std::erase_if(entities_, [](const auto& e) { return e->isDead(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntityManager::notifyPlayerTilePainted(int col, int row) {
|
void EntityManager::notifyPlayerTilePainted(int col, int row) {
|
||||||
|
|||||||
@@ -41,9 +41,17 @@ struct PaintedTile {
|
|||||||
int tile_index{-1};
|
int tile_index{-1};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Coordenades en píxels al canvas. Payload genèric per a events que
|
||||||
|
// volen indicar "una cosa ha passat aquí" — actualment `player_jumped`
|
||||||
|
// i `player_landed`, que el GameScene escolta per spawnejar polvet.
|
||||||
|
struct WorldPos {
|
||||||
|
float x{0.0F};
|
||||||
|
float y{0.0F};
|
||||||
|
};
|
||||||
|
|
||||||
struct Event {
|
struct Event {
|
||||||
std::string name;
|
std::string name;
|
||||||
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos, PaintedTile>;
|
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos, PaintedTile, WorldPos>;
|
||||||
Payload payload;
|
Payload payload;
|
||||||
// Jugador que ha originat (o provocat) l'event. -1 = sense jugador
|
// Jugador que ha originat (o provocat) l'event. -1 = sense jugador
|
||||||
// associat (events del món, com `sarcophagus_opened` des de la pròpia
|
// associat (events del món, com `sarcophagus_opened` des de la pròpia
|
||||||
@@ -90,6 +98,9 @@ class EventBus {
|
|||||||
void publish(const std::string& name, PaintedTile painted) {
|
void publish(const std::string& name, PaintedTile painted) {
|
||||||
publish(Event{.name = name, .payload = painted});
|
publish(Event{.name = name, .payload = painted});
|
||||||
}
|
}
|
||||||
|
void publish(const std::string& name, WorldPos pos) {
|
||||||
|
publish(Event{.name = name, .payload = pos});
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Subscription {
|
struct Subscription {
|
||||||
|
|||||||
@@ -579,6 +579,15 @@ void Player::startJump() {
|
|||||||
if (!jump_config.sound_key.empty() && Audio::get() != nullptr) {
|
if (!jump_config.sound_key.empty() && Audio::get() != nullptr) {
|
||||||
Audio::get()->playSound(jump_config.sound_key, Audio::Group::GAME);
|
Audio::get()->playSound(jump_config.sound_key, Audio::Group::GAME);
|
||||||
}
|
}
|
||||||
|
// Avisem al bus perquè el GameScene puga spawnejar el polvet de
|
||||||
|
// despegue al peu del jugador.
|
||||||
|
if (bus_ != nullptr) {
|
||||||
|
bus_->publish(Event{
|
||||||
|
.name = "player_jumped",
|
||||||
|
.payload = WorldPos{.x = getPosX() + (static_cast<float>(sprite_w) * 0.5F), .y = getGroundFeetY()},
|
||||||
|
.player_id = id_,
|
||||||
|
});
|
||||||
|
}
|
||||||
// Prou: la velocitat horitzontal del salt és la `intended_v*` actual
|
// Prou: la velocitat horitzontal del salt és la `intended_v*` actual
|
||||||
// (input del frame). El facing també queda com està. Ja no es
|
// (input del frame). El facing també queda com està. Ja no es
|
||||||
// tocaran fins a aterrar.
|
// tocaran fins a aterrar.
|
||||||
@@ -676,6 +685,13 @@ auto Player::integrateJump(float delta_time) -> bool {
|
|||||||
if (!jump_config.land_sound_key.empty() && Audio::get() != nullptr) {
|
if (!jump_config.land_sound_key.empty() && Audio::get() != nullptr) {
|
||||||
Audio::get()->playSound(jump_config.land_sound_key, Audio::Group::GAME);
|
Audio::get()->playSound(jump_config.land_sound_key, Audio::Group::GAME);
|
||||||
}
|
}
|
||||||
|
if (bus_ != nullptr) {
|
||||||
|
bus_->publish(Event{
|
||||||
|
.name = "player_landed",
|
||||||
|
.payload = WorldPos{.x = getPosX() + (static_cast<float>(sprite_w) * 0.5F), .y = getGroundFeetY()},
|
||||||
|
.player_id = id_,
|
||||||
|
});
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -26,7 +26,9 @@
|
|||||||
#include "core/system/event_buffer.hpp"
|
#include "core/system/event_buffer.hpp"
|
||||||
#include "core/ui/ui_config.hpp"
|
#include "core/ui/ui_config.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "game/entities/concrete/dust.hpp"
|
||||||
#include "game/entities/entity.hpp"
|
#include "game/entities/entity.hpp"
|
||||||
|
#include "game/entities/entity_loader.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
#include "game/entities/player.hpp"
|
#include "game/entities/player.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
@@ -273,6 +275,21 @@ GameScene::GameScene(Config config)
|
|||||||
<< " dmg=" << (dmg != nullptr ? *dmg : 0) << '\n';
|
<< " dmg=" << (dmg != nullptr ? *dmg : 0) << '\n';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Polvet: precarreguem el surface del Dust una sola vegada perquè
|
||||||
|
// els spawns runtime de l'entitat no facin disc al primer salt.
|
||||||
|
// També ens subscrivim a `player_jumped` i `player_landed` per
|
||||||
|
// spawnejar el polvet de despegue / aterratge a la posició dels
|
||||||
|
// peus que el Player ens dóna al payload.
|
||||||
|
Resource::Cache::get().loadSurface("dust.gif", "data/entities/dust/dust.gif");
|
||||||
|
entity_manager_.getBus().subscribe("player_jumped", [this](const Event& ev) {
|
||||||
|
const auto* pos = std::get_if<WorldPos>(&ev.payload);
|
||||||
|
if (pos != nullptr) { spawnDust(pos->x, pos->y, false); }
|
||||||
|
});
|
||||||
|
entity_manager_.getBus().subscribe("player_landed", [this](const Event& ev) {
|
||||||
|
const auto* pos = std::get_if<WorldPos>(&ev.payload);
|
||||||
|
if (pos != nullptr) { spawnDust(pos->x, pos->y, true); }
|
||||||
|
});
|
||||||
|
|
||||||
// Controls per jugador: P1 amb fletxes, P2 amb WASD. Cada Player
|
// Controls per jugador: P1 amb fletxes, P2 amb WASD. Cada Player
|
||||||
// escolta les seues pròpies tecles dins del seu `update`, no hi ha
|
// escolta les seues pròpies tecles dins del seu `update`, no hi ha
|
||||||
// estat global d'input.
|
// estat global d'input.
|
||||||
@@ -382,6 +399,30 @@ GameScene::GameScene(Config config)
|
|||||||
|
|
||||||
GameScene::~GameScene() = default;
|
GameScene::~GameScene() = default;
|
||||||
|
|
||||||
|
void GameScene::spawnDust(float feet_x_center, float feet_y, bool is_landing) {
|
||||||
|
// Mida del sprite de Dust llegida del seu YAML. Hardcoded per
|
||||||
|
// simplificar — si canvia, només cal tocar dust.yaml + aquesta
|
||||||
|
// constant. La caixa de render es centra horitzontalment al peu i
|
||||||
|
// s'alinea al sòl amb un pixel de marge cap amunt perquè la sombra
|
||||||
|
// quede pintada per davant del polvet i el polvet, en l'y-sort,
|
||||||
|
// entre per davant del jugador (mateixa lògica que la sombra).
|
||||||
|
constexpr float DUST_W = 40.0F;
|
||||||
|
constexpr float DUST_H = 16.0F;
|
||||||
|
auto dust = std::make_unique<Dust>(EntityLoader::loadTemplate("dust"));
|
||||||
|
dust->setVariant(is_landing ? Dust::Variant::LAND : Dust::Variant::JUMP);
|
||||||
|
dust->setSurface(Resource::Cache::get().getSurface("dust.gif"));
|
||||||
|
// Recolzat al sòl, amb la base del rect 1 px per sobre del peu del
|
||||||
|
// jugador → en l'y-sort surt amb feet_y un pèl menor i, per
|
||||||
|
// estabilitat de stable_sort, queda darrere del jugador.
|
||||||
|
dust->setPixelRect({
|
||||||
|
.x = feet_x_center - (DUST_W * 0.5F),
|
||||||
|
.y = feet_y - DUST_H - 1.0F,
|
||||||
|
.w = DUST_W,
|
||||||
|
.h = DUST_H,
|
||||||
|
});
|
||||||
|
entity_manager_.add(std::move(dust));
|
||||||
|
}
|
||||||
|
|
||||||
void GameScene::handlePauseInput() {
|
void GameScene::handlePauseInput() {
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
// Mutex amb l'editor: si està actiu, ignorem la tecla de pausa.
|
// Mutex amb l'editor: si està actiu, ignorem la tecla de pausa.
|
||||||
|
|||||||
@@ -56,6 +56,12 @@ class GameScene : public Scene {
|
|||||||
// (no escalat) perquè la transició viu en temps real.
|
// (no escalat) perquè la transició viu en temps real.
|
||||||
void updatePauseTransition(int delta_ms);
|
void updatePauseTransition(int delta_ms);
|
||||||
|
|
||||||
|
// Spawna un Dust transitori centrat a (`feet_x_center`, `feet_y`).
|
||||||
|
// Cridat per la subscripció del bus a `player_jumped` /
|
||||||
|
// `player_landed` per pintar el polvet de despegue / aterratge.
|
||||||
|
// `is_landing` selecciona la variant de l'animació al `Dust`.
|
||||||
|
void spawnDust(float feet_x_center, float feet_y, bool is_landing);
|
||||||
|
|
||||||
Config config_;
|
Config config_;
|
||||||
Tilemap tilemap_;
|
Tilemap tilemap_;
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user