feat(entities): bloc collision i anchor:feet per entitats amb falsa perspectiva
This commit is contained in:
1 parent
1af5ef861f
commit
e6b612d164
7 files changed
+162
-5
No files matched your search
@@ -26,3 +26,18 @@ void Entity::onPlayerContact(Player* /*player*/) {
|
||||
void Entity::notifyTileEntered(int /*col*/, int /*row*/) {
|
||||
// No-op per defecte; només `Sarcophagus` l'usa.
|
||||
}
|
||||
|
||||
auto Entity::getCollisionRect() const -> SDL_FRect {
|
||||
if (!collision_box_.has_value()) { return pixel_rect_; }
|
||||
return {
|
||||
.x = pixel_rect_.x + static_cast<float>(collision_box_->offset_x),
|
||||
.y = pixel_rect_.y + static_cast<float>(collision_box_->offset_y),
|
||||
.w = static_cast<float>(collision_box_->width),
|
||||
.h = static_cast<float>(collision_box_->height),
|
||||
};
|
||||
}
|
||||
|
||||
auto Entity::getFeetY() const -> float {
|
||||
const auto RECT = getCollisionRect();
|
||||
return RECT.y + RECT.h;
|
||||
}
|
||||
@@ -37,6 +37,18 @@ class Entity {
|
||||
int row{0};
|
||||
};
|
||||
|
||||
// Sub-rectangle de col·lisió dins de `pixel_rect_`. Pensat per a
|
||||
// personatges amb falsa perspectiva (sprite alt, caixa AABB petita
|
||||
// encastada a baix) — mateixa convenció que el Player. `offset_x/y`
|
||||
// són relatius a la cantonada superior-esquerra del sprite. Si l'entitat
|
||||
// no té `CollisionBox`, `getCollisionRect()` retorna tot el `pixel_rect_`.
|
||||
struct CollisionBox {
|
||||
int offset_x{0};
|
||||
int offset_y{0};
|
||||
int width{0};
|
||||
int height{0};
|
||||
};
|
||||
|
||||
Entity() = default;
|
||||
Entity(std::string name, TilePos tile, SDL_FRect pixel_rect);
|
||||
virtual ~Entity() = default;
|
||||
@@ -100,11 +112,28 @@ class Entity {
|
||||
// Entitats sense animació retornen `nullopt` → frame 0 per defecte.
|
||||
[[nodiscard]] auto getAnimation() const -> const std::optional<AnimationComponent>& { return anim_; }
|
||||
|
||||
// Caixa de col·lisió: per defecte és el `pixel_rect_` sencer (entitats
|
||||
// AABB-aligned). Si s'ha configurat un `CollisionBox`, retorna el
|
||||
// sub-rect dins del sprite — vida útil: validar col·lisions amb el
|
||||
// jugador o el mapa sense que la caixa inclogui "l'alçada" del sprite.
|
||||
[[nodiscard]] auto getCollisionRect() const -> SDL_FRect;
|
||||
|
||||
// Y dels peus: la vora inferior de la caixa de col·lisió. La usarà el
|
||||
// Z-order per ordenar entitats i jugador de menys a més profunditat
|
||||
// (REND-01, quan s'implemente).
|
||||
[[nodiscard]] auto getFeetY() const -> float;
|
||||
|
||||
// El loader li assigna una caixa explícita llegida del YAML; sense
|
||||
// aquesta crida, l'entitat queda sense `CollisionBox` i la caixa de
|
||||
// col·lisió coincideix amb el sprite.
|
||||
void setCollisionBox(CollisionBox box) { collision_box_ = box; }
|
||||
|
||||
protected:
|
||||
std::string name_;
|
||||
TilePos tile_{};
|
||||
SDL_FRect pixel_rect_{};
|
||||
std::optional<AnimationComponent> anim_;
|
||||
std::optional<CollisionBox> collision_box_;
|
||||
std::shared_ptr<Surface> surface_{nullptr};
|
||||
bool collides_{false};
|
||||
bool interactable_{false};
|
||||
|
||||
@@ -88,6 +88,41 @@ namespace {
|
||||
return info;
|
||||
}
|
||||
|
||||
// Bloc `collision: { size: [w, h], offset: [x, y] }` de la plantilla
|
||||
// d'entitat. `offset` és opcional: si no apareix, s'entén "la caixa
|
||||
// queda bottom-center del sprite" (convenció de falsa perspectiva).
|
||||
// Retorna nullopt si no hi ha bloc, i l'entitat es queda sense caixa
|
||||
// explícita (getCollisionRect torna tot el sprite).
|
||||
auto readCollisionBox(const fkyaml::node& tmpl, float sprite_w, float sprite_h) -> std::optional<Entity::CollisionBox> {
|
||||
if (!tmpl.contains("collision")) { return std::nullopt; }
|
||||
const auto& c = tmpl["collision"];
|
||||
if (!c.contains("size")) { return std::nullopt; }
|
||||
const auto& sz = c["size"];
|
||||
if (!sz.is_sequence() || sz.size() != 2) { return std::nullopt; }
|
||||
const int W = sz[0].get_value<int>();
|
||||
const int H = sz[1].get_value<int>();
|
||||
int off_x = static_cast<int>((sprite_w - static_cast<float>(W)) / 2.0F);
|
||||
int off_y = static_cast<int>(sprite_h - static_cast<float>(H));
|
||||
if (c.contains("offset")) {
|
||||
const auto& o = c["offset"];
|
||||
if (o.is_sequence() && o.size() == 2) {
|
||||
off_x = o[0].get_value<int>();
|
||||
off_y = o[1].get_value<int>();
|
||||
}
|
||||
}
|
||||
return Entity::CollisionBox{.offset_x = off_x, .offset_y = off_y, .width = W, .height = H};
|
||||
}
|
||||
|
||||
// Ancoratge d'spawn. "feet" vol dir que el `position: [col, row]` del
|
||||
// map és el tile dels peus: el sprite es centra horitzontalment dins
|
||||
// del tile i s'alinea la seua vora inferior amb la vora inferior del
|
||||
// tile. "top_left" (per defecte) posa el sprite amb el cantonal
|
||||
// superior-esquerre a la cantonada superior-esquerra del tile.
|
||||
auto readAnchor(const fkyaml::node& tmpl) -> std::string {
|
||||
if (!tmpl.contains("anchor")) { return "top_left"; }
|
||||
return tmpl["anchor"].get_value<std::string>();
|
||||
}
|
||||
|
||||
// Coordenades píxel (canvas) d'una cel·la (col, row) del tilemap. El
|
||||
// grid del tilemap està 16-alineat respecte a `tilemap_origin_*`, que
|
||||
// pot ser negatiu per overscan CPC.
|
||||
@@ -115,7 +150,10 @@ namespace {
|
||||
// Crea l'entitat per factory i li adjunta la surface si n'hi ha. Retorna
|
||||
// nullptr si la plantilla no es pot carregar o si el tipus no està
|
||||
// registrat al factory — el loop principal ja imprimeix el missatge.
|
||||
auto buildEntity(const std::string& type, SpriteInfo& sprite_out) -> std::unique_ptr<Entity> {
|
||||
// També llegeix el bloc `collision:` de la plantilla (si existeix) i
|
||||
// el bota a l'entitat, i retorna l'`anchor` per a que el loader sàpiga
|
||||
// com col·locar el sprite a l'spawn ("feet" o "top_left").
|
||||
auto buildEntity(const std::string& type, SpriteInfo& sprite_out, std::string& anchor_out) -> std::unique_ptr<Entity> {
|
||||
fkyaml::node tmpl;
|
||||
try {
|
||||
tmpl = EntityLoader::loadTemplate(type);
|
||||
@@ -133,6 +171,10 @@ namespace {
|
||||
Resource::Cache::get().loadSurface(sprite_out.key, sprite_out.full_path);
|
||||
entity->setSurface(Resource::Cache::get().getSurface(sprite_out.key));
|
||||
}
|
||||
if (const auto BOX = readCollisionBox(tmpl, sprite_out.width, sprite_out.height); BOX.has_value()) {
|
||||
entity->setCollisionBox(*BOX);
|
||||
}
|
||||
anchor_out = readAnchor(tmpl);
|
||||
return entity;
|
||||
}
|
||||
|
||||
@@ -180,14 +222,26 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto placeGeneric(EntityManager& manager, std::unique_ptr<Entity> entity, const std::string& type, const SpriteInfo& sprite, const fkyaml::node& spawn) -> bool {
|
||||
auto placeGeneric(EntityManager& manager, std::unique_ptr<Entity> entity, const std::string& type, const SpriteInfo& sprite, const std::string& anchor, const fkyaml::node& spawn) -> bool {
|
||||
if (!spawn.contains("position")) {
|
||||
std::cerr << "EntityManager: " << type << " sense position\n";
|
||||
return false;
|
||||
}
|
||||
const auto& p = spawn["position"];
|
||||
const auto PX = tileToPixel(p[0].get_value<int>(), p[1].get_value<int>());
|
||||
entity->setPixelRect({.x = PX.x, .y = PX.y, .w = sprite.width, .h = sprite.height});
|
||||
|
||||
// Anchor "feet": el tile [col,row] és el tile dels peus. El sprite
|
||||
// es centra horitzontalment sobre el tile i la seua vora inferior
|
||||
// s'alinea amb la vora inferior del tile. Els píxels que sobresurten
|
||||
// (alçada del sprite per damunt del tile dels peus) queden "per
|
||||
// sobre" sense afectar cap col·lisió — falsa perspectiva.
|
||||
float rect_x = PX.x;
|
||||
float rect_y = PX.y;
|
||||
if (anchor == "feet") {
|
||||
rect_x = PX.x + (static_cast<float>(Map::tile_size) - sprite.width) / 2.0F;
|
||||
rect_y = PX.y + static_cast<float>(Map::tile_size) - sprite.height;
|
||||
}
|
||||
entity->setPixelRect({.x = rect_x, .y = rect_y, .w = sprite.width, .h = sprite.height});
|
||||
|
||||
if (type == "door") {
|
||||
auto* door_ptr = dynamic_cast<Door*>(entity.get());
|
||||
@@ -209,12 +263,13 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
|
||||
const auto TYPE = spawn["type"].get_value<std::string>();
|
||||
|
||||
SpriteInfo sprite;
|
||||
auto entity = buildEntity(TYPE, sprite);
|
||||
std::string anchor;
|
||||
auto entity = buildEntity(TYPE, sprite, anchor);
|
||||
if (entity == nullptr) { continue; }
|
||||
|
||||
const bool OK = (TYPE == "sarcophagus")
|
||||
? placeSarcophagus(*this, std::move(entity), sprite, spawn)
|
||||
: placeGeneric(*this, std::move(entity), TYPE, sprite, spawn);
|
||||
: placeGeneric(*this, std::move(entity), TYPE, sprite, anchor, spawn);
|
||||
if (OK) { ++loaded; }
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user