fix(bullet): col·lisió swept, sense grace_timer, mor al border visual

This commit is contained in:
2026-05-22 18:24:54 +02:00
parent b80216dce1
commit bf79eecca0
6 changed files with 115 additions and 84 deletions
-1
View File
@@ -26,7 +26,6 @@ namespace Defaults::Game {
// Friendly fire system // Friendly fire system
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%) constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
constexpr float BULLET_GRACE_PERIOD = 0.2F; // Inmunidad post-disparo (s)
constexpr float BULLET_SPEED = 700.0F; // Velocidad escalar (px/s). Pascal: 7 px/frame × 20 FPS constexpr float BULLET_SPEED = 700.0F; // Velocidad escalar (px/s). Pascal: 7 px/frame × 20 FPS
// Transición LEVEL_START (mensajes aleatorios PRE-level) // Transición LEVEL_START (mensajes aleatorios PRE-level)
+50 -18
View File
@@ -3,30 +3,62 @@
#pragma once #pragma once
#include <algorithm>
#include "core/entities/entity.hpp" #include "core/entities/entity.hpp"
#include "core/types.hpp" #include "core/types.hpp"
namespace Physics { namespace Physics {
// Comprobación genèrica de colisión entre dues entidades // Comprobación genèrica de colisión entre dues entidades
inline auto checkCollision(const Entities::Entity& a, const Entities::Entity& b, float amplifier = 1.0F) -> bool { inline auto checkCollision(const Entities::Entity& a, const Entities::Entity& b, float amplifier = 1.0F) -> bool {
// Comprovar si ambdós són col·lisionables // Comprovar si ambdós són col·lisionables
if (!a.isCollidable() || !b.isCollidable()) { if (!a.isCollidable() || !b.isCollidable()) {
return false; return false;
}
// Calcular radi combinat (con amplificador per hitbox generós)
float suma_radis = (a.getCollisionRadius() + b.getCollisionRadius()) * amplifier;
float suma_radis_sq = suma_radis * suma_radis;
// Comprobación distancia al cuadrado (sin sqrt)
const Vec2& pos_a = a.getCenter();
const Vec2& pos_b = b.getCenter();
float dx = pos_a.x - pos_b.x;
float dy = pos_a.y - pos_b.y;
float dist_sq = (dx * dx) + (dy * dy);
return dist_sq <= suma_radis_sq;
} }
// Calcular radi combinat (con amplificador per hitbox generós) // Swept collision: una entitat mòbil (radi r_a) s'ha desplaçat de p0 a p1 aquest
float suma_radis = (a.getCollisionRadius() + b.getCollisionRadius()) * amplifier; // frame. Comprova si el segment expandit pel radi conjunt (r_a + radi de b, amb
float suma_radis_sq = suma_radis * suma_radis; // amplificador) toca el cercle de l'entity b. Equival al check discrete quan
// p0 == p1 (sense moviment). Evita tunneling a velocitats altes.
// Comprobación distancia al cuadrado (sin sqrt) inline auto checkCollisionSwept(const Vec2& p0, const Vec2& p1, float r_a, const Entities::Entity& b, float amplifier = 1.0F) -> bool {
const Vec2& pos_a = a.getCenter(); if (!b.isCollidable()) {
const Vec2& pos_b = b.getCenter(); return false;
float dx = pos_a.x - pos_b.x; }
float dy = pos_a.y - pos_b.y; const float SUM_R = (r_a + b.getCollisionRadius()) * amplifier;
float dist_sq = (dx * dx) + (dy * dy); const float SUM_R_SQ = SUM_R * SUM_R;
const Vec2& center_b = b.getCenter();
return dist_sq <= suma_radis_sq; const float DX_SEG = p1.x - p0.x;
} const float DY_SEG = p1.y - p0.y;
const float LEN_SQ = (DX_SEG * DX_SEG) + (DY_SEG * DY_SEG);
// Degenerat: punt-cercle (frame de spawn, o entitat parada).
if (LEN_SQ <= 0.0F) {
const float DX = p0.x - center_b.x;
const float DY = p0.y - center_b.y;
return ((DX * DX) + (DY * DY)) <= SUM_R_SQ;
}
// Projecció del centre sobre la recta del segment, clamp a [0,1] per acotar al segment.
const float T_RAW = (((center_b.x - p0.x) * DX_SEG) + ((center_b.y - p0.y) * DY_SEG)) / LEN_SQ;
const float T_CLAMPED = std::clamp(T_RAW, 0.0F, 1.0F);
const float CLOSEST_X = p0.x + (DX_SEG * T_CLAMPED);
const float CLOSEST_Y = p0.y + (DY_SEG * T_CLAMPED);
const float DX = CLOSEST_X - center_b.x;
const float DY = CLOSEST_Y - center_b.y;
return ((DX * DX) + (DY * DY)) <= SUM_R_SQ;
}
} // namespace Physics } // namespace Physics
+15 -21
View File
@@ -3,7 +3,6 @@
#include "game/entities/bullet.hpp" #include "game/entities/bullet.hpp"
#include <algorithm>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <iostream> #include <iostream>
@@ -43,8 +42,8 @@ void Bullet::init() {
// Inicialment inactiva // Inicialment inactiva
is_active_ = false; is_active_ = false;
center_ = {.x = 0.0F, .y = 0.0F}; center_ = {.x = 0.0F, .y = 0.0F};
prev_position_ = {.x = 0.0F, .y = 0.0F};
angle_ = 0.0F; angle_ = 0.0F;
grace_timer_ = 0.0F;
// Reset del cuerpo físico // Reset del cuerpo físico
body_.position = Vec2{}; body_.position = Vec2{};
@@ -61,11 +60,9 @@ void Bullet::disparar(const Vec2& position, float angle, uint8_t owner_id) {
// Almacenar propietario (0=P1, 1=P2) // Almacenar propietario (0=P1, 1=P2)
owner_id_ = owner_id; owner_id_ = owner_id;
// Activar grace period (prevents instant self-collision)
grace_timer_ = Defaults::Game::BULLET_GRACE_PERIOD;
// Posición y orientación iniciales = ship // Posición y orientación iniciales = ship
center_ = position; center_ = position;
prev_position_ = position; // Al spawn no hi ha moviment encara: swept degenera a punt-cercle
angle_ = angle; angle_ = angle;
// Sincronizar el body físico: posición + velocidad cartesiana // Sincronizar el body físico: posición + velocidad cartesiana
@@ -82,37 +79,34 @@ void Bullet::disparar(const Vec2& position, float angle, uint8_t owner_id) {
Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME); Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME);
} }
void Bullet::update(float delta_time) { void Bullet::update(float /*delta_time*/) {
if (!is_active_) { if (!is_active_) {
return; return;
} }
// Decrementar grace timer
if (grace_timer_ > 0.0F) {
grace_timer_ -= delta_time;
grace_timer_ = std::max(grace_timer_, 0.0F);
}
// El movimiento real lo hace PhysicsWorld::update() (integración). // El movimiento real lo hace PhysicsWorld::update() (integración).
// Aquí solo lógica de estado: detectar salida del PLAYAREA y desactivar. // Aquí solo lógica de estado: detectar salida del PLAYAREA i desactivar.
// Sense marge de seguretat: la bala mor quan la seva aresta toca el border visual
// (centre a BULLET_RADIUS del límit). El MARGE_SEGURETAT de getSafePlayAreaBounds
// és per a spawn d'enemics, no per a desactivació de bales.
float min_x; float min_x;
float max_x; float max_x;
float min_y; float min_y;
float max_y; float max_y;
Constants::getSafePlayAreaBounds(Defaults::Entities::BULLET_RADIUS, Constants::getPlayAreaBounds(min_x, max_x, min_y, max_y);
min_x, constexpr float R = Defaults::Entities::BULLET_RADIUS;
max_x,
min_y,
max_y);
if (body_.position.x < min_x || body_.position.x > max_x || if (body_.position.x < min_x + R || body_.position.x > max_x - R ||
body_.position.y < min_y || body_.position.y > max_y) { body_.position.y < min_y + R || body_.position.y > max_y - R) {
desactivar(); desactivar();
} }
} }
void Bullet::postUpdate(float /*delta_time*/) { void Bullet::postUpdate(float /*delta_time*/) {
// Sincronizar mirror desde body_ tras la integración del world. // Captura la posició al final del frame anterior abans de sobreescriure center_;
// així el sistema de col·lisions pot fer swept (segment-vs-cercle) entre prev_position_
// i la nova center_, evitant tunneling a velocitats altes.
prev_position_ = center_;
center_ = body_.position; center_ = body_.position;
// angle_ no cambia (las balas no rotan visualmente). // angle_ no cambia (las balas no rotan visualmente).
} }
+30 -29
View File
@@ -11,38 +11,39 @@
#include "core/types.hpp" #include "core/types.hpp"
class Bullet : public Entities::Entity { class Bullet : public Entities::Entity {
public: public:
Bullet() Bullet()
: Entity(nullptr) {} : Entity(nullptr) {}
explicit Bullet(Rendering::Renderer* renderer); explicit Bullet(Rendering::Renderer* renderer);
void init() override; void init() override;
void disparar(const Vec2& position, float angle, uint8_t owner_id); void disparar(const Vec2& position, float angle, uint8_t owner_id);
void update(float delta_time) override; void update(float delta_time) override;
void postUpdate(float delta_time) override; void postUpdate(float delta_time) override;
void draw() const override; void draw() const override;
// Override: Interfaz de Entity // Override: Interfaz de Entity
[[nodiscard]] auto isActive() const -> bool override { return is_active_; } [[nodiscard]] auto isActive() const -> bool override { return is_active_; }
// Override: Interfaz de colisión (gameplay-level: PLAYAREA bounds-check) // Override: Interfaz de colisión (gameplay-level: PLAYAREA bounds-check)
[[nodiscard]] auto getCollisionRadius() const -> float override { [[nodiscard]] auto getCollisionRadius() const -> float override {
return Defaults::Entities::BULLET_RADIUS; return Defaults::Entities::BULLET_RADIUS;
} }
[[nodiscard]] auto isCollidable() const -> bool override { [[nodiscard]] auto isCollidable() const -> bool override {
return is_active_ && grace_timer_ <= 0.0F; return is_active_;
} }
// Getters (API pública sin cambios) // Getters (API pública sin cambios)
[[nodiscard]] auto getOwnerId() const -> uint8_t { return owner_id_; } [[nodiscard]] auto getOwnerId() const -> uint8_t { return owner_id_; }
[[nodiscard]] auto getGraceTimer() const -> float { return grace_timer_; } // Posició al final del frame anterior, per a CCD segment-vs-cercle.
void desactivar(); [[nodiscard]] auto getPrevPosition() const -> const Vec2& { return prev_position_; }
void desactivar();
private: private:
// Miembros específicos de Bullet (heredados: renderer_, shape_, center_, angle_, brightness_, body_). // Miembros específicos de Bullet (heredados: renderer_, shape_, center_, angle_, brightness_, body_).
// Inicializados en la declaración para que tanto el ctor por defecto como el que toma renderer // Inicializados en la declaración para que tanto el ctor por defecto como el que toma renderer
// dejen el objeto en estado coherente (proyectil inactivo, sin owner, sin grace timer). // dejen el objeto en estado coherente (proyectil inactivo, sin owner).
bool is_active_{false}; bool is_active_{false};
uint8_t owner_id_{0}; // 0=P1, 1=P2 uint8_t owner_id_{0}; // 0=P1, 1=P2
float grace_timer_{0.0F}; // Grace period timer (0.0 = vulnerable) Vec2 prev_position_{}; // Posició al final del frame anterior (per a swept collision)
}; };
+5 -2
View File
@@ -465,11 +465,14 @@ void GameScene::runStagePlaying(float delta_time) {
for (auto& enemy : enemies_) { for (auto& enemy : enemies_) {
enemy.update(delta_time); enemy.update(delta_time);
} }
// Col·lisions primer, després `bullet.update()`: si una bala el mateix frame xoca
// amb un enemic i alhora surt del PLAYAREA, ha de comptar com a impacte abans de
// ser desactivada per fora-de-zona.
runCollisionDetections();
for (auto& bullet : bullets_) { for (auto& bullet : bullets_) {
bullet.update(delta_time); bullet.update(delta_time);
} }
runCollisionDetections();
debris_manager_.update(delta_time); debris_manager_.update(delta_time);
firework_manager_.update(delta_time); firework_manager_.update(delta_time);
floating_score_manager_.update(delta_time); floating_score_manager_.update(delta_time);
+15 -13
View File
@@ -87,8 +87,11 @@ namespace Systems::Collision {
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER; constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER;
for (auto& bullet : ctx.bullets) { for (auto& bullet : ctx.bullets) {
if (!bullet.isActive()) {
continue;
}
for (auto& enemy : ctx.enemies) { for (auto& enemy : ctx.enemies) {
if (!Physics::checkCollision(bullet, enemy, AMPLIFIER)) { if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), Defaults::Entities::BULLET_RADIUS, enemy, AMPLIFIER)) {
continue; continue;
} }
@@ -207,12 +210,17 @@ namespace Systems::Collision {
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_PLAYER_AMPLIFIER; constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_PLAYER_AMPLIFIER;
for (auto& bullet : ctx.bullets) { for (auto& bullet : ctx.bullets) {
if (!bullet.isActive() || bullet.getGraceTimer() > 0.0F) { if (!bullet.isActive()) {
continue; continue;
} }
const uint8_t BULLET_OWNER = bullet.getOwnerId(); const uint8_t BULLET_OWNER = bullet.getOwnerId();
for (uint8_t player_id = 0; player_id < 2; player_id++) { for (uint8_t player_id = 0; player_id < 2; player_id++) {
// Una bala mai no impacta al seu propi shooter: les bales d'aquest joc no
// reboten ni el shooter pot atrapar-les, així que la prevenció és per disseny.
if (BULLET_OWNER == player_id) {
continue;
}
if (ctx.hit_timer_per_player[player_id] > 0.0F || if (ctx.hit_timer_per_player[player_id] > 0.0F ||
!ctx.ships[player_id].isActive() || !ctx.ships[player_id].isActive() ||
ctx.ships[player_id].isInvulnerable()) { ctx.ships[player_id].isInvulnerable()) {
@@ -225,20 +233,14 @@ namespace Systems::Collision {
continue; continue;
} }
if (!Physics::checkCollision(bullet, ctx.ships[player_id], AMPLIFIER)) { if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), Defaults::Entities::BULLET_RADIUS, ctx.ships[player_id], AMPLIFIER)) {
continue; continue;
} }
// *** FRIENDLY FIRE HIT *** // *** TEAMMATE HIT (friendly fire) ***
if (BULLET_OWNER == player_id) { // Víctima perd 1 vida, atacant en guanya 1.
// Self-hit: víctima pierde 1 vida. ctx.on_player_hit(player_id);
ctx.on_player_hit(player_id); ctx.lives_per_player[BULLET_OWNER]++;
} else {
// Teammate hit: víctima pierde 1, atacante gana 1.
ctx.on_player_hit(player_id);
ctx.lives_per_player[BULLET_OWNER]++;
}
Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME); Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME);
bullet.desactivar(); bullet.desactivar();
break; // Una bullet solo impacta una vez por frame break; // Una bullet solo impacta una vez por frame