feat(player): col·lisió i empenta entre jugadors amb push_factor al YAML del mapa
This commit is contained in:
1 parent
1cc2e28b1b
commit
e22f3240da
4 files changed
+143
-10
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user