Font 8bithud, classe Text i overlay RenderInfo (F3, persistit a config.yaml) (v0.7.0)
This commit is contained in:
1 parent
ce5bec1b34
commit
e6f3c888e6
19 files changed
+712
-6
No files matched your search
@@ -0,0 +1,114 @@
|
||||
#include "core/rendering/render_info.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
|
||||
#include "core/rendering/screen.hpp"
|
||||
#include "core/rendering/text.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "utils/defines.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// La paleta de 8bithud.gif només té 2 entrades (0=negre, 1=blanc); per a
|
||||
// poder colorar el text lliurement caldrà una paleta compartida, com al
|
||||
// projecte_2026. De moment ens quedem amb blanc.
|
||||
constexpr Uint8 TEXT_COLOR = 1;
|
||||
constexpr int TEXT_SIZE = 8;
|
||||
constexpr int PADDING_V = (RenderInfo::HEIGHT - TEXT_SIZE) / 2;
|
||||
|
||||
auto prettify(std::string s) -> std::string {
|
||||
std::ranges::replace(s, '-', ' ');
|
||||
std::ranges::transform(s, s.begin(), [](unsigned char c) { return std::tolower(c); });
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto RenderInfo::get() -> RenderInfo& {
|
||||
static RenderInfo instance_;
|
||||
return instance_;
|
||||
}
|
||||
|
||||
RenderInfo::~RenderInfo() = default;
|
||||
|
||||
void RenderInfo::init() {
|
||||
// El fitxer s'indexa al Cache perquè també el puguen reutilitzar altres
|
||||
// HUDs que vulguen la mateixa font sense recarregar-la.
|
||||
Resource::Cache::get().loadSurface("8bithud.gif", "data/font/8bithud.gif");
|
||||
auto font_surface = Resource::Cache::get().getSurface("8bithud.gif");
|
||||
text_ = std::make_unique<Text>(font_surface, "data/font/8bithud.fnt");
|
||||
|
||||
// Restaura l'estat guardat a config.yaml (video.render_info).
|
||||
if (Options::video.render_info) {
|
||||
status_ = Status::ACTIVE;
|
||||
y_ = 0.0F;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderInfo::update(int delta_ms) {
|
||||
const float DT = static_cast<float>(delta_ms) / 1000.0F;
|
||||
switch (status_) {
|
||||
case Status::RISING:
|
||||
y_ += SLIDE_SPEED * DT;
|
||||
if (y_ >= 0.0F) {
|
||||
y_ = 0.0F;
|
||||
status_ = Status::ACTIVE;
|
||||
}
|
||||
break;
|
||||
case Status::VANISHING:
|
||||
y_ -= SLIDE_SPEED * DT;
|
||||
if (y_ <= static_cast<float>(-HEIGHT)) {
|
||||
y_ = static_cast<float>(-HEIGHT);
|
||||
status_ = Status::HIDDEN;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderInfo::render() {
|
||||
if (status_ == Status::HIDDEN || text_ == nullptr) { return; }
|
||||
|
||||
std::string line = std::to_string(Screen::get().getLastFPS()) + " fps";
|
||||
|
||||
const auto& driver = Screen::get().getGPUDriver();
|
||||
line += " | " + (driver.empty() ? std::string("sdl") : driver);
|
||||
|
||||
if (Options::video.shader.enabled) {
|
||||
const std::string& shader = Options::video.shader.current_shader;
|
||||
const std::string& preset = (shader == "crtpi")
|
||||
? Options::video.shader.current_crtpi_preset
|
||||
: Options::video.shader.current_postfx_preset;
|
||||
line += " | " + shader + " " + prettify(preset);
|
||||
}
|
||||
|
||||
line = prettify(line);
|
||||
|
||||
text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||
GameCanvas::CENTER_X,
|
||||
static_cast<int>(y_) + PADDING_V,
|
||||
line,
|
||||
1,
|
||||
TEXT_COLOR);
|
||||
}
|
||||
|
||||
void RenderInfo::toggle() {
|
||||
switch (status_) {
|
||||
case Status::HIDDEN:
|
||||
status_ = Status::RISING;
|
||||
Options::video.render_info = true;
|
||||
Options::saveToFile();
|
||||
break;
|
||||
case Status::ACTIVE:
|
||||
status_ = Status::VANISHING;
|
||||
Options::video.render_info = false;
|
||||
Options::saveToFile();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
class Text;
|
||||
|
||||
// Overlay de debug que mostra FPS, driver GPU i shader + preset actiu. Es
|
||||
// dibuixa a la part superior de la pantalla amb una animació de lliscament
|
||||
// (RISING / ACTIVE / VANISHING / HIDDEN). L'activa la tecla F3 a l'escena.
|
||||
class RenderInfo {
|
||||
public:
|
||||
static auto get() -> RenderInfo&;
|
||||
|
||||
RenderInfo(const RenderInfo&) = delete;
|
||||
RenderInfo(RenderInfo&&) = delete;
|
||||
auto operator=(const RenderInfo&) -> RenderInfo& = delete;
|
||||
auto operator=(RenderInfo&&) -> RenderInfo& = delete;
|
||||
|
||||
// Carrega la font 8bithud. S'ha de cridar després de configurar Screen.
|
||||
void init();
|
||||
|
||||
void update(int delta_ms);
|
||||
void render();
|
||||
void toggle();
|
||||
|
||||
[[nodiscard]] auto isActive() const -> bool { return status_ != Status::HIDDEN; }
|
||||
|
||||
// Alçada fixa de l'overlay: TEXT_SIZE (8) + padding vertical.
|
||||
static constexpr int HEIGHT = 10;
|
||||
static constexpr float SLIDE_SPEED = 120.0F;
|
||||
|
||||
private:
|
||||
enum class Status : std::uint8_t { HIDDEN,
|
||||
RISING,
|
||||
ACTIVE,
|
||||
VANISHING };
|
||||
|
||||
RenderInfo() = default;
|
||||
~RenderInfo();
|
||||
|
||||
std::unique_ptr<Text> text_;
|
||||
Status status_{Status::HIDDEN};
|
||||
float y_{static_cast<float>(-HEIGHT)};
|
||||
};
|
||||
@@ -110,12 +110,14 @@ void Screen::createSdl() {
|
||||
backend->setPreferredDriver(config_.preferred_driver);
|
||||
backend->setVSync(config_.vsync);
|
||||
if (backend->init(window_, canvas_width_, canvas_height_)) {
|
||||
std::cout << "Screen: path GPU actiu (driver: " << backend->getDriverName() << ")\n";
|
||||
gpu_driver_ = backend->getDriverName();
|
||||
std::cout << "Screen: path GPU actiu (driver: " << gpu_driver_ << ")\n";
|
||||
shader_backend_ = std::move(backend);
|
||||
return;
|
||||
}
|
||||
std::cerr << "Screen: GPU no disponible, fallback a SDL_Renderer\n";
|
||||
}
|
||||
gpu_driver_ = "sdl";
|
||||
|
||||
renderer_ = SDL_CreateRenderer(window_, nullptr);
|
||||
if (renderer_ == nullptr) {
|
||||
@@ -160,6 +162,9 @@ void Screen::clear(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
||||
}
|
||||
|
||||
void Screen::present() {
|
||||
fps_.increment();
|
||||
fps_.calculate(SDL_GetTicks());
|
||||
|
||||
if (shader_backend_ != nullptr) {
|
||||
shader_backend_->uploadPixels(canvas_.data(), canvas_width_, canvas_height_);
|
||||
shader_backend_->render();
|
||||
|
||||
@@ -62,6 +62,10 @@ class Screen {
|
||||
[[nodiscard]] auto getCanvasWidth() const -> int { return canvas_width_; }
|
||||
[[nodiscard]] auto getCanvasHeight() const -> int { return canvas_height_; }
|
||||
|
||||
// Mètriques per a overlays de debug (RenderInfo).
|
||||
[[nodiscard]] auto getLastFPS() const -> int { return fps_.last_value; }
|
||||
[[nodiscard]] auto getGPUDriver() const -> const std::string& { return gpu_driver_; }
|
||||
|
||||
private:
|
||||
Screen() = default;
|
||||
~Screen() = default;
|
||||
@@ -69,6 +73,23 @@ class Screen {
|
||||
void destroySdl();
|
||||
void createSdl();
|
||||
|
||||
// Comptador intern de frames per segon. S'incrementa a cada present() i
|
||||
// es recalcula quan ha passat un segon complet (SDL_GetTicks en ms).
|
||||
struct FPS {
|
||||
std::uint32_t ticks{0};
|
||||
int frame_count{0};
|
||||
int last_value{0};
|
||||
|
||||
void increment() { ++frame_count; }
|
||||
void calculate(std::uint32_t current_ticks) {
|
||||
if (current_ticks - ticks >= 1000) {
|
||||
last_value = frame_count;
|
||||
frame_count = 0;
|
||||
ticks = current_ticks;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bool subsystem_initialized_{false};
|
||||
|
||||
Config config_{};
|
||||
@@ -83,4 +104,7 @@ class Screen {
|
||||
int canvas_width_{0};
|
||||
int canvas_height_{0};
|
||||
std::vector<std::uint32_t> canvas_;
|
||||
|
||||
FPS fps_{};
|
||||
std::string gpu_driver_;
|
||||
};
|
||||
@@ -25,6 +25,10 @@ void Sprite::render() {
|
||||
surface_->render(static_cast<int>(pos_.x), static_cast<int>(pos_.y), &clip_);
|
||||
}
|
||||
|
||||
void Sprite::render(Uint8 source_color, Uint8 target_color) {
|
||||
surface_->renderWithColorReplace(static_cast<int>(pos_.x), static_cast<int>(pos_.y), source_color, target_color, &clip_);
|
||||
}
|
||||
|
||||
void Sprite::setPosition(float x, float y) {
|
||||
pos_.x = x;
|
||||
pos_.y = y;
|
||||
|
||||
@@ -18,6 +18,9 @@ class Sprite {
|
||||
|
||||
virtual void update(float delta_time);
|
||||
virtual void render();
|
||||
// Renderitza substituint l'índex source_color per target_color (ús típic:
|
||||
// la classe Text, que recolora glifs monocroms per paràmetre).
|
||||
virtual void render(Uint8 source_color, Uint8 target_color);
|
||||
virtual void clear();
|
||||
|
||||
[[nodiscard]] auto getX() const -> float { return pos_.x; }
|
||||
|
||||
@@ -113,3 +113,33 @@ void Surface::render(int x, int y, const SDL_FRect* src_rect, SDL_FlipMode flip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Surface::renderWithColorReplace(int x, int y, Uint8 source_color, Uint8 target_color, const SDL_FRect* src_rect, SDL_FlipMode flip) const {
|
||||
Uint32* dst = Screen::get().getCanvasArgb();
|
||||
const int DST_W = Screen::get().getCanvasWidth();
|
||||
const int DST_H = Screen::get().getCanvasHeight();
|
||||
|
||||
const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0;
|
||||
const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0;
|
||||
int sw = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : surface_data_->width;
|
||||
int sh = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : surface_data_->height;
|
||||
|
||||
sw = std::min(sw, surface_data_->width - SX);
|
||||
sh = std::min(sh, surface_data_->height - SY);
|
||||
|
||||
const Uint8* src = surface_data_->data.get();
|
||||
for (int iy = 0; iy < sh; ++iy) {
|
||||
for (int ix = 0; ix < sw; ++ix) {
|
||||
const int SRC_X = (flip == SDL_FLIP_HORIZONTAL) ? (SX + sw - 1 - ix) : (SX + ix);
|
||||
const int SRC_Y = (flip == SDL_FLIP_VERTICAL) ? (SY + sh - 1 - iy) : (SY + iy);
|
||||
const int DST_X = x + ix;
|
||||
const int DST_Y = y + iy;
|
||||
if (DST_X < 0 || DST_X >= DST_W || DST_Y < 0 || DST_Y >= DST_H) { continue; }
|
||||
|
||||
const Uint8 COLOR = src[SRC_X + (SRC_Y * surface_data_->width)];
|
||||
if (COLOR == transparent_color_) { continue; }
|
||||
const Uint8 REMAPPED = (COLOR == source_color) ? target_color : sub_palette_[COLOR];
|
||||
dst[DST_X + (DST_Y * DST_W)] = palette_[REMAPPED];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,10 @@ class Surface {
|
||||
|
||||
// Vobla la regió indicada al framebuffer ARGB del joc usant la paleta propia.
|
||||
void render(int x, int y, const SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE) const;
|
||||
// Igual que render() però substituint els píxels amb valor source_color per
|
||||
// target_color abans d'aplicar la paleta. Útil per a la classe Text: els
|
||||
// glifs guarden "tinta" amb un únic índex i es recoloren per paràmetre.
|
||||
void renderWithColorReplace(int x, int y, Uint8 source_color, Uint8 target_color, const SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE) const;
|
||||
|
||||
void clear(Uint8 color);
|
||||
void putPixel(int x, int y, Uint8 color);
|
||||
@@ -62,7 +66,7 @@ class Surface {
|
||||
[[nodiscard]] auto getHeight() const -> int { return surface_data_->height; }
|
||||
|
||||
[[nodiscard]] auto getTransparentColor() const -> Uint8 { return transparent_color_; }
|
||||
void setTransparentColor(Uint8 color = 255) { transparent_color_ = color; }
|
||||
void setTransparentColor(Uint8 color = 0) { transparent_color_ = color; }
|
||||
|
||||
void setPalette(const Palette& palette) { palette_ = palette; }
|
||||
[[nodiscard]] auto getPaletteColor(Uint8 index) const -> Uint32 { return palette_[index]; }
|
||||
@@ -73,5 +77,8 @@ class Surface {
|
||||
std::shared_ptr<SurfaceData> surface_data_;
|
||||
Palette palette_{};
|
||||
SubPalette sub_palette_{};
|
||||
Uint8 transparent_color_{255};
|
||||
// Convenció de tots els GIFs del projecte (sprites i fonts): l'índex 0
|
||||
// és el color de fons/transparent. Els tilesets que usen un altre índex
|
||||
// han de cridar setTransparentColor() explícitament.
|
||||
Uint8 transparent_color_{0};
|
||||
};
|
||||
@@ -0,0 +1,258 @@
|
||||
#include "core/rendering/text.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "core/rendering/sprite/sprite.hpp"
|
||||
#include "core/rendering/surface.hpp"
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "utils/utils.hpp"
|
||||
|
||||
// Decodifica el pròxim codepoint UTF-8 de la cadena i avança 'pos' al byte
|
||||
// següent. Retorna U+FFFD davant seqüències malformades perquè el caller
|
||||
// seguisca dibuixant (cau al glif '?' més endavant).
|
||||
auto Text::nextCodepoint(const std::string& s, std::size_t& pos) -> std::uint32_t {
|
||||
auto c = static_cast<unsigned char>(s[pos]);
|
||||
std::uint32_t cp = 0;
|
||||
std::size_t extra = 0;
|
||||
|
||||
if (c < 0x80) {
|
||||
cp = c;
|
||||
extra = 0;
|
||||
} else if (c < 0xC0 || c >= 0xF8) {
|
||||
pos++;
|
||||
return 0xFFFD;
|
||||
} else if (c < 0xE0) {
|
||||
cp = c & 0x1F;
|
||||
extra = 1;
|
||||
} else if (c < 0xF0) {
|
||||
cp = c & 0x0F;
|
||||
extra = 2;
|
||||
} else {
|
||||
cp = c & 0x07;
|
||||
extra = 3;
|
||||
}
|
||||
|
||||
pos++;
|
||||
for (std::size_t i = 0; i < extra && pos < s.size(); ++i, ++pos) {
|
||||
auto cb = static_cast<unsigned char>(s[pos]);
|
||||
if ((cb & 0xC0) != 0x80) { return 0xFFFD; }
|
||||
cp = (cp << 6) | (cb & 0x3F);
|
||||
}
|
||||
return cp;
|
||||
}
|
||||
|
||||
auto Text::codepointToUtf8(std::uint32_t cp) -> std::string {
|
||||
std::string result;
|
||||
if (cp < 0x80) {
|
||||
result += static_cast<char>(cp);
|
||||
} else if (cp < 0x800) {
|
||||
result += static_cast<char>(0xC0 | (cp >> 6));
|
||||
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
} else if (cp < 0x10000) {
|
||||
result += static_cast<char>(0xE0 | (cp >> 12));
|
||||
result += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
} else {
|
||||
result += static_cast<char>(0xF0 | (cp >> 18));
|
||||
result += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||
result += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Format .fnt: claus "box_width/box_height/columns/cell_spacing/row_spacing"
|
||||
// i una línia per glif amb "codepoint_decimal amplada_visual". Els glifs
|
||||
// s'ordenen al png per ordre d'aparició al .fnt (glyph_index).
|
||||
auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> {
|
||||
auto tf = std::make_shared<File>();
|
||||
|
||||
auto file_data = Resource::Helper::loadFile(file_path);
|
||||
if (file_data.empty()) {
|
||||
std::cerr << "Error: Fitxer no trobat " << getFileName(file_path) << '\n';
|
||||
throw std::runtime_error("Fitxer no trobat: " + getFileName(file_path));
|
||||
}
|
||||
|
||||
std::string content(file_data.begin(), file_data.end());
|
||||
std::istringstream stream(content);
|
||||
std::string line;
|
||||
int glyph_index = 0;
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
if (!line.empty() && line.back() == '\r') { line.pop_back(); }
|
||||
if (line.empty() || line[0] == '#') { continue; }
|
||||
|
||||
std::istringstream ls(line);
|
||||
std::string key;
|
||||
ls >> key;
|
||||
|
||||
if (key == "box_width") {
|
||||
ls >> tf->box_width;
|
||||
} else if (key == "box_height") {
|
||||
ls >> tf->box_height;
|
||||
} else if (key == "columns") {
|
||||
ls >> tf->columns;
|
||||
} else if (key == "cell_spacing") {
|
||||
ls >> tf->cell_spacing;
|
||||
} else if (key == "row_spacing") {
|
||||
ls >> tf->row_spacing;
|
||||
} else {
|
||||
std::uint32_t codepoint = 0;
|
||||
int width = 0;
|
||||
try {
|
||||
codepoint = static_cast<std::uint32_t>(std::stoul(key));
|
||||
ls >> width;
|
||||
} catch (...) {
|
||||
continue;
|
||||
}
|
||||
Offset off{};
|
||||
const int ROW_SP = tf->row_spacing > 0 ? tf->row_spacing : tf->cell_spacing;
|
||||
off.x = ((glyph_index % tf->columns) * (tf->box_width + tf->cell_spacing)) + tf->cell_spacing;
|
||||
off.y = ((glyph_index / tf->columns) * (tf->box_height + ROW_SP)) + tf->cell_spacing;
|
||||
off.w = width;
|
||||
tf->offset[codepoint] = off;
|
||||
++glyph_index;
|
||||
}
|
||||
}
|
||||
|
||||
printWithDots("Text File : ", getFileName(file_path), "[ LOADED ]");
|
||||
return tf;
|
||||
}
|
||||
|
||||
Text::Text(const std::shared_ptr<Surface>& surface, const std::string& text_file) {
|
||||
auto tf = loadTextFile(text_file);
|
||||
box_height_ = tf->box_height;
|
||||
box_width_ = tf->box_width;
|
||||
offset_ = tf->offset;
|
||||
sprite_ = std::make_unique<Sprite>(surface, SDL_FRect{.x = 0.0F, .y = 0.0F, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||
}
|
||||
|
||||
Text::Text(const std::shared_ptr<Surface>& surface, const std::shared_ptr<File>& text_file)
|
||||
: sprite_(std::make_unique<Sprite>(surface, SDL_FRect{.x = 0.0F, .y = 0.0F, .w = static_cast<float>(text_file->box_width), .h = static_cast<float>(text_file->box_height)})),
|
||||
box_width_(text_file->box_width),
|
||||
box_height_(text_file->box_height),
|
||||
offset_(text_file->offset) {
|
||||
}
|
||||
|
||||
// Dibuixa la cadena amb la tinta per defecte (codi 15 a la paleta del png).
|
||||
// Els glifs amb codepoint no registrat es substitueixen pel glif '?'.
|
||||
void Text::write(int x, int y, const std::string& text, int kerning, int length) {
|
||||
int shift = 0;
|
||||
int glyphs_done = 0;
|
||||
std::size_t pos = 0;
|
||||
|
||||
sprite_->setY(static_cast<float>(y));
|
||||
while (pos < text.size()) {
|
||||
if (length != -1 && glyphs_done >= length) { break; }
|
||||
std::uint32_t cp = nextCodepoint(text, pos);
|
||||
auto it = offset_.find(cp);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) {
|
||||
sprite_->setClip(static_cast<float>(it->second.x), static_cast<float>(it->second.y), static_cast<float>(box_width_), static_cast<float>(box_height_));
|
||||
sprite_->setX(static_cast<float>(x + shift));
|
||||
sprite_->render(1, 15);
|
||||
shift += it->second.w + kerning;
|
||||
}
|
||||
++glyphs_done;
|
||||
}
|
||||
}
|
||||
|
||||
void Text::writeColored(int x, int y, const std::string& text, Uint8 color, int kerning, int length) {
|
||||
int shift = 0;
|
||||
int glyphs_done = 0;
|
||||
std::size_t pos = 0;
|
||||
|
||||
sprite_->setY(static_cast<float>(y));
|
||||
while (pos < text.size()) {
|
||||
if (length != -1 && glyphs_done >= length) { break; }
|
||||
std::uint32_t cp = nextCodepoint(text, pos);
|
||||
auto it = offset_.find(cp);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) {
|
||||
sprite_->setClip(static_cast<float>(it->second.x), static_cast<float>(it->second.y), static_cast<float>(box_width_), static_cast<float>(box_height_));
|
||||
sprite_->setX(static_cast<float>(x + shift));
|
||||
sprite_->render(1, color);
|
||||
shift += it->second.w + kerning;
|
||||
}
|
||||
++glyphs_done;
|
||||
}
|
||||
}
|
||||
|
||||
void Text::writeShadowed(int x, int y, const std::string& text, Uint8 color, Uint8 shadow_distance, int kerning, int length) {
|
||||
writeColored(x + shadow_distance, y + shadow_distance, text, color, kerning, length);
|
||||
write(x, y, text, kerning, length);
|
||||
}
|
||||
|
||||
void Text::writeCentered(int x, int y, const std::string& text, int kerning, int length) {
|
||||
x -= (Text::length(text, kerning) / 2);
|
||||
write(x, y, text, kerning, length);
|
||||
}
|
||||
|
||||
void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int length) {
|
||||
const bool CENTERED = ((flags & CENTER_FLAG) == CENTER_FLAG);
|
||||
const bool SHADOWED = ((flags & SHADOW_FLAG) == SHADOW_FLAG);
|
||||
const bool COLORED = ((flags & COLOR_FLAG) == COLOR_FLAG);
|
||||
const bool STROKED = ((flags & STROKE_FLAG) == STROKE_FLAG);
|
||||
|
||||
if (CENTERED) { x -= (Text::length(text, kerning) / 2); }
|
||||
|
||||
if (SHADOWED) {
|
||||
writeColored(x + shadow_distance, y + shadow_distance, text, shadow_color, kerning, length);
|
||||
}
|
||||
|
||||
if (STROKED) {
|
||||
const int MAX_DIST = static_cast<int>(shadow_distance);
|
||||
for (int dist = 1; dist <= MAX_DIST; ++dist) {
|
||||
for (int dy = -dist; dy <= dist; ++dy) {
|
||||
for (int dx = -dist; dx <= dist; ++dx) {
|
||||
writeColored(x + dx, y + dy, text, shadow_color, kerning, length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (COLORED) {
|
||||
writeColored(x, y, text, text_color, kerning, length);
|
||||
} else {
|
||||
write(x, y, text, kerning, length);
|
||||
}
|
||||
}
|
||||
|
||||
auto Text::length(const std::string& text, int kerning) const -> int {
|
||||
int shift = 0;
|
||||
std::size_t pos = 0;
|
||||
|
||||
while (pos < text.size()) {
|
||||
std::uint32_t cp = nextCodepoint(text, pos);
|
||||
auto it = offset_.find(cp);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) {
|
||||
shift += it->second.w + kerning;
|
||||
}
|
||||
}
|
||||
|
||||
return shift > 0 ? shift - kerning : 0;
|
||||
}
|
||||
|
||||
auto Text::glyphWidth(std::uint32_t codepoint, int kerning) const -> int {
|
||||
auto it = offset_.find(codepoint);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) { return it->second.w + kerning; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto Text::getGlyphClip(std::uint32_t codepoint) const -> SDL_FRect {
|
||||
auto it = offset_.find(codepoint);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it == offset_.end()) { return {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; }
|
||||
return {.x = static_cast<float>(it->second.x),
|
||||
.y = static_cast<float>(it->second.y),
|
||||
.w = static_cast<float>(box_width_),
|
||||
.h = static_cast<float>(box_height_)};
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "core/rendering/sprite/sprite.hpp"
|
||||
class Surface;
|
||||
|
||||
// Bitmap font amb suport UTF-8. Els glifs es guarden en una única graella
|
||||
// al png, amb ample variable per caràcter i una tinta monocroma que es
|
||||
// recolora al vol via Sprite::render(src, tgt).
|
||||
class Text {
|
||||
public:
|
||||
struct Offset {
|
||||
int x{0};
|
||||
int y{0};
|
||||
int w{0};
|
||||
};
|
||||
|
||||
struct File {
|
||||
int box_width{0};
|
||||
int box_height{0};
|
||||
int columns{16};
|
||||
int cell_spacing{0};
|
||||
int row_spacing{0};
|
||||
std::unordered_map<std::uint32_t, Offset> offset;
|
||||
};
|
||||
|
||||
// Flags per a writeDX (composables amb |).
|
||||
static constexpr Uint8 COLOR_FLAG = 1;
|
||||
static constexpr Uint8 SHADOW_FLAG = 2;
|
||||
static constexpr Uint8 CENTER_FLAG = 4;
|
||||
static constexpr Uint8 STROKE_FLAG = 8;
|
||||
|
||||
Text(const std::shared_ptr<Surface>& surface, const std::string& text_file);
|
||||
Text(const std::shared_ptr<Surface>& surface, const std::shared_ptr<File>& text_file);
|
||||
~Text() = default;
|
||||
|
||||
Text(const Text&) = delete;
|
||||
Text(Text&&) = delete;
|
||||
auto operator=(const Text&) -> Text& = delete;
|
||||
auto operator=(Text&&) -> Text& = delete;
|
||||
|
||||
void write(int x, int y, const std::string& text, int kerning = 1, int length = -1);
|
||||
void writeColored(int x, int y, const std::string& text, Uint8 color, int kerning = 1, int length = -1);
|
||||
void writeShadowed(int x, int y, const std::string& text, Uint8 color, Uint8 shadow_distance = 1, int kerning = 1, int length = -1);
|
||||
void writeCentered(int x, int y, const std::string& text, int kerning = 1, int length = -1);
|
||||
void writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning = 1, Uint8 text_color = 0, Uint8 shadow_distance = 1, Uint8 shadow_color = 0, int length = -1);
|
||||
|
||||
[[nodiscard]] auto length(const std::string& text, int kerning = 1) const -> int;
|
||||
[[nodiscard]] auto getCharacterSize() const -> int { return box_width_; }
|
||||
[[nodiscard]] auto glyphWidth(std::uint32_t codepoint, int kerning = 0) const -> int;
|
||||
[[nodiscard]] auto getGlyphClip(std::uint32_t codepoint) const -> SDL_FRect;
|
||||
[[nodiscard]] auto getSprite() const -> Sprite* { return sprite_.get(); }
|
||||
|
||||
static auto loadTextFile(const std::string& file_path) -> std::shared_ptr<File>;
|
||||
static auto codepointToUtf8(std::uint32_t cp) -> std::string;
|
||||
static auto nextCodepoint(const std::string& s, std::size_t& pos) -> std::uint32_t;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Sprite> sprite_;
|
||||
int box_width_{0};
|
||||
int box_height_{0};
|
||||
std::unordered_map<std::uint32_t, Offset> offset_;
|
||||
};
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/render_info.hpp"
|
||||
#include "core/rendering/screen.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "core/scene/scene.hpp"
|
||||
@@ -51,6 +52,10 @@ Director::Director() {
|
||||
Screen::get().applyCrtPiParams(Options::buildCrtPiParams());
|
||||
(void)Resource::Cache::get();
|
||||
|
||||
// L'overlay de debug necessita Screen inicialitzat (llegirà el canvas
|
||||
// i carregarà la font 8bithud via Resource::Cache).
|
||||
RenderInfo::get().init();
|
||||
|
||||
current_scene_ = std::make_unique<GameScene>();
|
||||
last_ticks_ms_ = SDL_GetTicks();
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ namespace {
|
||||
if (node.contains("texture_filter")) {
|
||||
Options::video.texture_filter = filterFromString(node["texture_filter"].get_value<std::string>());
|
||||
}
|
||||
if (node.contains("render_info")) { Options::video.render_info = node["render_info"].get_value<bool>(); }
|
||||
parseGpu(node);
|
||||
parseShader(node);
|
||||
}
|
||||
@@ -285,6 +286,7 @@ namespace Options {
|
||||
out << " vsync: " << (video.vsync ? "true" : "false") << "\n";
|
||||
out << " scaling_mode: \"" << scalingModeToString(video.scaling_mode) << "\"\n";
|
||||
out << " texture_filter: \"" << filterToString(video.texture_filter) << "\"\n";
|
||||
out << " render_info: " << (video.render_info ? "true" : "false") << "\n";
|
||||
out << " gpu:\n";
|
||||
out << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << "\n";
|
||||
out << " preferred_driver: \"" << video.gpu.preferred_driver << "\"\n";
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace Options {
|
||||
bool vsync{Defaults::Video::VSYNC};
|
||||
Defaults::ScalingMode scaling_mode{Defaults::Video::SCALING_MODE};
|
||||
Defaults::TextureFilter texture_filter{Defaults::Video::TEXTURE_FILTER};
|
||||
bool render_info{Defaults::Video::RENDER_INFO};
|
||||
Gpu gpu{};
|
||||
Shader shader{};
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "core/rendering/render_info.hpp"
|
||||
#include "core/rendering/screen.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "core/system/event_buffer.hpp"
|
||||
@@ -26,17 +27,20 @@ GameScene::~GameScene() = default;
|
||||
|
||||
void GameScene::update(int delta_ms) {
|
||||
for (const auto& event : EventBuffer::events) {
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||
wants_quit_ = true;
|
||||
if (event.type == SDL_EVENT_KEY_DOWN) {
|
||||
if (event.key.scancode == SDL_SCANCODE_ESCAPE) { wants_quit_ = true; }
|
||||
if (event.key.scancode == SDL_SCANCODE_F3) { RenderInfo::get().toggle(); }
|
||||
}
|
||||
}
|
||||
|
||||
const float DT = static_cast<float>(delta_ms) / 1000.0F;
|
||||
player_->update(DT);
|
||||
RenderInfo::get().update(delta_ms);
|
||||
}
|
||||
|
||||
void GameScene::render() {
|
||||
Screen::get().clear(0x00, 0x00, 0x00);
|
||||
player_->render();
|
||||
RenderInfo::get().render();
|
||||
Screen::get().present();
|
||||
}
|
||||
@@ -40,6 +40,7 @@ namespace Defaults {
|
||||
constexpr const char* CRTPI_DEFAULT_PRESET = "default"; // Preset inicial CrtPi.
|
||||
constexpr const char* POSTFX_FILE = "shaders/postfx.yaml"; // Relatiu a la carpeta de sistema.
|
||||
constexpr const char* CRTPI_FILE = "shaders/crtpi.yaml"; // Relatiu a la carpeta de sistema.
|
||||
constexpr bool RENDER_INFO = false; // Overlay de debug (FPS/driver/shader) amagat per defecte.
|
||||
} // namespace Video
|
||||
|
||||
namespace Audio {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Texts {
|
||||
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
||||
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
||||
constexpr const char* VERSION = "0.6.0"; // Single source of truth — Makefile el grep
|
||||
constexpr const char* VERSION = "0.7.0"; // Single source of truth — Makefile el grep
|
||||
} // namespace Texts
|
||||
|
||||
namespace GameCanvas {
|
||||
|
||||
Reference in new issue
Block a user