File GridEngine.h¶
Go to the documentation of this file
#ifndef GRID_PLUS_PLUS_GRID_ENGINE_H_
#define GRID_PLUS_PLUS_GRID_ENGINE_H_
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <stdexcept>
#include <string>
#include <unordered_set>
#include <vector>
#include "GridAssetManager.h"
#include "GridObject.h"
#include "Overlay.h"
#include "raylib.h"
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#endif
namespace gridpp {
class GridEngine {
public:
static constexpr int kMaxWindowSize = 8192;
GridEngine(int cols, int rows, int grid_size = 32);
~GridEngine();
GridEngine(const GridEngine& other) = delete;
GridEngine& operator=(const GridEngine& other) = delete;
void LoadAssets(const std::filesystem::path& database_path);
void set_background_color(Color color);
Color background_color() const;
void set_show_grid(bool show);
bool show_grid() const;
int cols() const;
int rows() const;
int grid_size() const;
GridObject* Spawn(GridObject* object);
GridObject* Spawn(const std::string& asset_name, int x, int y, CallbackGridObject::UpdateFn update,
CallbackGridObject::CollideFn collide = nullptr);
void Destroy(GridObject* object);
void ClearObjects();
void AddOverlay(Overlay* overlay);
void Run();
void DrawCell(const std::string& asset_name, int grid_x, int grid_y, int direction = 0, Color tint = WHITE);
private:
void Tick();
bool IsPendingDestroy(GridObject* object) const;
void InvokeOnSpawn(GridObject* object);
void FlushLifecycleChanges();
void DrawGrid();
void DeleteAllObjects() noexcept;
void ClearOverlays() noexcept;
int cols_;
int rows_;
int grid_size_;
Color background_color_ = RAYWHITE;
bool show_grid_ = false;
GridAssetManager assets_;
std::vector<GridObject*> objects_;
std::vector<GridObject*> objects_to_spawn_;
std::unordered_set<GridObject*> objects_to_destroy_;
bool ticking_ = false;
std::vector<Overlay*> overlays_;
};
// Inline definitions
inline GridEngine::GridEngine(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("GridEngine Error: cols, rows, and grid size must be greater than 0");
}
const std::int64_t window_width = static_cast<std::int64_t>(cols_) * grid_size_;
const std::int64_t window_height = static_cast<std::int64_t>(rows_) * grid_size_;
if (window_width > kMaxWindowSize || window_height > kMaxWindowSize) {
throw std::invalid_argument("GridEngine Error: window width and height must not exceed 8192 pixels");
}
// 材質需要在視窗建立後才能上傳至 GPU。
InitWindow(static_cast<int>(window_width), static_cast<int>(window_height), "Grid++ Game");
SetTargetFPS(60);
}
inline GridEngine::~GridEngine() {
DeleteAllObjects();
ClearOverlays();
// raylib 的 Texture 必須在 OpenGL context 關閉前釋放。
assets_.Clear();
CloseWindow();
}
inline void GridEngine::LoadAssets(const std::filesystem::path& database_path) { assets_.Load(database_path); }
inline void GridEngine::set_background_color(Color color) { background_color_ = color; }
inline Color GridEngine::background_color() const { return background_color_; }
inline void GridEngine::set_show_grid(bool show) { show_grid_ = show; }
inline bool GridEngine::show_grid() const { return show_grid_; }
inline int GridEngine::cols() const { return cols_; }
inline int GridEngine::rows() const { return rows_; }
inline int GridEngine::grid_size() const { return grid_size_; }
inline GridObject* GridEngine::Spawn(GridObject* object) {
if (object == nullptr) throw std::invalid_argument("GridEngine Error: Cannot Spawn nullptr GridObject");
if (object->engine() != nullptr)
throw std::logic_error("GridEngine Error: Same GridObject cannot be Spawned twice");
object->set_engine(this);
try {
if (ticking_) {
objects_to_spawn_.push_back(object);
} else {
objects_.push_back(object);
}
} catch (...) {
delete object;
throw;
}
if (!ticking_) InvokeOnSpawn(object);
return object;
}
inline GridObject* GridEngine::Spawn(const std::string& asset_name, int x, int y, CallbackGridObject::UpdateFn update,
CallbackGridObject::CollideFn collide) {
return Spawn(new CallbackGridObject(asset_name, x, y, update, collide));
}
inline void GridEngine::Destroy(GridObject* object) {
if (object == nullptr) return;
if (ticking_) {
objects_to_destroy_.insert(object);
return;
}
const auto it = std::find(objects_.begin(), objects_.end(), object);
if (it != objects_.end()) {
delete *it;
objects_.erase(it);
}
}
inline void GridEngine::ClearObjects() {
if (ticking_) {
objects_to_destroy_.insert(objects_.begin(), objects_.end());
objects_to_destroy_.insert(objects_to_spawn_.begin(), objects_to_spawn_.end());
return;
}
DeleteAllObjects();
}
inline void GridEngine::AddOverlay(Overlay* overlay) {
if (overlay == nullptr) throw std::invalid_argument("Grid++ 錯誤:不能加入 nullptr Overlay");
if (overlay->engine_ != nullptr) throw std::logic_error("Grid++ 錯誤:同一個 Overlay 不能加入兩次");
overlays_.push_back(overlay);
overlay->engine_ = this;
}
inline void GridEngine::Run() {
#ifdef __EMSCRIPTEN__
// WebAssembly 主迴圈由瀏覽器排程。
emscripten_set_main_loop_arg([](void* self) { static_cast<GridEngine*>(self)->Tick(); }, this, 0, 1);
#else
while (!WindowShouldClose()) Tick();
#endif
}
inline void GridEngine::DrawCell(const std::string& asset_name, 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 (!asset_name.empty() && assets_.Has(asset_name)) {
const Texture2D texture = assets_.Get(asset_name);
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 GridEngine::Tick() {
ticking_ = true;
// 執行期間加入的 Overlay 從下一幀開始更新與繪製。
const std::size_t overlay_count = overlays_.size();
// 更新
for (GridObject* object : objects_) {
if (!IsPendingDestroy(object)) object->OnUpdate();
}
for (std::size_t i = 0; i < overlay_count; ++i) overlays_[i]->OnUpdate();
// 碰撞
const auto can_collide = [&](GridObject* object) {
return !IsPendingDestroy(object) && object->visible() && object->x() >= 0 && object->x() < cols_ &&
object->y() >= 0 && object->y() < rows_;
};
for (std::size_t i = 0; i < objects_.size(); ++i) {
if (!can_collide(objects_[i])) continue;
for (std::size_t j = i + 1; j < objects_.size(); ++j) {
if (!can_collide(objects_[j])) continue;
if (objects_[i]->x() == objects_[j]->x() && objects_[i]->y() == objects_[j]->y()) {
objects_[i]->OnCollide(objects_[j]);
if (!can_collide(objects_[i])) break;
if (!can_collide(objects_[j])) continue;
objects_[j]->OnCollide(objects_[i]);
if (!can_collide(objects_[i])) break;
}
}
}
// 繪製
BeginDrawing();
ClearBackground(background_color_);
if (show_grid_) DrawGrid();
std::vector<GridObject*> draw_order = objects_;
std::stable_sort(draw_order.begin(), draw_order.end(), [](const GridObject* left, const GridObject* right) {
return left->z_index() < right->z_index();
});
for (GridObject* object : draw_order) {
if (!IsPendingDestroy(object) && object->visible()) object->Render(this);
}
for (std::size_t i = 0; i < overlay_count; ++i) overlays_[i]->Draw();
EndDrawing();
ticking_ = false;
FlushLifecycleChanges();
}
inline bool GridEngine::IsPendingDestroy(GridObject* object) const { return objects_to_destroy_.count(object) != 0; }
inline void GridEngine::InvokeOnSpawn(GridObject* object) {
try {
object->OnSpawn();
} catch (...) {
const auto it = std::find(objects_.begin(), objects_.end(), object);
if (it != objects_.end()) {
delete *it;
objects_.erase(it);
}
throw;
}
}
inline void GridEngine::FlushLifecycleChanges() {
objects_.erase(std::remove_if(objects_.begin(), objects_.end(),
[&](GridObject* object) {
if (!IsPendingDestroy(object)) return false;
delete object;
return true;
}),
objects_.end());
objects_to_spawn_.erase(std::remove_if(objects_to_spawn_.begin(), objects_to_spawn_.end(),
[&](GridObject* object) {
if (!IsPendingDestroy(object)) return false;
delete object;
return true;
}),
objects_to_spawn_.end());
objects_to_destroy_.clear();
while (!objects_to_spawn_.empty()) {
GridObject* object = objects_to_spawn_.front();
objects_.push_back(object);
objects_to_spawn_.erase(objects_to_spawn_.begin());
InvokeOnSpawn(object);
}
}
inline void GridEngine::DrawGrid() {
for (int x = 0; x <= cols_; ++x) DrawLine(x * grid_size_, 0, x * grid_size_, rows_ * grid_size_, LIGHTGRAY);
for (int y = 0; y <= rows_; ++y) DrawLine(0, y * grid_size_, cols_ * grid_size_, y * grid_size_, LIGHTGRAY);
}
inline void GridEngine::DeleteAllObjects() noexcept {
for (GridObject* object : objects_) delete object;
for (GridObject* object : objects_to_spawn_) delete object;
objects_.clear();
objects_to_spawn_.clear();
objects_to_destroy_.clear();
}
inline void GridEngine::ClearOverlays() noexcept {
for (Overlay* overlay : overlays_) delete overlay;
overlays_.clear();
}
} // namespace gridpp
#endif // GRID_PLUS_PLUS_GRID_ENGINE_H_