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/wall_trap.cpp
|
||||
source/game/entities/concrete/floor_trap.cpp
|
||||
source/game/entities/concrete/dust.cpp
|
||||
)
|
||||
|
||||
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 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.
|
||||
// `EntityManager`). Entitats que no publiquen events (Torch, trampes
|
||||
// estàtiques de moment) el poden ignorar.
|
||||
@@ -162,6 +170,7 @@ class Entity {
|
||||
bool collides_{false};
|
||||
bool interactable_{false};
|
||||
int height_{0};
|
||||
bool dead_{false};
|
||||
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
||||
EventBus* bus_{nullptr};
|
||||
};
|
||||
@@ -64,7 +64,16 @@ auto EntityManager::addSarcophagus(std::unique_ptr<Sarcophagus> sarc) -> Sarcoph
|
||||
}
|
||||
|
||||
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); }
|
||||
// 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) {
|
||||
|
||||
@@ -41,9 +41,17 @@ struct PaintedTile {
|
||||
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 {
|
||||
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;
|
||||
// Jugador que ha originat (o provocat) l'event. -1 = sense jugador
|
||||
// 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) {
|
||||
publish(Event{.name = name, .payload = painted});
|
||||
}
|
||||
void publish(const std::string& name, WorldPos pos) {
|
||||
publish(Event{.name = name, .payload = pos});
|
||||
}
|
||||
|
||||
private:
|
||||
struct Subscription {
|
||||
|
||||
@@ -579,6 +579,15 @@ void Player::startJump() {
|
||||
if (!jump_config.sound_key.empty() && Audio::get() != nullptr) {
|
||||
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
|
||||
// (input del frame). El facing també queda com està. Ja no es
|
||||
// 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) {
|
||||
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 false;
|
||||
|
||||
@@ -26,7 +26,9 @@
|
||||
#include "core/system/event_buffer.hpp"
|
||||
#include "core/ui/ui_config.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/concrete/dust.hpp"
|
||||
#include "game/entities/entity.hpp"
|
||||
#include "game/entities/entity_loader.hpp"
|
||||
#include "game/entities/event_bus.hpp"
|
||||
#include "game/entities/player.hpp"
|
||||
#include "game/map/map.hpp"
|
||||
@@ -273,6 +275,21 @@ GameScene::GameScene(Config config)
|
||||
<< " 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
|
||||
// escolta les seues pròpies tecles dins del seu `update`, no hi ha
|
||||
// estat global d'input.
|
||||
@@ -382,6 +399,30 @@ GameScene::GameScene(Config config)
|
||||
|
||||
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() {
|
||||
#ifdef _DEBUG
|
||||
// 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.
|
||||
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_;
|
||||
Tilemap tilemap_;
|
||||
|
||||
|
||||
Reference in new issue
Block a user