跳轉到

File GameEngine.h

File List > GameEngine.h

Go to the documentation of this file

#ifndef GRID_PLUS_PLUS_GAME_ENGINE_H_
#define GRID_PLUS_PLUS_GAME_ENGINE_H_

#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <limits>
#include <memory>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

#include "GridAssetManager.h"
#include "GridMaze.h"
#include "GridObject.h"
#include "Overlay.h"
#include "raylib.h"

#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#endif

namespace gridpp {

class GameEngineImpl;
class GameEngine {
public:
    GameEngine(int cols, int rows, int grid_size = 32);

    void loadAssets(const std::string& database_path);

    void setBackgroundColor(Color color);

    void showGrid(bool show);

    int cols() const;

    int rows() const;

    int gridSize() const;

    GridObject addObject(const std::string& image, GridObjectCallback init = nullptr,
                         GridObjectCallback update = nullptr, CollisionCallback collide = nullptr);

    GridObject addSquare(int x, int y, int size, Color color = BLACK, GridObjectCallback init = nullptr,
                         GridObjectCallback update = nullptr, CollisionCallback collide = nullptr);

    GridObject addCircle(int x, int y, int size, Color color = BLACK, GridObjectCallback init = nullptr,
                         GridObjectCallback update = nullptr, CollisionCallback collide = nullptr);

    GridObject addTriangle(int x, int y, int size, Color color = BLACK, GridObjectCallback init = nullptr,
                           GridObjectCallback update = nullptr, CollisionCallback collide = nullptr);

    GridObject addPentagon(int x, int y, int size, Color color = BLACK, GridObjectCallback init = nullptr,
                           GridObjectCallback update = nullptr, CollisionCallback collide = nullptr);

    GridObject addStar(int x, int y, int size, Color color = BLACK, GridObjectCallback init = nullptr,
                       GridObjectCallback update = nullptr, CollisionCallback collide = nullptr);

    Maze addMaze(int cols, int rows, MazeCallback init = nullptr, MazeCallback update = nullptr);

    void clearObjects();

    Overlay addOverlay(const std::string& image, OverlayCallback init = nullptr, OverlayCallback update = nullptr);

    Overlay addTextOverlay(const std::string& text, int x, int y, int font_size = 20, Color color = BLACK,
                           OverlayCallback init = nullptr, OverlayCallback update = nullptr);

    Overlay addButton(const std::string& text, int x, int y, int width, int height, OverlayCallback click = nullptr);

    void clearOverlays();

    bool keyPressed(int key) const;

    bool keyDown(int key) const;

    bool mousePressed(int button) const;

    int mouseX() const;

    int mouseY() const;

    double time() const;

    int random(int min, int max) const;

    void run();

private:
    friend class GameEngineImpl;

    explicit GameEngine(std::shared_ptr<GameEngineImpl> impl);
    std::shared_ptr<GameEngineImpl> impl_;
};

class GameEngineImpl : public std::enable_shared_from_this<GameEngineImpl> {
public:
    static constexpr int kMaxWindowSize = 8192;
    ~GameEngineImpl();

private:
    friend class GameEngine;
    friend class GridObject;
    friend class Overlay;
    friend class Maze;

    enum class WorldType { Object, Maze };

    struct WorldId {
        WorldType type;
        std::uint64_t id;
    };

    GameEngineImpl(int cols, int rows, int grid_size);

    std::uint64_t allocateId();
    GameEngine publicEngine();
    GridObject publicObject(std::uint64_t id);
    Overlay publicOverlay(std::uint64_t id);
    Maze publicMaze(std::uint64_t id);

    bool hasObject(std::uint64_t id) const;
    bool hasOverlay(std::uint64_t id) const;
    bool hasMaze(std::uint64_t id) const;
    GridObjectImpl& requireObject(std::uint64_t id);
    const GridObjectImpl& requireObject(std::uint64_t id) const;
    OverlayImpl& requireOverlay(std::uint64_t id);
    const OverlayImpl& requireOverlay(std::uint64_t id) const;
    MazeImpl& requireMaze(std::uint64_t id);
    const MazeImpl& requireMaze(std::uint64_t id) const;

    GridObject addObject(const std::string& image, GridObjectCallback init, GridObjectCallback update,
                         CollisionCallback collide);
    GridObject addShape(GridObjectType type, int x, int y, int size, Color color, GridObjectCallback init,
                        GridObjectCallback update, CollisionCallback collide);
    Overlay addImageOverlay(const std::string& image, OverlayCallback init, OverlayCallback update);
    Overlay addTextOverlay(const std::string& text, int x, int y, int font_size, Color color, OverlayCallback init,
                           OverlayCallback update);
    Overlay addButton(const std::string& text, int x, int y, int width, int height, OverlayCallback click);
    Maze addMaze(int cols, int rows, MazeCallback init, MazeCallback update);

    GridObject cloneObject(std::uint64_t id);
    Overlay cloneOverlay(std::uint64_t id);
    Maze cloneMaze(std::uint64_t id);
    void removeObject(std::uint64_t id);
    void removeOverlay(std::uint64_t id);
    void removeMaze(std::uint64_t id);
    void clearObjects();
    void clearOverlays();

    void initializePending();
    void initializeWorld(WorldId item);
    void initializeOverlay(std::uint64_t id);
    void updateWorld(const std::vector<WorldId>& frame_world);
    void collideObjects(const std::vector<WorldId>& frame_world);
    void updateOverlays(const std::vector<std::uint64_t>& frame_overlays);
    void draw(const std::vector<WorldId>& frame_world, const std::vector<std::uint64_t>& frame_overlays);
    void drawObject(const GridObjectImpl& object);
    void drawMaze(const MazeImpl& maze);
    void drawOverlay(const OverlayImpl& overlay);
    void drawCell(const std::string& image, int grid_x, int grid_y, int direction = 0, Color tint = WHITE);
    void drawOverlayImage(const std::string& image, int pixel_x, int pixel_y, Color tint);
    void drawGrid();
    void tick();
    void run();

    static void eraseWorld(std::vector<WorldId>& source, WorldType type, std::uint64_t id);
    static void eraseId(std::vector<std::uint64_t>& source, std::uint64_t id);
    int worldLayer(const WorldId& item) const;
    bool worldActive(const WorldId& item) const;

    int cols_;
    int rows_;
    int grid_size_;
    Color background_color_ = RAYWHITE;
    bool show_grid_ = false;
    GridAssetManager assets_;
    std::uint64_t next_id_ = 1;
    std::unordered_map<std::uint64_t, GridObjectImpl> objects_;
    std::unordered_map<std::uint64_t, OverlayImpl> overlays_;
    std::unordered_map<std::uint64_t, MazeImpl> mazes_;
    std::vector<WorldId> world_order_;
    std::vector<std::uint64_t> overlay_order_;
    std::vector<WorldId> pending_world_;
    std::vector<std::uint64_t> pending_overlays_;
};

// 引擎建立與實體查找 -----------------------------------------------------

inline GameEngineImpl::GameEngineImpl(int cols, int rows, int grid_size)
    : cols_(cols), rows_(rows), grid_size_(grid_size) {
    if (cols < 1 || rows < 1 || grid_size < 1) {
        throw std::invalid_argument("Grid++ Error: cols, rows, and grid size must be greater than 0");
    }
    const std::int64_t width = static_cast<std::int64_t>(cols) * grid_size;
    const std::int64_t height = static_cast<std::int64_t>(rows) * grid_size;
    if (width > kMaxWindowSize || height > kMaxWindowSize) {
        throw std::invalid_argument("Grid++ Error: window width and height must not exceed 8192 pixels");
    }
    InitWindow(static_cast<int>(width), static_cast<int>(height), "Grid++ Game");
    SetTargetFPS(60);
}

inline GameEngineImpl::~GameEngineImpl() {
    assets_.Clear();
    if (IsWindowReady()) CloseWindow();
}

inline std::uint64_t GameEngineImpl::allocateId() {
    if (next_id_ == std::numeric_limits<std::uint64_t>::max()) {
        throw std::overflow_error("Grid++ Error: no more element IDs are available");
    }
    return next_id_++;
}

inline GameEngine GameEngineImpl::publicEngine() { return GameEngine(shared_from_this()); }
inline GridObject GameEngineImpl::publicObject(std::uint64_t id) { return GridObject(shared_from_this(), id); }
inline Overlay GameEngineImpl::publicOverlay(std::uint64_t id) { return Overlay(shared_from_this(), id); }
inline Maze GameEngineImpl::publicMaze(std::uint64_t id) { return Maze(shared_from_this(), id); }

inline bool GameEngineImpl::hasObject(std::uint64_t id) const { return objects_.count(id) != 0; }
inline bool GameEngineImpl::hasOverlay(std::uint64_t id) const { return overlays_.count(id) != 0; }
inline bool GameEngineImpl::hasMaze(std::uint64_t id) const { return mazes_.count(id) != 0; }

inline GridObjectImpl& GameEngineImpl::requireObject(std::uint64_t id) {
    const auto found = objects_.find(id);
    if (found == objects_.end()) throw std::runtime_error("Grid++ Error: GridObject no longer exists");
    return found->second;
}

inline const GridObjectImpl& GameEngineImpl::requireObject(std::uint64_t id) const {
    const auto found = objects_.find(id);
    if (found == objects_.end()) throw std::runtime_error("Grid++ Error: GridObject no longer exists");
    return found->second;
}

inline OverlayImpl& GameEngineImpl::requireOverlay(std::uint64_t id) {
    const auto found = overlays_.find(id);
    if (found == overlays_.end()) throw std::runtime_error("Grid++ Error: Overlay no longer exists");
    return found->second;
}

inline const OverlayImpl& GameEngineImpl::requireOverlay(std::uint64_t id) const {
    const auto found = overlays_.find(id);
    if (found == overlays_.end()) throw std::runtime_error("Grid++ Error: Overlay no longer exists");
    return found->second;
}

inline MazeImpl& GameEngineImpl::requireMaze(std::uint64_t id) {
    const auto found = mazes_.find(id);
    if (found == mazes_.end()) throw std::runtime_error("Grid++ Error: Maze no longer exists");
    return found->second;
}

inline const MazeImpl& GameEngineImpl::requireMaze(std::uint64_t id) const {
    const auto found = mazes_.find(id);
    if (found == mazes_.end()) throw std::runtime_error("Grid++ Error: Maze no longer exists");
    return found->second;
}

// 建立、複製與移除 -------------------------------------------------------

inline GridObject GameEngineImpl::addObject(const std::string& image, GridObjectCallback init,
                                            GridObjectCallback update, CollisionCallback collide) {
    const std::uint64_t id = allocateId();
    GridObjectImpl object(image);
    object.init_ = init;
    object.update_ = update;
    object.collide_ = collide;
    objects_.emplace(id, std::move(object));
    world_order_.push_back({WorldType::Object, id});
    pending_world_.push_back({WorldType::Object, id});
    return publicObject(id);
}

inline GridObject GameEngineImpl::addShape(GridObjectType type, int x, int y, int size, Color color,
                                           GridObjectCallback init, GridObjectCallback update,
                                           CollisionCallback collide) {
    if (size < 1) throw std::invalid_argument("Grid++ Error: shape size must be greater than 0");
    const std::uint64_t id = allocateId();
    GridObjectImpl object(type, x, y, size, color);
    object.init_ = init;
    object.update_ = update;
    object.collide_ = collide;
    objects_.emplace(id, std::move(object));
    world_order_.push_back({WorldType::Object, id});
    pending_world_.push_back({WorldType::Object, id});
    return publicObject(id);
}

inline Overlay GameEngineImpl::addImageOverlay(const std::string& image, OverlayCallback init, OverlayCallback update) {
    const std::uint64_t id = allocateId();
    OverlayImpl overlay(OverlayType::Image);
    overlay.content_ = image;
    overlay.color_ = WHITE;
    overlay.init_ = init;
    overlay.update_ = update;
    overlays_.emplace(id, std::move(overlay));
    overlay_order_.push_back(id);
    pending_overlays_.push_back(id);
    return publicOverlay(id);
}

inline Overlay GameEngineImpl::addTextOverlay(const std::string& text, int x, int y, int font_size, Color color,
                                              OverlayCallback init, OverlayCallback update) {
    if (font_size < 1) throw std::invalid_argument("Grid++ Error: text size must be greater than 0");
    const std::uint64_t id = allocateId();
    OverlayImpl overlay(OverlayType::Text);
    overlay.content_ = text;
    overlay.x_ = x;
    overlay.y_ = y;
    overlay.font_size_ = font_size;
    overlay.color_ = color;
    overlay.init_ = init;
    overlay.update_ = update;
    overlays_.emplace(id, std::move(overlay));
    overlay_order_.push_back(id);
    pending_overlays_.push_back(id);
    return publicOverlay(id);
}

inline Overlay GameEngineImpl::addButton(const std::string& text, int x, int y, int width, int height,
                                         OverlayCallback click) {
    if (width < 1 || height < 1) {
        throw std::invalid_argument("Grid++ Error: button width and height must be greater than 0");
    }
    const std::uint64_t id = allocateId();
    OverlayImpl overlay(OverlayType::Button);
    overlay.content_ = text;
    overlay.x_ = x;
    overlay.y_ = y;
    overlay.width_ = width;
    overlay.height_ = height;
    overlay.click_ = click;
    overlays_.emplace(id, std::move(overlay));
    overlay_order_.push_back(id);
    pending_overlays_.push_back(id);
    return publicOverlay(id);
}

inline Maze GameEngineImpl::addMaze(int cols, int rows, MazeCallback init, MazeCallback update) {
    const std::uint64_t id = allocateId();
    MazeImpl maze(cols, rows);
    maze.init_ = init;
    maze.update_ = update;
    mazes_.emplace(id, std::move(maze));
    world_order_.push_back({WorldType::Maze, id});
    pending_world_.push_back({WorldType::Maze, id});
    return publicMaze(id);
}

inline GridObject GameEngineImpl::cloneObject(std::uint64_t id) {
    GridObjectImpl copy = requireObject(id);
    copy.active_ = false;
    const std::uint64_t copy_id = allocateId();
    objects_.emplace(copy_id, std::move(copy));
    world_order_.push_back({WorldType::Object, copy_id});
    pending_world_.push_back({WorldType::Object, copy_id});
    return publicObject(copy_id);
}

inline Overlay GameEngineImpl::cloneOverlay(std::uint64_t id) {
    OverlayImpl copy = requireOverlay(id);
    copy.active_ = false;
    const std::uint64_t copy_id = allocateId();
    overlays_.emplace(copy_id, std::move(copy));
    overlay_order_.push_back(copy_id);
    pending_overlays_.push_back(copy_id);
    return publicOverlay(copy_id);
}

inline Maze GameEngineImpl::cloneMaze(std::uint64_t id) {
    MazeImpl copy = requireMaze(id);
    copy.active_ = false;
    const std::uint64_t copy_id = allocateId();
    mazes_.emplace(copy_id, std::move(copy));
    world_order_.push_back({WorldType::Maze, copy_id});
    pending_world_.push_back({WorldType::Maze, copy_id});
    return publicMaze(copy_id);
}

inline void GameEngineImpl::eraseWorld(std::vector<WorldId>& source, WorldType type, std::uint64_t id) {
    source.erase(std::remove_if(source.begin(), source.end(),
                                [&](const WorldId& item) { return item.type == type && item.id == id; }),
                 source.end());
}

inline void GameEngineImpl::eraseId(std::vector<std::uint64_t>& source, std::uint64_t id) {
    source.erase(std::remove(source.begin(), source.end(), id), source.end());
}

inline void GameEngineImpl::removeObject(std::uint64_t id) {
    if (objects_.erase(id) == 0) return;
    eraseWorld(world_order_, WorldType::Object, id);
    eraseWorld(pending_world_, WorldType::Object, id);
}

inline void GameEngineImpl::removeOverlay(std::uint64_t id) {
    if (overlays_.erase(id) == 0) return;
    eraseId(overlay_order_, id);
    eraseId(pending_overlays_, id);
}

inline void GameEngineImpl::removeMaze(std::uint64_t id) {
    if (mazes_.erase(id) == 0) return;
    eraseWorld(world_order_, WorldType::Maze, id);
    eraseWorld(pending_world_, WorldType::Maze, id);
}

inline void GameEngineImpl::clearObjects() {
    objects_.clear();
    mazes_.clear();
    world_order_.clear();
    pending_world_.clear();
}

inline void GameEngineImpl::clearOverlays() {
    overlays_.clear();
    overlay_order_.clear();
    pending_overlays_.clear();
}

// 生命週期 -------------------------------------------------------------------

inline void GameEngineImpl::initializeWorld(WorldId item) {
    if (item.type == WorldType::Object) {
        const auto found = objects_.find(item.id);
        if (found == objects_.end()) return;
        if (!found->second.initialized_) {
            found->second.initialized_ = true;
            const GridObjectCallback callback = found->second.init_;
            if (callback != nullptr) callback(publicEngine(), publicObject(item.id));
        }
        const auto current = objects_.find(item.id);
        if (current != objects_.end()) current->second.active_ = true;
        return;
    }

    const auto found = mazes_.find(item.id);
    if (found == mazes_.end()) return;
    if (!found->second.initialized_) {
        found->second.initialized_ = true;
        const MazeCallback callback = found->second.init_;
        if (callback != nullptr) callback(publicEngine(), publicMaze(item.id));
    }
    const auto current = mazes_.find(item.id);
    if (current != mazes_.end()) current->second.active_ = true;
}

inline void GameEngineImpl::initializeOverlay(std::uint64_t id) {
    const auto found = overlays_.find(id);
    if (found == overlays_.end()) return;
    if (!found->second.initialized_) {
        found->second.initialized_ = true;
        const OverlayCallback callback = found->second.init_;
        if (callback != nullptr) callback(publicEngine(), publicOverlay(id));
    }
    const auto current = overlays_.find(id);
    if (current != overlays_.end()) current->second.active_ = true;
}

inline void GameEngineImpl::initializePending() {
    while (!pending_world_.empty() || !pending_overlays_.empty()) {
        std::vector<WorldId> world = std::move(pending_world_);
        std::vector<std::uint64_t> overlays = std::move(pending_overlays_);
        pending_world_.clear();
        pending_overlays_.clear();
        for (const WorldId item : world) initializeWorld(item);
        for (const std::uint64_t id : overlays) initializeOverlay(id);
    }
}

inline void GameEngineImpl::updateWorld(const std::vector<WorldId>& frame_world) {
    for (const WorldId item : frame_world) {
        if (item.type == WorldType::Object) {
            const auto found = objects_.find(item.id);
            if (found == objects_.end() || !found->second.active_) continue;
            const GridObjectCallback callback = found->second.update_;
            if (callback != nullptr) callback(publicEngine(), publicObject(item.id));
        } else {
            const auto found = mazes_.find(item.id);
            if (found == mazes_.end() || !found->second.active_) continue;
            const MazeCallback callback = found->second.update_;
            if (callback != nullptr) callback(publicEngine(), publicMaze(item.id));
        }
    }
}

inline void GameEngineImpl::collideObjects(const std::vector<WorldId>& frame_world) {
    std::vector<std::uint64_t> ids;
    for (const WorldId item : frame_world) {
        if (item.type == WorldType::Object) ids.push_back(item.id);
    }

    const auto can_collide = [&](std::uint64_t id) {
        const auto found = objects_.find(id);
        return found != objects_.end() && found->second.active_ && found->second.visible_ && found->second.x_ >= 0 &&
               found->second.x_ < cols_ && found->second.y_ >= 0 && found->second.y_ < rows_;
    };

    for (std::size_t i = 0; i < ids.size(); ++i) {
        if (!can_collide(ids[i])) continue;
        for (std::size_t j = i + 1; j < ids.size(); ++j) {
            if (!can_collide(ids[j])) continue;
            const GridObjectImpl& left = objects_.find(ids[i])->second;
            const GridObjectImpl& right = objects_.find(ids[j])->second;
            if (left.x_ != right.x_ || left.y_ != right.y_) continue;

            const CollisionCallback left_callback = left.collide_;
            if (left_callback != nullptr) {
                left_callback(publicEngine(), publicObject(ids[i]), publicObject(ids[j]));
            }
            if (!can_collide(ids[i])) break;
            if (!can_collide(ids[j])) continue;

            const CollisionCallback right_callback = objects_.find(ids[j])->second.collide_;
            if (right_callback != nullptr) {
                right_callback(publicEngine(), publicObject(ids[j]), publicObject(ids[i]));
            }
            if (!can_collide(ids[i])) break;
        }
    }
}

inline void GameEngineImpl::updateOverlays(const std::vector<std::uint64_t>& frame_overlays) {
    for (const std::uint64_t id : frame_overlays) {
        const auto found = overlays_.find(id);
        if (found == overlays_.end() || !found->second.active_) continue;
        const OverlayCallback update = found->second.update_;
        if (update != nullptr) update(publicEngine(), publicOverlay(id));

        const auto current = overlays_.find(id);
        if (current == overlays_.end() || !current->second.active_ || !current->second.visible_ ||
            current->second.type_ != OverlayType::Button) {
            continue;
        }
        OverlayImpl& button = current->second;
        const Vector2 mouse = GetMousePosition();
        button.hover_ = mouse.x >= button.x_ && mouse.x <= button.x_ + button.width_ && mouse.y >= button.y_ &&
                        mouse.y <= button.y_ + button.height_;
        const OverlayCallback click = button.click_;
        if (button.hover_ && IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && click != nullptr) {
            click(publicEngine(), publicOverlay(id));
        }
    }
}

inline int GameEngineImpl::worldLayer(const WorldId& item) const {
    if (item.type == WorldType::Maze) return 0;
    const auto found = objects_.find(item.id);
    return found == objects_.end() ? 0 : found->second.layer_;
}

inline bool GameEngineImpl::worldActive(const WorldId& item) const {
    if (item.type == WorldType::Object) {
        const auto found = objects_.find(item.id);
        return found != objects_.end() && found->second.active_;
    }
    const auto found = mazes_.find(item.id);
    return found != mazes_.end() && found->second.active_;
}

inline void GameEngineImpl::tick() {
    const std::vector<WorldId> frame_world = world_order_;
    const std::vector<std::uint64_t> frame_overlays = overlay_order_;
    updateWorld(frame_world);
    collideObjects(frame_world);
    updateOverlays(frame_overlays);
    draw(frame_world, frame_overlays);
    initializePending();
}

inline void GameEngineImpl::run() {
    // 第一幀更新前先讓所有預先建立的實體完成初始化。
    initializePending();
#ifdef __EMSCRIPTEN__
    emscripten_set_main_loop_arg([](void* self) { static_cast<GameEngineImpl*>(self)->tick(); }, this, 0, 1);
#else
    while (!WindowShouldClose()) tick();
#endif
}

// 繪製 -----------------------------------------------------------------------

inline void GameEngineImpl::draw(const std::vector<WorldId>& frame_world,
                                 const std::vector<std::uint64_t>& frame_overlays) {
    BeginDrawing();
    ClearBackground(background_color_);
    if (show_grid_) drawGrid();

    std::vector<WorldId> draw_order;
    for (const WorldId item : frame_world) {
        if (worldActive(item)) draw_order.push_back(item);
    }
    std::stable_sort(draw_order.begin(), draw_order.end(),
                     [&](const WorldId& left, const WorldId& right) { return worldLayer(left) < worldLayer(right); });
    for (const WorldId item : draw_order) {
        if (item.type == WorldType::Object) {
            const auto found = objects_.find(item.id);
            if (found != objects_.end() && found->second.visible_) drawObject(found->second);
        } else {
            const auto found = mazes_.find(item.id);
            if (found != mazes_.end()) drawMaze(found->second);
        }
    }
    for (const std::uint64_t id : frame_overlays) {
        const auto found = overlays_.find(id);
        if (found != overlays_.end() && found->second.active_ && found->second.visible_) drawOverlay(found->second);
    }
    EndDrawing();
}

inline void GameEngineImpl::drawObject(const GridObjectImpl& object) {
    if (object.type_ == GridObjectType::Image) {
        drawCell(object.image_, object.x_, object.y_, object.direction_, object.color_);
        return;
    }

    const float center_x = (static_cast<float>(object.x_) + 0.5f) * grid_size_;
    const float center_y = (static_cast<float>(object.y_) + 0.5f) * grid_size_;
    const Vector2 center = {center_x, center_y};
    if (object.type_ == GridObjectType::Square) {
        DrawRectangle(object.x_ * grid_size_ + (grid_size_ - object.size_) / 2,
                      object.y_ * grid_size_ + (grid_size_ - object.size_) / 2, object.size_, object.size_,
                      object.color_);
    } else if (object.type_ == GridObjectType::Circle) {
        DrawCircle(static_cast<int>(center_x), static_cast<int>(center_y), object.size_ / 2.0f, object.color_);
    } else if (object.type_ == GridObjectType::Triangle) {
        DrawPoly(center, 3, object.size_ / 2.0f, -90.0f, object.color_);
    } else if (object.type_ == GridObjectType::Pentagon) {
        DrawPoly(center, 5, object.size_ / 2.0f, -90.0f, object.color_);
    } else {
        constexpr float kPi = 3.14159265358979323846f;
        std::array<Vector2, 12> points = {};
        points[0] = center;
        for (std::size_t i = 0; i <= 10; ++i) {
            const std::size_t vertex = i % 10;
            const float radius = object.size_ / 2.0f * (vertex % 2 == 0 ? 1.0f : 0.45f);
            const float angle = (-90.0f + static_cast<float>(vertex) * 36.0f) * kPi / 180.0f;
            points[i + 1] = {center.x + std::cos(angle) * radius, center.y + std::sin(angle) * radius};
        }
        DrawTriangleFan(points.data(), static_cast<int>(points.size()), object.color_);
    }
}

inline void GameEngineImpl::drawMaze(const MazeImpl& maze) {
    for (int y = 0; y < maze.height_; ++y) {
        for (int x = 0; x < maze.width_; ++x) {
            if (!maze.walls_[y][x]) continue;
            if (maze.use_wall_images_) {
                const auto [shape, direction] = MazeImpl::shapeFor(maze.connectivityMask(x, y));
                drawCell(maze.wall_images_[shape], x, y, direction);
            } else {
                drawCell(maze.single_wall_, x, y);
            }
        }
    }
}

inline void GameEngineImpl::drawOverlay(const OverlayImpl& overlay) {
    if (overlay.type_ == OverlayType::Image) {
        drawOverlayImage(overlay.content_, overlay.x_, overlay.y_, overlay.color_);
    } else if (overlay.type_ == OverlayType::Text) {
        DrawText(overlay.content_.c_str(), overlay.x_, overlay.y_, overlay.font_size_, overlay.color_);
    } else {
        DrawRectangle(overlay.x_, overlay.y_, overlay.width_, overlay.height_, overlay.hover_ ? SKYBLUE : LIGHTGRAY);
        DrawRectangleLines(overlay.x_, overlay.y_, overlay.width_, overlay.height_, DARKGRAY);
        const int text_width = MeasureText(overlay.content_.c_str(), overlay.font_size_);
        DrawText(overlay.content_.c_str(), overlay.x_ + (overlay.width_ - text_width) / 2,
                 overlay.y_ + (overlay.height_ - overlay.font_size_) / 2, overlay.font_size_, overlay.color_);
    }
}

inline void GameEngineImpl::drawCell(const std::string& image, int grid_x, int grid_y, int direction, Color tint) {
    const int pixel_x = grid_x * grid_size_;
    const int pixel_y = grid_y * grid_size_;
    if (!image.empty() && assets_.Has(image)) {
        const Texture2D texture = assets_.Get(image);
        const Rectangle source = {0, 0, static_cast<float>(texture.width), static_cast<float>(texture.height)};
        const Rectangle destination = {static_cast<float>(pixel_x) + grid_size_ / 2.0f,
                                       static_cast<float>(pixel_y) + grid_size_ / 2.0f, static_cast<float>(grid_size_),
                                       static_cast<float>(grid_size_)};
        const Vector2 origin = {grid_size_ / 2.0f, grid_size_ / 2.0f};
        DrawTexturePro(texture, source, destination, origin, -90.0f * direction, tint);
    } else {
        DrawRectangle(pixel_x, pixel_y, grid_size_, grid_size_, RED);
    }
}

inline void GameEngineImpl::drawOverlayImage(const std::string& image, int pixel_x, int pixel_y, Color tint) {
    constexpr int kAssetSize = 32;
    if (!image.empty() && assets_.Has(image)) {
        const Texture2D texture = assets_.Get(image);
        const Rectangle source = {0, 0, static_cast<float>(texture.width), static_cast<float>(texture.height)};
        const Rectangle destination = {static_cast<float>(pixel_x), static_cast<float>(pixel_y),
                                       static_cast<float>(kAssetSize), static_cast<float>(kAssetSize)};
        DrawTexturePro(texture, source, destination, {0, 0}, 0, tint);
    } else {
        DrawRectangle(pixel_x, pixel_y, kAssetSize, kAssetSize, RED);
    }
}

inline void GameEngineImpl::drawGrid() {
    const Color color = Fade(LIGHTGRAY, 0.5f);
    for (int x = 0; x <= cols_; ++x) DrawLine(x * grid_size_, 0, x * grid_size_, rows_ * grid_size_, color);
    for (int y = 0; y <= rows_; ++y) DrawLine(0, y * grid_size_, cols_ * grid_size_, y * grid_size_, color);
}

// MazeImpl ---------------------------------------------------------------------

inline MazeImpl::MazeImpl(int cols, int rows) : width_(cols), height_(rows) {
    if (cols < 1 || rows < 1 || cols > kMaxWidth || rows > kMaxHeight) {
        throw std::invalid_argument("Grid++ Error: maze dimensions must be between 1x1 and 64x64");
    }
}

inline int MazeImpl::connectivityMask(int x, int y) const {
    const auto wall = [&](int test_x, int test_y) {
        return test_x < 0 || test_y < 0 || test_x >= width_ || test_y >= height_ || walls_[test_y][test_x];
    };
    int mask = 0;
    if (wall(x, y - 1)) mask |= 1;
    if (wall(x + 1, y)) mask |= 2;
    if (wall(x, y + 1)) mask |= 4;
    if (wall(x - 1, y)) mask |= 8;
    return mask;
}

inline std::pair<int, int> MazeImpl::shapeFor(int mask) {
    static constexpr int kShapes[16] = {0, 1, 1, 3, 1, 2, 3, 4, 1, 3, 2, 4, 3, 4, 4, 5};
    static constexpr int kDirections[16] = {0, 0, 3, 0, 2, 0, 3, 0, 1, 1, 1, 1, 2, 2, 3, 0};
    return {kShapes[mask], kDirections[mask]};
}
// GridObject handler -----------------------------------------------------------

inline GridObject::GridObject(std::weak_ptr<GameEngineImpl> engine, std::uint64_t id)
    : engine_(std::move(engine)), id_(id) {}

inline std::shared_ptr<GameEngineImpl> GridObject::lockEngine() const {
    std::shared_ptr<GameEngineImpl> engine = engine_.lock();
    if (engine == nullptr) throw std::runtime_error("Grid++ Error: GridObject's GameEngine no longer exists");
    return engine;
}
inline bool GridObject::exists() const {
    const std::shared_ptr<GameEngineImpl> engine = engine_.lock();
    return engine != nullptr && engine->hasObject(id_);
}

inline void GridObject::remove() {
    const std::shared_ptr<GameEngineImpl> engine = lockEngine();
    engine->requireObject(id_);
    engine->removeObject(id_);
}

inline GridObject GridObject::deepCopy() const { return lockEngine()->cloneObject(id_); }
inline int GridObject::x() const { return lockEngine()->requireObject(id_).x_; }
inline int GridObject::y() const { return lockEngine()->requireObject(id_).y_; }

inline void GridObject::setPosition(int x, int y) {
    GridObjectImpl& object = lockEngine()->requireObject(id_);
    object.x_ = x;
    object.y_ = y;
}

inline void GridObject::move(int dx, int dy) {
    GridObjectImpl& object = lockEngine()->requireObject(id_);
    object.x_ += dx;
    object.y_ += dy;
}

inline std::string GridObject::image() const {
    const GridObjectImpl& object = lockEngine()->requireObject(id_);
    if (object.type_ != GridObjectType::Image)
        throw std::runtime_error("Grid++ Error: image() requires an image object");
    return object.image_;
}

inline void GridObject::setImage(const std::string& image) {
    GridObjectImpl& object = lockEngine()->requireObject(id_);
    if (object.type_ != GridObjectType::Image) {
        throw std::runtime_error("Grid++ Error: setImage() requires an image object");
    }
    object.image_ = image;
}

inline int GridObject::direction() const { return lockEngine()->requireObject(id_).direction_; }
inline void GridObject::setDirection(int direction) {
    lockEngine()->requireObject(id_).direction_ = ((direction % 4) + 4) % 4;
}
inline Color GridObject::color() const { return lockEngine()->requireObject(id_).color_; }
inline void GridObject::setColor(Color color) { lockEngine()->requireObject(id_).color_ = color; }
inline int GridObject::layer() const { return lockEngine()->requireObject(id_).layer_; }
inline void GridObject::setLayer(int layer) { lockEngine()->requireObject(id_).layer_ = layer; }
inline bool GridObject::visible() const { return lockEngine()->requireObject(id_).visible_; }
inline void GridObject::show() { lockEngine()->requireObject(id_).visible_ = true; }
inline void GridObject::hide() { lockEngine()->requireObject(id_).visible_ = false; }

inline void GridObject::set(const std::string& key, int value) { set(key, static_cast<long long>(value)); }
inline void GridObject::set(const std::string& key, long long value) {
    lockEngine()->requireObject(id_).values_.set(key, value);
}
inline void GridObject::set(const std::string& key, double value) {
    lockEngine()->requireObject(id_).values_.set(key, value);
}
inline void GridObject::set(const std::string& key, bool value) {
    lockEngine()->requireObject(id_).values_.set(key, value);
}
inline void GridObject::set(const std::string& key, const std::string& value) {
    lockEngine()->requireObject(id_).values_.set(key, value);
}
inline void GridObject::set(const std::string& key, const char* value) { set(key, std::string(value)); }

inline int GridObject::get(const std::string& key, int& value) const {
    long long stored = 0;
    const int status = get(key, stored);
    if (stored < std::numeric_limits<int>::min() || stored > std::numeric_limits<int>::max()) {
        throw std::runtime_error("Grid++ Error: Value '" + key + "' does not fit in int");
    }
    value = static_cast<int>(stored);
    return status;
}
inline int GridObject::get(const std::string& key, long long& value) const {
    return lockEngine()->requireObject(id_).values_.get(key, value);
}
inline int GridObject::get(const std::string& key, double& value) const {
    return lockEngine()->requireObject(id_).values_.get(key, value);
}
inline int GridObject::get(const std::string& key, bool& value) const {
    return lockEngine()->requireObject(id_).values_.get(key, value);
}
inline int GridObject::get(const std::string& key, std::string& value) const {
    return lockEngine()->requireObject(id_).values_.get(key, value);
}
template <typename T>
inline void GridObject::set(const std::string& key, const std::vector<T>& value) {
    lockEngine()->requireObject(id_).values_.set(key, value);
}
template <typename T>
inline int GridObject::get(const std::string& key, std::vector<T>& value) const {
    return lockEngine()->requireObject(id_).values_.get(key, value);
}
inline void GridObject::setInitFunction(GridObjectCallback function) {
    lockEngine()->requireObject(id_).init_ = function;
}
inline void GridObject::setUpdateFunction(GridObjectCallback function) {
    lockEngine()->requireObject(id_).update_ = function;
}
inline void GridObject::setCollideFunction(CollisionCallback function) {
    lockEngine()->requireObject(id_).collide_ = function;
}

// Overlay handler --------------------------------------------------------------

inline Overlay::Overlay(std::weak_ptr<GameEngineImpl> engine, std::uint64_t id) : engine_(std::move(engine)), id_(id) {}

inline std::shared_ptr<GameEngineImpl> Overlay::lockEngine() const {
    std::shared_ptr<GameEngineImpl> engine = engine_.lock();
    if (engine == nullptr) throw std::runtime_error("Grid++ Error: Overlay's GameEngine no longer exists");
    return engine;
}
inline bool Overlay::exists() const {
    const std::shared_ptr<GameEngineImpl> engine = engine_.lock();
    return engine != nullptr && engine->hasOverlay(id_);
}
inline void Overlay::remove() {
    const std::shared_ptr<GameEngineImpl> engine = lockEngine();
    engine->requireOverlay(id_);
    engine->removeOverlay(id_);
}
inline Overlay Overlay::deepCopy() const { return lockEngine()->cloneOverlay(id_); }
inline int Overlay::x() const { return lockEngine()->requireOverlay(id_).x_; }
inline int Overlay::y() const { return lockEngine()->requireOverlay(id_).y_; }
inline void Overlay::setPosition(int x, int y) {
    OverlayImpl& overlay = lockEngine()->requireOverlay(id_);
    overlay.x_ = x;
    overlay.y_ = y;
}
inline void Overlay::move(int dx, int dy) {
    OverlayImpl& overlay = lockEngine()->requireOverlay(id_);
    overlay.x_ += dx;
    overlay.y_ += dy;
}
inline bool Overlay::visible() const { return lockEngine()->requireOverlay(id_).visible_; }
inline void Overlay::show() { lockEngine()->requireOverlay(id_).visible_ = true; }
inline void Overlay::hide() { lockEngine()->requireOverlay(id_).visible_ = false; }

inline std::string Overlay::image() const {
    const OverlayImpl& overlay = lockEngine()->requireOverlay(id_);
    if (overlay.type_ != OverlayType::Image)
        throw std::runtime_error("Grid++ Error: image() requires an image overlay");
    return overlay.content_;
}
inline void Overlay::setImage(const std::string& image) {
    OverlayImpl& overlay = lockEngine()->requireOverlay(id_);
    if (overlay.type_ != OverlayType::Image) {
        throw std::runtime_error("Grid++ Error: setImage() requires an image overlay");
    }
    overlay.content_ = image;
}
inline std::string Overlay::text() const {
    const OverlayImpl& overlay = lockEngine()->requireOverlay(id_);
    if (overlay.type_ == OverlayType::Image) {
        throw std::runtime_error("Grid++ Error: text() requires a text overlay or button");
    }
    return overlay.content_;
}
inline void Overlay::setText(const std::string& text) {
    OverlayImpl& overlay = lockEngine()->requireOverlay(id_);
    if (overlay.type_ == OverlayType::Image) {
        throw std::runtime_error("Grid++ Error: setText() requires a text overlay or button");
    }
    overlay.content_ = text;
}
inline void Overlay::setColor(Color color) { lockEngine()->requireOverlay(id_).color_ = color; }

inline void Overlay::set(const std::string& key, int value) { set(key, static_cast<long long>(value)); }
inline void Overlay::set(const std::string& key, long long value) {
    lockEngine()->requireOverlay(id_).values_.set(key, value);
}
inline void Overlay::set(const std::string& key, double value) {
    lockEngine()->requireOverlay(id_).values_.set(key, value);
}
inline void Overlay::set(const std::string& key, bool value) {
    lockEngine()->requireOverlay(id_).values_.set(key, value);
}
inline void Overlay::set(const std::string& key, const std::string& value) {
    lockEngine()->requireOverlay(id_).values_.set(key, value);
}
inline void Overlay::set(const std::string& key, const char* value) { set(key, std::string(value)); }
inline int Overlay::get(const std::string& key, int& value) const {
    long long stored = 0;
    const int status = get(key, stored);
    if (stored < std::numeric_limits<int>::min() || stored > std::numeric_limits<int>::max()) {
        throw std::runtime_error("Grid++ Error: Value '" + key + "' does not fit in int");
    }
    value = static_cast<int>(stored);
    return status;
}
inline int Overlay::get(const std::string& key, long long& value) const {
    return lockEngine()->requireOverlay(id_).values_.get(key, value);
}
inline int Overlay::get(const std::string& key, double& value) const {
    return lockEngine()->requireOverlay(id_).values_.get(key, value);
}
inline int Overlay::get(const std::string& key, bool& value) const {
    return lockEngine()->requireOverlay(id_).values_.get(key, value);
}
inline int Overlay::get(const std::string& key, std::string& value) const {
    return lockEngine()->requireOverlay(id_).values_.get(key, value);
}
template <typename T>
inline void Overlay::set(const std::string& key, const std::vector<T>& value) {
    lockEngine()->requireOverlay(id_).values_.set(key, value);
}
template <typename T>
inline int Overlay::get(const std::string& key, std::vector<T>& value) const {
    return lockEngine()->requireOverlay(id_).values_.get(key, value);
}
inline void Overlay::setInitFunction(OverlayCallback function) { lockEngine()->requireOverlay(id_).init_ = function; }
inline void Overlay::setUpdateFunction(OverlayCallback function) {
    lockEngine()->requireOverlay(id_).update_ = function;
}
inline void Overlay::setClickFunction(OverlayCallback function) {
    OverlayImpl& overlay = lockEngine()->requireOverlay(id_);
    if (overlay.type_ != OverlayType::Button) {
        throw std::runtime_error("Grid++ Error: setClickFunction() requires a button");
    }
    overlay.click_ = function;
}

// Maze handler -----------------------------------------------------------------

inline Maze::Maze(std::weak_ptr<GameEngineImpl> engine, std::uint64_t id) : engine_(std::move(engine)), id_(id) {}
inline std::shared_ptr<GameEngineImpl> Maze::lockEngine() const {
    std::shared_ptr<GameEngineImpl> engine = engine_.lock();
    if (engine == nullptr) throw std::runtime_error("Grid++ Error: Maze's GameEngine no longer exists");
    return engine;
}
inline bool Maze::exists() const {
    const std::shared_ptr<GameEngineImpl> engine = engine_.lock();
    return engine != nullptr && engine->hasMaze(id_);
}
inline void Maze::remove() {
    const std::shared_ptr<GameEngineImpl> engine = lockEngine();
    engine->requireMaze(id_);
    engine->removeMaze(id_);
}
inline Maze Maze::deepCopy() const { return lockEngine()->cloneMaze(id_); }
inline void Maze::setWall(int x, int y, bool wall) {
    MazeImpl& maze = lockEngine()->requireMaze(id_);
    if (x < 0 || y < 0 || x >= maze.width_ || y >= maze.height_) {
        throw std::out_of_range("Grid++ Error: wall position is outside the maze");
    }
    maze.walls_[y][x] = wall;
}
inline bool Maze::isWall(int x, int y) const {
    const MazeImpl& maze = lockEngine()->requireMaze(id_);
    return x < 0 || y < 0 || x >= maze.width_ || y >= maze.height_ || maze.walls_[y][x];
}
inline void Maze::setWallImage(const std::string& image) {
    MazeImpl& maze = lockEngine()->requireMaze(id_);
    maze.single_wall_ = image;
    maze.use_wall_images_ = false;
}
inline void Maze::setWallImages(const std::string& isolated, const std::string& end, const std::string& straight,
                                const std::string& corner, const std::string& tee, const std::string& cross) {
    MazeImpl& maze = lockEngine()->requireMaze(id_);
    maze.wall_images_[0] = isolated;
    maze.wall_images_[1] = end;
    maze.wall_images_[2] = straight;
    maze.wall_images_[3] = corner;
    maze.wall_images_[4] = tee;
    maze.wall_images_[5] = cross;
    maze.use_wall_images_ = true;
}
inline int Maze::width() const { return lockEngine()->requireMaze(id_).width_; }
inline int Maze::height() const { return lockEngine()->requireMaze(id_).height_; }
inline void Maze::setInitFunction(MazeCallback function) { lockEngine()->requireMaze(id_).init_ = function; }
inline void Maze::setUpdateFunction(MazeCallback function) { lockEngine()->requireMaze(id_).update_ = function; }

// GameEngine handler -----------------------------------------------------------

inline GameEngine::GameEngine(int cols, int rows, int grid_size)
    : impl_(std::shared_ptr<GameEngineImpl>(new GameEngineImpl(cols, rows, grid_size))) {}
inline GameEngine::GameEngine(std::shared_ptr<GameEngineImpl> impl) : impl_(std::move(impl)) {}
inline void GameEngine::loadAssets(const std::string& database_path) { impl_->assets_.Load(database_path); }
inline void GameEngine::setBackgroundColor(Color color) { impl_->background_color_ = color; }
inline void GameEngine::showGrid(bool show) { impl_->show_grid_ = show; }
inline int GameEngine::cols() const { return impl_->cols_; }
inline int GameEngine::rows() const { return impl_->rows_; }
inline int GameEngine::gridSize() const { return impl_->grid_size_; }
inline GridObject GameEngine::addObject(const std::string& image, GridObjectCallback init, GridObjectCallback update,
                                        CollisionCallback collide) {
    return impl_->addObject(image, init, update, collide);
}
inline GridObject GameEngine::addSquare(int x, int y, int size, Color color, GridObjectCallback init,
                                        GridObjectCallback update, CollisionCallback collide) {
    return impl_->addShape(GridObjectType::Square, x, y, size, color, init, update, collide);
}
inline GridObject GameEngine::addCircle(int x, int y, int size, Color color, GridObjectCallback init,
                                        GridObjectCallback update, CollisionCallback collide) {
    return impl_->addShape(GridObjectType::Circle, x, y, size, color, init, update, collide);
}
inline GridObject GameEngine::addTriangle(int x, int y, int size, Color color, GridObjectCallback init,
                                          GridObjectCallback update, CollisionCallback collide) {
    return impl_->addShape(GridObjectType::Triangle, x, y, size, color, init, update, collide);
}
inline GridObject GameEngine::addPentagon(int x, int y, int size, Color color, GridObjectCallback init,
                                          GridObjectCallback update, CollisionCallback collide) {
    return impl_->addShape(GridObjectType::Pentagon, x, y, size, color, init, update, collide);
}
inline GridObject GameEngine::addStar(int x, int y, int size, Color color, GridObjectCallback init,
                                      GridObjectCallback update, CollisionCallback collide) {
    return impl_->addShape(GridObjectType::Star, x, y, size, color, init, update, collide);
}
inline Maze GameEngine::addMaze(int cols, int rows, MazeCallback init, MazeCallback update) {
    return impl_->addMaze(cols, rows, init, update);
}
inline void GameEngine::clearObjects() { impl_->clearObjects(); }
inline Overlay GameEngine::addOverlay(const std::string& image, OverlayCallback init, OverlayCallback update) {
    return impl_->addImageOverlay(image, init, update);
}
inline Overlay GameEngine::addTextOverlay(const std::string& text, int x, int y, int font_size, Color color,
                                          OverlayCallback init, OverlayCallback update) {
    return impl_->addTextOverlay(text, x, y, font_size, color, init, update);
}
inline Overlay GameEngine::addButton(const std::string& text, int x, int y, int width, int height,
                                     OverlayCallback click) {
    return impl_->addButton(text, x, y, width, height, click);
}
inline void GameEngine::clearOverlays() { impl_->clearOverlays(); }
inline bool GameEngine::keyPressed(int key) const { return IsKeyPressed(key); }
inline bool GameEngine::keyDown(int key) const { return IsKeyDown(key); }
inline bool GameEngine::mousePressed(int button) const { return IsMouseButtonPressed(button); }
inline int GameEngine::mouseX() const { return static_cast<int>(GetMousePosition().x); }
inline int GameEngine::mouseY() const { return static_cast<int>(GetMousePosition().y); }
inline double GameEngine::time() const { return GetTime(); }
inline int GameEngine::random(int min, int max) const { return GetRandomValue(min, max); }
inline void GameEngine::run() { impl_->run(); }

}  // namespace gridpp

#endif  // GRID_PLUS_PLUS_GAME_ENGINE_H_