feat(entities): integració del sistema d'entitats al GameScene

This commit is contained in:
JailDesigner committed 2026-04-21 13:55:21 +02:00
1 parent 96b71b0f65
commit 7191a08618
16 files changed
+683 -15

No files matched your search

+5 -3
View File
@@ -12,9 +12,11 @@ void Entity::update(float delta_time) {
}
void Entity::render() {
// Andamiatge: el render real es cablejarà quan s'integri al GameScene.
// Les subclasses poden sobreescriure si volen lògica especial (p.ex.
// no dibuixar-se en estat inactiu); de moment la base no fa res.
// No-op de base. El dibuix real el fa `EntityManager::renderAll` (viu
// a `entity_manager_loadlevel.cpp`, que sí depèn de `Surface`). Així
// `entity.cpp` no arrossega el pipeline de render al smoke test
// stand-alone. Les subclasses futures poden sobreescriure si volen
// lògica pròpia (p.ex. render condicional segons estat).
}
void Entity::onPlayerContact(Player* /*player*/) {
+19 -1
View File
@@ -2,6 +2,7 @@
#include <SDL3/SDL.h>
#include <memory>
#include <optional>
#include <string>
@@ -10,9 +11,10 @@
// Forward-declaracions: mantenim aquest header lleuger. `Player` viu a
// `source/game/entities/player.hpp` i només es referencia per referència
// dins `onPlayerContact`; `EventBus` és opcional a cada entitat i viu al
// seu header propi.
// seu header propi. `Surface` és el backend de render paletitzat del motor.
class Player;
class EventBus;
class Surface;
// 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é
@@ -78,11 +80,27 @@ class Entity {
// estàtiques de moment) el poden ignorar.
void setEventBus(EventBus* bus) { bus_ = bus; }
// Assigna la caixa de render en píxels. La invoca `EntityManager` durant
// `loadFromLevel` un cop sap la posició definitiva de l'entitat al mapa.
void setPixelRect(SDL_FRect rect) { pixel_rect_ = rect; }
// Assigna la surface per al dibuix. `shared_ptr` perquè les surfaces
// viuen al `Resource::Cache` i es comparteixen. Si no s'assigna,
// `render()` no fa res.
void setSurface(std::shared_ptr<Surface> surface) { surface_ = std::move(surface); }
// Accessor públic (la viewable rep per tipus complet `shared_ptr`, però
// el valor pot ser nul). L'usa `EntityManager::renderAll` per dibuixar
// sense dependre de la dispatch virtual — el dibuix és centralitzat al
// manager per tal de confinar la dependència cap a `Surface`.
[[nodiscard]] auto getSurface() const -> const std::shared_ptr<Surface>& { return surface_; }
protected:
std::string name_;
TilePos tile_{};
SDL_FRect pixel_rect_{};
std::optional<AnimationComponent> anim_;
std::shared_ptr<Surface> surface_{nullptr};
bool collides_{false};
bool interactable_{false};
EventBus* bus_{nullptr};
+19
View File
@@ -6,6 +6,11 @@
#include "game/entities/concrete/sarcophagus.hpp"
#include "game/entities/entity.hpp"
// NOTA: `loadFromLevel` viu a `entity_manager_loadlevel.cpp` per evitar
// arrossegar `Resource::Cache`, `Surface` i les seues dependències (GIF,
// àudio) a consumidors que només volen la base del manager — en
// particular, el smoke test stand-alone.
EntityManager::EntityManager() {
// Subscripció al bus: cada sarcòfag obert incrementa el comptador;
// quan iguala el total registrat, obrim la porta. La subscripció
@@ -16,10 +21,19 @@ EntityManager::EntityManager() {
door_->open();
}
});
// Subscripció al moviment del jugador: quan publica `on_tile_entered`
// amb payload `TilePos`, fem fan-out a totes les entitats perquè els
// sarcòfags puguin actualitzar el seu `visited_`.
tile_sub_id_ = bus_.subscribe("on_tile_entered", [this](const Event& ev) {
const auto* tile = std::get_if<TilePos>(&ev.payload);
if (tile != nullptr) { notifyPlayerEnteredTile(tile->col, tile->row); }
});
}
EntityManager::~EntityManager() {
bus_.unsubscribe(sarc_sub_id_);
bus_.unsubscribe(tile_sub_id_);
}
auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
@@ -53,3 +67,8 @@ void EntityManager::update(float delta_time) {
void EntityManager::notifyPlayerEnteredTile(int col, int row) {
for (auto& e : entities_) { e->notifyTileEntered(col, row); }
}
// NOTA: `renderAll` es defineix a `entity_manager_loadlevel.cpp`. Conté
// blits via `Surface::render`, que dragaria el pipeline complet de render
// (GIF, Canvas, Screen, àudio...) al smoke test. Separar-lo manté el smoke
// autocontingut.
+17
View File
@@ -2,6 +2,7 @@
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "game/entities/event_bus.hpp"
@@ -47,11 +48,26 @@ class EntityManager {
// Tick de totes les entitats.
void update(float delta_time);
// Dibuix de totes les entitats. Sense ordenació per z: la crida dura
// l'ordre d'inserció. El z-order fi (interleaving amb el Player per
// feetY) queda per a una tasca posterior.
void renderAll();
// Fan-out del hook de moviment de jugador: per a cada sarcòfag,
// cridar notifyTileEntered. Les entitats no-sarcòfag ho ignoren per
// defecte (veure Entity::notifyTileEntered).
void notifyPlayerEnteredTile(int col, int row);
// Carrega els spawns d'un YAML de nivell i els afegeix al manager:
// 1. Llegeix el bloc `entities:` via EntityLoader.
// 2. Per cada spawn: crea via factory, carrega la seva surface al
// Resource::Cache, calcula pixel_rect_ segons el tipus
// (sarc: Map::getSarcRect; altres: col/row del tilemap) i l'afegeix.
// 3. Loguea `Entities: N loaded` per facilitar el smoke d'arrencada.
// Requereix que Map::loadFrom ja s'hagi cridat (necessita
// `Map::tile_size`, `tilemap_origin_*` i `getSarcRect`).
void loadFromLevel(const std::string& level_yaml_path);
[[nodiscard]] auto getBus() -> EventBus& { return bus_; }
[[nodiscard]] auto entityCount() const -> std::size_t { return entities_.size(); }
[[nodiscard]] auto sarcophagusCount() const -> std::size_t { return total_sarcs_; }
@@ -64,4 +80,5 @@ class EntityManager {
std::size_t total_sarcs_{0};
std::size_t opened_sarcs_{0};
EventBus::SubId sarc_sub_id_{0};
EventBus::SubId tile_sub_id_{0};
};
@@ -0,0 +1,188 @@
// Implementació d'`EntityManager::loadFromLevel`. Viu en un TU separat
// perquè aquesta és l'única via per la qual el manager toca
// `Resource::Cache`, `Surface` i les seues dependències (GIF, àudio). Els
// consumidors que només volen la base del manager (p.ex. el smoke test
// stand-alone) enllacen `entity_manager.cpp` sense arrossegar aquest fitxer.
#include <filesystem>
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp"
#include "external/fkyaml_node.hpp"
#include "game/entities/concrete/door.hpp"
#include "game/entities/concrete/sarcophagus.hpp"
#include "game/entities/entity.hpp"
#include "game/entities/entity_factory.hpp"
#include "game/entities/entity_loader.hpp"
#include "game/entities/entity_manager.hpp"
#include "game/map/map.hpp"
// Render centralitzat al manager per confinar la dependència cap a
// `Surface`. Les entitats exposen `getSurface()` + `getPixelRect()` i el
// manager dispatcha el blit. Si una entitat no té surface (trigger
// invisible, encara sense sprite) simplement es salta.
void EntityManager::renderAll() {
for (const auto& e : entities_) {
const auto& surface = e->getSurface();
if (surface == nullptr) { continue; }
const auto RECT = e->getPixelRect();
surface->render(static_cast<int>(RECT.x), static_cast<int>(RECT.y));
}
}
namespace {
// Llegeix `sprite.size` o `sprite.path` d'una plantilla i retorna
// (surface_key, pixel_size). Si falta algun camp, torna valors
// raonables (empty key → no carrega; size [16,16] per defecte).
struct SpriteInfo {
std::string key; // clau al Resource::Cache (basename del GIF)
std::string full_path; // ruta absoluta per a loadSurface
float width{16.0F};
float height{16.0F};
bool valid{false};
};
auto readSpriteInfo(const fkyaml::node& tmpl, const std::string& type) -> SpriteInfo {
SpriteInfo info;
if (!tmpl.contains("sprite")) { return info; }
const auto& s = tmpl["sprite"];
if (s.contains("path")) {
const auto REL = s["path"].get_value<std::string>();
info.key = std::filesystem::path(REL).filename().string();
info.full_path = "data/entities/" + type + "/" + REL;
info.valid = true;
}
if (s.contains("size")) {
const auto& sz = s["size"];
if (sz.is_sequence() && sz.size() == 2) {
info.width = static_cast<float>(sz[0].get_value<int>());
info.height = static_cast<float>(sz[1].get_value<int>());
}
}
return info;
}
// Coordenades píxel (canvas) d'una cel·la (col, row) del tilemap. El
// grid del tilemap està 16-alineat respecte a `tilemap_origin_*`, que
// pot ser negatiu per overscan CPC.
auto tileToPixel(int col, int row) -> SDL_FPoint {
return {
.x = static_cast<float>((col * Map::tile_size) + Map::tilemap_origin_x),
.y = static_cast<float>((row * Map::tile_size) + Map::tilemap_origin_y),
};
}
// Converteix un rect píxel (canvas) a la seua cantonada superior-esquerra
// en coordenades de tile del tilemap. Només per a sarcòfags, que naixen
// ancorats a una cel·la exacta de la graella.
struct TileAnchor {
int col;
int row;
};
auto pixelToTileAnchor(const SDL_FRect& rect) -> TileAnchor {
return {
.col = (static_cast<int>(rect.x) - Map::tilemap_origin_x) / Map::tile_size,
.row = (static_cast<int>(rect.y) - Map::tilemap_origin_y) / Map::tile_size,
};
}
// Crea l'entitat per factory i li adjunta la surface si n'hi ha. Retorna
// nullptr si la plantilla no es pot carregar o si el tipus no està
// registrat al factory — el loop principal ja imprimeix el missatge.
auto buildEntity(const std::string& type, SpriteInfo& sprite_out) -> std::unique_ptr<Entity> {
fkyaml::node tmpl;
try {
tmpl = EntityLoader::loadTemplate(type);
} catch (const std::exception& e) {
std::cerr << "EntityManager: " << e.what() << '\n';
return nullptr;
}
auto entity = EntityFactory::instance().create(type, tmpl);
if (entity == nullptr) {
std::cerr << "EntityManager: tipus desconegut '" << type << "'\n";
return nullptr;
}
sprite_out = readSpriteInfo(tmpl, type);
if (sprite_out.valid) {
Resource::Cache::get().loadSurface(sprite_out.key, sprite_out.full_path);
entity->setSurface(Resource::Cache::get().getSurface(sprite_out.key));
}
return entity;
}
} // namespace
// Col·loca i afegeix un Sarcophagus a partir del spawn. `entity` es consumeix
// en èxit (ownership transferit al manager); en error es destrueix aquí.
// Membre privat amagat dins del TU: `EntityManager::` qualifica, però com
// que no hi ha declaració al header, només és visible a aquest fitxer.
namespace {
auto placeSarcophagus(EntityManager& manager, std::unique_ptr<Entity> entity, const fkyaml::node& spawn) -> bool {
if (!spawn.contains("sarc_position")) {
std::cerr << "EntityManager: sarcophagus sense sarc_position\n";
return false;
}
const auto& sp = spawn["sarc_position"];
const auto RECT = Map::getSarcRect(sp[0].get_value<int>(), sp[1].get_value<int>());
const auto ANCHOR = pixelToTileAnchor(RECT);
entity->setPixelRect(RECT);
// Downcast segur: la factory ens garanteix que "sarcophagus" crea
// un `Sarcophagus`. Usant el retorn de `release` al ctor del nou
// `unique_ptr` evitem el lint `bugprone-unused-return-value`.
if (dynamic_cast<Sarcophagus*>(entity.get()) == nullptr) {
std::cerr << "EntityManager: la factory no ha creat un Sarcophagus\n";
return false;
}
std::unique_ptr<Sarcophagus> sarc(static_cast<Sarcophagus*>(entity.release()));
sarc->place(ANCHOR.col, ANCHOR.row, {});
manager.addSarcophagus(std::move(sarc));
return true;
}
auto placeGeneric(EntityManager& manager, std::unique_ptr<Entity> entity, const std::string& type, const SpriteInfo& sprite, const fkyaml::node& spawn) -> bool {
if (!spawn.contains("position")) {
std::cerr << "EntityManager: " << type << " sense position\n";
return false;
}
const auto& p = spawn["position"];
const auto PX = tileToPixel(p[0].get_value<int>(), p[1].get_value<int>());
entity->setPixelRect({.x = PX.x, .y = PX.y, .w = sprite.width, .h = sprite.height});
if (type == "door") {
auto* door_ptr = dynamic_cast<Door*>(entity.get());
manager.add(std::move(entity));
if (door_ptr != nullptr) { manager.setDoor(door_ptr); }
} else {
manager.add(std::move(entity));
}
return true;
}
} // namespace
void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
const auto SPAWNS = EntityLoader::readSpawns(level_yaml_path);
int loaded = 0;
for (const auto& spawn : SPAWNS) {
if (!spawn.contains("type")) { continue; }
const auto TYPE = spawn["type"].get_value<std::string>();
SpriteInfo sprite;
auto entity = buildEntity(TYPE, sprite);
if (entity == nullptr) { continue; }
const bool OK = (TYPE == "sarcophagus")
? placeSarcophagus(*this, std::move(entity), spawn)
: placeGeneric(*this, std::move(entity), TYPE, sprite, spawn);
if (OK) { ++loaded; }
}
std::cout << "EntityManager: " << loaded << " entitats carregades des de "
<< level_yaml_path << '\n';
}
+14 -3
View File
@@ -16,15 +16,23 @@ class Entity;
// Event pub/sub minimalista. Hi ha un únic `EventBus` per mundus (propietat
// de l'`EntityManager`); les entitats que el necessiten en reben un punter
// via `Entity::setEventBus`. Aquest sistema cobreix les necessitats
// actuals (sarcophagus_opened, level_complete, player_hit) sense portar-hi
// senyalització més pesada.
// actuals (sarcophagus_opened, level_complete, player_hit, on_tile_entered)
// sense portar-hi senyalització més pesada.
//
// Decisió: `std::variant` per al payload — evita any/void* i facilita
// consumir events tipats amb `std::get_if`. Si apareixen més tipus de
// payload, s'amplia aquí sense tocar cridants existents.
// Tile (col, row) — payload de `on_tile_entered`. No és un `pair<int,int>`
// per evitar alias ambigus al variant (dos ints sense nom).
struct TilePos {
int col{0};
int row{0};
};
struct Event {
std::string name;
using Payload = std::variant<std::monostate, int, std::string, Entity*>;
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos>;
Payload payload;
};
@@ -60,6 +68,9 @@ class EventBus {
void publish(const std::string& name, int value) {
publish(Event{.name = name, .payload = value});
}
void publish(const std::string& name, TilePos tile) {
publish(Event{.name = name, .payload = tile});
}
private:
struct Subscription {
+30
View File
@@ -6,9 +6,11 @@
#include <cmath>
#include <fstream>
#include <stdexcept>
#include <utility>
#include "core/resources/resource_cache.hpp"
#include "external/fkyaml_node.hpp"
#include "game/entities/event_bus.hpp"
#include "game/map/map.hpp"
#include "utils/defaults.hpp"
@@ -106,6 +108,34 @@ void Player::update(float delta_time) {
if (Map::collides(baseBox())) { setPosY(OLD_Y); }
clampToPlayfield();
// Detecció de canvi de tile. Es calcula *després* del clamp i de les
// col·lisions perquè la posició aquí ja és la definitiva del frame;
// així els sarcòfags no reben falsos positius mentre el Player rellisca
// contra una paret. Si el tile ha canviat, publiquem `on_tile_entered`
// al bus; l'EntityManager farà fan-out a notifyTileEntered.
if (bus_ != nullptr) {
const auto [COL, ROW] = getCurrentTile();
if (COL != last_tile_col_ || ROW != last_tile_row_) {
last_tile_col_ = COL;
last_tile_row_ = ROW;
bus_->publish("on_tile_entered", TilePos{.col = COL, .row = ROW});
}
}
}
auto Player::getCurrentTile() const -> std::pair<int, int> {
// El grid del tilemap és de `Map::tile_size` píxels i està 16-alineat
// respecte a l'origen del tilemap (pot ser negatiu per overscan). El
// centre de la caixa de col·lisió ens dóna el tile on "està" el Player.
const auto BOX = baseBox();
const float CX = BOX.x + (BOX.w * 0.5F);
const float CY = BOX.y + (BOX.h * 0.5F);
const int COL = static_cast<int>(std::floor(
(CX - static_cast<float>(Map::tilemap_origin_x)) / static_cast<float>(Map::tile_size)));
const int ROW = static_cast<int>(std::floor(
(CY - static_cast<float>(Map::tilemap_origin_y)) / static_cast<float>(Map::tile_size)));
return {COL, ROW};
}
void Player::clampToPlayfield() {
+22
View File
@@ -3,9 +3,12 @@
#include <SDL3/SDL.h>
#include <string>
#include <utility>
#include "core/rendering/sprite/animated_sprite.hpp"
class EventBus;
class Player : public AnimatedSprite {
public:
// Mapeig de tecles per al moviment del jugador. Cada jugador local en pot
@@ -47,6 +50,16 @@ class Player : public AnimatedSprite {
return getPosY() + static_cast<float>(collision_y_offset + collision_size);
}
// Injecta el bus del nivell. El Player publica `on_tile_entered` cada
// vegada que el centre de la seua caixa de col·lisió entra en una
// cel·la nova del tilemap. Si el bus no s'injecta, no emet res.
void setEventBus(EventBus* bus) { bus_ = bus; }
// Tile actual del jugador (centre de `baseBox()` convertit a
// coordenades de tile del tilemap, usant Map::tile_size i
// Map::tilemap_origin_*). Retorna (col, row).
[[nodiscard]] auto getCurrentTile() const -> std::pair<int, int>;
private:
// Rectangle de col·lisió del jugador, usat pel mapa de colisions i com
// a referència per al Z-order.
@@ -72,4 +85,13 @@ class Player : public AnimatedSprite {
void (Player::*set_pos)(float));
Controls controls_;
// Bus del nivell (opcional). Propietat externa (EntityManager); el
// Player només en guarda un punter per publicar events.
EventBus* bus_{nullptr};
// Últim tile publicat. Inicialitzats a un sentinel fora de rang perquè
// el primer frame en què el bus estigui connectat ja emeti un event.
int last_tile_col_{-9999};
int last_tile_row_{-9999};
};
+15
View File
@@ -41,6 +41,21 @@ namespace Map {
back_wall_h = g["back_wall_h"].get_value<int>();
side_wall_w = g["side_wall_w"].get_value<int>();
// Tile size + origen del tilemap (per a consumidors externs que
// converteixen px ↔ tile). Fallbacks conservadors: tile_size per
// defecte 16, origen [0, 0]. Si falten del YAML, només el sistema
// d'entitats es veu afectat; la lògica de col·lisions actual no.
tile_size = yaml.contains("tile_size") ? yaml["tile_size"].get_value<int>() : 16;
tilemap_origin_x = 0;
tilemap_origin_y = 0;
if (yaml.contains("origin")) {
const auto& o = yaml["origin"];
if (o.is_sequence() && o.size() == 2) {
tilemap_origin_x = o[0].get_value<int>();
tilemap_origin_y = o[1].get_value<int>();
}
}
// Derivats: el passadís és el sobrant del pitch sobre el sarcòfag, i
// l'exterior reutilitza la mateixa amplada per mantenir el pas lliure.
corridor_w = pitch_x - sarc_w;
+10
View File
@@ -24,6 +24,16 @@ namespace Map {
inline int pitch_x = 0;
inline int pitch_y = 0;
// Metadades del tilemap (carregades de l'arrel del YAML: `tile_size:` i
// `origin: [x, y]`). Són necessàries per a consumidors externs com el
// sistema d'entitats, que tradueix coordenades píxel ↔ tile amb
// tile_col = floor((px - tilemap_origin_x) / tile_size)
// El grid és 16-alineat respecte a l'origen del tilemap (no del canvas);
// origen habitual a pyramid.yaml és [-8, 0] (overscan horitzontal CPC).
inline int tile_size = 0;
inline int tilemap_origin_x = 0;
inline int tilemap_origin_y = 0;
// Cromat de la sala: marcador (HUD), mur posterior (dalt) i murs
// laterals. El mur frontal es deriva del passadís exterior inferior.
inline int hud_h = 0;
+34
View File
@@ -5,8 +5,10 @@
#include <algorithm>
#include <cstddef>
#include <filesystem>
#include <iostream>
#include <iterator>
#include <utility>
#include <variant>
#include <vector>
#include "core/audio/audio.hpp"
@@ -15,6 +17,7 @@
#include "core/rendering/render_info.hpp"
#include "core/rendering/screen.hpp"
#include "core/resources/resource_cache.hpp"
#include "game/entities/event_bus.hpp"
#include "game/entities/player.hpp"
#include "game/map/map.hpp"
#include "game/options.hpp"
@@ -59,6 +62,23 @@ GameScene::GameScene(Config config)
// Player perquè el spawn consulta `collision_*`.
Player::loadFrom(config_.player_yaml);
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
// `makeTilemap`), perquè consulta `Map::tile_size` i `getSarcRect`.
// Les surfaces de les entitats es carreguen al Resource::Cache aquí.
entity_manager_.loadFromLevel(config_.map_yaml);
// Subscripcions bàsiques al bus del nivell: de moment només logs, sense
// tocar ni la vida del jugador ni l'estat de la escena. Serà la base
// sobre la qual cablejar HUD i fi de nivell més endavant.
entity_manager_.getBus().subscribe("level_complete", [](const Event& /*ev*/) {
std::cout << "[LEVEL COMPLETE]" << '\n';
});
entity_manager_.getBus().subscribe("player_hit", [](const Event& ev) {
const auto* dmg = std::get_if<int>(&ev.payload);
std::cout << "[PLAYER HIT] dmg=" << (dmg != nullptr ? *dmg : 0) << '\n';
});
// Jugador 0: fletxes del teclat (Defaults::KeysGame). Instanciar-ne un
// segon només requereix afegir una altra entrada amb uns Controls
// distints (p.ex. WASD).
@@ -79,6 +99,10 @@ GameScene::GameScene(Config config)
const auto SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(Player::collision_y_offset) - (static_cast<float>(Player::collision_size) / 2.0F);
players_.emplace_back(std::make_unique<Player>(ANIM_KEY, SPAWN_X, SPAWN_Y, CONTROLS_P1));
// Connectem el Player al bus perquè emeti `on_tile_entered` i els
// sarcòfags rebin les notificacions de pas per tile.
players_.back()->setEventBus(&entity_manager_.getBus());
// Música de fons en bucle infinit. El track ja està al Resource::Cache
// (preload al Director), així que playMusic només mou el punter cap a
// jail_audio i obri l'stream. Audio::get() està garantit no-null:
@@ -94,6 +118,11 @@ void GameScene::update(int delta_ms) {
const float DT = static_cast<float>(delta_ms) / 1000.0F;
for (auto& player : players_) { player->update(DT); }
// El tick d'entitats DESPRÉS del tick dels jugadors: si el jugador ha
// emès `on_tile_entered` aquest frame, l'EntityManager ja l'ha encaminat
// als sarcòfags via la subscripció del bus, i aquest `update` pot
// aplicar la transició OPENING → OPEN en el mateix frame.
entity_manager_.update(DT);
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW));
Notifier::get().update(delta_ms);
}
@@ -108,6 +137,11 @@ void GameScene::render() {
// col·lisions del jugador.
tilemap_.render();
// Entitats (sarcòfags, porta, momia, antorxa...) dibuixades per sobre
// dels tiles però sota dels players. Z-order fi (interleaving per feetY
// amb els sarcs) queda per a una tasca posterior; "sin pulido".
entity_manager_.renderAll();
// Amb un sol jugador, l'ordre és irrellevant i ens estalviem el vector
// temporal. Quan n'hi haja més d'un (o quan els sarcs siguen sprites)
// caldrà ordenar per `getFeetY()` ascendent i interleaveing amb les
+6
View File
@@ -5,6 +5,7 @@
#include <vector>
#include "core/scene/scene.hpp"
#include "game/entities/entity_manager.hpp"
#include "game/map/tilemap.hpp"
class Player;
@@ -33,6 +34,11 @@ class GameScene : public Scene {
Config config_;
Tilemap tilemap_;
// Gestor d'entitats del nivell (sarcòfags, porta, momia, antorxa,
// etc.). Viu aquí perquè la seua vida coincideix amb la de la escena;
// cada entitat del nivell es carrega al ctor via loadFromLevel.
EntityManager entity_manager_;
// Vector des del principi: AEE suportarà multijugador local; cada Player
// té els seus propis Controls i entra al vector amb una crida extra.
std::vector<std::unique_ptr<Player>> players_;