Motor de sprites/animacions/surfaces i jugador centrat (v0.2.0)

This commit is contained in:
JailDesigner committed 2026-04-18 21:02:55 +02:00
1 parent 7bb1edd08a
commit de5a670fdb
34 files changed
+16137 -81

No files matched your search

+2 -2
View File
@@ -11,8 +11,8 @@ Checks:
- -modernize-avoid-c-arrays,-warnings-as-errors - -modernize-avoid-c-arrays,-warnings-as-errors
WarningsAsErrors: '*' WarningsAsErrors: '*'
# Solo headers del propio código fuente (external/ y spv/ tienen su propio .clang-tidy dummy) # Headers nostres (excloem source/external/ que conté dependències de tercers no editables)
HeaderFilterRegex: 'source/.*' HeaderFilterRegex: 'source/(core|game|utils)/'
FormatStyle: file FormatStyle: file
CheckOptions: CheckOptions:
+2 -1
View File
@@ -78,7 +78,8 @@ if ! cppcheck \
--inline-suppr \ --inline-suppr \
--suppress=missingIncludeSystem \ --suppress=missingIncludeSystem \
--suppress=toomanyconfigs \ --suppress=toomanyconfigs \
--suppress='*:*/source/external/*' \ --suppress='*:*source/external/*' \
--suppress=normalCheckLevelMaxBranches \
-D_DEBUG \ -D_DEBUG \
-DLINUX_BUILD \ -DLINUX_BUILD \
--quiet \ --quiet \
+21 -2
View File
@@ -34,7 +34,16 @@ 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/game/scenes/black_scene.cpp source/core/rendering/gif.cpp
source/core/rendering/surface.cpp
source/core/rendering/sprite/sprite.cpp
source/core/rendering/sprite/moving_sprite.cpp
source/core/rendering/sprite/animated_sprite.cpp
source/core/resources/resource_helper.cpp
source/core/resources/resource_cache.cpp
source/utils/utils.cpp
source/game/scenes/game_scene.cpp
source/game/entities/player.cpp
) )
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3) find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
@@ -48,6 +57,15 @@ target_include_directories(${PROJECT_NAME} PRIVATE
target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3) target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
# Copia recursius de la carpeta data/ al costat del binari per que les rutes
# relatives ("data/player/player.gif") funcionen sense desplegament.
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/data"
"$<TARGET_FILE_DIR:${PROJECT_NAME}>/data"
COMMENT "Copying data/ next to the binary"
)
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic) target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic)
target_compile_definitions(${PROJECT_NAME} PRIVATE target_compile_definitions(${PROJECT_NAME} PRIVATE
$<$<CONFIG:Debug>:_DEBUG> $<$<CONFIG:Debug>:_DEBUG>
@@ -120,7 +138,8 @@ if(CPPCHECK_EXE)
--inline-suppr --inline-suppr
--suppress=missingIncludeSystem --suppress=missingIncludeSystem
--suppress=toomanyconfigs --suppress=toomanyconfigs
--suppress=*:*/source/external/* --suppress=*:*source/external/*
--suppress=normalCheckLevelMaxBranches
-D_DEBUG -D_DEBUG
-DLINUX_BUILD -DLINUX_BUILD
--quiet --quiet
Binary file not shown.

After

Width:  |  Height:  |  Size: 828 B

+24
View File
@@ -0,0 +1,24 @@
# player animation
tileSetFile: player.gif
frameWidth: 12
frameHeight: 24
animations:
- name: stand
frames: [1]
- name: walk
speed: 0.07
loopFrom: 0
frames: [2, 3, 4, 1]
- name: turn_walk
speed: [0.1, 0.07, 0.07, 0.07, 0.07]
loopFrom: 1
frames: [0, 2, 3, 4, 1]
- name: jump
frames: [6]
- name: jump_peak
frames: [5]
+295
View File
@@ -0,0 +1,295 @@
#include "core/rendering/gif.hpp"
#include <cstring> // Para memcpy, size_t
#include <iostream> // Para std::cout
#include <stdexcept> // Para runtime_error
#include <string> // Para allocator, char_traits, operator==, basic_string
namespace GIF {
// Función inline para reemplazar el macro READ.
// Actualiza el puntero 'buffer' tras copiar 'size' bytes a 'dst'.
inline void readBytes(const uint8_t*& buffer, void* dst, size_t size) {
std::memcpy(dst, buffer, size);
buffer += size;
}
// Inicializa el diccionario LZW con los valores iniciales
inline void initializeDictionary(std::vector<DictionaryEntry>& dictionary, int code_length, int& dictionary_ind) { // NOLINT(readability-identifier-naming)
int size = 1 << code_length;
dictionary.resize(1 << (code_length + 1));
for (dictionary_ind = 0; dictionary_ind < size; dictionary_ind++) {
dictionary[dictionary_ind].byte = static_cast<uint8_t>(dictionary_ind);
dictionary[dictionary_ind].prev = -1;
dictionary[dictionary_ind].len = 1;
}
dictionary_ind += 2; // Reservamos espacio para clear y stop codes
}
// Lee los próximos bits del stream de entrada para formar un código
inline auto readNextCode(const uint8_t*& input, int& input_length, unsigned int& mask, int code_length) -> int {
int code = 0;
for (int i = 0; i < (code_length + 1); i++) {
if (input_length <= 0) {
throw std::runtime_error("Unexpected end of input in decompress");
}
int bit = ((*input & mask) != 0) ? 1 : 0;
mask <<= 1;
if (mask == 0x100) {
mask = 0x01;
input++;
input_length--;
}
code |= (bit << i);
}
return code;
}
// Encuentra el primer byte de una cadena del diccionario
inline auto findFirstByte(const std::vector<DictionaryEntry>& dictionary, int code) -> uint8_t {
int ptr = code;
while (dictionary[ptr].prev != -1) {
ptr = dictionary[ptr].prev;
}
return dictionary[ptr].byte;
}
// Agrega una nueva entrada al diccionario
inline void addDictionaryEntry(std::vector<DictionaryEntry>& dictionary, int& dictionary_ind, int& code_length, int prev, int code) { // NOLINT(readability-identifier-naming)
uint8_t first_byte;
if (code == dictionary_ind) {
first_byte = findFirstByte(dictionary, prev);
} else {
first_byte = findFirstByte(dictionary, code);
}
dictionary[dictionary_ind].byte = first_byte;
dictionary[dictionary_ind].prev = prev;
dictionary[dictionary_ind].len = dictionary[prev].len + 1;
dictionary_ind++;
if ((dictionary_ind == (1 << (code_length + 1))) && (code_length < 11)) {
code_length++;
dictionary.resize(1 << (code_length + 1));
}
}
// Escribe la cadena decodificada al buffer de salida
inline auto writeDecodedString(const std::vector<DictionaryEntry>& dictionary, int code, uint8_t*& out) -> int {
int cur_code = code;
int match_len = dictionary[cur_code].len;
while (cur_code != -1) {
out[dictionary[cur_code].len - 1] = dictionary[cur_code].byte;
if (dictionary[cur_code].prev == cur_code) {
std::cerr << "Internal error; self-reference detected." << '\n';
throw std::runtime_error("Internal error in decompress: self-reference");
}
cur_code = dictionary[cur_code].prev;
}
out += match_len;
return match_len;
}
void Gif::decompress(int code_length, const uint8_t* input, int input_length, uint8_t* out) {
// Verifica que el code_length tenga un rango razonable.
if (code_length < 2 || code_length > 12) {
throw std::runtime_error("Invalid LZW code length");
}
int prev = -1;
std::vector<DictionaryEntry> dictionary;
int dictionary_ind;
unsigned int mask = 0x01;
int reset_code_length = code_length;
int clear_code = 1 << code_length;
int stop_code = clear_code + 1;
// Inicializamos el diccionario con el tamaño correspondiente.
initializeDictionary(dictionary, code_length, dictionary_ind);
// Bucle principal: procesar el stream comprimido.
while (input_length > 0) {
int code = readNextCode(input, input_length, mask, code_length);
if (code == clear_code) {
// Reinicia el diccionario.
code_length = reset_code_length;
initializeDictionary(dictionary, code_length, dictionary_ind);
prev = -1;
continue;
}
if (code == stop_code) {
break;
}
if (prev > -1 && code_length < 12) {
if (code > dictionary_ind) {
std::cerr << "code = " << std::hex << code
<< ", but dictionary_ind = " << dictionary_ind << '\n';
throw std::runtime_error("LZW error: code exceeds dictionary_ind.");
}
addDictionaryEntry(dictionary, dictionary_ind, code_length, prev, code);
}
prev = code;
// Verifica que 'code' sea un índice válido antes de usarlo.
if (code < 0 || static_cast<size_t>(code) >= dictionary.size()) {
std::cerr << "Invalid LZW code " << code
<< ", dictionary size " << dictionary.size() << '\n';
throw std::runtime_error("LZW error: invalid code encountered");
}
writeDecodedString(dictionary, code, out);
}
}
auto Gif::readSubBlocks(const uint8_t*& buffer) -> std::vector<uint8_t> {
std::vector<uint8_t> data;
uint8_t block_size = *buffer;
buffer++;
while (block_size != 0) {
data.insert(data.end(), buffer, buffer + block_size);
buffer += block_size;
block_size = *buffer;
buffer++;
}
return data;
}
auto Gif::processImageDescriptor(const uint8_t*& buffer, [[maybe_unused]] const std::vector<RGB>& gct, [[maybe_unused]] int resolution_bits) -> std::vector<uint8_t> {
ImageDescriptor image_descriptor;
// Lee 9 bytes para el image descriptor.
readBytes(buffer, &image_descriptor, sizeof(ImageDescriptor));
uint8_t lzw_code_size;
readBytes(buffer, &lzw_code_size, sizeof(uint8_t));
std::vector<uint8_t> compressed_data = readSubBlocks(buffer);
int uncompressed_data_length = image_descriptor.image_width * image_descriptor.image_height;
std::vector<uint8_t> uncompressed_data(uncompressed_data_length);
decompress(lzw_code_size, compressed_data.data(), static_cast<int>(compressed_data.size()), uncompressed_data.data());
return uncompressed_data;
}
auto Gif::loadPalette(const uint8_t* buffer) -> std::vector<uint32_t> {
uint8_t header[6];
std::memcpy(header, buffer, 6);
buffer += 6;
ScreenDescriptor screen_descriptor;
std::memcpy(&screen_descriptor, buffer, sizeof(ScreenDescriptor));
buffer += sizeof(ScreenDescriptor);
std::vector<uint32_t> global_color_table;
if ((screen_descriptor.fields & 0x80) != 0) {
int global_color_table_size = 1 << ((screen_descriptor.fields & 0x07) + 1);
global_color_table.resize(global_color_table_size);
for (int i = 0; i < global_color_table_size; ++i) {
uint8_t r = buffer[0];
uint8_t g = buffer[1];
uint8_t b = buffer[2];
global_color_table[i] = (r << 16) | (g << 8) | b;
buffer += 3;
}
}
return global_color_table;
}
auto Gif::processGifStream(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t> {
// Leer la cabecera de 6 bytes ("GIF87a" o "GIF89a")
uint8_t header[6];
std::memcpy(header, buffer, 6);
buffer += 6;
// Opcional: Validar header
std::string header_str(reinterpret_cast<char*>(header), 6);
if (header_str != "GIF87a" && header_str != "GIF89a") {
throw std::runtime_error("Formato de archivo GIF inválido.");
}
// Leer el Screen Descriptor (7 bytes, empaquetado sin padding)
ScreenDescriptor screen_descriptor;
readBytes(buffer, &screen_descriptor, sizeof(ScreenDescriptor));
// Asigna ancho y alto
w = screen_descriptor.width;
h = screen_descriptor.height;
int color_resolution_bits = ((screen_descriptor.fields & 0x70) >> 4) + 1;
std::vector<RGB> global_color_table;
if ((screen_descriptor.fields & 0x80) != 0) {
int global_color_table_size = 1 << ((screen_descriptor.fields & 0x07) + 1);
global_color_table.resize(global_color_table_size);
std::memcpy(global_color_table.data(), buffer, static_cast<size_t>(3) * static_cast<size_t>(global_color_table_size));
buffer += static_cast<ptrdiff_t>(3) * global_color_table_size;
}
// Supongamos que 'buffer' es el puntero actual y TRAILER es 0x3B
uint8_t block_type = *buffer++;
while (block_type != TRAILER) {
if (block_type == EXTENSION_INTRODUCER) // 0x21
{
// Se lee la etiqueta de extensión, la cual indica el tipo de extensión.
uint8_t extension_label = *buffer++;
switch (extension_label) {
case GRAPHIC_CONTROL: // 0xF9
{
// Procesar Graphic Control Extension:
uint8_t block_size = *buffer++; // Normalmente, blockSize == 4
buffer += block_size; // Saltamos los 4 bytes del bloque fijo
// Saltar los sub-bloques
uint8_t sub_block_size = *buffer++;
while (sub_block_size != 0) {
buffer += sub_block_size;
sub_block_size = *buffer++;
}
break;
}
case APPLICATION_EXTENSION: // 0xFF
case COMMENT_EXTENSION: // 0xFE
case PLAINTEXT_EXTENSION: // 0x01
{
// Para estas extensiones, saltamos el bloque fijo y los sub-bloques.
uint8_t block_size = *buffer++;
buffer += block_size;
uint8_t sub_block_size = *buffer++;
while (sub_block_size != 0) {
buffer += sub_block_size;
sub_block_size = *buffer++;
}
break;
}
default: {
// Si la etiqueta de extensión es desconocida, saltarla también:
uint8_t block_size = *buffer++;
buffer += block_size;
uint8_t sub_block_size = *buffer++;
while (sub_block_size != 0) {
buffer += sub_block_size;
sub_block_size = *buffer++;
}
break;
}
}
} else if (block_type == IMAGE_DESCRIPTOR) {
// Procesar el Image Descriptor y retornar los datos de imagen
return processImageDescriptor(buffer, global_color_table, color_resolution_bits);
} else {
std::cerr << "Unrecognized block type " << std::hex << static_cast<int>(block_type) << '\n';
return std::vector<uint8_t>{};
}
block_type = *buffer++;
}
return std::vector<uint8_t>{};
}
auto Gif::loadGif(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t> {
return processGifStream(buffer, w, h);
}
} // namespace GIF
+92
View File
@@ -0,0 +1,92 @@
#pragma once
#include <cstdint> // Para uint8_t, uint16_t, uint32_t
#include <vector> // Para vector
namespace GIF {
// Constantes definidas con constexpr, en lugar de macros
constexpr uint8_t EXTENSION_INTRODUCER = 0x21;
constexpr uint8_t IMAGE_DESCRIPTOR = 0x2C;
constexpr uint8_t TRAILER = 0x3B;
constexpr uint8_t GRAPHIC_CONTROL = 0xF9;
constexpr uint8_t APPLICATION_EXTENSION = 0xFF;
constexpr uint8_t COMMENT_EXTENSION = 0xFE;
constexpr uint8_t PLAINTEXT_EXTENSION = 0x01;
#pragma pack(push, 1)
struct ScreenDescriptor {
uint16_t width;
uint16_t height;
uint8_t fields;
uint8_t background_color_index;
uint8_t pixel_aspect_ratio;
};
struct RGB {
uint8_t r, g, b;
};
struct ImageDescriptor {
uint16_t image_left_position;
uint16_t image_top_position;
uint16_t image_width;
uint16_t image_height;
uint8_t fields;
};
#pragma pack(pop)
struct DictionaryEntry {
uint8_t byte;
int prev;
int len;
};
struct Extension {
uint8_t extension_code;
uint8_t block_size;
};
struct GraphicControlExtension {
uint8_t fields;
uint16_t delay_time;
uint8_t transparent_color_index;
};
struct ApplicationExtension {
uint8_t application_id[8];
uint8_t version[3];
};
struct PlaintextExtension {
uint16_t left, top, width, height;
uint8_t cell_width, cell_height;
uint8_t foreground_color, background_color;
};
class Gif {
public:
// Descompone (uncompress) el bloque comprimido usando LZW.
// Este método puede lanzar std::runtime_error en caso de error.
static void decompress(int code_length, const uint8_t* input, int input_length, uint8_t* out);
// Carga la paleta (global color table) a partir de un buffer,
// retornándola en un vector de uint32_t (cada color se compone de R, G, B).
static auto loadPalette(const uint8_t* buffer) -> std::vector<uint32_t>;
// Carga el stream GIF; devuelve un vector con los datos de imagen sin comprimir y
// asigna el ancho y alto mediante referencias.
static auto loadGif(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t>;
private:
// Lee los sub-bloques de datos y los acumula en un std::vector<uint8_t>.
static auto readSubBlocks(const uint8_t*& buffer) -> std::vector<uint8_t>;
// Procesa el Image Descriptor y retorna el vector de datos sin comprimir.
static auto processImageDescriptor(const uint8_t*& buffer, const std::vector<RGB>& gct, int resolution_bits) -> std::vector<uint8_t>;
// Procesa el stream completo del GIF y devuelve los datos sin comprimir.
static auto processGifStream(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t>;
};
} // namespace GIF
+17 -23
View File
@@ -2,8 +2,11 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <algorithm>
#include <cstddef>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <ranges>
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
@@ -32,24 +35,15 @@ namespace {
} // namespace } // namespace
Screen* Screen::instance = nullptr; auto Screen::get() -> Screen& {
static Screen instance_;
void Screen::init() { return instance_;
if (instance == nullptr) {
instance = new Screen();
}
} }
void Screen::destroy() { Screen::Screen()
delete instance; : canvas_width_(GameCanvas::WIDTH),
instance = nullptr; canvas_height_(GameCanvas::HEIGHT),
} canvas_(static_cast<std::size_t>(GameCanvas::WIDTH) * static_cast<std::size_t>(GameCanvas::HEIGHT), 0xFF000000U) {
auto Screen::get() -> Screen* {
return instance;
}
Screen::Screen() {
if (!SDL_InitSubSystem(SDL_INIT_VIDEO)) { if (!SDL_InitSubSystem(SDL_INIT_VIDEO)) {
std::fprintf(stderr, "SDL_InitSubSystem(VIDEO) failed: %s\n", SDL_GetError()); std::fprintf(stderr, "SDL_InitSubSystem(VIDEO) failed: %s\n", SDL_GetError());
std::exit(EXIT_FAILURE); std::exit(EXIT_FAILURE);
@@ -76,9 +70,9 @@ Screen::Screen() {
SDL_SetRenderVSync(renderer_, Defaults::Video::VSYNC ? 1 : 0); SDL_SetRenderVSync(renderer_, Defaults::Video::VSYNC ? 1 : 0);
SDL_SetRenderLogicalPresentation(renderer_, GameCanvas::WIDTH, GameCanvas::HEIGHT, toSdlPresentation(Defaults::Video::SCALING_MODE)); SDL_SetRenderLogicalPresentation(renderer_, GameCanvas::WIDTH, GameCanvas::HEIGHT, toSdlPresentation(Defaults::Video::SCALING_MODE));
// Destination ARGB texture: paletted surfaces will be blitted onto this // STREAMING: el framebuffer ARGB del joc s'actualitza per CPU cada frame
// (each surface contributes its own palette, so the final image is full ARGB). // i s'envia a la GPU via SDL_UpdateTexture en present().
game_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, GameCanvas::WIDTH, GameCanvas::HEIGHT); game_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, GameCanvas::WIDTH, GameCanvas::HEIGHT);
if (game_texture_ == nullptr) { if (game_texture_ == nullptr) {
std::fprintf(stderr, "SDL_CreateTexture failed: %s\n", SDL_GetError()); std::fprintf(stderr, "SDL_CreateTexture failed: %s\n", SDL_GetError());
std::exit(EXIT_FAILURE); std::exit(EXIT_FAILURE);
@@ -94,13 +88,13 @@ Screen::~Screen() {
} }
void Screen::clear(std::uint8_t r, std::uint8_t g, std::uint8_t b) { void Screen::clear(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
SDL_SetRenderTarget(renderer_, game_texture_); const std::uint32_t COLOR = (0xFFU << 24) | (static_cast<std::uint32_t>(r) << 16) | (static_cast<std::uint32_t>(g) << 8) | static_cast<std::uint32_t>(b);
SDL_SetRenderDrawColor(renderer_, r, g, b, 0xFF); std::ranges::fill(canvas_, COLOR);
SDL_RenderClear(renderer_);
SDL_SetRenderTarget(renderer_, nullptr);
} }
void Screen::present() { void Screen::present() {
SDL_UpdateTexture(game_texture_, nullptr, canvas_.data(), canvas_width_ * static_cast<int>(sizeof(std::uint32_t)));
SDL_SetRenderDrawColor(renderer_, 0x00, 0x00, 0x00, 0xFF); SDL_SetRenderDrawColor(renderer_, 0x00, 0x00, 0x00, 0xFF);
SDL_RenderClear(renderer_); SDL_RenderClear(renderer_);
SDL_RenderTexture(renderer_, game_texture_, nullptr, nullptr); SDL_RenderTexture(renderer_, game_texture_, nullptr, nullptr);
+18 -6
View File
@@ -3,26 +3,38 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <cstdint> #include <cstdint>
#include <vector>
class Screen { class Screen {
public: public:
static void init(); // Meyers singleton: la destrucció ocorre durant la termianció estàtica
static void destroy(); // (LIFO respecte a Resource::Cache). El SDL_Quit s'ha de registrar via
static auto get() -> Screen*; // std::atexit ABANS de la primera crida a get(); així Screen::~Screen
// (que crida SDL_Destroy*/SDL_QuitSubSystem) s'executa amb SDL encara viu.
static auto get() -> Screen&;
Screen(const Screen&) = delete;
Screen(Screen&&) = delete;
auto operator=(const Screen&) -> Screen& = delete;
auto operator=(Screen&&) -> Screen& = delete;
void clear(std::uint8_t r = 0, std::uint8_t g = 0, std::uint8_t b = 0); void clear(std::uint8_t r = 0, std::uint8_t g = 0, std::uint8_t b = 0);
void present(); void present();
[[nodiscard]] auto getRenderer() const -> SDL_Renderer* { return renderer_; } [[nodiscard]] auto getRenderer() const -> SDL_Renderer* { return renderer_; }
[[nodiscard]] auto getTarget() const -> SDL_Texture* { return game_texture_; } [[nodiscard]] auto getCanvasArgb() -> std::uint32_t* { return canvas_.data(); }
[[nodiscard]] auto getCanvasWidth() const -> int { return canvas_width_; }
[[nodiscard]] auto getCanvasHeight() const -> int { return canvas_height_; }
private: private:
Screen(); Screen();
~Screen(); ~Screen();
static Screen* instance;
SDL_Window* window_{nullptr}; SDL_Window* window_{nullptr};
SDL_Renderer* renderer_{nullptr}; SDL_Renderer* renderer_{nullptr};
SDL_Texture* game_texture_{nullptr}; SDL_Texture* game_texture_{nullptr};
int canvas_width_{0};
int canvas_height_{0};
std::vector<std::uint32_t> canvas_;
}; };
@@ -0,0 +1,201 @@
#include "core/rendering/sprite/animated_sprite.hpp"
#include <cmath>
#include <iostream>
#include <numeric>
#include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp"
#include "external/fkyaml_node.hpp"
namespace {
auto convertYamlFramesToRects(const fkyaml::node& frames_node, float frame_width, float frame_height, int frames_per_row, int max_tiles) -> std::vector<SDL_FRect> {
std::vector<SDL_FRect> frames;
SDL_FRect rect = {.x = 0.0F, .y = 0.0F, .w = frame_width, .h = frame_height};
for (const auto& frame_index_node : frames_node) {
const int NUM_TILE = frame_index_node.get_value<int>();
if (NUM_TILE <= max_tiles) {
rect.x = static_cast<float>(NUM_TILE % frames_per_row) * frame_width;
rect.y = static_cast<float>(NUM_TILE / frames_per_row) * frame_height;
frames.emplace_back(rect);
}
}
return frames;
}
auto parseAnimations(const fkyaml::node& yaml, float frame_width, float frame_height, int frames_per_row, int max_tiles) -> std::vector<AnimatedSprite::AnimationData> {
std::vector<AnimatedSprite::AnimationData> animations;
if (!yaml.contains("animations") || !yaml["animations"].is_sequence()) {
return animations;
}
for (const auto& anim_node : yaml["animations"]) {
AnimatedSprite::AnimationData animation;
if (anim_node.contains("name")) {
animation.name = anim_node["name"].get_value<std::string>();
}
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
animation.frames = convertYamlFramesToRects(
anim_node["frames"],
frame_width,
frame_height,
frames_per_row,
max_tiles);
}
if (anim_node.contains("speed")) {
const auto& speed_node = anim_node["speed"];
if (speed_node.is_sequence()) {
for (const auto& s : speed_node) {
animation.speeds.push_back(s.get_value<float>());
}
} else {
auto spd = speed_node.get_value<float>();
if (spd > 0.0F) { animation.speeds.assign(animation.frames.size(), spd); }
}
}
if (anim_node.contains("loopFrom")) {
animation.loop_from = anim_node["loopFrom"].get_value<int>();
}
animations.push_back(animation);
}
return animations;
}
struct SheetParams {
float frame_width{0.0F};
float frame_height{0.0F};
int frames_per_row{1};
int max_tiles{1};
};
auto parseGlobalConfig(const fkyaml::node& yaml, std::shared_ptr<Surface>& surface) -> SheetParams {
SheetParams params;
if (yaml.contains("tileSetFile")) {
surface = Resource::Cache::get().getSurface(yaml["tileSetFile"].get_value<std::string>());
}
if (yaml.contains("frameWidth")) {
params.frame_width = static_cast<float>(yaml["frameWidth"].get_value<int>());
}
if (yaml.contains("frameHeight")) {
params.frame_height = static_cast<float>(yaml["frameHeight"].get_value<int>());
}
if (surface && params.frame_width > 0.0F && params.frame_height > 0.0F) {
params.frames_per_row = surface->getWidth() / static_cast<int>(params.frame_width);
const int H_TILES = surface->getHeight() / static_cast<int>(params.frame_height);
params.max_tiles = params.frames_per_row * H_TILES;
}
return params;
}
} // namespace
AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
std::string yaml_content(cached_data.yaml_data.begin(), cached_data.yaml_data.end());
try {
auto yaml = fkyaml::node::deserialize(yaml_content);
auto params = parseGlobalConfig(yaml, surface_);
animations_ = parseAnimations(yaml, params.frame_width, params.frame_height, params.frames_per_row, params.max_tiles);
buildNameIndex();
setWidth(params.frame_width);
setHeight(params.frame_height);
if (!animations_.empty() && !animations_[0].frames.empty()) {
setClip(animations_[0].frames[0]);
}
} catch (const fkyaml::exception& e) {
std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n';
throw;
} catch (const std::exception& e) {
std::cerr << "Error loading animation " << cached_data.name << ": " << e.what() << '\n';
throw;
}
}
void AnimatedSprite::buildNameIndex() {
animation_index_.clear();
for (int i = 0; i < static_cast<int>(animations_.size()); ++i) {
animation_index_[animations_[i].name] = i;
}
}
void AnimatedSprite::animate(float delta_time) {
if (animations_.empty()) { return; }
auto& anim = animations_[current_animation_];
if (anim.speeds.empty()) { return; }
anim.accumulated_time += delta_time;
const float TOTAL = std::accumulate(anim.speeds.begin(), anim.speeds.end(), 0.0F);
if (anim.accumulated_time >= TOTAL) {
if (anim.loop_from < 0) {
anim.current_frame = static_cast<int>(anim.frames.size()) - 1;
anim.completed = true;
setClip(anim.frames[anim.current_frame]);
return;
}
float loop_start = 0.0F;
for (int i = 0; i < anim.loop_from; ++i) { loop_start += anim.speeds[i]; }
float loop_len = TOTAL - loop_start;
anim.accumulated_time = loop_start + std::fmod(anim.accumulated_time - loop_start, loop_len);
}
float cursor = 0.0F;
for (int i = 0; i < static_cast<int>(anim.frames.size()); ++i) {
cursor += anim.speeds[i];
if (anim.accumulated_time < cursor) {
anim.current_frame = i;
setClip(anim.frames[i]);
return;
}
}
anim.current_frame = static_cast<int>(anim.frames.size()) - 1;
setClip(anim.frames[anim.current_frame]);
}
auto AnimatedSprite::animationIsCompleted() -> bool {
return animations_[current_animation_].completed;
}
void AnimatedSprite::setCurrentAnimation(const std::string& name) {
auto it = animation_index_.find(name);
if (it == animation_index_.end()) {
std::cout << "** Warning: could not find \"" << name << "\" animation" << '\n';
return;
}
setCurrentAnimation(it->second);
}
void AnimatedSprite::setCurrentAnimation(int index) {
if (current_animation_ != index) {
current_animation_ = index;
animations_[current_animation_].current_frame = 0;
animations_[current_animation_].accumulated_time = 0.0F;
animations_[current_animation_].completed = false;
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
}
}
void AnimatedSprite::update(float delta_time) {
animate(delta_time);
MovingSprite::update(delta_time);
}
void AnimatedSprite::resetAnimation() {
animations_[current_animation_].current_frame = 0;
animations_[current_animation_].accumulated_time = 0.0F;
animations_[current_animation_].completed = false;
}
void AnimatedSprite::setCurrentAnimationFrame(int num) {
if (num < 0 || num >= static_cast<int>(animations_[current_animation_].frames.size())) {
num = 0;
}
animations_[current_animation_].current_frame = num;
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
}
@@ -0,0 +1,44 @@
#pragma once
#include <SDL3/SDL.h>
#include <string>
#include <unordered_map>
#include <vector>
#include "core/rendering/sprite/moving_sprite.hpp"
#include "core/resources/resource_types.hpp"
class AnimatedSprite : public MovingSprite {
public:
struct AnimationData {
std::string name;
std::vector<SDL_FRect> frames;
std::vector<float> speeds;
int loop_from{-1};
bool completed{false};
int current_frame{0};
float accumulated_time{0.0F};
};
explicit AnimatedSprite(const AnimationResource& cached_data);
~AnimatedSprite() override = default;
void update(float delta_time) override;
auto animationIsCompleted() -> bool;
auto getCurrentAnimationSize() -> int { return static_cast<int>(animations_[current_animation_].frames.size()); }
void setCurrentAnimation(const std::string& name = "default");
void setCurrentAnimation(int index = 0);
void resetAnimation();
void setCurrentAnimationFrame(int num);
void animate(float delta_time);
private:
void buildNameIndex();
std::vector<AnimationData> animations_;
std::unordered_map<std::string, int> animation_index_;
int current_animation_{0};
};
@@ -0,0 +1,72 @@
#include "core/rendering/sprite/moving_sprite.hpp"
#include <utility>
#include "core/rendering/surface.hpp"
MovingSprite::MovingSprite() { Sprite::clear(); }
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface)
: Sprite(std::move(surface)) { Sprite::clear(); }
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
: Sprite(std::move(surface), pos),
x_(pos.x),
y_(pos.y) { Sprite::pos_ = pos; }
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos, SDL_FlipMode flip)
: Sprite(std::move(surface), pos),
x_(pos.x),
y_(pos.y),
flip_(flip) { Sprite::pos_ = pos; }
void MovingSprite::clear() {
x_ = 0.0F;
y_ = 0.0F;
vx_ = 0.0F;
vy_ = 0.0F;
ax_ = 0.0F;
ay_ = 0.0F;
flip_ = SDL_FLIP_NONE;
Sprite::clear();
}
void MovingSprite::move(float delta_time) {
vx_ += ax_ * delta_time;
vy_ += ay_ * delta_time;
x_ += vx_ * delta_time;
y_ += vy_ * delta_time;
pos_.x = static_cast<int>(x_);
pos_.y = static_cast<int>(y_);
}
void MovingSprite::update(float delta_time) {
move(delta_time);
}
void MovingSprite::render() {
surface_->render(static_cast<int>(pos_.x), static_cast<int>(pos_.y), &clip_, flip_);
}
void MovingSprite::setPos(SDL_FRect rect) {
x_ = rect.x;
y_ = rect.y;
pos_ = rect;
}
void MovingSprite::setPos(float x, float y) {
x_ = x;
y_ = y;
pos_.x = static_cast<int>(x_);
pos_.y = static_cast<int>(y_);
}
void MovingSprite::setPosX(float value) {
x_ = value;
pos_.x = static_cast<int>(x_);
}
void MovingSprite::setPosY(float value) {
y_ = value;
pos_.y = static_cast<int>(y_);
}
@@ -0,0 +1,53 @@
#pragma once
#include <SDL3/SDL.h>
#include <memory>
#include "core/rendering/sprite/sprite.hpp"
class Surface;
class MovingSprite : public Sprite {
public:
MovingSprite();
explicit MovingSprite(std::shared_ptr<Surface> surface);
MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos);
MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos, SDL_FlipMode flip);
~MovingSprite() override = default;
void update(float delta_time) override;
void render() override;
void clear() override;
[[nodiscard]] auto getPosX() const -> float { return x_; }
[[nodiscard]] auto getPosY() const -> float { return y_; }
[[nodiscard]] auto getVelX() const -> float { return vx_; }
[[nodiscard]] auto getVelY() const -> float { return vy_; }
[[nodiscard]] auto getAccelX() const -> float { return ax_; }
[[nodiscard]] auto getAccelY() const -> float { return ay_; }
void setPos(SDL_FRect rect);
void setPos(float x, float y);
void setPosX(float value);
void setPosY(float value);
void setVelX(float value) { vx_ = value; }
void setVelY(float value) { vy_ = value; }
void setAccelX(float value) { ax_ = value; }
void setAccelY(float value) { ay_ = value; }
void setFlip(SDL_FlipMode flip) { flip_ = flip; }
auto getFlip() -> SDL_FlipMode { return flip_; }
void flip() { flip_ = (flip_ == SDL_FLIP_HORIZONTAL) ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL; }
protected:
void move(float delta_time);
float x_{0.0F};
float y_{0.0F};
float vx_{0.0F};
float vy_{0.0F};
float ax_{0.0F};
float ay_{0.0F};
SDL_FlipMode flip_{SDL_FLIP_NONE};
};
+45
View File
@@ -0,0 +1,45 @@
#include "core/rendering/sprite/sprite.hpp"
#include <utility>
#include "core/rendering/surface.hpp"
Sprite::Sprite() = default;
Sprite::Sprite(std::shared_ptr<Surface> surface, float x, float y, float w, float h)
: surface_(std::move(surface)),
pos_{.x = x, .y = y, .w = w, .h = h},
clip_{.x = 0.0F, .y = 0.0F, .w = pos_.w, .h = pos_.h} {}
Sprite::Sprite(std::shared_ptr<Surface> surface, SDL_FRect rect)
: surface_(std::move(surface)),
pos_(rect),
clip_{.x = 0.0F, .y = 0.0F, .w = pos_.w, .h = pos_.h} {}
Sprite::Sprite(std::shared_ptr<Surface> surface)
: surface_(std::move(surface)),
pos_{0.0F, 0.0F, static_cast<float>(surface_->getWidth()), static_cast<float>(surface_->getHeight())},
clip_(pos_) {}
void Sprite::render() {
surface_->render(static_cast<int>(pos_.x), static_cast<int>(pos_.y), &clip_);
}
void Sprite::setPosition(float x, float y) {
pos_.x = x;
pos_.y = y;
}
void Sprite::setPosition(SDL_FPoint p) {
pos_.x = p.x;
pos_.y = p.y;
}
void Sprite::clear() {
pos_ = {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
clip_ = {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
}
void Sprite::update(float delta_time) {
(void)delta_time;
}
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include <SDL3/SDL.h>
#include <memory>
#include <utility>
class Surface;
class Sprite {
public:
Sprite();
Sprite(std::shared_ptr<Surface> surface, float x, float y, float w, float h);
Sprite(std::shared_ptr<Surface> surface, SDL_FRect rect);
explicit Sprite(std::shared_ptr<Surface> surface);
virtual ~Sprite() = default;
virtual void update(float delta_time);
virtual void render();
virtual void clear();
[[nodiscard]] auto getX() const -> float { return pos_.x; }
[[nodiscard]] auto getY() const -> float { return pos_.y; }
[[nodiscard]] auto getWidth() const -> float { return pos_.w; }
[[nodiscard]] auto getHeight() const -> float { return pos_.h; }
[[nodiscard]] auto getPosition() const -> SDL_FRect { return pos_; }
[[nodiscard]] auto getClip() const -> SDL_FRect { return clip_; }
[[nodiscard]] auto getSurface() const -> std::shared_ptr<Surface> { return surface_; }
auto getRect() -> SDL_FRect& { return pos_; }
void setX(float x) { pos_.x = x; }
void setY(float y) { pos_.y = y; }
void setWidth(float w) { pos_.w = w; }
void setHeight(float h) { pos_.h = h; }
void setPosition(float x, float y);
void setPosition(SDL_FPoint p);
void setPosition(SDL_FRect r) { pos_ = r; }
void incX(float value) { pos_.x += value; }
void incY(float value) { pos_.y += value; }
void setClip(SDL_FRect rect) { clip_ = rect; }
void setClip(float x, float y, float w, float h) { clip_ = SDL_FRect{.x = x, .y = y, .w = w, .h = h}; }
void setSurface(std::shared_ptr<Surface> surface) { surface_ = std::move(surface); }
protected:
std::shared_ptr<Surface> surface_{nullptr};
SDL_FRect pos_{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
SDL_FRect clip_{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
};
+115
View File
@@ -0,0 +1,115 @@
#include "core/rendering/surface.hpp"
#include <algorithm>
#include <cstring>
#include <iostream>
#include <stdexcept>
#include <vector>
#include "core/rendering/gif.hpp"
#include "core/rendering/screen.hpp"
#include "core/resources/resource_helper.hpp"
#include "utils/utils.hpp"
auto loadPaletteFromGif(const std::string& file_path) -> Palette {
auto buffer = Resource::Helper::loadFile(file_path);
if (buffer.empty()) {
throw std::runtime_error("Surface: cannot open palette file " + file_path);
}
std::vector<std::uint32_t> rgb = GIF::Gif::loadPalette(buffer.data());
if (rgb.empty()) {
throw std::runtime_error("Surface: no palette in GIF " + file_path);
}
Palette palette = {};
// Convertim RGB → ARGB amb canal alpha 0xFF.
const std::size_t COUNT = std::min(rgb.size(), palette.size());
for (std::size_t i = 0; i < COUNT; ++i) {
palette[i] = 0xFF000000U | rgb[i];
}
return palette;
}
Surface::Surface(int w, int h)
: surface_data_(std::make_shared<SurfaceData>(w, h)) {
initializeSubPalette(sub_palette_);
}
Surface::Surface(const std::string& file_path) {
SurfaceData loaded = loadSurface(file_path);
surface_data_ = std::make_shared<SurfaceData>(std::move(loaded));
initializeSubPalette(sub_palette_);
palette_ = loadPaletteFromGif(file_path);
}
auto Surface::loadSurface(const std::string& file_path) -> SurfaceData {
auto buffer = Resource::Helper::loadFile(file_path);
if (buffer.empty()) {
throw std::runtime_error("Surface: cannot open " + file_path);
}
Uint16 w = 0;
Uint16 h = 0;
std::vector<Uint8> raw_pixels = GIF::Gif::loadGif(buffer.data(), w, h);
if (raw_pixels.empty()) {
throw std::runtime_error("Surface: failed to decode GIF " + file_path);
}
auto pixels = std::shared_ptr<Uint8[]>(new Uint8[raw_pixels.size()], std::default_delete<Uint8[]>());
std::memcpy(pixels.get(), raw_pixels.data(), raw_pixels.size());
printWithDots("Surface : ", getFileName(file_path), "[ LOADED ]");
return {static_cast<int>(w), static_cast<int>(h), pixels};
}
void Surface::loadPalette(const std::string& file_path) {
palette_ = loadPaletteFromGif(file_path);
}
void Surface::loadPalette(const Palette& palette) {
palette_ = palette;
}
void Surface::clear(Uint8 color) {
const std::size_t TOTAL = static_cast<std::size_t>(surface_data_->width) * static_cast<std::size_t>(surface_data_->height);
std::fill(surface_data_->data.get(), surface_data_->data.get() + TOTAL, color);
}
void Surface::putPixel(int x, int y, Uint8 color) {
if (x < 0 || y < 0 || x >= surface_data_->width || y >= surface_data_->height) { return; }
surface_data_->data.get()[x + (y * surface_data_->width)] = color;
}
auto Surface::getPixel(int x, int y) const -> Uint8 {
return surface_data_->data.get()[x + (y * surface_data_->width)];
}
void Surface::render(int x, int y, 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; }
dst[DST_X + (DST_Y * DST_W)] = palette_[sub_palette_[COLOR]];
}
}
}
+77
View File
@@ -0,0 +1,77 @@
#pragma once
#include <SDL3/SDL.h>
#include <array>
#include <cstdint>
#include <memory>
#include <numeric>
#include <string>
// Cada Surface manté la seua propia paleta (256 colors ARGB). El destí del
// render es l'ARGB framebuffer del joc, mantingut per Screen, així que
// l'arquitectura és: sprites paletats → imatge final ARGB (>256 colors).
using Palette = std::array<Uint32, 256>;
using SubPalette = std::array<Uint8, 256>;
auto loadPaletteFromGif(const std::string& file_path) -> Palette;
struct SurfaceData {
std::shared_ptr<Uint8[]> data;
int width{0};
int height{0};
SurfaceData() = default;
SurfaceData(int w, int h)
: data(std::shared_ptr<Uint8[]>(new Uint8[static_cast<std::size_t>(w) * static_cast<std::size_t>(h)](), std::default_delete<Uint8[]>())),
width(w),
height(h) {}
SurfaceData(int w, int h, std::shared_ptr<Uint8[]> pixels)
: data(std::move(pixels)),
width(w),
height(h) {}
SurfaceData(SurfaceData&&) noexcept = default;
auto operator=(SurfaceData&&) noexcept -> SurfaceData& = default;
SurfaceData(const SurfaceData&) = delete;
auto operator=(const SurfaceData&) -> SurfaceData& = delete;
};
class Surface {
public:
// Crea una surface buida (paleta a zero) amb les dimensions donades.
Surface(int w, int h);
// Carrega pixels + paleta des d'un fitxer GIF.
explicit Surface(const std::string& file_path);
~Surface() = default;
static auto loadSurface(const std::string& file_path) -> SurfaceData;
void loadPalette(const std::string& file_path);
void loadPalette(const Palette& palette);
// 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 clear(Uint8 color);
void putPixel(int x, int y, Uint8 color);
[[nodiscard]] auto getPixel(int x, int y) const -> Uint8;
[[nodiscard]] auto getSurfaceData() const -> std::shared_ptr<SurfaceData> { return surface_data_; }
[[nodiscard]] auto getWidth() const -> int { return surface_data_->width; }
[[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 setPalette(const Palette& palette) { palette_ = palette; }
[[nodiscard]] auto getPaletteColor(Uint8 index) const -> Uint32 { return palette_[index]; }
static void initializeSubPalette(SubPalette& palette) { std::iota(palette.begin(), palette.end(), 0); }
private:
std::shared_ptr<SurfaceData> surface_data_;
Palette palette_{};
SubPalette sub_palette_{};
Uint8 transparent_color_{255};
};
+49
View File
@@ -0,0 +1,49 @@
#include "core/resources/resource_cache.hpp"
#include <algorithm>
#include <iostream>
#include <stdexcept>
#include "core/resources/resource_helper.hpp"
#include "utils/utils.hpp"
namespace Resource {
auto Cache::get() -> Cache& {
static Cache instance_;
return instance_;
}
void Cache::loadSurface(const std::string& name, const std::string& file_path) {
auto surface = std::make_shared<Surface>(file_path);
surfaces_.emplace_back(SurfaceResource{.name = name, .surface = std::move(surface)});
}
void Cache::loadAnimation(const std::string& name, const std::string& file_path) {
auto bytes = Helper::loadFile(file_path);
if (bytes.empty()) {
throw std::runtime_error("Cache: cannot load animation " + file_path);
}
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = std::move(bytes)});
printWithDots("Animation : ", getFileName(file_path), "[ LOADED ]");
}
auto Cache::getSurface(const std::string& name) -> std::shared_ptr<Surface> {
auto it = std::ranges::find_if(surfaces_, [&name](const auto& s) { return s.name == name; });
if (it == surfaces_.end()) {
std::cerr << "Cache: surface not found: " << name << '\n';
throw std::runtime_error("Surface not found: " + name);
}
return it->surface;
}
auto Cache::getAnimationData(const std::string& name) -> const AnimationResource& {
auto it = std::ranges::find_if(animations_, [&name](const auto& a) { return a.name == name; });
if (it == animations_.end()) {
std::cerr << "Cache: animation not found: " << name << '\n';
throw std::runtime_error("Animation not found: " + name);
}
return *it;
}
} // namespace Resource
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <memory>
#include <string>
#include <vector>
#include "core/resources/resource_types.hpp"
namespace Resource {
// Cache mínim per a la fase 2: només surfaces i animacions, càrrega
// explícita per nom + path. Per ara la cerca és lineal sobre vector.
// Meyers singleton: la destrucció estàtica només toca smart pointers, no
// depén d'SDL ni de Screen, així que és segura sense ordenació explícita.
class Cache {
public:
static auto get() -> Cache&;
Cache(const Cache&) = delete;
Cache(Cache&&) = delete;
auto operator=(const Cache&) -> Cache& = delete;
auto operator=(Cache&&) -> Cache& = delete;
// Carrega una surface des de fitxer i la registra amb el nom donat.
void loadSurface(const std::string& name, const std::string& file_path);
// Llig el YAML d'animació en cru i el registra amb el nom donat.
void loadAnimation(const std::string& name, const std::string& file_path);
auto getSurface(const std::string& name) -> std::shared_ptr<Surface>;
auto getAnimationData(const std::string& name) -> const AnimationResource&;
private:
Cache() = default;
~Cache() = default;
std::vector<SurfaceResource> surfaces_;
std::vector<AnimationResource> animations_;
};
} // namespace Resource
+24
View File
@@ -0,0 +1,24 @@
#include "core/resources/resource_helper.hpp"
#include <fstream>
#include <iostream>
namespace Resource::Helper {
auto loadFile(const std::string& filepath) -> std::vector<std::uint8_t> {
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "Resource::Helper: cannot open " << filepath << '\n';
return {};
}
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<std::uint8_t> data(static_cast<std::size_t>(size));
if (!file.read(reinterpret_cast<char*>(data.data()), size)) {
std::cerr << "Resource::Helper: failed to read " << filepath << '\n';
return {};
}
return data;
}
} // namespace Resource::Helper
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace Resource::Helper {
// Carga un fitxer del filesystem (a aquesta fase encara no hi ha pack).
auto loadFile(const std::string& filepath) -> std::vector<std::uint8_t>;
} // namespace Resource::Helper
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include "core/rendering/surface.hpp"
struct SurfaceResource {
std::string name;
std::shared_ptr<Surface> surface;
};
struct AnimationResource {
std::string name;
std::vector<std::uint8_t> yaml_data;
};
+17 -5
View File
@@ -4,29 +4,41 @@
#include <algorithm> #include <algorithm>
#include <cstdio> #include <cstdio>
#include <cstdlib>
#include <memory> #include <memory>
#include <utility> #include <utility>
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/resources/resource_cache.hpp"
#include "core/scene/scene.hpp" #include "core/scene/scene.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
#include "game/scenes/black_scene.hpp" #include "game/scenes/game_scene.hpp"
Director::Director() { Director::Director() {
if (!SDL_Init(0)) { if (!SDL_Init(0)) {
std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
} }
Screen::init(); // Registrem SDL_Quit ABANS de tocar cap singleton: així el handler atexit
// s'executa DESPRÉS dels destructors estàtics de Screen i Cache (atexit
// registrat abans de la construcció d'un objecte estàtic corre després
// del seu destructor). Sense açò, Screen::~Screen cridaria SDL_Destroy*
// amb SDL ja apagat → caos de destrucció clàssic.
std::atexit(+[] { SDL_Quit(); });
current_scene_ = std::make_unique<BlackScene>(); // Forcem la construcció dels singletons en l'ordre desitjat: primer
// Screen (necessari per a renderitzar), després Cache. La destrucció
// estàtica anirà LIFO: Cache primer, Screen últim.
(void)Screen::get();
(void)Resource::Cache::get();
current_scene_ = std::make_unique<GameScene>();
last_ticks_ms_ = SDL_GetTicks(); last_ticks_ms_ = SDL_GetTicks();
} }
Director::~Director() { Director::~Director() {
current_scene_.reset(); current_scene_.reset();
Screen::destroy(); // SDL_Quit corre via atexit; els singletons via destrucció estàtica.
SDL_Quit();
} }
void Director::processEvents() { void Director::processEvents() {
+14726
View File
File diff suppressed because it is too large. Load diff
+9
View File
@@ -0,0 +1,9 @@
#include "game/entities/player.hpp"
#include "core/resources/resource_cache.hpp"
Player::Player(float x, float y)
: AnimatedSprite(Resource::Cache::get().getAnimationData("player.yaml")) {
setPos(x, y);
setCurrentAnimation("stand");
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include "core/rendering/sprite/animated_sprite.hpp"
class Player : public AnimatedSprite {
public:
// Construeix el jugador a partir del recurs d'animació "player.yaml"
// ja registrat al cache. Comença en l'animació "stand".
Player(float x, float y);
};
-23
View File
@@ -1,23 +0,0 @@
#include "game/scenes/black_scene.hpp"
#include <SDL3/SDL.h>
#include "core/rendering/screen.hpp"
#include "core/system/event_buffer.hpp"
BlackScene::BlackScene() = default;
void BlackScene::update(int delta_ms) {
elapsed_ms_ += delta_ms;
for (const auto& event : EventBuffer::events) {
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE) {
wants_quit_ = true;
}
}
}
void BlackScene::render() {
Screen::get()->clear(0x00, 0x00, 0x00);
Screen::get()->present();
}
-14
View File
@@ -1,14 +0,0 @@
#pragma once
#include "core/scene/scene.hpp"
class BlackScene : public Scene {
public:
BlackScene();
void update(int delta_ms) override;
void render() override;
private:
int elapsed_ms_{0};
};
+42
View File
@@ -0,0 +1,42 @@
#include "game/scenes/game_scene.hpp"
#include <SDL3/SDL.h>
#include "core/rendering/screen.hpp"
#include "core/resources/resource_cache.hpp"
#include "core/system/event_buffer.hpp"
#include "game/entities/player.hpp"
#include "utils/defines.hpp"
namespace {
constexpr float PLAYER_FRAME_W = 12.0F;
constexpr float PLAYER_FRAME_H = 24.0F;
} // namespace
GameScene::GameScene() {
Resource::Cache::get().loadSurface("player.gif", "data/player/player.gif");
Resource::Cache::get().loadAnimation("player.yaml", "data/player/player.yaml");
const float X = static_cast<float>(GameCanvas::CENTER_X) - (PLAYER_FRAME_W / 2.0F);
const float Y = static_cast<float>(GameCanvas::CENTER_Y) - (PLAYER_FRAME_H / 2.0F);
player_ = std::make_unique<Player>(X, Y);
}
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;
}
}
const float DT = static_cast<float>(delta_ms) / 1000.0F;
player_->update(DT);
}
void GameScene::render() {
Screen::get().clear(0x00, 0x00, 0x00);
player_->render();
Screen::get().present();
}
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <memory>
#include "core/scene/scene.hpp"
class Player;
class GameScene : public Scene {
public:
GameScene();
~GameScene() override;
void update(int delta_ms) override;
void render() override;
private:
std::unique_ptr<Player> player_;
};
+18 -4
View File
@@ -3,6 +3,9 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3/SDL_main.h> #include <SDL3/SDL_main.h>
#include <iostream>
#include <memory>
#include "core/system/director.hpp" #include "core/system/director.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
@@ -10,8 +13,13 @@ auto SDL_AppInit(void** appstate, int argc, char* argv[]) -> SDL_AppResult {
(void)argc; (void)argc;
(void)argv; (void)argv;
auto* director = new Director(); // Marcador d'inici: ha de ser la PRIMERA línia de log del programa.
*appstate = director; std::cout << "Game start\n";
// Construïm el Director amb make_unique i l'entreguem en cru a SDL.
// El SDL_AppQuit reclamarà la propietat amb un altre unique_ptr (RAII).
auto director = std::make_unique<Director>();
*appstate = director.release();
return SDL_APP_CONTINUE; return SDL_APP_CONTINUE;
} }
@@ -28,6 +36,12 @@ auto SDL_AppIterate(void* appstate) -> SDL_AppResult {
void SDL_AppQuit(void* appstate, SDL_AppResult result) { void SDL_AppQuit(void* appstate, SDL_AppResult result) {
(void)result; (void)result;
auto* director = static_cast<Director*>(appstate); // Reabsorbim la propietat: el destructor neteja l'escena; els singletons
delete director; // (Screen, Cache) viuen fins a la destrucció estàtica i SDL_Quit dispara
// via atexit després d'ells.
{
std::unique_ptr<Director> director(static_cast<Director*>(appstate));
}
// Marcador de fi: ha de ser l'ÚLTIMA línia de log del programa.
std::cout << "Bye!\n";
} }
+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.1.0"; // Single source of truth — Makefile el grep constexpr const char* VERSION = "0.2.0"; // Single source of truth — Makefile el grep
} // namespace Texts } // namespace Texts
namespace GameCanvas { namespace GameCanvas {
+18
View File
@@ -0,0 +1,18 @@
#include "utils/utils.hpp"
#include <filesystem>
#include <ios>
#include <iostream>
auto getFileName(const std::string& path) -> std::string {
return std::filesystem::path(path).filename().string();
}
void printWithDots(const std::string& text1, const std::string& text2, const std::string& text3) {
std::cout.setf(std::ios::left, std::ios::adjustfield);
std::cout << text1;
std::cout.width(50 - text1.length() - text3.length());
std::cout.fill('.');
std::cout << text2;
std::cout << text3 << '\n';
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include <string>
auto getFileName(const std::string& path) -> std::string;
void printWithDots(const std::string& text1, const std::string& text2, const std::string& text3);