refactor(entities): polimorfisme per substituir dynamic_casts

Quatre virtuals nous a `Entity` capten decisions que abans vivien al
manager com a discriminacions de tipus:

- `onWhipHit(WhipAttack*)` → bool. Default no-op/false. Override a
  `Mummy` per entrar HURT si és whippable. WhipAttack::applyDamage
  passa de `dynamic_cast<Mummy*>` + `isWhippable()` a una crida
  polimòrfica neta. RollingRock no override → immune al látigo per
  disseny, sense flag.

- `onJustSpawned()`. Default no-op. Override a `DynamicHazard` per
  disparar `triggerSpawnExit()`. `spawnPendingActors` i `spawnActor`
  passen de `dynamic_cast<DynamicHazard*>(observer)->triggerSpawnExit()`
  a `observer->onJustSpawned()`.

- `onRemoved()`. Default no-op. Override a `Sarcophagus` per cridar
  `entity_manager_->onSarcophagusRemoved()`. `removeEntity` passa de
  `getName() == "sarcophagus"` a dispatch polimòrfic.

- `shouldReceiveContact()`. Default `collides() || triggersOnOverlap()`.
  Override a `DynamicHazard` per excloure SPAWN. `checkPlayerCollisions`
  passa de tres condicions inline per a sacs distints a una crida
  unificada.

`whipDirToMummyDir` es mou de whip_attack.cpp a mummy.cpp — el mapping
viu on s'usa (dins de `Mummy::onWhipHit`).

`hitMummyIfApplicable` es renomena a `hitEntityIfApplicable`: ja no
discrimina tipus, aplica `entity->onWhipHit` i marca `already_hit_`
segons el retorn.

Comportament idèntic, tests verds. Aquest commit prepara el següent:
amb el polimorfisme madurat, el sac dual ja no aporta valor i podem
tornar a un únic `entities_`.
This commit is contained in:
JailDesigner committed 2026-05-11 13:01:26 +02:00
1 parent e8418f89e0
commit 28ff425c89
11 files changed
+177 -58

No files matched your search

+28
View File
@@ -10,6 +10,7 @@
#include "core/audio/audio.hpp" #include "core/audio/audio.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "game/entities/concrete/whip_attack.hpp"
#include "game/entities/entity_factory.hpp" #include "game/entities/entity_factory.hpp"
#include "game/entities/entity_manager.hpp" #include "game/entities/entity_manager.hpp"
#include "game/entities/event_bus.hpp" #include "game/entities/event_bus.hpp"
@@ -65,6 +66,24 @@ namespace {
// més d'una lane (cas marginal a velocitats típiques, però pot passar // més d'una lane (cas marginal a velocitats típiques, però pot passar
// amb dt grans). Comparació estricta a un costat: no dispara quan la // amb dt grans). Comparació estricta a un costat: no dispara quan la
// caixa ja es troba alineada a la lane (hi està després del snap). // caixa ja es troba alineada a la lane (hi està després del snap).
// Mapatge WhipAttack::Direction → Mummy::Direction. Viuen amb noms i
// valors paral·lels però són dos enums distints (cada classe modela el
// seu propi domini). Conversió 1-a-1; `NONE` no apareix com a possible
// sortida del whip, però la deixem per coherència defensiva.
auto whipDirToMummyDir(WhipAttack::Direction d) -> Mummy::Direction {
switch (d) {
case WhipAttack::Direction::NORTH:
return Mummy::Direction::NORTH;
case WhipAttack::Direction::SOUTH:
return Mummy::Direction::SOUTH;
case WhipAttack::Direction::EAST:
return Mummy::Direction::EAST;
case WhipAttack::Direction::WEST:
return Mummy::Direction::WEST;
}
return Mummy::Direction::NONE;
}
auto findLaneCross(float prev, float delta, float base, float pitch, int max_idx) -> std::optional<float> { auto findLaneCross(float prev, float delta, float base, float pitch, int max_idx) -> std::optional<float> {
const float NEXT = prev + delta; const float NEXT = prev + delta;
if (delta > 0.0F) { if (delta > 0.0F) {
@@ -520,6 +539,15 @@ void Mummy::applyChoice(Direction dir) {
} }
} }
auto Mummy::onWhipHit(WhipAttack* attack) -> bool {
// Defensiu: el filtre `shouldReceiveContact` ja salta DynamicHazards en
// SPAWN, però blindem aquesta crida si arribara per un camí no central.
if (isSpawning()) { return false; }
if (!whippable_ || attack == nullptr) { return false; }
enterHurt(whipDirToMummyDir(attack->getDirection()));
return true;
}
void Mummy::onPlayerContact(Player* player) { void Mummy::onPlayerContact(Player* player) {
// Defensiu: el filtre central a EntityManager::checkPlayerCollisions ja // Defensiu: el filtre central a EntityManager::checkPlayerCollisions ja
// salta DynamicHazards en SPAWN, però blindem la crida directa. // salta DynamicHazards en SPAWN, però blindem la crida directa.
+6
View File
@@ -64,6 +64,12 @@ class Mummy : public DynamicHazard {
// Contacte amb el jugador: publica `player_hit` amb el dany. // Contacte amb el jugador: publica `player_hit` amb el dany.
void onPlayerContact(Player* player) override; void onPlayerContact(Player* player) override;
// Contacte amb el fueteig: si la mòmia és whippable, entra a HURT amb
// la direcció derivada del whip. Retorna true si efectivament ha
// rebut dany — el WhipAttack ho usa per al `already_hit_`. Substitueix
// el `dynamic_cast<Mummy*>` que feia el whip abans per discriminar.
auto onWhipHit(WhipAttack* attack) -> bool override;
// Activa l'estat HURT amb la direcció de fuga preferida `flee_dir`. // Activa l'estat HURT amb la direcció de fuga preferida `flee_dir`.
// Si està bloquejada, prova perpendiculars; si totes estan bloquejades, // Si està bloquejada, prova perpendiculars; si totes estan bloquejades,
// la mòmia queda quieta però en HURT durant tot el timer. // la mòmia queda quieta però en HURT durant tot el timer.
@@ -4,6 +4,7 @@
#include <vector> #include <vector>
#include "game/entities/entity_factory.hpp" #include "game/entities/entity_factory.hpp"
#include "game/entities/entity_manager.hpp"
#include "game/entities/event_bus.hpp" #include "game/entities/event_bus.hpp"
namespace { namespace {
@@ -124,4 +125,16 @@ void Sarcophagus::notifyTilePainted(int col, int row) {
} }
} }
void Sarcophagus::onRemoved() {
// El manager comptabilitza els sarcòfags actius en `total_sarcs_` per
// saber quan ha d'obrir la porta automàticament. Quan el editor
// n'esborra un, cal informar-lo: sense aquesta crida, `total_sarcs_`
// quedaria descompassat i la porta no s'obriria mai. Substitueix el
// `getName() == "sarcophagus"` que el manager feia abans —
// polimorfisme net, no comparació de strings.
if (entity_manager_ != nullptr) {
entity_manager_->onSarcophagusRemoved();
}
}
REGISTER_ENTITY("sarcophagus", Sarcophagus) REGISTER_ENTITY("sarcophagus", Sarcophagus)
@@ -43,6 +43,7 @@ class Sarcophagus : public Interactable {
void update(float delta_time) override; void update(float delta_time) override;
void notifyTilePainted(int col, int row) override; void notifyTilePainted(int col, int row) override;
void onRemoved() override;
[[nodiscard]] auto getState() const -> State { return state_; } [[nodiscard]] auto getState() const -> State { return state_; }
[[nodiscard]] auto getPerimeterSize() const -> std::size_t { return perimeter_tiles_.size(); } [[nodiscard]] auto getPerimeterSize() const -> std::size_t { return perimeter_tiles_.size(); }
+26 -34
View File
@@ -262,19 +262,10 @@ void WhipAttack::update(float delta_time) {
namespace { namespace {
auto whipDirToMummyDir(WhipAttack::Direction d) -> Mummy::Direction { // (El mapping WhipAttack::Direction → Mummy::Direction viu ara a
switch (d) { // mummy.cpp, on s'usa des de `Mummy::onWhipHit`. Aquí només queda el
case WhipAttack::Direction::NORTH: // mapping a Player::Cardinal — usat per al friendly-fire entre
return Mummy::Direction::NORTH; // jugadors, que no és polimòrfic via Entity.)
case WhipAttack::Direction::SOUTH:
return Mummy::Direction::SOUTH;
case WhipAttack::Direction::EAST:
return Mummy::Direction::EAST;
case WhipAttack::Direction::WEST:
return Mummy::Direction::WEST;
}
return Mummy::Direction::NONE;
}
auto whipDirToPlayerCardinal(WhipAttack::Direction d) -> Player::Cardinal { auto whipDirToPlayerCardinal(WhipAttack::Direction d) -> Player::Cardinal {
switch (d) { switch (d) {
@@ -296,25 +287,24 @@ namespace {
return STATE != Player::State::HURT && STATE != Player::State::EXITED && STATE != Player::State::DISABLED; return STATE != Player::State::HURT && STATE != Player::State::EXITED && STATE != Player::State::DISABLED;
} }
// Tracta una mòmia candidata: si és whippable i la seua caja solapa // Tracta una entitat candidata: si la seva caixa solapa amb
// amb `damage_rect`, la posa a HURT i la marca al set. Extret per // `damage_rect`, delega la decisió a `entity->onWhipHit(attack)`.
// acotar la complexitat cognitiva d'`applyDamage` (clang-tidy llindar). // Cada entitat decideix què fer (Mummy entra HURT, Torch retorna
// Rep un `DynamicHazard*` (l'iterador d'`applyDamage` ja filtra el // false sense efecte, RollingRock no override → no-op). El retorn
// sac d'actors); només cal el dynamic_cast a Mummy per descartar // true marca el set `already_hit` per evitar segons impactes dins del
// altres hermanes (RollingRock i futurs DynamicHazards no whippables). // mateix swing. Polimorfisme net — sense dynamic_cast.
void hitMummyIfApplicable( void hitEntityIfApplicable(
DynamicHazard* actor, Entity* entity,
const SDL_FRect& damage_rect, const SDL_FRect& damage_rect,
WhipAttack::Direction dir, WhipAttack* attack,
std::unordered_set<void*>& already_hit) { std::unordered_set<void*>& already_hit) {
if (actor == nullptr || actor->isDead()) { return; } if (entity == nullptr || entity->isDead()) { return; }
if (already_hit.contains(static_cast<void*>(actor))) { return; } if (already_hit.contains(static_cast<void*>(entity))) { return; }
auto* mummy = dynamic_cast<Mummy*>(actor); const auto BOX = entity->getCollisionRect();
if (mummy == nullptr || !mummy->isWhippable()) { return; }
const auto BOX = mummy->getCollisionRect();
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; } if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
mummy->enterHurt(whipDirToMummyDir(dir)); if (entity->onWhipHit(attack)) {
already_hit.insert(static_cast<void*>(actor)); already_hit.insert(static_cast<void*>(entity));
}
} }
void hitPlayerIfApplicable( void hitPlayerIfApplicable(
@@ -339,12 +329,14 @@ namespace {
void WhipAttack::applyDamage() { void WhipAttack::applyDamage() {
if (entity_manager_ != nullptr) { if (entity_manager_ != nullptr) {
// Només els actors poden rebre el fuetejo (decoracions, sarcs, // Iterem el sac d'actors (DynamicHazards). Les entitats
// forats... no són hits del whip). Iterem el sac `actors_` // estàtiques (sarc, antorxa, forat, traps) no reben fueteig per
// directament — sense recórrer tot el mapa per descartar-ne la // disseny — la decisió viu en el flag de saco, no en filtres
// major part. // 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()) { for (auto* actor : entity_manager_->getActors()) {
hitMummyIfApplicable(actor, damage_rect_, direction_, already_hit_); hitEntityIfApplicable(actor, damage_rect_, this, already_hit_);
} }
} }
if (players_ != nullptr) { if (players_ != nullptr) {
+25
View File
@@ -28,6 +28,31 @@ void Entity::notifyTilePainted(int /*col*/, int /*row*/) {
// No-op per defecte; només `Sarcophagus` l'usa. // No-op per defecte; només `Sarcophagus` l'usa.
} }
auto Entity::onWhipHit(WhipAttack* /*attack*/) -> bool {
// No-op per defecte; les entitats no-whippables (Torch, Hole, Sarc,
// Door, RollingRock...) retornen `false`. `Mummy` n'és l'override
// efectiu. El caller usa el retorn per marcar `already_hit_` —
// entitats que no han rebut dany NO bloquegen futurs frames del whip.
return false;
}
void Entity::onJustSpawned() {
// No-op per defecte. `DynamicHazard` override per disparar el seu
// `triggerSpawnExit` (anima la fase EMERGING just després de néixer).
}
void Entity::onRemoved() {
// No-op per defecte. `Sarcophagus` override per decrementar el
// comptador `total_sarcs_` del manager.
}
auto Entity::shouldReceiveContact() const -> bool {
// Default: l'entitat rep contacte si és sòlida (`collides_`) o si és
// un trigger d'àrea (`triggers_on_overlap_`). `DynamicHazard` afegeix
// el filtre de SPAWN — mentre està emergint, ignora contactes.
return collides_ || triggers_on_overlap_;
}
auto Entity::getCollisionRect() const -> SDL_FRect { auto Entity::getCollisionRect() const -> SDL_FRect {
if (!collision_box_.has_value()) { return pixel_rect_; } if (!collision_box_.has_value()) { return pixel_rect_; }
return { return {
+33
View File
@@ -19,6 +19,7 @@ class Player;
class EventBus; class EventBus;
class Surface; class Surface;
class EntityManager; class EntityManager;
class WhipAttack;
// Entitat genèrica del món. Actua com a dada en comú per a tots els actors // Entitat genèrica del món. Actua com a dada en comú per a tots els actors
// del joc (decoratius, interactables, perills mòbils i estàtics): manté // del joc (decoratius, interactables, perills mòbils i estàtics): manté
@@ -86,6 +87,38 @@ class Entity {
// (per marcar perímetre); per defecte no-op. // (per marcar perímetre); per defecte no-op.
virtual void notifyTilePainted(int col, int row); virtual void notifyTilePainted(int col, int row);
// L'entitat ha rebut un fueteig amb solapament. Es passa el punter al
// `WhipAttack` perquè la subclasse pugui llegir-ne direcció o atributs
// (mateix patró que `onPlayerContact(Player*)`). Retorna `true` si
// efectivament ha aplicat dany — el caller en marca el set
// `already_hit_` per evitar segons impactes dins del mateix swing.
// Default: no fa res, retorna `false`. Override a `Mummy` (whippable)
// i a futurs enemics whippables. `RollingRock` no override → roca
// immune al látigo per defecte.
virtual auto onWhipHit(WhipAttack* attack) -> bool;
// Hook just després de néixer al món. El manager el crida quan
// `addSpawn` / `spawnActor` / `spawnPendingActors` instancien una
// entitat. La base no fa res; `DynamicHazard` override per disparar
// el seu `triggerSpawnExit` (anima la fase EMERGING). Substitueix el
// `dynamic_cast<DynamicHazard*>(observer)->triggerSpawnExit()` que el
// manager feia abans — ara el dispatch és polimòrfic.
virtual void onJustSpawned();
// Hook just abans que el manager esborri l'entitat (via
// `removeEntity`, típicament al editor). Permet a subclasses
// notificar al manager o publicar events per mantenir invariants
// (p.ex. `Sarcophagus` decrementa `total_sarcs_`). Default no-op.
virtual void onRemoved();
// Decisió polimòrfica de si aquesta entitat ha de rebre el callback
// `onPlayerContact` aquest frame. La base permet contacte si `collides_`
// o `triggers_on_overlap_` estan posats. Subclasses poden afegir
// condicions transitòries (p.ex. `DynamicHazard` retorna `false` mentre
// està en SPAWN). Centralitza el filtre que abans vivia al manager
// amb dynamic_casts/isSpawning explícits.
[[nodiscard]] virtual auto shouldReceiveContact() const -> bool;
[[nodiscard]] auto getName() const -> const std::string& { return name_; } [[nodiscard]] auto getName() const -> const std::string& { return name_; }
[[nodiscard]] auto getTilePos() const -> TilePos { return tile_; } [[nodiscard]] auto getTilePos() const -> TilePos { return tile_; }
[[nodiscard]] auto getPixelRect() const -> SDL_FRect { return pixel_rect_; } [[nodiscard]] auto getPixelRect() const -> SDL_FRect { return pixel_rect_; }
+5 -2
View File
@@ -102,7 +102,11 @@ void EntityManager::setDoor(Door* door) {
auto EntityManager::removeEntity(Entity* observer) -> bool { auto EntityManager::removeEntity(Entity* observer) -> bool {
if (observer == nullptr) { return false; } if (observer == nullptr) { return false; }
const bool WAS_DOOR = (door_ == observer); const bool WAS_DOOR = (door_ == observer);
const bool WAS_SARC = (observer->getName() == "sarcophagus"); // `onRemoved` és polimòrfic: cada subclasse decideix què cal
// notificar quan se l'esborra. Avui només Sarcophagus l'override per
// 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 // Busquem en els dos sacs. El sarc i la door sempre viuen a
// `map_entities_` per disseny; un actor (DynamicHazard) viu a `actors_`. // `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()) { if (auto it = std::ranges::find_if(map_entities_, [observer](const auto& up) { return up.get() == observer; }); it != map_entities_.end()) {
@@ -113,7 +117,6 @@ auto EntityManager::removeEntity(Entity* observer) -> bool {
return false; return false;
} }
if (WAS_DOOR) { door_ = nullptr; } if (WAS_DOOR) { door_ = nullptr; }
if (WAS_SARC && total_sarcs_ > 0) { --total_sarcs_; }
// No tocar `opened_sarcs_` aquí: l'editor congela el joc i no pot // No tocar `opened_sarcs_` aquí: l'editor congela el joc i no pot
// disparar-se cap esdeveniment derivat. Quan el GameScene faça el // disparar-se cap esdeveniment derivat. Quan el GameScene faça el
// reload posterior, els comptadors es re-deriven d'inici. // reload posterior, els comptadors es re-deriven d'inici.
+9
View File
@@ -173,6 +173,15 @@ class EntityManager {
// sarcs. No-op si la porta ja està oberta o si el nivell no en té. // sarcs. No-op si la porta ja està oberta o si el nivell no en té.
void forceOpenDoor(); void forceOpenDoor();
// Hook que `Sarcophagus::onRemoved` crida per decrementar el
// comptador `total_sarcs_`. Públic perquè el sarcòfag és la única
// entitat que necessita aquest manteniment, però l'API es manté
// genèrica: si en el futur Door volgués notificar quelcom semblant,
// afegim el seu propi callback.
void onSarcophagusRemoved() {
if (total_sarcs_ > 0) { --total_sarcs_; }
}
private: private:
EventBus bus_; EventBus bus_;
// Dos sacs lògics: // Dos sacs lògics:
@@ -100,20 +100,17 @@ void EntityManager::checkPlayerCollisions(const std::vector<Player*>& players) {
} }
}; };
// Sac de mapa: filtre per col·lisió / trigger. Tanquem el gap latent // Tots dos sacs apliquen el mateix filtre polimòrfic
// de FloorTrap/WallTrap (que tenien `collides_=false` i mai no es // `shouldReceiveContact()`. Per defecte equival a `collides() ||
// disparaven) i obrim la porta a hazards no-físics com `Hole`. // 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_) { for (const auto& entity : map_entities_) {
if (!entity->collides() && !entity->triggersOnOverlap()) { continue; } if (!entity->shouldReceiveContact()) { continue; }
apply_contacts(entity.get()); apply_contacts(entity.get());
} }
// Sac d'actors (DynamicHazards): tots fan dany quan són actius.
// Saltem els que estan en SPAWN: visibles però sense interacció
// durant la fase inicial de ROOM_INTRO i durant futurs spawns en
// runtime. Sense dynamic_cast: el tipus ja és DynamicHazard.
for (const auto& actor : actors_) { for (const auto& actor : actors_) {
if (actor->isSpawning()) { continue; } if (!actor->shouldReceiveContact()) { continue; }
if (!actor->collides() && !actor->triggersOnOverlap()) { continue; }
apply_contacts(actor.get()); apply_contacts(actor.get());
} }
} }
@@ -500,17 +497,13 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
void EntityManager::spawnPendingActors() { void EntityManager::spawnPendingActors() {
// Itera els pending i els instancia via `addSpawn`. El dispatch d'`add()` // Itera els pending i els instancia via `addSpawn`. El dispatch d'`add()`
// els enruta a `actors_`. El `triggerSpawnExit()` posterior arma el // els enruta a `actors_`. `onJustSpawned()` és polimòrfic: la base
// comptador de SPAWN — la mòmia neix ja en EMERGING, sense esperes. // no fa res, DynamicHazard l'override per disparar `triggerSpawnExit()`
// i entrar a EMERGING. Sense dynamic_cast: la decisió viu dins de
// cada entitat.
for (const auto& pa : pending_actors_) { for (const auto& pa : pending_actors_) {
Entity* observer = addSpawn(pa.spawn_node); Entity* observer = addSpawn(pa.spawn_node);
if (observer == nullptr) { continue; } if (observer != nullptr) { observer->onJustSpawned(); }
// Per categoria YAML (`type: dynamic_hazard`) sabem que és
// DynamicHazard — el dynamic_cast aquí val zero i evita
// assumptions implícites sobre la jerarquia.
if (auto* dh = dynamic_cast<DynamicHazard*>(observer); dh != nullptr) {
dh->triggerSpawnExit();
}
} }
pending_actors_.clear(); pending_actors_.clear();
} }
@@ -530,7 +523,9 @@ auto EntityManager::spawnActor(const std::string& type, int col, int row) -> Dyn
} }
Entity* observer = addSpawn(spawn); Entity* observer = addSpawn(spawn);
if (observer == nullptr) { return nullptr; } if (observer == nullptr) { return nullptr; }
auto* dh = dynamic_cast<DynamicHazard*>(observer); // `onJustSpawned` polimòrfic: per a DynamicHazards arma EMERGING; per
if (dh != nullptr) { dh->triggerSpawnExit(); } // a un type erroni (no dynamic_hazard) queda no-op i retornem nullptr
return dh; // mitjançant el cast final, deixant la signatura `DynamicHazard*` neta.
observer->onJustSpawned();
return dynamic_cast<DynamicHazard*>(observer);
} }
+14
View File
@@ -99,6 +99,20 @@ class DynamicHazard : public Hazard {
[[nodiscard]] auto isSpawning() const -> bool { return spawn_state_ == SpawnState::SPAWNING; } [[nodiscard]] auto isSpawning() const -> bool { return spawn_state_ == SpawnState::SPAWNING; }
[[nodiscard]] auto getSpawnDurationS() const -> float { return spawn_duration_s_; } [[nodiscard]] auto getSpawnDurationS() const -> float { return spawn_duration_s_; }
// Just després d'instanciar-se al món (via spawnPendingActors /
// spawnActor), l'actor entra automàticament en la fase EMERGING:
// arma el comptador i comença l'animació de sortida. Substitueix el
// dynamic_cast que el manager feia abans per cridar `triggerSpawnExit`.
void onJustSpawned() override { triggerSpawnExit(); }
// Mentre està en SPAWN (HIDDEN/EMERGING), l'actor NO rep contacte
// ni amb el jugador ni amb attacks: visible però "no juga". Quan
// passa a ACTIVE, recupera el comportament estàndard d'Entity.
[[nodiscard]] auto shouldReceiveContact() const -> bool override {
if (isSpawning()) { return false; }
return Entity::shouldReceiveContact();
}
// Política de moviment per frame. La base desplaça el pixel_rect_ // Política de moviment per frame. La base desplaça el pixel_rect_
// proporcionalment a velocitat i direcció; les subclasses amb IA la // proporcionalment a velocitat i direcció; les subclasses amb IA la
// sobreescriuen per triar nova direcció a les interseccions o rebotar. // sobreescriuen per triar nova direcció a les interseccions o rebotar.