Motor de sprites/animacions/surfaces i jugador centrat (v0.2.0)
This commit is contained in:
1 parent
7bb1edd08a
commit
de5a670fdb
34 files changed
+16137
-81
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <ranges>
|
||||
|
||||
#include "utils/defaults.hpp"
|
||||
#include "utils/defines.hpp"
|
||||
@@ -32,24 +35,15 @@ namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
Screen* Screen::instance = nullptr;
|
||||
|
||||
void Screen::init() {
|
||||
if (instance == nullptr) {
|
||||
instance = new Screen();
|
||||
}
|
||||
auto Screen::get() -> Screen& {
|
||||
static Screen instance_;
|
||||
return instance_;
|
||||
}
|
||||
|
||||
void Screen::destroy() {
|
||||
delete instance;
|
||||
instance = nullptr;
|
||||
}
|
||||
|
||||
auto Screen::get() -> Screen* {
|
||||
return instance;
|
||||
}
|
||||
|
||||
Screen::Screen() {
|
||||
Screen::Screen()
|
||||
: canvas_width_(GameCanvas::WIDTH),
|
||||
canvas_height_(GameCanvas::HEIGHT),
|
||||
canvas_(static_cast<std::size_t>(GameCanvas::WIDTH) * static_cast<std::size_t>(GameCanvas::HEIGHT), 0xFF000000U) {
|
||||
if (!SDL_InitSubSystem(SDL_INIT_VIDEO)) {
|
||||
std::fprintf(stderr, "SDL_InitSubSystem(VIDEO) failed: %s\n", SDL_GetError());
|
||||
std::exit(EXIT_FAILURE);
|
||||
@@ -76,9 +70,9 @@ Screen::Screen() {
|
||||
SDL_SetRenderVSync(renderer_, Defaults::Video::VSYNC ? 1 : 0);
|
||||
SDL_SetRenderLogicalPresentation(renderer_, GameCanvas::WIDTH, GameCanvas::HEIGHT, toSdlPresentation(Defaults::Video::SCALING_MODE));
|
||||
|
||||
// Destination ARGB texture: paletted surfaces will be blitted onto this
|
||||
// (each surface contributes its own palette, so the final image is full ARGB).
|
||||
game_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, GameCanvas::WIDTH, GameCanvas::HEIGHT);
|
||||
// STREAMING: el framebuffer ARGB del joc s'actualitza per CPU cada frame
|
||||
// i s'envia a la GPU via SDL_UpdateTexture en present().
|
||||
game_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, GameCanvas::WIDTH, GameCanvas::HEIGHT);
|
||||
if (game_texture_ == nullptr) {
|
||||
std::fprintf(stderr, "SDL_CreateTexture failed: %s\n", SDL_GetError());
|
||||
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) {
|
||||
SDL_SetRenderTarget(renderer_, game_texture_);
|
||||
SDL_SetRenderDrawColor(renderer_, r, g, b, 0xFF);
|
||||
SDL_RenderClear(renderer_);
|
||||
SDL_SetRenderTarget(renderer_, nullptr);
|
||||
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);
|
||||
std::ranges::fill(canvas_, COLOR);
|
||||
}
|
||||
|
||||
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_RenderClear(renderer_);
|
||||
SDL_RenderTexture(renderer_, game_texture_, nullptr, nullptr);
|
||||
|
||||
@@ -3,26 +3,38 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
class Screen {
|
||||
public:
|
||||
static void init();
|
||||
static void destroy();
|
||||
static auto get() -> Screen*;
|
||||
// Meyers singleton: la destrucció ocorre durant la termianció estàtica
|
||||
// (LIFO respecte a Resource::Cache). El SDL_Quit s'ha de registrar via
|
||||
// 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 present();
|
||||
|
||||
[[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:
|
||||
Screen();
|
||||
~Screen();
|
||||
|
||||
static Screen* instance;
|
||||
|
||||
SDL_Window* window_{nullptr};
|
||||
SDL_Renderer* renderer_{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};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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};
|
||||
};
|
||||
@@ -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]];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
@@ -4,29 +4,41 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/screen.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "core/scene/scene.hpp"
|
||||
#include "core/system/event_buffer.hpp"
|
||||
#include "game/scenes/black_scene.hpp"
|
||||
#include "game/scenes/game_scene.hpp"
|
||||
|
||||
Director::Director() {
|
||||
if (!SDL_Init(0)) {
|
||||
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();
|
||||
}
|
||||
|
||||
Director::~Director() {
|
||||
current_scene_.reset();
|
||||
Screen::destroy();
|
||||
SDL_Quit();
|
||||
// SDL_Quit corre via atexit; els singletons via destrucció estàtica.
|
||||
}
|
||||
|
||||
void Director::processEvents() {
|
||||
|
||||
Vendored
+14726
File diff suppressed because it is too large.
Load diff
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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};
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -3,6 +3,9 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_main.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
|
||||
#include "core/system/director.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)argv;
|
||||
|
||||
auto* director = new Director();
|
||||
*appstate = director;
|
||||
// Marcador d'inici: ha de ser la PRIMERA línia de log del programa.
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -28,6 +36,12 @@ auto SDL_AppIterate(void* appstate) -> SDL_AppResult {
|
||||
|
||||
void SDL_AppQuit(void* appstate, SDL_AppResult result) {
|
||||
(void)result;
|
||||
auto* director = static_cast<Director*>(appstate);
|
||||
delete director;
|
||||
// Reabsorbim la propietat: el destructor neteja l'escena; els singletons
|
||||
// (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";
|
||||
}
|
||||
@@ -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.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 GameCanvas {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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);
|
||||
Reference in new issue
Block a user