refactor(em): un sol vector entities_ — polimorfisme per la
discriminació
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<DynamicHazard*>` 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`.
This commit is contained in:
1 parent
28ff425c89
commit
a29ea641ab
5 files changed
+61
-118
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -74,19 +74,7 @@ auto EntityManager::add(std::unique_ptr<Entity> 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<DynamicHazard*>(entity.get()) != nullptr) {
|
||||
// Transferim l'ownership al unique_ptr<DynamicHazard> 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<DynamicHazard> hazard{static_cast<DynamicHazard*>(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<Sarcophagus> 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<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 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<Entity*> {
|
||||
// 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<Entity*> 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<Entity*> {
|
||||
std::vector<Entity*> 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<DynamicHazard*> {
|
||||
std::vector<DynamicHazard*> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Entity*>;
|
||||
[[nodiscard]] auto getMapEntities() const -> std::vector<Entity*>;
|
||||
[[nodiscard]] auto getActors() const -> std::vector<DynamicHazard*>;
|
||||
|
||||
// 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<std::unique_ptr<Entity>> map_entities_;
|
||||
std::vector<std::unique_ptr<DynamicHazard>> 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<std::unique_ptr<Entity>> 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)
|
||||
|
||||
@@ -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<Player*>& 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() {
|
||||
|
||||
@@ -1793,18 +1793,12 @@ void GameScene::render() {
|
||||
};
|
||||
std::vector<Renderable> 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
|
||||
|
||||
Reference in new issue
Block a user