feat(player): inèrcia de frenada amb acumulador de moviment
This commit is contained in:
1 parent
291cbdc957
commit
ea0f6d98a9
5 files changed
+153
-7
No files matched your search
@@ -41,6 +41,19 @@ animations:
|
||||
loopFrom: 0
|
||||
frames: [8, 9, 10, 11]
|
||||
|
||||
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
|
||||
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
|
||||
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
||||
# El `threshold_ms` filtra els tocs curts: si el temps acumulat de
|
||||
# moviment continu no ha arribat al llindar, la parada és instantània
|
||||
# (permet "passets" d'un tile sense rellisquera). L'acumulador es
|
||||
# reseteja només quan no hi ha ni input ni velocitat residual, així
|
||||
# els canvis de direcció enmig del coast no rearmen el llindar.
|
||||
# `brake_time_ms: 0` o absència del bloc = inèrcia desactivada.
|
||||
inertia:
|
||||
brake_time_ms: 150
|
||||
threshold_ms: 300
|
||||
|
||||
# Gir de 180°: mentre dura, el facing queda fixat a la direcció
|
||||
# intermèdia (efecte purament visual; el moviment continua sense
|
||||
# alterar-se). Els girs de 90° no passen per cap intermedi.
|
||||
|
||||
@@ -41,6 +41,19 @@ animations:
|
||||
loopFrom: 0
|
||||
frames: [8, 9, 10, 11]
|
||||
|
||||
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
|
||||
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
|
||||
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
||||
# El `threshold_ms` filtra els tocs curts: si el temps acumulat de
|
||||
# moviment continu no ha arribat al llindar, la parada és instantània
|
||||
# (permet "passets" d'un tile sense rellisquera). L'acumulador es
|
||||
# reseteja només quan no hi ha ni input ni velocitat residual, així
|
||||
# els canvis de direcció enmig del coast no rearmen el llindar.
|
||||
# `brake_time_ms: 0` o absència del bloc = inèrcia desactivada.
|
||||
inertia:
|
||||
brake_time_ms: 150
|
||||
threshold_ms: 300
|
||||
|
||||
# Gir de 180°: mentre dura, el facing queda fixat a la direcció
|
||||
# intermèdia (efecte purament visual; el moviment continua sense
|
||||
# alterar-se). Els girs de 90° no passen per cap intermedi.
|
||||
|
||||
@@ -49,6 +49,7 @@ namespace {
|
||||
} // namespace
|
||||
|
||||
Player::TurnConfig Player::turn_config;
|
||||
Player::InertiaConfig Player::inertia_config;
|
||||
|
||||
void Player::loadFrom(const std::string& yaml_path) {
|
||||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||
@@ -85,6 +86,15 @@ void Player::loadFrom(const std::string& yaml_path) {
|
||||
hit_x_offset = collision_x_offset + HIT_INSET;
|
||||
hit_y_offset = collision_y_offset + HIT_INSET;
|
||||
|
||||
// Bloc opcional `inertia:` — si manca, `brake_time_ms` queda a 0 i
|
||||
// la inèrcia de frenada no s'activa (parada instantània com abans).
|
||||
inertia_config = InertiaConfig{};
|
||||
if (yaml.contains("inertia")) {
|
||||
const auto& i = yaml["inertia"];
|
||||
if (i.contains("brake_time_ms")) { inertia_config.brake_time_ms = i["brake_time_ms"].get_value<int>(); }
|
||||
if (i.contains("threshold_ms")) { inertia_config.threshold_ms = i["threshold_ms"].get_value<int>(); }
|
||||
}
|
||||
|
||||
// Bloc opcional `turn:` — si manca, `duration_ms` queda a 0 i la
|
||||
// mecànica del gir de 180° no s'activa (canvis de direcció instantanis).
|
||||
turn_config = TurnConfig{};
|
||||
@@ -269,6 +279,38 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
||||
});
|
||||
}
|
||||
|
||||
void Player::integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input) {
|
||||
// Acceleració instantània: mentre hi ha input, la velocitat salta a
|
||||
// ±SPEED sense transició. Cobreix també els canvis de direcció (un
|
||||
// coast cap a la dreta passa a -SPEED immediatament si es prem esquerra).
|
||||
if (input != 0.0F) {
|
||||
phys = input;
|
||||
return;
|
||||
}
|
||||
if (phys == 0.0F) { return; }
|
||||
// Hi ha velocitat residual i aquest eix no té input. Tres camins:
|
||||
// · Input a l'altre eix (gir de 90°): parada en sec — no arrosseguem
|
||||
// en diagonal mentre l'usuari està premant una altra direcció.
|
||||
// · Input solta del tot i llindar superat: inèrcia (decaiment lineal).
|
||||
// · Input solta del tot i llindar NO superat: parada en sec (passets).
|
||||
if (any_input) {
|
||||
phys = 0.0F;
|
||||
return;
|
||||
}
|
||||
const bool INERTIA_ACTIVE = (inertia_config.brake_time_ms > 0) && (motion_time_ms >= static_cast<float>(inertia_config.threshold_ms));
|
||||
if (!INERTIA_ACTIVE) {
|
||||
phys = 0.0F;
|
||||
return;
|
||||
}
|
||||
const float DECEL = SPEED_PX_PER_S / (static_cast<float>(inertia_config.brake_time_ms) / 1000.0F);
|
||||
const float STEP = DECEL * delta_time;
|
||||
if (std::fabs(phys) <= STEP) {
|
||||
phys = 0.0F;
|
||||
} else {
|
||||
phys -= std::copysign(STEP, phys);
|
||||
}
|
||||
}
|
||||
|
||||
void Player::tick(float delta_time) {
|
||||
// Reset per frame: el resolver del GameScene el tornarà a posar a
|
||||
// true si aquest frame hi ha push actiu. Així quan el push acaba
|
||||
@@ -292,12 +334,39 @@ void Player::tick(float delta_time) {
|
||||
// els botons tornen false/0 i no contribueixen a la velocitat.
|
||||
accumulateGamepadMovement(id_, vx, vy);
|
||||
|
||||
intended_vx_ = vx;
|
||||
intended_vy_ = vy;
|
||||
setVelX(vx);
|
||||
setVelY(vy);
|
||||
input_vx_ = vx;
|
||||
input_vy_ = vy;
|
||||
|
||||
updateFacingAndTurn(delta_time, vx, vy);
|
||||
// Integració física per eix. `intended_v*` fa de memòria entre frames:
|
||||
// si l'usuari té input, se sobreescriu a ±SPEED; si no, decau per
|
||||
// inèrcia o es posa a 0. El `motion_time_ms_` que passem és el del
|
||||
// final del frame anterior — serveix per decidir si la parada d'aquest
|
||||
// frame és suau (llindar superat) o en sec. HAS_INPUT bloca la inèrcia
|
||||
// en girs de 90° (un eix a 0 amb l'altre actiu no ha de "patinar").
|
||||
const bool HAS_INPUT = (input_vx_ != 0.0F) || (input_vy_ != 0.0F);
|
||||
integrateAxis(intended_vx_, input_vx_, delta_time, motion_time_ms_, HAS_INPUT);
|
||||
integrateAxis(intended_vy_, input_vy_, delta_time, motion_time_ms_, HAS_INPUT);
|
||||
|
||||
// Actualitzem l'acumulador DESPRÉS de la integració. Creix mentre hi
|
||||
// haja intenció (input) o moviment residual (coast); només es buida
|
||||
// quan el jugador està genuïnament aturat (ni input ni velocitat). Així
|
||||
// un canvi de direcció enmig del coast o un toc curt contra el coast
|
||||
// no rearma mai el llindar.
|
||||
const bool HAS_VELOCITY = (intended_vx_ != 0.0F) || (intended_vy_ != 0.0F);
|
||||
if (HAS_INPUT || HAS_VELOCITY) {
|
||||
motion_time_ms_ += delta_time * 1000.0F;
|
||||
} else {
|
||||
motion_time_ms_ = 0.0F;
|
||||
}
|
||||
|
||||
setVelX(intended_vx_);
|
||||
setVelY(intended_vy_);
|
||||
|
||||
// Important: el facing i la detecció de gir de 180° s'alimenten amb
|
||||
// input CRU, no amb la velocitat física. Un coast sense input NO
|
||||
// canvia el facing (la darrera orientació es preserva), i els girs
|
||||
// només disparen amb intenció explícita del jugador.
|
||||
updateFacingAndTurn(delta_time, input_vx_, input_vy_);
|
||||
}
|
||||
|
||||
void Player::applyMovement(float delta_time) {
|
||||
|
||||
@@ -84,6 +84,8 @@ class Player : public AnimatedSprite {
|
||||
// GameScene els toca entre `tick` i `applyMovement` per implementar
|
||||
// col·lisions jugador↔jugador (escalar a push_factor, anul·lar en
|
||||
// head-on). Expressades en píxels/segon, no en píxels/frame.
|
||||
// Amb inèrcia activada, `intended_v*` pot ser no-zero encara que
|
||||
// l'usuari no estiga prement res (coast residual).
|
||||
[[nodiscard]] auto getIntendedVX() const -> float { return intended_vx_; }
|
||||
[[nodiscard]] auto getIntendedVY() const -> float { return intended_vy_; }
|
||||
void setIntendedVelocity(float vx, float vy) {
|
||||
@@ -91,6 +93,15 @@ class Player : public AnimatedSprite {
|
||||
intended_vy_ = vy;
|
||||
}
|
||||
|
||||
// Velocitat crua d'input d'aquest frame (-SPEED, 0 o +SPEED per eix).
|
||||
// A diferència de `intended_v*`, no té en compte la inèrcia: un jugador
|
||||
// que coasta sense prémer res té `input_v* == 0`. El resolver PvP del
|
||||
// GameScene l'usa per decidir "parat vs en moviment" (criteri: un
|
||||
// coast sense input compta com parat — l'empenta i el head-on només
|
||||
// es disparen quan hi ha intenció explícita del jugador).
|
||||
[[nodiscard]] auto getInputVX() const -> float { return input_vx_; }
|
||||
[[nodiscard]] auto getInputVY() const -> float { return input_vy_; }
|
||||
|
||||
// Llista de tots els jugadors del nivell (incloent-se ell mateix —
|
||||
// la callback ignora `this`). La caixa del jugador es considera un
|
||||
// sòlid a la callback de col·lisió: així `tryMove` no permet que un
|
||||
@@ -186,9 +197,26 @@ class Player : public AnimatedSprite {
|
||||
// Velocitat intencionada calculada a `tick` i aplicada a
|
||||
// `applyMovement`. El GameScene pot sobreescriure-la entremig per
|
||||
// resoldre interaccions jugador↔jugador (empentes, head-on).
|
||||
// També actua com a estat físic persistent entre frames: amb inèrcia
|
||||
// activada, si l'usuari solta l'input, aquí queda la velocitat
|
||||
// residual que `tick` anirà decaient linealment cap a 0.
|
||||
float intended_vx_{0.0F};
|
||||
float intended_vy_{0.0F};
|
||||
|
||||
// Velocitat desitjada per input aquest frame (teclat + gamepad). Cada
|
||||
// eix és -SPEED, 0 o +SPEED. És la "intenció del jugador"; la física
|
||||
// resultant viu a `intended_v*` i pot diferir (coast sense input).
|
||||
float input_vx_{0.0F};
|
||||
float input_vy_{0.0F};
|
||||
|
||||
// Acumulador de temps en moviment continu. Creix cada frame amb
|
||||
// `|input_v*| > 0` o `|intended_v*| > 0`, i es reseteja a 0 només
|
||||
// quan NO hi ha ni input ni velocitat residual. Activa la inèrcia
|
||||
// quan passa el llindar configurat al YAML (`threshold_ms`); no es
|
||||
// buida mai enmig d'un coast, ni tan sols en canvis de direcció,
|
||||
// així un toc curt en sentit oposat no "frena en sec" un coast llarg.
|
||||
float motion_time_ms_{0.0F};
|
||||
|
||||
// Punter a la llista de tots els jugadors del nivell (propietat del
|
||||
// GameScene). `nullptr` = jugador aïllat, sense col·lisió inter-P.
|
||||
const std::vector<Player*>* all_players_{nullptr};
|
||||
@@ -223,6 +251,24 @@ class Player : public AnimatedSprite {
|
||||
EAST,
|
||||
WEST };
|
||||
|
||||
// Configuració d'inèrcia llegida del bloc `inertia:` del YAML. Amb
|
||||
// `brake_time_ms == 0` (o bloc absent) la inèrcia està desactivada i
|
||||
// el comportament és instantani: soltar input para el jugador en sec.
|
||||
struct InertiaConfig {
|
||||
int brake_time_ms{0}; // Temps en ms perquè la velocitat caiga de ±SPEED a 0.
|
||||
int threshold_ms{0}; // Temps mínim en moviment continu per activar la inèrcia.
|
||||
};
|
||||
static InertiaConfig inertia_config;
|
||||
|
||||
// Integra un eix. Si hi ha input en aquest eix, la velocitat física
|
||||
// salta a aquest valor (acceleració instantània). Si no, i hi ha
|
||||
// input en l'altre eix (`any_input == true`), la velocitat d'aquest
|
||||
// eix es posa a 0 en sec: el jugador està fent un gir de 90°, no
|
||||
// soltant l'input — no volem que arrossegue en diagonal. Només quan
|
||||
// l'usuari deixa anar tot l'input (`any_input == false`) s'activa la
|
||||
// inèrcia de frenada (si `motion_time_ms` ha passat el llindar).
|
||||
static void integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input);
|
||||
|
||||
// Configuració del gir de 180°, llegida del bloc `turn:` del YAML del
|
||||
// player. Si `duration_ms` és 0 (o el bloc no existeix) la mecànica
|
||||
// queda desactivada i els canvis de direcció són instantanis.
|
||||
|
||||
@@ -92,6 +92,11 @@ namespace {
|
||||
// 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.
|
||||
// Important: el criteri "mou-se vs parat" consulta INPUT cru (no la
|
||||
// velocitat física), així un jugador que coasta per inèrcia sense
|
||||
// input compta com a parat — ni dispara head-on ni empeny l'altre.
|
||||
// L'overlap predictiu sí que usa velocitat física (intended), perquè
|
||||
// és el desplaçament real d'aquest frame.
|
||||
void resolvePlayerVsPlayer(const std::vector<Player*>& ps, float dt, float push_factor) {
|
||||
if (ps.size() < 2) { return; }
|
||||
auto* a = ps[0];
|
||||
@@ -119,8 +124,8 @@ namespace {
|
||||
};
|
||||
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);
|
||||
const bool MOVING_A = (a->getInputVX() != 0.0F) || (a->getInputVY() != 0.0F);
|
||||
const bool MOVING_B = (b->getInputVX() != 0.0F) || (b->getInputVY() != 0.0F);
|
||||
|
||||
if (MOVING_A && MOVING_B) {
|
||||
// Head-on o encreuament amb col·lisió: cap dels dos es mou.
|
||||
|
||||
Reference in new issue
Block a user