Font 8bithud, classe Text i overlay RenderInfo (F3, persistit a config.yaml) (v0.7.0)

This commit is contained in:
JailDesigner committed 2026-04-19 00:31:02 +02:00
1 parent ce5bec1b34
commit e6f3c888e6
19 files changed
+712 -6

No files matched your search

+2
View File
@@ -34,8 +34,10 @@ set(APP_SOURCES
source/main.cpp source/main.cpp
source/core/system/director.cpp source/core/system/director.cpp
source/core/rendering/screen.cpp source/core/rendering/screen.cpp
source/core/rendering/render_info.cpp
source/core/rendering/gif.cpp source/core/rendering/gif.cpp
source/core/rendering/surface.cpp source/core/rendering/surface.cpp
source/core/rendering/text.cpp
source/core/rendering/sprite/sprite.cpp source/core/rendering/sprite/sprite.cpp
source/core/rendering/sprite/moving_sprite.cpp source/core/rendering/sprite/moving_sprite.cpp
source/core/rendering/sprite/animated_sprite.cpp source/core/rendering/sprite/animated_sprite.cpp
+132
View File
@@ -0,0 +1,132 @@
# Font: 8bithud — generado desde 8-bit-hud.ttf size 5
# Generado con tools/font_gen/font_gen.py
box_width 8
box_height 8
columns 15
# codepoint_decimal ancho_visual
32 3 # U+0020
33 2 # !
34 5 # "
35 6 # #
36 6 # $
37 6 # %
38 6 # &
39 2 # '
40 3 # (
41 3 # )
42 4 # *
43 3 # +
44 2 # ,
45 3 # -
46 2 # .
47 4 # /
48 6 # 0
49 3 # 1
50 6 # 2
51 6 # 3
52 6 # 4
53 6 # 5
54 6 # 6
55 6 # 7
56 6 # 8
57 6 # 9
58 2 # :
59 2 # ;
60 4 # <
61 3 # =
62 4 # >
63 6 # ?
64 8 # @
65 6 # A
66 6 # B
67 6 # C
68 6 # D
69 6 # E
70 6 # F
71 6 # G
72 6 # H
73 6 # I
74 6 # J
75 6 # K
76 6 # L
77 6 # M
78 6 # N
79 6 # O
80 6 # P
81 6 # Q
82 6 # R
83 6 # S
84 6 # T
85 6 # U
86 5 # V
87 6 # W
88 6 # X
89 6 # Y
90 6 # Z
91 3 # [
92 4 # \
93 3 # ]
94 4 # ^
95 6 # _
96 2 # `
97 5 # a
98 5 # b
99 5 # c
100 5 # d
101 5 # e
102 5 # f
103 5 # g
104 5 # h
105 4 # i
106 5 # j
107 5 # k
108 5 # l
109 6 # m
110 5 # n
111 5 # o
112 5 # p
113 5 # q
114 5 # r
115 5 # s
116 4 # t
117 5 # u
118 5 # v
119 6 # w
120 4 # x
121 4 # y
122 5 # z
123 4 # {
124 1 # |
125 4 # }
126 4 # ~
192 6 # À
193 6 # Á
200 6 # È
201 6 # É
204 6 # Ì
205 6 # Í
210 6 # Ò
211 6 # Ó
219 6 # Ù
218 6 # Ú
209 6 # Ñ
199 6 # Ç
224 5 # à
225 5 # á
232 5 # è
233 5 # é
236 4 # ì
237 4 # í
242 5 # ò
243 5 # ó
249 5 # ù
250 5 # ú
241 5 # ñ
231 5 # ç
161 2 # ¡
191 6 # ¿
171 4 # «
187 4 # »
183 2 # ·
Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

+114
View File
@@ -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;
}
}
+45
View File
@@ -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)};
};
+6 -1
View File
@@ -110,12 +110,14 @@ void Screen::createSdl() {
backend->setPreferredDriver(config_.preferred_driver); backend->setPreferredDriver(config_.preferred_driver);
backend->setVSync(config_.vsync); backend->setVSync(config_.vsync);
if (backend->init(window_, canvas_width_, canvas_height_)) { 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); shader_backend_ = std::move(backend);
return; return;
} }
std::cerr << "Screen: GPU no disponible, fallback a SDL_Renderer\n"; std::cerr << "Screen: GPU no disponible, fallback a SDL_Renderer\n";
} }
gpu_driver_ = "sdl";
renderer_ = SDL_CreateRenderer(window_, nullptr); renderer_ = SDL_CreateRenderer(window_, nullptr);
if (renderer_ == 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() { void Screen::present() {
fps_.increment();
fps_.calculate(SDL_GetTicks());
if (shader_backend_ != nullptr) { if (shader_backend_ != nullptr) {
shader_backend_->uploadPixels(canvas_.data(), canvas_width_, canvas_height_); shader_backend_->uploadPixels(canvas_.data(), canvas_width_, canvas_height_);
shader_backend_->render(); shader_backend_->render();
+24
View File
@@ -62,6 +62,10 @@ class Screen {
[[nodiscard]] auto getCanvasWidth() const -> int { return canvas_width_; } [[nodiscard]] auto getCanvasWidth() const -> int { return canvas_width_; }
[[nodiscard]] auto getCanvasHeight() const -> int { return canvas_height_; } [[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: private:
Screen() = default; Screen() = default;
~Screen() = default; ~Screen() = default;
@@ -69,6 +73,23 @@ class Screen {
void destroySdl(); void destroySdl();
void createSdl(); 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}; bool subsystem_initialized_{false};
Config config_{}; Config config_{};
@@ -83,4 +104,7 @@ class Screen {
int canvas_width_{0}; int canvas_width_{0};
int canvas_height_{0}; int canvas_height_{0};
std::vector<std::uint32_t> canvas_; std::vector<std::uint32_t> canvas_;
FPS fps_{};
std::string gpu_driver_;
}; };
+4
View File
@@ -25,6 +25,10 @@ void Sprite::render() {
surface_->render(static_cast<int>(pos_.x), static_cast<int>(pos_.y), &clip_); 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) { void Sprite::setPosition(float x, float y) {
pos_.x = x; pos_.x = x;
pos_.y = y; pos_.y = y;
+3
View File
@@ -18,6 +18,9 @@ class Sprite {
virtual void update(float delta_time); virtual void update(float delta_time);
virtual void render(); 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(); virtual void clear();
[[nodiscard]] auto getX() const -> float { return pos_.x; } [[nodiscard]] auto getX() const -> float { return pos_.x; }
+30
View File
@@ -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];
}
}
}
+9 -2
View File
@@ -52,6 +52,10 @@ class Surface {
// Vobla la regió indicada al framebuffer ARGB del joc usant la paleta propia. // 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; 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 clear(Uint8 color);
void putPixel(int x, int y, 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 getHeight() const -> int { return surface_data_->height; }
[[nodiscard]] auto getTransparentColor() const -> Uint8 { return transparent_color_; } [[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; } void setPalette(const Palette& palette) { palette_ = palette; }
[[nodiscard]] auto getPaletteColor(Uint8 index) const -> Uint32 { return palette_[index]; } [[nodiscard]] auto getPaletteColor(Uint8 index) const -> Uint32 { return palette_[index]; }
@@ -73,5 +77,8 @@ class Surface {
std::shared_ptr<SurfaceData> surface_data_; std::shared_ptr<SurfaceData> surface_data_;
Palette palette_{}; Palette palette_{};
SubPalette sub_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};
}; };
+258
View File
@@ -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_)};
}
+69
View File
@@ -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_;
};
+5
View File
@@ -10,6 +10,7 @@
#include <memory> #include <memory>
#include <utility> #include <utility>
#include "core/rendering/render_info.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "core/scene/scene.hpp" #include "core/scene/scene.hpp"
@@ -51,6 +52,10 @@ Director::Director() {
Screen::get().applyCrtPiParams(Options::buildCrtPiParams()); Screen::get().applyCrtPiParams(Options::buildCrtPiParams());
(void)Resource::Cache::get(); (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>(); current_scene_ = std::make_unique<GameScene>();
last_ticks_ms_ = SDL_GetTicks(); last_ticks_ms_ = SDL_GetTicks();
} }
+2
View File
@@ -97,6 +97,7 @@ namespace {
if (node.contains("texture_filter")) { if (node.contains("texture_filter")) {
Options::video.texture_filter = filterFromString(node["texture_filter"].get_value<std::string>()); 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); parseGpu(node);
parseShader(node); parseShader(node);
} }
@@ -285,6 +286,7 @@ namespace Options {
out << " vsync: " << (video.vsync ? "true" : "false") << "\n"; out << " vsync: " << (video.vsync ? "true" : "false") << "\n";
out << " scaling_mode: \"" << scalingModeToString(video.scaling_mode) << "\"\n"; out << " scaling_mode: \"" << scalingModeToString(video.scaling_mode) << "\"\n";
out << " texture_filter: \"" << filterToString(video.texture_filter) << "\"\n"; out << " texture_filter: \"" << filterToString(video.texture_filter) << "\"\n";
out << " render_info: " << (video.render_info ? "true" : "false") << "\n";
out << " gpu:\n"; out << " gpu:\n";
out << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << "\n"; out << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << "\n";
out << " preferred_driver: \"" << video.gpu.preferred_driver << "\"\n"; out << " preferred_driver: \"" << video.gpu.preferred_driver << "\"\n";
+1
View File
@@ -34,6 +34,7 @@ namespace Options {
bool vsync{Defaults::Video::VSYNC}; bool vsync{Defaults::Video::VSYNC};
Defaults::ScalingMode scaling_mode{Defaults::Video::SCALING_MODE}; Defaults::ScalingMode scaling_mode{Defaults::Video::SCALING_MODE};
Defaults::TextureFilter texture_filter{Defaults::Video::TEXTURE_FILTER}; Defaults::TextureFilter texture_filter{Defaults::Video::TEXTURE_FILTER};
bool render_info{Defaults::Video::RENDER_INFO};
Gpu gpu{}; Gpu gpu{};
Shader shader{}; Shader shader{};
}; };
+6 -2
View File
@@ -2,6 +2,7 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include "core/rendering/render_info.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
@@ -26,17 +27,20 @@ GameScene::~GameScene() = default;
void GameScene::update(int delta_ms) { void GameScene::update(int delta_ms) {
for (const auto& event : EventBuffer::events) { for (const auto& event : EventBuffer::events) {
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE) { if (event.type == SDL_EVENT_KEY_DOWN) {
wants_quit_ = true; 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; const float DT = static_cast<float>(delta_ms) / 1000.0F;
player_->update(DT); player_->update(DT);
RenderInfo::get().update(delta_ms);
} }
void GameScene::render() { void GameScene::render() {
Screen::get().clear(0x00, 0x00, 0x00); Screen::get().clear(0x00, 0x00, 0x00);
player_->render(); player_->render();
RenderInfo::get().render();
Screen::get().present(); Screen::get().present();
} }
+1
View File
@@ -40,6 +40,7 @@ namespace Defaults {
constexpr const char* CRTPI_DEFAULT_PRESET = "default"; // Preset inicial CrtPi. 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* 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 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 Video
namespace Audio { namespace Audio {
+1 -1
View File
@@ -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.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 Texts
namespace GameCanvas { namespace GameCanvas {