From a29ea641abc077518e3b367a95e4bf01e547e9af Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 11 May 2026 13:14:53 +0200 Subject: [PATCH] =?UTF-8?q?refactor(em):=20un=20sol=20vector=20`entities?= =?UTF-8?q?=5F`=20=E2=80=94=20polimorfisme=20per=20la=20=20discriminaci?= =?UTF-8?q?=C3=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverteix la separació en sacs introduïda al commit 62f4b85 (rama anterior). Amb els virtuals afegits al commit precedent (`onWhipHit`, `onJustSpawned`, `shouldReceiveContact`, `onRemoved`), ja no cal separar `map_entities_` i `actors_`: cada entitat decideix internament què fa, el manager itera un sol vector i confia en el dispatch virtual. Canvis: - `entities_` torna a ser el contenidor únic. Es desfà el dispatch `dynamic_cast` que `add()` feia per enrutar. - `addSarcophagus`, `removeEntity`, `update`, `notifyPlayerTilePainted`, `findFloorTriggerAt`, `renderAll`, `reload` simplificats: un sol bucle, sense distingir categories. - `checkPlayerCollisions` només recorre `entities_` cridant `shouldReceiveContact` polimòrfic — la lògica de "saltar SPAWN" viu a `DynamicHazard::shouldReceiveContact`. - `getMapEntities()` i `getActors()` desapareixen. Es manté `getEntities()` com a accessor únic. - `WhipAttack::applyDamage` itera `getEntities()` cridant `onWhipHit` polimòrfic. Les entitats no-whippables retornen false (cost: una crida virtual buida) i el callsite no discrimina tipus. - `GameScene::render` mergeja entities + players (dos transforms en comptes de tres) per al z-order. `pending_actors_` es manté: és un concepte ortogonal — entitats que encara no s'han instanciat al món fins que GameScene les "spawneja" al text-exit de ROOM_INTRO. Beneficis del nou disseny: - Cap dynamic_cast dispers per a la lògica de joc (només queden els inevitables: validació de factory i ruteig de la Door). - Afegir un nou enemic implica `class X : public DynamicHazard` i override dels virtuals rellevants — res al manager. - Si un sarc, antorxa, o decoració futura volgués reaccionar al fueteig (trencar-se, encendre's), basta amb un `onWhipHit` override i no cal tocar `WhipAttack::applyDamage`. --- source/game/entities/concrete/whip_attack.cpp | 16 ++-- source/game/entities/entity_manager.cpp | 81 +++++-------------- source/game/entities/entity_manager.hpp | 34 ++++---- .../entities/entity_manager_loadlevel.cpp | 34 ++++---- source/game/scenes/game_scene.cpp | 14 +--- 5 files changed, 61 insertions(+), 118 deletions(-) diff --git a/source/game/entities/concrete/whip_attack.cpp b/source/game/entities/concrete/whip_attack.cpp index 3860816..aa03d57 100644 --- a/source/game/entities/concrete/whip_attack.cpp +++ b/source/game/entities/concrete/whip_attack.cpp @@ -329,14 +329,14 @@ namespace { void WhipAttack::applyDamage() { if (entity_manager_ != nullptr) { - // Iterem el sac d'actors (DynamicHazards). Les entitats - // estàtiques (sarc, antorxa, forat, traps) no reben fueteig per - // disseny — la decisió viu en el flag de saco, no en filtres - // dispersos. Si en el futur cal que un sarc reaccioni al fueteig - // (p.ex. trencar-se), basta amb afegir un override de - // `onWhipHit` allà i iterar també getMapEntities(). - for (auto* actor : entity_manager_->getActors()) { - hitEntityIfApplicable(actor, damage_rect_, this, already_hit_); + // Iterem totes les entitats. La discriminació la fa cada una via + // `onWhipHit`: Mummy entra HURT si és whippable, la resta retorna + // false (no-op). El cost d'iterar antorxes/sarcs/forats és una + // crida virtual buida cada una — menyspreable comparat amb la + // claredat del callsite. Si demà un sarc vol trencar-se al + // fueteig, només cal override de `onWhipHit` allà. + for (auto* entity : entity_manager_->getEntities()) { + hitEntityIfApplicable(entity, damage_rect_, this, already_hit_); } } if (players_ != nullptr) { diff --git a/source/game/entities/entity_manager.cpp b/source/game/entities/entity_manager.cpp index 3a148a1..2bdce84 100644 --- a/source/game/entities/entity_manager.cpp +++ b/source/game/entities/entity_manager.cpp @@ -74,19 +74,7 @@ auto EntityManager::add(std::unique_ptr entity) -> Entity* { entity->setEventBus(&bus_); entity->setEntityManager(this); Entity* raw = entity.get(); - // Enrutament per categoria semàntica: si l'entitat és (o deriva de) - // DynamicHazard va al sac `actors_`, la resta al sac `map_entities_`. - // Així els consumidors que només volen actors itera el sac petit i - // ens estalviem dynamic_casts dispersos al codi (colisió, whip...). - if (dynamic_cast(entity.get()) != nullptr) { - // Transferim l'ownership al unique_ptr tipat fort. - // El `release()` consumeix el seu valor com a argument del ctor — - // sense crida nua que el linter pugui marcar com a ignorada. - std::unique_ptr hazard{static_cast(entity.release())}; - actors_.push_back(std::move(hazard)); - } else { - map_entities_.push_back(std::move(entity)); - } + entities_.push_back(std::move(entity)); // cppcheck-suppress returnDanglingLifetime return raw; } @@ -107,15 +95,9 @@ auto EntityManager::removeEntity(Entity* observer) -> bool { // decrementar el seu comptador. La door es detecta per comparació de // punter (és única, no cal RTTI). observer->onRemoved(); - // Busquem en els dos sacs. El sarc i la door sempre viuen a - // `map_entities_` per disseny; un actor (DynamicHazard) viu a `actors_`. - if (auto it = std::ranges::find_if(map_entities_, [observer](const auto& up) { return up.get() == observer; }); it != map_entities_.end()) { - map_entities_.erase(it); - } else if (auto it_a = std::ranges::find_if(actors_, [observer](const auto& up) { return up.get() == observer; }); it_a != actors_.end()) { - actors_.erase(it_a); - } else { - return false; - } + auto it = std::ranges::find_if(entities_, [observer](const auto& up) { return up.get() == observer; }); + if (it == entities_.end()) { return false; } + entities_.erase(it); if (WAS_DOOR) { door_ = nullptr; } // No tocar `opened_sarcs_` aquí: l'editor congela el joc i no pot // disparar-se cap esdeveniment derivat. Quan el GameScene faça el @@ -128,8 +110,7 @@ auto EntityManager::addSarcophagus(std::unique_ptr sarc) -> Sarcoph sarc->setEventBus(&bus_); sarc->setEntityManager(this); Sarcophagus* raw = sarc.get(); - // El sarc és Interactable, viu al sac `map_entities_`. - map_entities_.push_back(std::move(sarc)); + entities_.push_back(std::move(sarc)); ++total_sarcs_; // cppcheck-suppress returnDanglingLifetime return raw; @@ -137,34 +118,32 @@ auto EntityManager::addSarcophagus(std::unique_ptr 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 als sacs), açò + // 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 : map_entities_) { e->update(delta_time); } - for (auto& a : actors_) { a->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(map_entities_, [](const auto& e) { return e->isDead(); }); - std::erase_if(actors_, [](const auto& a) { return a->isDead(); }); + std::erase_if(entities_, [](const auto& e) { return e->isDead(); }); } void EntityManager::notifyPlayerTilePainted(int col, int row) { - // Fan-out als dos sacs. Avui només Sarcophagus consumeix (viu a - // map_entities_), però els actors també reben la notificació per - // si en el futur volen reaccionar (cost: una crida virtual buida). - for (auto& e : map_entities_) { e->notifyTilePainted(col, row); } - for (auto& a : actors_) { a->notifyTilePainted(col, row); } + // Fan-out polimòrfic a totes les entitats: cada una decideix què fer + // (`Sarcophagus` actualitza el seu perímetre, la resta queden en + // no-op). Cost d'iterar 40 elements amb crida virtual buida ~ 1 µs; + // qui paga el cost guanya simplicitat al callsite. + for (auto& e : entities_) { e->notifyTilePainted(col, row); } } auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Entity* { - // Cerca lineal sobre `map_entities_`: els floor decals trigger són - // exclusivament StaticHazards (Hole; en el futur pinchos plans, - // plaques, charcos). Els actors no qualifiquen com a decals — saltem - // directament aquest sac, sense `triggersOnOverlap`/`isFloorDecal` per - // a cada un. La doble condició filtra dins de map_entities_. - for (const auto& entity : map_entities_) { + // Cerca lineal: només `Hole` (i futurs decals trigger) compleixen la + // doble condició `triggersOnOverlap` + `isFloorDecal`. Costos típics + // (poques desenes d'entitats) menyspreables, una crida per frame des + // dels consumidors actius (Player a l'aterratge, Mòmia a la decisió + // de direcció). + for (const auto& entity : entities_) { if (!entity->triggersOnOverlap()) { continue; } if (!entity->isFloorDecal()) { continue; } const auto BOX = entity->getCollisionRect(); @@ -175,27 +154,9 @@ auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Ent } auto EntityManager::getEntities() const -> std::vector { - // Concatenació map + actors per retro-compat (editor, z-sort debug). - // Els callers que ja saben quin sac volen poden cridar - // `getMapEntities` o `getActors` i estalviar la construcció del temp. std::vector out; - out.reserve(map_entities_.size() + actors_.size()); - std::ranges::transform(map_entities_, std::back_inserter(out), [](const auto& e) { return e.get(); }); - std::ranges::transform(actors_, std::back_inserter(out), [](const auto& a) -> Entity* { return a.get(); }); - return out; -} - -auto EntityManager::getMapEntities() const -> std::vector { - std::vector out; - out.reserve(map_entities_.size()); - std::ranges::transform(map_entities_, std::back_inserter(out), [](const auto& e) { return e.get(); }); - return out; -} - -auto EntityManager::getActors() const -> std::vector { - std::vector out; - out.reserve(actors_.size()); - std::ranges::transform(actors_, std::back_inserter(out), [](const auto& a) { return a.get(); }); + out.reserve(entities_.size()); + std::ranges::transform(entities_, std::back_inserter(out), [](const auto& e) { return e.get(); }); return out; } diff --git a/source/game/entities/entity_manager.hpp b/source/game/entities/entity_manager.hpp index 1eeab28..6432762 100644 --- a/source/game/entities/entity_manager.hpp +++ b/source/game/entities/entity_manager.hpp @@ -9,12 +9,12 @@ #include "external/fkyaml_node.hpp" #include "game/entities/event_bus.hpp" -#include "game/entities/hazard.hpp" class Entity; class Sarcophagus; class Door; class Player; +class DynamicHazard; // Contenidor i coordinador d'entitats d'un nivell. Responsabilitats: // 1. `add` registra una entitat, la connecta al bus i manté propietat. @@ -74,14 +74,12 @@ class EntityManager { static void renderEntity(const Entity& entity); // Accés als punters observacionals a les entitats per a consumidors - // externs que necessiten construir una llista ordenada (z-order). - // `getEntities()` concatena els dos sacs interns (mapa + actors) per - // a consumidors retro-compat (editor, z-sort debug). `getMapEntities` - // i `getActors` exposen cada sac per separat — útil per a iteracions - // que ja saben quin tipus volen i estalvien filtres O(n). + // externs que necessiten iterar (z-order, aplicar dany, etc.). El + // polimorfisme dins de cada entitat fa la discriminació: l'iterador + // recorre tot i les entitats que no hagin de respondre tornen no-op + // (p.ex. Torch::onWhipHit retorna false sense efecte). Sense sacs + // separats — el `if (entity_is_X)` viu dins de l'entitat, no del caller. [[nodiscard]] auto getEntities() const -> std::vector; - [[nodiscard]] auto getMapEntities() const -> std::vector; - [[nodiscard]] auto getActors() const -> std::vector; // Comprova contacte del(s) jugador(s) amb entitats `collides()=true`. // Si la caixa base del jugador solapa amb la caixa de col·lisió de @@ -160,7 +158,7 @@ class EntityManager { void setExpectedPlayerCount(int n); [[nodiscard]] auto getBus() -> EventBus& { return bus_; } - [[nodiscard]] auto entityCount() const -> std::size_t { return map_entities_.size() + actors_.size(); } + [[nodiscard]] auto entityCount() const -> std::size_t { return entities_.size(); } [[nodiscard]] auto pendingActorCount() const -> std::size_t { return pending_actors_.size(); } [[nodiscard]] auto sarcophagusCount() const -> std::size_t { return total_sarcs_; } [[nodiscard]] auto openedSarcophagusCount() const -> std::size_t { return opened_sarcs_; } @@ -184,17 +182,13 @@ class EntityManager { private: EventBus bus_; - // Dos sacs lògics: - // `map_entities_` — decoració, static_hazards (Hole, FloorTrap, - // WallTrap) i interactables (Sarcophagus, Door). - // Cargades al loadFromLevel i persistents durant - // tota la sala. - // `actors_` — DynamicHazards (Mummy, RollingRock, futurs - // enemics). Separats per eliminar dynamic_casts - // i preparar el spawn diferit (commit posterior). - // `add()` enruta automàticament segons sigui o no DynamicHazard. - std::vector> map_entities_; - std::vector> actors_; + // Un sol contenidor lògic per a totes les entitats instanciades. La + // discriminació de tipus la fan els virtuals (`onPlayerContact`, + // `onWhipHit`, `shouldReceiveContact`, `notifyTilePainted`...) — el + // manager itera i cada entitat decideix què fa. Hi conviuen + // decoracions, static_hazards, interactables i dynamic_hazards + // (actors). + std::vector> entities_; // Cua d'actors declarats al YAML del mapa però encara no instanciats: // `loadFromLevel` hi empeny tots els DynamicHazards. `spawnPendingActors` // o `spawnActor` consumeixen aquesta cua (o creen un actor on-demand) diff --git a/source/game/entities/entity_manager_loadlevel.cpp b/source/game/entities/entity_manager_loadlevel.cpp index f1a0ee9..cda17b6 100644 --- a/source/game/entities/entity_manager_loadlevel.cpp +++ b/source/game/entities/entity_manager_loadlevel.cpp @@ -57,11 +57,10 @@ void EntityManager::renderEntity(const Entity& e) { } void EntityManager::renderAll() { - // Sense ordenació per z: l'ordre és el d'inserció dins de cada sac. - // El render del joc real (GameScene) construeix el seu propi vector + // Sense ordenació per z: l'ordre és el d'inserció al vector. El + // render del joc real (GameScene) construeix el seu propi vector // ordenat per `feet_y`; aquest helper queda per al editor i tests. - for (const auto& e : map_entities_) { renderEntity(*e); } - for (const auto& a : actors_) { renderEntity(*a); } + for (const auto& e : entities_) { renderEntity(*e); } } namespace { @@ -100,19 +99,15 @@ void EntityManager::checkPlayerCollisions(const std::vector& players) { } }; - // Tots dos sacs apliquen el mateix filtre polimòrfic - // `shouldReceiveContact()`. Per defecte equival a `collides() || - // triggersOnOverlap()`; `DynamicHazard` ho afina afegint el filtre de - // SPAWN (mentre està emergint torna false). Sense dynamic_cast, - // sense filtres específics al callsite. - for (const auto& entity : map_entities_) { + // Filtre polimòrfic unificat: cada entitat decideix si ha de rebre + // contacte (`shouldReceiveContact`). Per defecte equival a `collides() + // || triggersOnOverlap()`; `DynamicHazard` afegeix el filtre de SPAWN + // (mentre està emergint torna false). El callsite no fa dynamic_cast + // ni distingeix categories. + for (const auto& entity : entities_) { if (!entity->shouldReceiveContact()) { continue; } apply_contacts(entity.get()); } - for (const auto& actor : actors_) { - if (!actor->shouldReceiveContact()) { continue; } - apply_contacts(actor.get()); - } } namespace { @@ -410,8 +405,7 @@ void EntityManager::reload(const std::string& level_yaml_path) { // subscripcions implícites desapareixen amb la seua vida (cap d'elles // se subscriu directament al bus — només publiquen). El bus i les // subscripcions externes (PaintLayer, HUD, GameScene) no es toquen. - map_entities_.clear(); - actors_.clear(); + entities_.clear(); pending_actors_.clear(); door_ = nullptr; total_sarcs_ = 0; @@ -475,9 +469,9 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) { // perquè tots els sarcòfags ja estan registrats al fan-out (la // subscripció del manager és al ctor, anterior a qualsevol load). int decal_broadcast = 0; - // Els decals de sòl (Hole, futurs pinchos plans...) són StaticHazards - // i viuen exclusivament a `map_entities_`. Saltem `actors_` per estalviar. - for (const auto& entity : map_entities_) { + // Els decals de sòl (Hole, futurs pinchos plans...) són StaticHazards; + // iterem tot `entities_` i el filtre per `isFloorDecal()` els tria. + for (const auto& entity : entities_) { if (!entity->isFloorDecal()) { continue; } const auto BOX = entity->getCollisionRect(); const auto [COL, ROW] = Map::pixelToTile(BOX.x, BOX.y); @@ -492,7 +486,7 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) { Log::info("EntityManager", "{} tile_painted broadcast inicial (decals de sòl)", decal_broadcast); } - Log::info("EntityManager", "{} map entities + {} actors pending des de {}", loaded, pending_actors_.size(), level_yaml_path); + Log::info("EntityManager", "{} entitats + {} actors pending des de {}", loaded, pending_actors_.size(), level_yaml_path); } void EntityManager::spawnPendingActors() { diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 1ad5b06..35eee74 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -1793,18 +1793,12 @@ void GameScene::render() { }; std::vector order; order.reserve(entity_manager_.entityCount() + players_.size()); - // Mergem els dos sacs (map + actors) i els jugadors en un sol - // vector temporal — el `stable_sort` per `feet_y` posterior fa el - // z-order interleaving entre els tres orígens sense importar el - // sac d'on venen. - const auto MAP = entity_manager_.getMapEntities(); - std::ranges::transform(MAP, std::back_inserter(order), [](Entity* e) { + // Mergem entitats i jugadors en un sol vector temporal — el + // `stable_sort` per `feet_y` posterior fa el z-order interleaving. + const auto ENTITIES = entity_manager_.getEntities(); + std::ranges::transform(ENTITIES, std::back_inserter(order), [](Entity* e) { return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr}; }); - const auto ACTORS = entity_manager_.getActors(); - std::ranges::transform(ACTORS, std::back_inserter(order), [](DynamicHazard* a) -> Renderable { - return Renderable{.feet_y = a->getFeetY(), .entity = a, .player = nullptr}; - }); // Tots els jugadors entren al z-sort. Els fora de joc fan no-op a // `render()`; els actius (inclosos AIRBORNE durant l'spawn aeri) // es pinten amb el seu feet_y normal. La sombra que afecta la