Nimbin[12]?SDL & Graphics / mapcreator / commits / b64e106

mapcreator git · master

SDL3 2.5D game and engine using assets, with map editor

sdl3 c++ game engine map-editor cmake · first commit 2025-10-31 · last commit 2026-09-07 (1 month ago) · synced 3 days ago · upstream: git.ide3.de/hsnr/sdl-spieleentwicklung/map_creator

C++ 99.3%
git clone https://git.christianimmanuel.de/sdl-graphics/mapcreator.gitwget https://git.christianimmanuel.de/sdl-graphics/mapcreator/archive/mapcreator.tar.gz

Add end screen 100%ai

Christian Immanuel · 2026-02-20 13:38 · b64e106adc4ffe7fbcdff757945d9209497c24f9

 quit/quit_text_be.aseprite        | Bin 0 -> 507 bytes
 quit/quit_text_be.png             | Bin 0 -> 1222 bytes
 quit/quit_text_continued.aseprite | Bin 0 -> 1188 bytes
 quit/quit_text_continued.png      | Bin 0 -> 1631 bytes
 quit/quit_text_nope.aseprite      | Bin 0 -> 618 bytes
 quit/quit_text_nope.png           | Bin 0 -> 1230 bytes
 quit/quit_text_to.aseprite        | Bin 0 -> 461 bytes
 quit/quit_text_to.png             | Bin 0 -> 1131 bytes
 src/end_screen/end_screen.cpp     | 245 ++++++++++++++++++++++++++++++++++++++
 src/end_screen/end_screen.hpp     |  72 +++++++++++
 src/game/game.cpp                 |   3 +
 11 files changed, 320 insertions(+)

diff --git a/quit/quit_text_be.aseprite b/quit/quit_text_be.aseprite
new file mode 100644
index 0000000..d09355e
Binary files /dev/null and b/quit/quit_text_be.aseprite differ
diff --git a/quit/quit_text_be.png b/quit/quit_text_be.png
new file mode 100644
index 0000000..5dee7a4
Binary files /dev/null and b/quit/quit_text_be.png differ
diff --git a/quit/quit_text_continued.aseprite b/quit/quit_text_continued.aseprite
new file mode 100644
index 0000000..9da6d84
Binary files /dev/null and b/quit/quit_text_continued.aseprite differ
diff --git a/quit/quit_text_continued.png b/quit/quit_text_continued.png
new file mode 100644
index 0000000..760528c
Binary files /dev/null and b/quit/quit_text_continued.png differ
diff --git a/quit/quit_text_nope.aseprite b/quit/quit_text_nope.aseprite
new file mode 100644
index 0000000..2db5a98
Binary files /dev/null and b/quit/quit_text_nope.aseprite differ
diff --git a/quit/quit_text_nope.png b/quit/quit_text_nope.png
new file mode 100644
index 0000000..d7788a9
Binary files /dev/null and b/quit/quit_text_nope.png differ
diff --git a/quit/quit_text_to.aseprite b/quit/quit_text_to.aseprite
new file mode 100644
index 0000000..08654bf
Binary files /dev/null and b/quit/quit_text_to.aseprite differ
diff --git a/quit/quit_text_to.png b/quit/quit_text_to.png
new file mode 100644
index 0000000..4c4cc34
Binary files /dev/null and b/quit/quit_text_to.png differ
diff --git a/src/end_screen/end_screen.cpp b/src/end_screen/end_screen.cpp
new file mode 100644
index 0000000..bac4e30
--- /dev/null
+++ b/src/end_screen/end_screen.cpp
@@ -0,0 +1,245 @@
+#include "end_screen.hpp"
+
+// ─────────────────────────────────────────────────────────────────────────────
+//  Helpers
+// ─────────────────────────────────────────────────────────────────────────────
+
+float EndScreen::Lerp(float a, float b, float t) {
+    return a + (b - a) * t;
+}
+
+// All images are 460×460 and share the same centred offset.
+SDL_FPoint EndScreen::LayerOffset() const {
+    if (!assets[0].surface) return {0.0f, 0.0f};
+    float ox = (screen_w - assets[0].surface->w) / 2.0f;
+    float oy = (screen_h - assets[0].surface->h) / 2.0f;
+    return {ox, oy};
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+//  Construction / Destruction
+// ─────────────────────────────────────────────────────────────────────────────
+
+EndScreen::EndScreen(SDL_Renderer* r) : renderer(r) { Init(); }
+
+EndScreen::~EndScreen() {
+    for (auto& a : assets)
+        if (a.surface) SDL_DestroySurface(a.surface);
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+//  Init
+// ─────────────────────────────────────────────────────────────────────────────
+
+void EndScreen::Init() {
+    SDL_GetRenderOutputSize(renderer, &screen_w, &screen_h);
+
+    const char* paths[NUM_LAYERS] = {
+        "quit/quit_text_to.png",
+        "quit/quit_text_be.png",
+        "quit/quit_text_continued.png",
+        "quit/quit_text_nope.png",
+    };
+
+    for (int i = 0; i < NUM_LAYERS; ++i) {
+        assets[i].surface  = IMG_Load(paths[i]);
+        layer_state[i]     = LayerState::WAITING;
+        layer_start_tick[i]= 0;
+        layer_due[i]       = 0;
+    }
+
+    rng.seed(std::random_device{}());
+    last_time = SDL_GetTicks();
+
+    // Extract particles for every layer before starting
+    for (int i = 0; i < NUM_LAYERS; ++i)
+        BuildParticles(i);
+
+    // Fire layer 0 immediately
+    StartLayer(0);
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+//  BuildParticles — extract visible pixels from a surface into EndParticles.
+//  current is initialised to screen-centre; origin is the final screen pos.
+// ─────────────────────────────────────────────────────────────────────────────
+
+void EndScreen::BuildParticles(int idx) {
+    auto& surf = assets[idx].surface;
+    if (!surf) return;
+
+    const SDL_PixelFormatDetails* details =
+        SDL_GetPixelFormatDetails(surf->format);
+    Uint32* pixels = static_cast<Uint32*>(surf->pixels);
+    int pitch = surf->pitch / (int)sizeof(Uint32);
+    int w = surf->w, h = surf->h;
+
+    SDL_FPoint off = LayerOffset(); // same for every layer
+
+    auto& parts = layer_particles[idx];
+    parts.clear();
+    parts.reserve(static_cast<size_t>(w * h / 4)); // rough upper bound
+
+    for (int y = 0; y < h; ++y) {
+        for (int x = 0; x < w; ++x) {
+            Uint32 p = pixels[y * pitch + x];
+            Uint8 r, g, b, a;
+            SDL_GetRGBA(p, details, nullptr, &r, &g, &b, &a);
+            if (a > 100) {
+                EndParticle ep;
+                ep.origin  = { off.x + static_cast<float>(x),
+                               off.y + static_cast<float>(y) };
+                ep.current = { screen_w / 2.0f, screen_h / 2.0f };
+                parts.push_back(ep);
+            }
+        }
+    }
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+//  StartLayer — begin the spread-in animation for one layer
+// ─────────────────────────────────────────────────────────────────────────────
+
+void EndScreen::StartLayer(int idx) {
+    if (idx < 0 || idx >= NUM_LAYERS) return;
+
+    layer_state[idx]      = LayerState::SPREADING_IN;
+    layer_start_tick[idx] = SDL_GetTicks();
+
+    // All particles burst from screen centre
+    for (auto& ep : layer_particles[idx])
+        ep.current = { screen_w / 2.0f, screen_h / 2.0f };
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+//  UpdateScreen — call on window resize
+// ─────────────────────────────────────────────────────────────────────────────
+
+void EndScreen::UpdateScreen() {
+    SDL_GetRenderOutputSize(renderer, &screen_w, &screen_h);
+    for (int i = 0; i < NUM_LAYERS; ++i)
+        BuildParticles(i);
+    // Re-centre any currently-spreading layer
+    for (int i = 0; i < NUM_LAYERS; ++i) {
+        if (layer_state[i] == LayerState::SPREADING_IN) {
+            for (auto& ep : layer_particles[i])
+                ep.current = { screen_w / 2.0f, screen_h / 2.0f };
+        }
+    }
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+//  Update
+// ─────────────────────────────────────────────────────────────────────────────
+
+void EndScreen::Update() {
+    Uint64 now = SDL_GetTicks();
+    last_time  = now;
+
+    // ── Advance any spreading layer ──────────────────────────────────────────
+    for (int i = 0; i < NUM_LAYERS; ++i) {
+        if (layer_state[i] != LayerState::SPREADING_IN) continue;
+
+        float t = static_cast<float>(now - layer_start_tick[i])
+                  / static_cast<float>(SPREAD_DURATION_MS);
+
+        if (t >= 1.0f) {
+            // Snap to final positions
+            for (auto& ep : layer_particles[i])
+                ep.current = ep.origin;
+
+            layer_state[i] = LayerState::VISIBLE;
+
+            // Schedule the next layer
+            if (i + 1 < NUM_LAYERS) {
+                Uint64 gap = 0;
+                if      (i == 0) gap = GAP_AFTER_0_MS;
+                else if (i == 1) gap = GAP_AFTER_1_MS;
+                else if (i == 2) gap = GAP_AFTER_2_MS;
+                layer_due[i + 1] = now + gap;
+            }
+        } else {
+            float cx = screen_w / 2.0f;
+            float cy = screen_h / 2.0f;
+            for (auto& ep : layer_particles[i]) {
+                ep.current.x = Lerp(cx, ep.origin.x, t);
+                ep.current.y = Lerp(cy, ep.origin.y, t);
+            }
+        }
+    }
+
+    // ── Trigger any WAITING layer whose due time has arrived ─────────────────
+    for (int i = 1; i < NUM_LAYERS; ++i) {
+        if (layer_state[i] == LayerState::WAITING
+            && layer_due[i] > 0
+            && now >= layer_due[i])
+        {
+            StartLayer(i);
+        }
+    }
+
+    // ── End condition: last layer fully visible ───────────────────────────────
+    if (layer_state[NUM_LAYERS - 1] == LayerState::VISIBLE) {
+        if (!end_timer_started) {
+            end_timer_started = true;
+            end_timer_start   = now;
+        } else if (now - end_timer_start >= END_PAUSE_MS) {
+            running = false;
+        }
+    }
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+//  Render
+//  Strategy: every non-WAITING layer is drawn as white 1×1 particle rects.
+//  VISIBLE layers have all particles snapped to their origins, so they look
+//  like a solid rendered image. No SDL_RenderTexture is used at all, which
+//  means there is zero risk of one layer's texture rect overwriting another.
+// ─────────────────────────────────────────────────────────────────────────────
+
+void EndScreen::Render() {
+    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
+    SDL_RenderClear(renderer);
+
+    SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
+
+    for (int i = 0; i < NUM_LAYERS; ++i) {
+        if (layer_state[i] == LayerState::WAITING) continue;
+
+        // Draw every particle — works for both SPREADING_IN and VISIBLE states.
+        for (const auto& ep : layer_particles[i]) {
+            SDL_FRect r { ep.current.x, ep.current.y, 1.0f, 1.0f };
+            SDL_RenderFillRect(renderer, &r);
+        }
+    }
+
+    SDL_RenderPresent(renderer);
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+//  Event loop
+// ─────────────────────────────────────────────────────────────────────────────
+
+namespace EndScreen_Event {
+    void event_Loop(SDL_Renderer* renderer) {
+        EndScreen* scene = new EndScreen(renderer);
+
+        while (scene->running) {
+            SDL_Event e;
+            while (SDL_PollEvent(&e)) {
+                if (e.type == SDL_EVENT_QUIT)
+                    scene->running = false;
+                if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE)
+                    scene->running = false;
+                if (e.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED ||
+                    e.type == SDL_EVENT_WINDOW_RESIZED)
+                    scene->UpdateScreen();
+            }
+
+            scene->Update();
+            scene->Render();
+        }
+
+        delete scene;
+    }
+}
diff --git a/src/end_screen/end_screen.hpp b/src/end_screen/end_screen.hpp
new file mode 100644
index 0000000..3177c34
--- /dev/null
+++ b/src/end_screen/end_screen.hpp
@@ -0,0 +1,72 @@
+// end_screen.hpp
+// Ending screen: 4 images revealed one by one as particle bursts from centre.
+// All layers are ALWAYS rendered as particles — no texture overdraw possible.
+// Clear color: black. Text is white.
+
+#pragma once
+
+#include <SDL3/SDL.h>
+#include <SDL3_image/SDL_image.h>
+#include <vector>
+#include <array>
+#include <random>
+#include <cstdint>
+
+struct EndParticle {
+    SDL_FPoint origin;   // final screen position (centred)
+    SDL_FPoint current;  // animated position
+};
+
+class EndScreen {
+public:
+    bool running = true;
+
+    explicit EndScreen(SDL_Renderer* r);
+    ~EndScreen();
+
+    void Update();
+    void Render();
+    void UpdateScreen();
+
+private:
+    enum class LayerState { WAITING, SPREADING_IN, VISIBLE };
+
+    // ── Timings (ms) ─────────────────────────────────────────────────────────
+    // Fast burst animation, long delays between words.
+    static constexpr Uint64 SPREAD_DURATION_MS = 420;   // fast particle burst
+    static constexpr Uint64 GAP_AFTER_0_MS     = 200;   // "to" → "be"   (quick follow-up)
+    static constexpr Uint64 GAP_AFTER_1_MS     = 1000;  // "be" → "continued"
+    static constexpr Uint64 GAP_AFTER_2_MS     = 2000;  // "continued" → "nope"
+    static constexpr Uint64 END_PAUSE_MS       = 2000;  // hold all visible before done
+
+    void Init();
+    void BuildParticles(int layer_idx);
+    void StartLayer(int idx);
+    SDL_FPoint LayerOffset() const;   // same offset for all layers (same image size)
+    static float Lerp(float a, float b, float t);
+
+    SDL_Renderer* renderer = nullptr;
+    int screen_w = 0, screen_h = 0;
+
+    static constexpr int NUM_LAYERS = 4;
+
+    struct LayerAsset {
+        SDL_Surface* surface = nullptr;
+        // No texture needed — we render entirely via particles
+    };
+    std::array<LayerAsset, NUM_LAYERS>               assets{};
+    std::array<LayerState, NUM_LAYERS>               layer_state{};
+    std::array<Uint64, NUM_LAYERS>                   layer_start_tick{}; // when spreading began
+    std::array<Uint64, NUM_LAYERS>                   layer_due{};        // scheduled start tick
+    std::array<std::vector<EndParticle>, NUM_LAYERS> layer_particles{};
+
+    bool   end_timer_started = false;
+    Uint64 end_timer_start   = 0;
+    Uint64 last_time         = 0;
+
+    std::mt19937 rng;
+};
+
+namespace EndScreen_Event {
+    void event_Loop(SDL_Renderer* renderer);
+}
diff --git a/src/game/game.cpp b/src/game/game.cpp
index eebb5ce..fdb19a1 100644
--- a/src/game/game.cpp
+++ b/src/game/game.cpp
@@ -25,6 +25,7 @@
 #include "defaults/input.hpp"
 #include "start_screen/start_screen.hpp"
 #include "cursor/animated_cursor.hpp"
+#include "end_screen/end_screen.hpp"
 
 
 // Game class encapsulating SDL initialization and loop
@@ -224,6 +225,8 @@ void Game::Run() {
             Default::fps_timer -= 1.0f;
         }
     }
+
+    EndScreen_Event::event_Loop(renderer);
 }
 
 void Game::handle_Events() {