From e22f3240da4f0744780da623ef25388cbc189f31 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 24 Apr 2026 18:54:19 +0200 Subject: [PATCH] =?UTF-8?q?feat(player):=20col=C2=B7lisi=C3=B3=20i=20empen?= =?UTF-8?q?ta=20entre=20jugadors=20amb=20push=5Ffactor=20al=20YAML=20del?= =?UTF-8?q?=20mapa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/maps/pyramid.yaml | 6 ++ source/game/scenes/game_scene.cpp | 130 +++++++++++++++++++++++++++--- source/game/scenes/game_scene.hpp | 12 +++ source/utils/defaults.hpp | 5 ++ 4 files changed, 143 insertions(+), 10 deletions(-) diff --git a/data/maps/pyramid.yaml b/data/maps/pyramid.yaml index 981b1fd..34ebc0a 100644 --- a/data/maps/pyramid.yaml +++ b/data/maps/pyramid.yaml @@ -21,6 +21,12 @@ origin: [-8, 0] player1_paint_tile: 0 player2_paint_tile: 4 +# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre +# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i +# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós +# s'anul·len (sense push, sense moviment). +push_factor: 0.30 + # Geometria lògica de la piràmide (separada del render visual del tileset). # La graella de sarcòfags, el pitch entre ells i les amplades dels murs # decideixen els límits del jugador i les seues caixes de col·lisió. Abans diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 7de426f..363dd49 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -62,6 +62,98 @@ namespace { return yaml[key].get_value(); } + auto readOptionalFloat(const std::string& yaml_path, const std::string& key, float fallback) -> float { + auto bytes = Resource::Helper::loadFile(yaml_path); + if (bytes.empty()) { return fallback; } + const auto* begin = reinterpret_cast(bytes.data()); + const auto* end = begin + bytes.size(); + auto yaml = fkyaml::node::deserialize(begin, end); + if (!yaml.contains(key)) { return fallback; } + return yaml[key].get_value(); + } + + // Comprova si `a` i `b` se solapen amb una tolerància positiva al + // contacte perfecte (cantos que es toquen = no-solapament). SDL3 ja + // ofereix `SDL_HasRectIntersectionFloat`, però aquí volem la mateixa + // semàntica que al Player::collidesWithWorld. + auto rectsOverlap(const SDL_FRect& a, const SDL_FRect& b) -> bool { + return SDL_HasRectIntersectionFloat(&a, &b); + } + + // Resolver jugador↔jugador. Opera sobre les velocitats intencionades + // (escrites per `Player::tick`, abans d'`applyMovement`) per a + // implementar les regles: + // - Sense overlap previst → no toca res. + // - Ambdós amb input que els faria col·lidir → head-on: velocitats + // anul·lades per als dos. + // - Un quiet, l'altre empentant-lo → push: ambdues velocitats + // escalades a `push_factor` i el pushed hereta la direcció del + // pusher. Si el pushed no pot avançar (mur/sarc), `tryMove` ja ho + // gestionarà a `applyMovement` mogent la distància parcial possible. + // Amb dos jugadors només és un parell; si en un futur n'hi ha més, + // la mateixa lògica es pot aplicar pairwise. + void resolvePlayerVsPlayer(const std::vector& ps, float dt, float push_factor) { + if (ps.size() < 2) { return; } + auto* a = ps[0]; + auto* b = ps[1]; + if (a == nullptr || b == nullptr) { return; } + + const SDL_FRect BOX_A = a->getBaseBox(); + const SDL_FRect BOX_B = b->getBaseBox(); + const SDL_FRect NEXT_A{ + .x = BOX_A.x + (a->getIntendedVX() * dt), + .y = BOX_A.y + (a->getIntendedVY() * dt), + .w = BOX_A.w, + .h = BOX_A.h, + }; + const SDL_FRect NEXT_B{ + .x = BOX_B.x + (b->getIntendedVX() * dt), + .y = BOX_B.y + (b->getIntendedVY() * dt), + .w = BOX_B.w, + .h = BOX_B.h, + }; + if (!rectsOverlap(NEXT_A, NEXT_B)) { return; } + + const bool MOVING_A = (a->getIntendedVX() != 0.0F) || (a->getIntendedVY() != 0.0F); + const bool MOVING_B = (b->getIntendedVX() != 0.0F) || (b->getIntendedVY() != 0.0F); + + if (MOVING_A && MOVING_B) { + // Head-on o encreuament amb col·lisió: cap dels dos es mou. + a->setIntendedVelocity(0.0F, 0.0F); + b->setIntendedVelocity(0.0F, 0.0F); + return; + } + + auto* pusher = MOVING_A ? a : b; + auto* pushed = MOVING_A ? b : a; + const float VX = pusher->getIntendedVX() * push_factor; + const float VY = pusher->getIntendedVY() * push_factor; + pusher->setIntendedVelocity(VX, VY); + pushed->setIntendedVelocity(VX, VY); + } + + // Ordre d'`applyMovement`: en push, el pushed ha de moure's abans + // perquè quan el pusher faça `tryMove` veja el pushed a la seua + // posició nova i no li bloqueje. Heurística barata: el jugador amb + // `intended*dt` que entra a l'altre és el pusher; però com a totes + // les configuracions push els dos tenen la mateixa velocitat resultant + // i comparteixen direcció, usem el producte (dot) del vector d'acció + // i el vector cap a l'altre jugador: positiu = pusher. + auto orderedForMovement(const std::vector& ps) -> std::vector { + std::vector out = ps; + if (out.size() < 2 || out[0] == nullptr || out[1] == nullptr) { return out; } + const auto* a = out[0]; + const auto* b = out[1]; + const SDL_FRect BOX_A = a->getBaseBox(); + const SDL_FRect BOX_B = b->getBaseBox(); + const float DX = (BOX_B.x + (BOX_B.w / 2.0F)) - (BOX_A.x + (BOX_A.w / 2.0F)); + const float DY = (BOX_B.y + (BOX_B.h / 2.0F)) - (BOX_A.y + (BOX_A.h / 2.0F)); + // dot(v_a, dir_a→b): positiu = a empenta a b → b es mou primer. + const float DOT_A = (a->getIntendedVX() * DX) + (a->getIntendedVY() * DY); + if (DOT_A > 0.0F) { std::swap(out[0], out[1]); } + return out; + } + } // namespace auto GameScene::defaultConfig() -> Config { @@ -189,6 +281,16 @@ GameScene::GameScene(Config config) // sarcòfags passa per `tile_painted`. for (auto& p : players_) { p->setEventBus(&entity_manager_.getBus()); } + // Física del contacte entre jugadors. El factor viu al YAML del mapa + // com a propietat de la piràmide (0.30 per defecte). La vista plana + // `players_view_` i el `setOtherPlayers` s'han de fer ara perquè a + // cada `applyMovement` la callback de col·lisió consulti els altres + // jugadors com a sòlids. + push_factor_ = readOptionalFloat(config_.map_yaml, "push_factor", Defaults::Player::PUSH_FACTOR); + players_view_.reserve(players_.size()); + std::ranges::transform(players_, std::back_inserter(players_view_), [](const auto& p) { return p.get(); }); + for (auto& p : players_) { p->setOtherPlayers(&players_view_); } + // Marcador (HUD) de la franja superior. Reutilitza la font 8bithud i // la paleta "ui" ja precarregades pel Director — no toca disc des // d'aquí. Subscrit al bus per reaccionar a `player_hit`, @@ -338,7 +440,21 @@ void GameScene::update(int delta_ms) { const int SCALED_DELTA_MS = static_cast(static_cast(delta_ms) * pause_.scale); if (!EDITOR_FROZEN && SCALED_DELTA_MS > 0) { const float DT = static_cast(SCALED_DELTA_MS) / 1000.0F; - for (auto& player : players_) { player->update(DT); } + + // Pipeline de moviment en tres passes per poder intercalar la + // resolució de contactes jugador↔jugador entre input i posició: + // 1) tick: cada jugador calcula la velocitat intencionada + // (teclat + mando + facing). + // 2) resolve: si els dos es trobarien, anul·lem velocitats + // (head-on) o escalem a push_factor (empenta). + // 3) applyMovement: en l'ordre "pushed primer" perquè quan el + // pusher faça `tryMove`, el pushed ja estiga fora de la + // seua caixa. La callback també tracta l'altre jugador com + // a sòlid: xarxa de seguretat davant de qualsevol inexactitud. + for (auto& player : players_) { player->tick(DT); } + resolvePlayerVsPlayer(players_view_, DT, push_factor_); + for (auto* player : orderedForMovement(players_view_)) { player->applyMovement(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 @@ -346,15 +462,9 @@ void GameScene::update(int delta_ms) { entity_manager_.update(DT); // Detecció de contacte player↔entitat després que totes les entitats - // hagin mogut aquest frame. La porta (si està oberta) emet - // `level_complete`; la momia/trampa emet `player_hit`. Els sarcs, encara - // que tinguen collides=true, tenen la caixa lògica fora del camí del - // jugador (gràcies a Map::collides al Player::update), així que aquí - // no generen falsos contactes. - std::vector player_ptrs; - player_ptrs.reserve(players_.size()); - std::ranges::transform(players_, std::back_inserter(player_ptrs), [](const auto& p) { return p.get(); }); - entity_manager_.checkPlayerCollisions(player_ptrs); + // hagin mogut aquest frame. Reutilitzem la vista plana que ja + // manté el GameScene per al resolver player↔player. + entity_manager_.checkPlayerCollisions(players_view_); } #ifdef _DEBUG diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index 6e4bf0a..a82db53 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -68,6 +68,18 @@ class GameScene : public Scene { // té els seus propis Controls i entra al vector amb una crida extra. std::vector> players_; + // Vista plana de punters crus — la fa servir Player com a llista + // d'obstacles mòbils a la callback de col·lisió. Es construeix un sol + // cop al ctor (els punters són estables perquè `players_` no es + // redimensiona dins de la vida de l'escena). + std::vector players_view_; + + // Factor multiplicatiu del push inter-jugador. Carregat de la clau + // `push_factor` del YAML del mapa; 0.30 per defecte (veure + // `Defaults::Player::PUSH_FACTOR`). Constant dins de la vida de + // l'escena — una piràmide, una física. + float push_factor_{0.30F}; + // Capa de "pintat" del terra. Escolta `on_tile_entered` al bus, // filtra pintabilitat (sòl transitable) i re-publica `tile_painted` // per als sarcòfags. `unique_ptr` per controlar l'ordre de diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index 77f6a72..dbef97f 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -93,6 +93,11 @@ namespace Defaults { namespace Player { constexpr float SPEED_PX_PER_S = 60.0F; // Velocitat base del jugador (sense sprint / slow-zones). + // Factor multiplicatiu aplicat a la velocitat quan un jugador empenta + // l'altre (0.30 = el pusher es ralentitza al 30% i el pushed és + // arrossegat a la mateixa velocitat). Sobrescrivible des del YAML + // del mapa amb la clau `push_factor`. + constexpr float PUSH_FACTOR = 0.30F; } // namespace Player // Fallbacks del tema d'UI: índexs de paleta, timings (ms) i fonts.