feat(players): suport del segon jugador amb events amb player_id
This commit is contained in:
1 parent
74dad71cc2
commit
4e9d5df872
21 files changed
+309
-49
No files matched your search
@@ -69,7 +69,10 @@ auto GameScene::defaultConfig() -> Config {
|
||||
.map_yaml = Defaults::Pyramid::MAP_YAML,
|
||||
.player_sprite = Defaults::Pyramid::PLAYER_SPRITE,
|
||||
.player_yaml = Defaults::Pyramid::PLAYER_YAML,
|
||||
.player2_sprite = Defaults::Pyramid::PLAYER_SPRITE_P2,
|
||||
.player2_yaml = Defaults::Pyramid::PLAYER_YAML_P2,
|
||||
.music_track = Defaults::Pyramid::MUSIC_TRACK,
|
||||
.starting_players = DebugConfig::StartingPlayers::BOTH,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,15 +81,32 @@ GameScene::GameScene(Config config)
|
||||
tilemap_(makeTilemap(config_.map_yaml)) {
|
||||
// Convenció: el Resource::Cache indexa per basename del fitxer, perquè
|
||||
// la mateixa clau l'usa el parseador d'animació (YAML > tileSetFile).
|
||||
const auto SPRITE_KEY = std::filesystem::path(config_.player_sprite).filename().string();
|
||||
const auto ANIM_KEY = std::filesystem::path(config_.player_yaml).filename().string();
|
||||
Resource::Cache::get().loadSurface(SPRITE_KEY, config_.player_sprite);
|
||||
Resource::Cache::get().loadAnimation(ANIM_KEY, config_.player_yaml);
|
||||
// Lambda perquè tenim dos conjunts d'assets (sam/morcus) i l'alta al
|
||||
// cache és idèntica per als dos; la clau d'animació ens la quedem per
|
||||
// passar-la al ctor del Player corresponent.
|
||||
auto load_player_assets = [](const std::string& sprite, const std::string& yaml) -> std::string {
|
||||
const auto SPRITE_KEY = std::filesystem::path(sprite).filename().string();
|
||||
const auto ANIM_KEY = std::filesystem::path(yaml).filename().string();
|
||||
Resource::Cache::get().loadSurface(SPRITE_KEY, sprite);
|
||||
Resource::Cache::get().loadAnimation(ANIM_KEY, yaml);
|
||||
return ANIM_KEY;
|
||||
};
|
||||
|
||||
const bool USE_P1 = (config_.starting_players == DebugConfig::StartingPlayers::P1 || config_.starting_players == DebugConfig::StartingPlayers::BOTH);
|
||||
const bool USE_P2 = (config_.starting_players == DebugConfig::StartingPlayers::P2 || config_.starting_players == DebugConfig::StartingPlayers::BOTH);
|
||||
|
||||
std::string anim_p1;
|
||||
std::string anim_p2;
|
||||
if (USE_P1) { anim_p1 = load_player_assets(config_.player_sprite, config_.player_yaml); }
|
||||
if (USE_P2) { anim_p2 = load_player_assets(config_.player2_sprite, config_.player2_yaml); }
|
||||
|
||||
// Geometria de col·lisió del jugador (caixa 16×16, base 12): carregada
|
||||
// del mateix YAML que l'animació. Ha d'estar feta abans d'instanciar
|
||||
// Player perquè el spawn consulta `collision_*`.
|
||||
Player::loadFrom(config_.player_yaml);
|
||||
// Player perquè el spawn consulta `collision_*`. Els membres de Player
|
||||
// són `static inline`, compartits per tots els jugadors: cridem
|
||||
// `loadFrom` amb el YAML del primer jugador actiu (P1 o P2). Els dos
|
||||
// personatges usen la mateixa geometria (morcus és un repaletat de sam).
|
||||
Player::loadFrom(USE_P1 ? config_.player_yaml : config_.player2_yaml);
|
||||
|
||||
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
|
||||
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
|
||||
@@ -97,23 +117,30 @@ GameScene::GameScene(Config config)
|
||||
// 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("level_complete", [](const Event& ev) {
|
||||
std::cout << "[LEVEL COMPLETE] player_id=" << ev.player_id << '\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';
|
||||
std::cout << "[PLAYER HIT] player_id=" << ev.player_id
|
||||
<< " 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).
|
||||
// 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.
|
||||
const Player::Controls CONTROLS_P1{
|
||||
.up = Defaults::KeysGame::UP,
|
||||
.down = Defaults::KeysGame::DOWN,
|
||||
.left = Defaults::KeysGame::LEFT,
|
||||
.right = Defaults::KeysGame::RIGHT,
|
||||
};
|
||||
const Player::Controls CONTROLS_P2{
|
||||
.up = Defaults::KeysGameP2::UP,
|
||||
.down = Defaults::KeysGameP2::DOWN,
|
||||
.left = Defaults::KeysGameP2::LEFT,
|
||||
.right = Defaults::KeysGameP2::RIGHT,
|
||||
};
|
||||
// Spawn centrat al passadís exterior superior: la caixa de col·lisió
|
||||
// queda dins del passadís vertical del mig (entre columnes 1 i 2) i
|
||||
// centrada en Y al passadís exterior superior. Així no naix dins de
|
||||
@@ -123,11 +150,31 @@ GameScene::GameScene(Config config)
|
||||
static_cast<float>(Map::playfield_y) + (static_cast<float>(Map::outer_corridor) / 2.0F);
|
||||
const auto SPAWN_X = CORRIDOR_CENTER_X - (static_cast<float>(Player::sprite_w) / 2.0F);
|
||||
const auto SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(Player::collision_y_offset) - (static_cast<float>(Player::collision_size) / 2.0F);
|
||||
|
||||
// En mode BOTH separem els jugadors un tile a banda i banda del
|
||||
// centre perquè no naixen superposats. En mode single-player
|
||||
// (P1 o P2 aïllats per a proves de dev) l'únic jugador arrenca al
|
||||
// centre exacte.
|
||||
const bool BOTH = (USE_P1 && USE_P2);
|
||||
const auto P1_SPAWN_X = BOTH ? SPAWN_X - static_cast<float>(Map::tile_size) : SPAWN_X;
|
||||
const auto P2_SPAWN_X = BOTH ? SPAWN_X + static_cast<float>(Map::tile_size) : SPAWN_X;
|
||||
|
||||
// Índex de tile amb què pinta cada jugador — llegit del YAML del mapa
|
||||
// (camps opcionals `player1_paint_tile` / `player2_paint_tile`). Si no
|
||||
// hi són, `paint_tile_index_ = -1` i el Player no deixa rastre.
|
||||
const int P1_PAINT = readOptionalInt(config_.map_yaml, "player1_paint_tile", -1);
|
||||
players_.emplace_back(std::make_unique<Player>(ANIM_KEY, SPAWN_X, SPAWN_Y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
|
||||
// Ids canònics dels jugadors: 0 = P1, 1 = P2. Els events del bus els
|
||||
// porten per permetre dispatch correcte al HUD i a futures regles de
|
||||
// puntuació (veure `Event::player_id`).
|
||||
constexpr int ID_P1 = 0;
|
||||
constexpr int ID_P2 = 1;
|
||||
if (USE_P1) {
|
||||
const int P1_PAINT = readOptionalInt(config_.map_yaml, "player1_paint_tile", -1);
|
||||
players_.emplace_back(std::make_unique<Player>(anim_p1, ID_P1, P1_SPAWN_X, SPAWN_Y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
|
||||
}
|
||||
if (USE_P2) {
|
||||
const int P2_PAINT = readOptionalInt(config_.map_yaml, "player2_paint_tile", -1);
|
||||
players_.emplace_back(std::make_unique<Player>(anim_p2, ID_P2, P2_SPAWN_X, SPAWN_Y, CONTROLS_P2, Map::makeInfo(), P2_PAINT));
|
||||
}
|
||||
|
||||
// PaintLayer: font única del pintat del nivell. Es crea després de
|
||||
// `loadFromLevel` perquè l'EntityManager ja ha de tenir els sarcòfags
|
||||
@@ -137,10 +184,10 @@ GameScene::GameScene(Config config)
|
||||
// manager per subscriure's a `on_tile_entered` i publicar `tile_painted`.
|
||||
paint_layer_ = std::make_unique<PaintLayer>(tilemap_, &entity_manager_.getBus());
|
||||
|
||||
// Connectem el Player al bus perquè emeti `on_tile_entered` i el
|
||||
// PaintLayer (subscrit ja) processe el pintat; el fan-out cap als
|
||||
// Connectem tots els jugadors al bus perquè emeten `on_tile_entered`;
|
||||
// el PaintLayer (subscrit ja) processa el pintat i el fan-out cap als
|
||||
// sarcòfags passa per `tile_painted`.
|
||||
players_.back()->setEventBus(&entity_manager_.getBus());
|
||||
for (auto& p : players_) { p->setEventBus(&entity_manager_.getBus()); }
|
||||
|
||||
// Marcador (HUD) de la franja superior. Reutilitza la font 8bithud i
|
||||
// la paleta "ui" ja precarregades pel Director — no toca disc des
|
||||
@@ -151,6 +198,10 @@ GameScene::GameScene(Config config)
|
||||
auto hud_font = std::make_shared<Text>(hud_font_surface, UiConfig::get().hudFont().fnt);
|
||||
hud_font->setDefaultKerning(UiConfig::get().hudFont().kerning);
|
||||
hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font);
|
||||
// Si hi ha P2 actiu, activem el seu bloc (per defecte el HUD naix amb
|
||||
// només P1). Mentre els events del bus no porten `player_id`, els hits
|
||||
// segueixen aplicant-se a P1 (veure TODO a Hud::onPlayerHit).
|
||||
if (USE_P2) { hud_->activatePlayer(2); }
|
||||
|
||||
// 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
|
||||
|
||||
Reference in new issue
Block a user