feat(player): animacions direccionals i stand automàtic en col·lisió; v0.18.0
This commit is contained in:
1 parent
13ae4a51a4
commit
a3592d6c73
4 files changed
+135
-10
No files matched your search
@@ -0,0 +1,78 @@
|
|||||||
|
# Animacions i direccionalitat del jugador
|
||||||
|
|
||||||
|
Documenta els afegits d'aquesta sessió a `source/game/entities/player.*`: sis
|
||||||
|
animacions direccionals amb `facing_` persistent, regla de flip només per al
|
||||||
|
lateral i transició automàtica a `stand` quan la col·lisió bloqueja el
|
||||||
|
moviment. Tot el canvi és local al `Player`; no s'ha tocat l'EntityManager
|
||||||
|
ni el bus.
|
||||||
|
|
||||||
|
## 1. YAML — sis animacions
|
||||||
|
|
||||||
|
El fitxer `data/player/player.yaml` defineix ara sis animacions, agrupades
|
||||||
|
en tres files de 4 frames sobre `player.gif` (convenció habitual: d'esquerra
|
||||||
|
a dreta i de dalt a baix):
|
||||||
|
|
||||||
|
| Nom | Frames | Descripció |
|
||||||
|
|---------------|------------|----------------------------|
|
||||||
|
| `stand_down` | `[0]` | parat mirant cap avall |
|
||||||
|
| `walk_down` | `[0..3]` | caminant cap avall |
|
||||||
|
| `stand_up` | `[4]` | parat mirant cap amunt |
|
||||||
|
| `walk_up` | `[4..7]` | caminant cap amunt |
|
||||||
|
| `stand` | `[8]` | parat lateral |
|
||||||
|
| `walk` | `[8..11]` | caminant lateral |
|
||||||
|
|
||||||
|
Les laterals (`stand` i `walk`) ja existien; les variants `_up` i `_down`
|
||||||
|
són noves i permeten distingir visualment la direcció de moviment.
|
||||||
|
|
||||||
|
## 2. `Facing` persistent
|
||||||
|
|
||||||
|
S'ha afegit l'enum privat `Facing { SIDE, UP, DOWN }` amb el membre
|
||||||
|
`facing_{SIDE}`. La tria es fa a cada `update`, amb prevalença lateral
|
||||||
|
per si un dia es permetessin diagonals:
|
||||||
|
|
||||||
|
- `vx != 0` → `SIDE` (inclou diagonals).
|
||||||
|
- `vx == 0 && vy < 0` → `UP`.
|
||||||
|
- `vx == 0 && vy > 0` → `DOWN`.
|
||||||
|
- Cap input → es conserva l'últim `facing_` perquè l'animació `stand_*`
|
||||||
|
coincideixi amb l'última direcció caminada.
|
||||||
|
|
||||||
|
## 3. Regla de flip
|
||||||
|
|
||||||
|
- `SIDE`: el sprite es flipeja horitzontalment si `vx < 0`, i es restaura
|
||||||
|
a `FLIP_NONE` si `vx > 0`. El sprite original mira a la dreta.
|
||||||
|
- `UP` / `DOWN`: el flip es força explícitament a `FLIP_NONE`. Els sprites
|
||||||
|
frontals/dorsals no s'han d'espillar només perquè l'últim lateral havia
|
||||||
|
estat cap a l'esquerra.
|
||||||
|
|
||||||
|
## 4. `stand` automàtic a la col·lisió
|
||||||
|
|
||||||
|
Quan el jugador prem una tecla però la col·lisió (mapa o sarcòfag) li
|
||||||
|
impedeix avançar, l'animació `walk*` s'atura i es força la variant
|
||||||
|
`stand_*` corresponent al facing. L'algorisme:
|
||||||
|
|
||||||
|
1. Al principi del frame es captura `FRAME_START_X` / `FRAME_START_Y`.
|
||||||
|
2. S'aplica snap al passadís perpendicular si hi ha pur moviment H o V.
|
||||||
|
3. `AnimatedSprite::update` integra la velocitat; la col·lisió
|
||||||
|
eix-separada reverteix cada eix si `Map::collides(baseBox())`.
|
||||||
|
4. Després de `clampToPlayfield`, es compara la posició final amb el
|
||||||
|
valor capturat al pas 1. Si és idèntica, `MOVED = false` i es tria
|
||||||
|
`stand` / `stand_up` / `stand_down` segons `facing_`.
|
||||||
|
5. Si hi ha hagut moviment real (incloent-hi snap), s'aplica la variant
|
||||||
|
`walk*` del mateix facing.
|
||||||
|
|
||||||
|
D'aquesta manera, xocar contra un mur o un sarcòfag fa que el jugador es
|
||||||
|
quede immòbil en compte d'animar el caminat "en el lloc". El facing
|
||||||
|
segueix reflectint la intenció (on vol anar), no el resultat, perquè
|
||||||
|
l'`stand_up` d'empentar-se contra un mur ha de mostrar el personatge
|
||||||
|
mirant amunt.
|
||||||
|
|
||||||
|
## 5. Estat
|
||||||
|
|
||||||
|
- DONE: sis animacions al YAML, `facing_` persistent, flip condicionat,
|
||||||
|
`stand` automàtic a la col·lisió.
|
||||||
|
- Build i smoke tests (14/14) verdes.
|
||||||
|
- Sense smoke test dedicat: la lògica està entrellaçada amb teclat i
|
||||||
|
`Map::collides`; l'smoke actual de `Player` no cobreix teclat simulat.
|
||||||
|
- Pendent (ajornat): si algun dia es vol feedback auditiu de wall-bump
|
||||||
|
(so en xocar), la condició natural és `!MOVED && (vx != 0 || vy != 0)`
|
||||||
|
al final d'`update`.
|
||||||
@@ -66,24 +66,38 @@ void Player::update(float delta_time) {
|
|||||||
setVelX(vx);
|
setVelX(vx);
|
||||||
setVelY(vy);
|
setVelY(vy);
|
||||||
|
|
||||||
// El sprite original mira a la dreta; flipem només si es mou a l'esquerra
|
// Tria de facing amb prevalença lateral: si hi ha component horitzontal
|
||||||
// i restaurem si torna a la dreta. Moviments purament verticals conserven
|
// (inclòs el diagonal), l'animació és lateral i el flip segueix el signe
|
||||||
// l'últim flip perquè el jugador no "gire" sense motiu.
|
// de `vx`. Només en moviment purament vertical passem a UP/DOWN, i en
|
||||||
if (vx < 0.0F) {
|
// aquest cas el flip es força a NONE perquè els sprites frontals no
|
||||||
setFlip(SDL_FLIP_HORIZONTAL);
|
// s'han d'espillar segons l'últim lateral. En aturar-se conservem
|
||||||
} else if (vx > 0.0F) {
|
// `facing_` per a l'animació stand corresponent.
|
||||||
|
if (vx != 0.0F) {
|
||||||
|
facing_ = Facing::SIDE;
|
||||||
|
setFlip(vx < 0.0F ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE);
|
||||||
|
} else if (vy < 0.0F) {
|
||||||
|
facing_ = Facing::UP;
|
||||||
|
setFlip(SDL_FLIP_NONE);
|
||||||
|
} else if (vy > 0.0F) {
|
||||||
|
facing_ = Facing::DOWN;
|
||||||
setFlip(SDL_FLIP_NONE);
|
setFlip(SDL_FLIP_NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool MOVING = vx != 0.0F || vy != 0.0F;
|
|
||||||
setCurrentAnimation(MOVING ? "walk" : "stand");
|
|
||||||
|
|
||||||
// Snap al passadís: si només hi ha moviment horitzontal (o vertical) i
|
// Snap al passadís: si només hi ha moviment horitzontal (o vertical) i
|
||||||
// la caixa està prou prop d'una lane perpendicular (≤ mitja caixa), la
|
// la caixa està prou prop d'una lane perpendicular (≤ mitja caixa), la
|
||||||
// Y (o X) es corregeix gradualment per enfilar el passadís. Amb
|
// Y (o X) es corregeix gradualment per enfilar el passadís. Amb
|
||||||
// moviment diagonal no es fa snap — l'usuari pilota manualment.
|
// moviment diagonal no es fa snap — l'usuari pilota manualment.
|
||||||
const bool ONLY_H = (vx != 0.0F) && (vy == 0.0F);
|
const bool ONLY_H = (vx != 0.0F) && (vy == 0.0F);
|
||||||
const bool ONLY_V = (vy != 0.0F) && (vx == 0.0F);
|
const bool ONLY_V = (vy != 0.0F) && (vx == 0.0F);
|
||||||
|
|
||||||
|
// Pos d'inici de frame (abans de snap i integració): si en acabar el
|
||||||
|
// frame coincideix amb la posició final, el jugador no s'ha mogut
|
||||||
|
// realment (ni per input, ni per snap, ni ha superat la col·lisió) i cal
|
||||||
|
// forçar l'animació `stand` corresponent al facing — així s'atura el
|
||||||
|
// caminat quan xoca contra un mur o un sarcòfag.
|
||||||
|
const float FRAME_START_X = getPosX();
|
||||||
|
const float FRAME_START_Y = getPosY();
|
||||||
|
|
||||||
if (ONLY_H) {
|
if (ONLY_H) {
|
||||||
snapToHorizontalLane(delta_time);
|
snapToHorizontalLane(delta_time);
|
||||||
} else if (ONLY_V) {
|
} else if (ONLY_V) {
|
||||||
@@ -109,6 +123,12 @@ void Player::update(float delta_time) {
|
|||||||
|
|
||||||
clampToPlayfield();
|
clampToPlayfield();
|
||||||
|
|
||||||
|
// Animació basada en moviment real: si la col·lisió (o l'absència
|
||||||
|
// d'input) ha deixat el jugador al mateix lloc que al principi del
|
||||||
|
// frame, queda en `stand` del facing actual.
|
||||||
|
const bool MOVED = (getPosX() != FRAME_START_X) || (getPosY() != FRAME_START_Y);
|
||||||
|
setCurrentAnimation(animationName(MOVED));
|
||||||
|
|
||||||
// Detecció de canvi de tile. Es calcula *després* del clamp i de les
|
// Detecció de canvi de tile. Es calcula *després* del clamp i de les
|
||||||
// col·lisions perquè la posició aquí ja és la definitiva del frame;
|
// col·lisions perquè la posició aquí ja és la definitiva del frame;
|
||||||
// així els sarcòfags no reben falsos positius mentre el Player rellisca
|
// així els sarcòfags no reben falsos positius mentre el Player rellisca
|
||||||
@@ -124,6 +144,18 @@ void Player::update(float delta_time) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto Player::animationName(bool moving) const -> const char* {
|
||||||
|
switch (facing_) {
|
||||||
|
case Facing::UP:
|
||||||
|
return moving ? "walk_up" : "stand_up";
|
||||||
|
case Facing::DOWN:
|
||||||
|
return moving ? "walk_down" : "stand_down";
|
||||||
|
case Facing::SIDE:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return moving ? "walk" : "stand";
|
||||||
|
}
|
||||||
|
|
||||||
auto Player::getCurrentTile() const -> std::pair<int, int> {
|
auto Player::getCurrentTile() const -> std::pair<int, int> {
|
||||||
// El grid del tilemap és de `Map::tile_size` píxels i està 16-alineat
|
// El grid del tilemap és de `Map::tile_size` píxels i està 16-alineat
|
||||||
// respecte a l'origen del tilemap (pot ser negatiu per overscan). El
|
// respecte a l'origen del tilemap (pot ser negatiu per overscan). El
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
@@ -81,6 +82,10 @@ class Player : public AnimatedSprite {
|
|||||||
// wrappers deleguen a `snapToLane` només canviant l'eix actiu.
|
// wrappers deleguen a `snapToLane` només canviant l'eix actiu.
|
||||||
void snapToHorizontalLane(float delta_time);
|
void snapToHorizontalLane(float delta_time);
|
||||||
void snapToVerticalLane(float delta_time);
|
void snapToVerticalLane(float delta_time);
|
||||||
|
|
||||||
|
// Nom d'animació per al `facing_` actual. Si `moving` és cert s'usa
|
||||||
|
// la variant `walk*`, altrament la variant `stand*`.
|
||||||
|
[[nodiscard]] auto animationName(bool moving) const -> const char*;
|
||||||
void snapToLane(
|
void snapToLane(
|
||||||
float delta_time,
|
float delta_time,
|
||||||
float base,
|
float base,
|
||||||
@@ -91,6 +96,16 @@ class Player : public AnimatedSprite {
|
|||||||
|
|
||||||
Controls controls_;
|
Controls controls_;
|
||||||
|
|
||||||
|
// Direcció "cap on mira" el jugador. Governa quin conjunt d'animacions
|
||||||
|
// s'aplica (lateral, amunt, avall) i si el sprite es flipeja o no. Els
|
||||||
|
// moviments purament horitzontals o diagonals prenen `SIDE` (prevalença
|
||||||
|
// lateral), els purament verticals fan `UP`/`DOWN`. En aturar-se es
|
||||||
|
// preserva l'última orientació perquè la versió stand coincideixi.
|
||||||
|
enum class Facing : std::uint8_t { SIDE,
|
||||||
|
UP,
|
||||||
|
DOWN };
|
||||||
|
Facing facing_{Facing::SIDE};
|
||||||
|
|
||||||
// Bus del nivell (opcional). Propietat externa (EntityManager); el
|
// Bus del nivell (opcional). Propietat externa (EntityManager); el
|
||||||
// Player només en guarda un punter per publicar events.
|
// Player només en guarda un punter per publicar events.
|
||||||
EventBus* bus_{nullptr};
|
EventBus* bus_{nullptr};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace Texts {
|
namespace Texts {
|
||||||
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
||||||
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
||||||
constexpr const char* VERSION = "0.15.0"; // Single source of truth — Makefile el grep
|
constexpr const char* VERSION = "0.18.0"; // Single source of truth — Makefile el grep
|
||||||
} // namespace Texts
|
} // namespace Texts
|
||||||
|
|
||||||
namespace GameCanvas {
|
namespace GameCanvas {
|
||||||
|
|||||||
Reference in new issue
Block a user