diff --git a/include/app/app.h b/include/app/app.h index c5f459a..6e74087 100644 --- a/include/app/app.h +++ b/include/app/app.h @@ -1,25 +1,189 @@ #include "binding/ruby.h" +#include "localization/localization.h" #include "utils.h" #include "window/window.h" +namespace app { struct Element { - Vec2 position; - float rotation; - Vec2 scale; - float z; + Vec2 position = {0, 0}; + float rotation = 0; + Vec2 scale = {1, 1}; + float z = 0; + bool click_through = false; Ruby::Block on_click; - Ruby::Block on_update; - Ruby::Block on_hover; - virtual void render(Window &window) {} + virtual ~Element() = default; + virtual void render(Window &window, uint64_t abs_time) = 0; + virtual Vec2 size(Window *window) = 0; + + void click() { + if (!mrb_nil_p(on_click.proc)) { + int x = mrb_gc_arena_save(Ruby::mrb); + on_click.call(); + mrb_gc_arena_restore(Ruby::mrb, x); + } + } }; +struct Image : Element { + Animation animation; + float alpha = 1.0f; + + Image(Animation animation) : animation(std::move(animation)) {} + + Vec2 size(Window *window) override { + if (animation.frames.empty()) + return {0, 0}; + return window->get_image_size(animation.frames[0]); + } + + void render(Window &window, uint64_t abs_time) override { + if (animation.frames.empty()) + return; + + int frame_index = (int)((abs_time * animation.frame_rate) / 1000) % animation.frames.size(); + window.draw(animation.frames[frame_index], position, z, rotation, scale, alpha); + } +}; + +struct Text : Element { + Localization::Key key; + int font_id; + int font_size = 16; + Color color = {255, 255, 255, 255}; + + Text(Localization::Key key, int font_id, int font_size, Color color) + : key(key), font_id(font_id), font_size(font_size), color(color) {} + + Vec2 size(Window *window) override { + return window->get_text_size(text_id); + }; + + void render(Window &window, uint64_t abs_time) override { + if (last_language != Localization::current_language || last_key != key || text_id == -1) { + std::string localized_str = Localization::get(key); + if (text_id != -1) + window.unload_text(text_id); + text_id = window.create_text(font_id, localized_str.c_str(), localized_str.size(), color); + last_language = Localization::current_language; + last_key = key; + } + window.write(text_id, position, z, rotation, scale); + } + +private: + int text_id = -1; + Localization::LanguageCode last_language = -1; + Localization::Key last_key = -1; +}; + +struct ERect : Element { + Vec2 shape = {0, 0}; + Color color = {255, 255, 255, 255}; + Vec2 size(Window *) override { + return shape; + }; + + ERect(Vec2 shape, Color color) : shape(shape), color(color) {} + + void render(Window &window, uint64_t abs_time) override { + window.draw_rect(Rect::from_size(position, shape), color, z, rotation); + } +}; + +extern Pool> element_pool; + struct Scene { - std::vector elements; + std::vector element_ids; + Ruby::Block update; + + void add_element(int element_id) { + auto it = std::lower_bound( + element_ids.begin(), + element_ids.end(), + element_id, + [](auto &a, auto &b) { + return element_pool[a]->z < element_pool[b]->z || (element_pool[a]->z == element_pool[b]->z && a < b); + } + ); + element_ids.insert(it, element_id); + } + + void update_scene(uint64_t delta_time) { + if (!mrb_nil_p(update.proc)) { + int x = mrb_gc_arena_save(Ruby::mrb); + mrb_value dt_val = mrb_int_value(Ruby::mrb, delta_time); + update.call(1, &dt_val); + mrb_gc_arena_restore(Ruby::mrb, x); + } + } + + void click(Window *window, Vec2 position) { + for (auto it = element_ids.rbegin(); it != element_ids.rend(); ++it) { + Element *e = element_pool[*it].get(); + Vec2 size = e->size(window); + Rect rect = Rect::from_size(e->position, size * e->scale); + if (rect.contains(position)) { + e->click(); + if (e->click_through) + continue; + else + break; + } + } + } + + void render(Window &window, uint64_t abs_time) { + for (const int &e : element_ids) + element_pool[e]->render(window, abs_time); + } }; +extern Pool> scene_pool; + struct App { Window window; + int current_scene_id = 0; + bool running = true; - App(Vec2 size, const char *title) : window(size, title) {} -}; \ No newline at end of file + App(Vec2 size, const char *title, Animation loading_animation) + : window(size, title) { + std::unique_ptr loading_scene = std::make_unique(); +#warning "Should try to center the loading image or scale it to fit the screen" + std::unique_ptr image = std::make_unique(loading_animation); + int image_id = element_pool.acquire(std::move(image)); + loading_scene->add_element(image_id); + scene_pool.acquire(std::move(loading_scene)); + } + + void loop() { + while (running) { + uint64_t dt = window.delta_time(); + uint64_t abs_time = window.get_time(); + + Scene *scene = scene_pool[current_scene_id].get(); + + scene->update_scene(dt); + + Event e; + while (window.poll(e)) { + switch (e.type) { + case Event::QUIT: + running = false; + break; + case Event::MOUSE_BUTTON_DOWN: + scene->click(&window, e.mouse_button.position); + break; + default: + break; + } + window.consume(e); + } + + scene->render(window, abs_time); + + window.render(); + } + } +}; +} // namespace app \ No newline at end of file diff --git a/include/binding/ruby.h b/include/binding/ruby.h index bb7de47..ab1b9f4 100644 --- a/include/binding/ruby.h +++ b/include/binding/ruby.h @@ -7,7 +7,20 @@ struct Block { mrb_value proc; Block(mrb_value proc) : proc(proc) { - mrb_gc_protect(mrb, proc); + mrb_gc_register(mrb, proc); + } + + Block() : proc(mrb_nil_value()) {} + + Block(const Block &) = delete; + Block &operator=(const Block &) = delete; + + mrb_value call(int argc = 0, mrb_value *argv = nullptr) const { + return mrb_funcall(mrb, proc, "call", argc, argv); + } + + ~Block() { + mrb_gc_unregister(mrb, proc); } }; }; // namespace Ruby \ No newline at end of file diff --git a/include/localization/localization.h b/include/localization/localization.h new file mode 100644 index 0000000..bc39898 --- /dev/null +++ b/include/localization/localization.h @@ -0,0 +1,28 @@ +#include "pch.h" +#include "utils.h" + +namespace Localization { +using Key = uint16_t; + +// Maps localization keys to their string identifiers. This is used for text elements to look up the correct localized string. +extern std::unordered_map localization_key_map; + +using LanguageCode = uint32_t; // 32-bit integer to store the language code (e.g., "en", "fr", "es") + +extern LanguageCode current_language; // The currently active language code + +using JoinedKey = uint64_t; // 64-bit integer to store the joined language code and key + +inline JoinedKey join_keys(LanguageCode lang_code, Key loc_key) { + return (static_cast(lang_code) << 16) | loc_key; +} + +extern std::unordered_map localized_strings; + +inline std::string get(Key key) { + auto it = localized_strings.find(join_keys(current_language, key)); + if (it != localized_strings.end()) + return it->second; + return ""; +} +} // namespace Localization \ No newline at end of file diff --git a/include/utils.h b/include/utils.h index ecfbfb8..1a3e550 100644 --- a/include/utils.h +++ b/include/utils.h @@ -14,6 +14,16 @@ template struct Vec2 { T x, y; + template + Vec2 operator*(Vec2 s) { + return {x * s.x, y * s.y}; + } + + template + Vec2 operator/(Vec2 s) { + return {x / s.x, y / s.y}; + } + template Vec2 operator*(U s) const { return {x * s, y * s}; @@ -98,6 +108,10 @@ struct Rect { }; } + static Rect from_size(Vec2 position, Vec2 size) { + return Rect(position, position + size); + } + bool contains(const Vec2 &point) const { return point.x >= diagonal.start.x && point.x <= diagonal.end.x && point.y >= diagonal.start.y && point.y <= diagonal.end.y; @@ -142,4 +156,44 @@ struct Rect { } }; +template +struct Pool { + Pool() { + items.reserve(20); + free_indices.reserve(20); + } + + T &operator[](int index) { + return items[index]; + } + + const std::vector &all() { + return items; + } + + int acquire(const T &item) { + if (!free_indices.empty()) { + int index = free_indices.back(); + free_indices.pop_back(); + items[index] = item; + return index; + } else { + items.push_back(item); + return items.size() - 1; + } + } + + void release(int index) { + free_indices.push_back(index); + } + + bool is_valid(size_t index) const { + return index >= 0 && index < items.size() && std::find(free_indices.begin(), free_indices.end(), index) == free_indices.end(); + } + +private: + std::vector items; + std::vector free_indices; +}; + #endif \ No newline at end of file diff --git a/include/window/window.h b/include/window/window.h index 8eb0b0b..ae46b9f 100644 --- a/include/window/window.h +++ b/include/window/window.h @@ -4,10 +4,6 @@ #include "pch.h" #include "utils.h" -using ImageID = uint32_t; -using FontID = uint32_t; -using TextID = uint32_t; - struct Font { TTF_Font *font; bool pixel_art; @@ -52,6 +48,12 @@ struct Event { }; }; +struct Animation { + std::vector frames; + float frame_rate; + bool loop; +}; + struct Window { public: // title can be free'd by the caller after the window is created, as it's copied internally. @@ -61,19 +63,21 @@ public: // Rendering functions - ImageID load_image(const char *path, bool pixel_art = false); - void unload_image(ImageID image_id); + int load_image(const char *path, bool pixel_art = false); + void unload_image(int image_id); + Vec2 get_image_size(int image_id); - FontID load_font(const char *path, int font_size, bool bold = false, bool italic = false, bool underline = false, bool pixel_art = false); - void unload_font(FontID font_id); + int load_font(const char *path, int font_size, bool bold = false, bool italic = false, bool underline = false, bool pixel_art = false); + void unload_font(int font_id); // text is copied internally, so it can be free'd by the caller after the text is created. - TextID create_text(FontID font_id, const char *text, uint32_t length, Color color); - void unload_text(TextID text_id); + int create_text(int font_id, const char *text, uint32_t length, Color color); + void unload_text(int text_id); + Vec2 get_text_size(int text_id); - bool write(TextID text_id, Vec2 position, float z, float angle = 0.0f, Vec2 scale = {1.0f, 1.0f}); + bool write(int text_id, Vec2 position, float z, float angle = 0.0f, Vec2 scale = {1.0f, 1.0f}); - bool draw(ImageID image_id, Vec2 position, float z, float angle = 0.0f, Vec2 scale = {1.0f, 1.0f}, float alpha = 1.0f); + bool draw(int image_id, Vec2 position, float z, float angle = 0.0f, Vec2 scale = {1.0f, 1.0f}, float alpha = 1.0f); bool draw_rect(Rect rect, Color color, float z, float angle = 0.0f); @@ -119,16 +123,13 @@ private: SDL_Texture *white_tex; // Image pool - std::vector images; - std::vector free_image_indices; + Pool image_pool; // Font pool - std::vector fonts; - std::vector free_font_indices; + Pool font_pool; // Text pool - std::vector texts; - std::vector free_text_indices; + Pool text_pool; struct RenderInstruction { enum Type { @@ -143,13 +144,13 @@ private: union { struct { - ImageID image_id; + int image_id; Vec2 position; float alpha; } draw_image; struct { - TextID text_id; + int text_id; Vec2 position; } draw_text; diff --git a/samples/button.rb b/samples/button.rb index 23ed3e4..8dfa7ce 100644 --- a/samples/button.rb +++ b/samples/button.rb @@ -4,7 +4,7 @@ main_scene = Scene.new default_font = Font.new "assets/DejaVuSans.ttf", 24, italic: true -text = ElementText.new "Click me!", font: default_font, position: {x: 50, y: 50}, z: 1 +text = ElementText.new "Click me!", font: default_font, position: {x: 50, y: 50}, z: 1, click_through: true rect = ElementRect.new {x: 50, y: 50, w: 100, h: 25}, color: 0xFF0000, z: 0 diff --git a/samples/main.rb b/samples/main.rb index fee3521..5ab6dea 100644 --- a/samples/main.rb +++ b/samples/main.rb @@ -3,9 +3,9 @@ width, height = 720, 480 -image = Image.new "assets/loading.png" +image = Tiles.from("assets/loading.png").first -app = App.new width, height, title: "My Game", loading_bg: image +app = App.new width, height, title: "My Game", loading_bg: image # can be image or animation app.map_key ?w, :forward app.map_key ?s, :backward @@ -57,8 +57,8 @@ world.spawn do |e| # Name is optional c.collision = true end e.add_component :sprite do |c| - c.tiles = Tiles.from "assets/sprite.png", tile_width: 50, tile_height: 50 - animation1 = {tiles: 0..3, frame_rate: 10, loop: true} + tiles = Tiles.from "assets/sprite.png", tile_width: 50, tile_height: 50 + animation1 = {tiles: tiles[0..3], frame_rate: 10, loop: true} c.animations = {idle: animation1} c.current_animation = :idle end @@ -81,8 +81,8 @@ world.spawn "Trap Preview" do |e| e.tags = [:trap_preview] e.active = false e.add_component :sprite do |c| - c.tiles = Tiles.from "assets/trap.png", tile_width: 50, tile_height: 50 - animation1 = {tiles: 0..3, frame_rate: 10, loop: true} + tiles = Tiles.from "assets/trap.png", tile_width: 50, tile_height: 50 + animation1 = {tiles: tiles[0..3], frame_rate: 10, loop: true} c.animations = {idle: animation1} c.current_animation = :idle end @@ -123,8 +123,9 @@ world.system :trap_placement, trigger: trigger do |dt| e.tags = [:trap] e.position = world.mouse_position_world e.add_component :sprite do |c| - c.tiles = Tiles.from "assets/trap.png", tile_width: 50, tile_height: 50 - animation1 = {tiles: 0..3, frame_rate: 10, loop: true} + tiles = Tiles.from "assets/trap.png", tile_width: 50, tile_height: 50 + tiles2 = Tiles.from "assets/trap_2.png", tile_width: 50, tile_height: 50 + animation1 = {tiles: tiles[0..3] + tiles2[0..3], frame_rate: 10, loop: true} c.animations = {idle: animation1} c.current_animation = :idle end diff --git a/src/main.cc b/src/main.cc index c78f72d..0493e64 100644 --- a/src/main.cc +++ b/src/main.cc @@ -5,8 +5,8 @@ int main() { Window w({800, 600}, "Render Test"); - ImageID img = w.load_image("./assets/images/arrow.png", true); - FontID font = w.load_font( + int img = w.load_image("./assets/images/arrow.png", true); + int font = w.load_font( "./assets/fonts/charybdis.ttf", 24, false, @@ -15,7 +15,7 @@ int main() { true ); - TextID text = w.create_text(font, "Hello Engine", 12, {255, 255, 255, 255}); + int text = w.create_text(font, "Hello Engine", 12, {255, 255, 255, 255}); float t = 0.0f; diff --git a/src/window/window.cc b/src/window/window.cc index 16ca1c9..e0dca51 100644 --- a/src/window/window.cc +++ b/src/window/window.cc @@ -15,13 +15,6 @@ Window::Window(Vec2 size, const char *title) { compute_transform(); - images.reserve(100); - free_image_indices.reserve(100); - fonts.reserve(100); - free_font_indices.reserve(100); - texts.reserve(100); - free_text_indices.reserve(100); - text_input_active = false; white_tex = SDL_CreateTexture( @@ -37,15 +30,15 @@ Window::Window(Vec2 size, const char *title) { }; Window::~Window() { - for (auto tex : images) + for (auto tex : image_pool.all()) if (tex) SDL_DestroyTexture(tex); - for (auto tex : texts) + for (auto tex : text_pool.all()) if (tex) SDL_DestroyTexture(tex); - for (auto &f : fonts) + for (auto &f : font_pool.all()) if (f.font) TTF_CloseFont(f.font); @@ -55,7 +48,7 @@ Window::~Window() { SDL_Quit(); } -ImageID Window::load_image(const char *path, bool pixel_art) { +int Window::load_image(const char *path, bool pixel_art) { SDL_Texture *texture = IMG_LoadTexture(renderer, path); if (pixel_art) { @@ -67,31 +60,20 @@ ImageID Window::load_image(const char *path, bool pixel_art) { return UINT32_MAX; } - if (!free_image_indices.empty()) { - ImageID id = free_image_indices.back(); - free_image_indices.pop_back(); - images[id] = texture; - return id; - } else { - images.push_back(texture); - return images.size() - 1; - } - - return UINT32_MAX; + return image_pool.acquire(texture); } -void Window::unload_image(ImageID image_id) { - if (image_id >= images.size() || images[image_id] == nullptr) { +void Window::unload_image(int image_id) { + if (!image_pool.is_valid(image_id)) { fprintf(stderr, "Invalid image ID: %d\n", image_id); return; } - SDL_DestroyTexture(images[image_id]); - images[image_id] = nullptr; - free_image_indices.push_back(image_id); + SDL_DestroyTexture(image_pool[image_id]); + image_pool.release(image_id); } -FontID Window::load_font(const char *path, int font_size, bool bold, bool italic, bool underline, bool pixel_art) { +int Window::load_font(const char *path, int font_size, bool bold, bool italic, bool underline, bool pixel_art) { TTF_Font *font = TTF_OpenFont(path, font_size); if (font == nullptr) { @@ -109,38 +91,27 @@ FontID Window::load_font(const char *path, int font_size, bool bold, bool italic TTF_SetFontStyle(font, style); - if (!free_font_indices.empty()) { - FontID id = free_font_indices.back(); - free_font_indices.pop_back(); - fonts[id] = {font, pixel_art}; - return id; - } else { - fonts.push_back({font, pixel_art}); - return fonts.size() - 1; - } - - return UINT32_MAX; + return font_pool.acquire({font, pixel_art}); } -void Window::unload_font(FontID font_id) { - if (font_id >= fonts.size() || fonts[font_id].font == nullptr) { +void Window::unload_font(int font_id) { + if (!font_pool.is_valid(font_id)) { fprintf(stderr, "Invalid font ID: %d\n", font_id); return; } - TTF_CloseFont(fonts[font_id].font); - fonts[font_id].font = nullptr; - free_font_indices.push_back(font_id); + TTF_CloseFont(font_pool[font_id].font); + font_pool.release(font_id); } -TextID Window::create_text(FontID font_id, const char *text, uint32_t length, Color color) { - if (font_id >= fonts.size() || fonts[font_id].font == nullptr) { +int Window::create_text(int font_id, const char *text, uint32_t length, Color color) { + if (!font_pool.is_valid(font_id)) { fprintf(stderr, "Invalid font ID: %d\n", font_id); return UINT32_MAX; } SDL_Color sdl_color = {color.r, color.g, color.b, color.a}; - SDL_Surface *surface = TTF_RenderText_Solid(fonts[font_id].font, text, length, sdl_color); + SDL_Surface *surface = TTF_RenderText_Solid(font_pool[font_id].font, text, length, sdl_color); if (surface == nullptr) { fprintf(stderr, "Failed to create text surface: %s\n", SDL_GetError()); @@ -155,35 +126,24 @@ TextID Window::create_text(FontID font_id, const char *text, uint32_t length, Co return UINT32_MAX; } - if (fonts[font_id].pixel_art) + if (font_pool[font_id].pixel_art) SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST); - if (!free_text_indices.empty()) { - TextID id = free_text_indices.back(); - free_text_indices.pop_back(); - texts[id] = texture; - return id; - } else { - texts.push_back(texture); - return texts.size() - 1; - } - - return UINT32_MAX; + return text_pool.acquire(texture); } -void Window::unload_text(TextID text_id) { - if (text_id >= texts.size() || texts[text_id] == nullptr) { +void Window::unload_text(int text_id) { + if (!text_pool.is_valid(text_id)) { fprintf(stderr, "Invalid text ID: %d\n", text_id); return; } - SDL_DestroyTexture(texts[text_id]); - texts[text_id] = nullptr; - free_text_indices.push_back(text_id); + SDL_DestroyTexture(text_pool[text_id]); + text_pool.release(text_id); } -bool Window::write(TextID text_id, Vec2 position, float z, float angle, Vec2 scale) { - if (text_id >= texts.size() || texts[text_id] == nullptr) { +bool Window::write(int text_id, Vec2 position, float z, float angle, Vec2 scale) { + if (!text_pool.is_valid(text_id)) { fprintf(stderr, "Invalid text ID: %d\n", text_id); return false; } @@ -202,8 +162,8 @@ bool Window::write(TextID text_id, Vec2 position, float z, float angle, V return true; }; -bool Window::draw(ImageID image_id, Vec2 position, float z, float angle, Vec2 scale, float alpha) { - if (image_id >= images.size() || images[image_id] == nullptr) { +bool Window::draw(int image_id, Vec2 position, float z, float angle, Vec2 scale, float alpha) { + if (!image_pool.is_valid(image_id)) { fprintf(stderr, "Invalid image ID: %d\n", image_id); return false; } @@ -284,7 +244,7 @@ void Window::render() { for (const RenderInstruction &instruction : render_instructions) { switch (instruction.type) { case RenderInstruction::DRAW_TEXT: { - SDL_Texture *texture = texts[instruction.draw_text.text_id]; + SDL_Texture *texture = text_pool[instruction.draw_text.text_id]; SDL_FlipMode flip = SDL_FLIP_NONE; @@ -317,7 +277,7 @@ void Window::render() { } break; case RenderInstruction::DRAW_IMAGE: { - SDL_Texture *texture = images[instruction.draw_image.image_id]; + SDL_Texture *texture = image_pool[instruction.draw_image.image_id]; float w, h; SDL_GetTextureSize(texture, &w, &h);