feat(player): col·lisió i empenta entre jugadors amb push_factor al YAML del mapa

This commit is contained in:
JailDesigner committed 2026-04-24 18:54:19 +02:00
1 parent 1cc2e28b1b
commit e22f3240da
4 files changed
+143 -10

No files matched your search

+120 -10
View File
@@ -62,6 +62,98 @@ namespace {
return yaml[key].get_value<int>();
}
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<const char*>(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<float>();
}
// 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<Player*>& 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<Player*>& ps) -> std::vector<Player*> {
std::vector<Player*> 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<int>(static_cast<float>(delta_ms) * pause_.scale);
if (!EDITOR_FROZEN && SCALED_DELTA_MS > 0) {
const float DT = static_cast<float>(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*> 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
+12
View File
@@ -68,6 +68,18 @@ class GameScene : public Scene {
// té els seus propis Controls i entra al vector amb una crida extra.
std::vector<std::unique_ptr<Player>> 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<Player*> 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
+5
View File
@@ -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.