From b46a0d9369fbaa1938f0968ab216bc2d564a9c37 Mon Sep 17 00:00:00 2001 From: fschildt Date: Mon, 21 Jul 2025 16:07:28 +0200 Subject: first commit --- src/basic/defs.hpp | 14 ++ src/basic/math.cpp | 98 +++++++++ src/basic/math.hpp | 59 ++++++ src/common/stb_truetype.cpp | 3 + src/games/Game.cpp | 39 ++++ src/games/Game.hpp | 26 +++ src/games/minesweeper/Minesweeper.cpp | 321 ++++++++++++++++++++++++++++ src/games/minesweeper/Minesweeper.hpp | 81 ++++++++ src/games/snake/Snake.cpp | 345 +++++++++++++++++++++++++++++++ src/games/snake/Snake.hpp | 62 ++++++ src/games/tetris/Board.cpp | 139 +++++++++++++ src/games/tetris/Board.hpp | 30 +++ src/games/tetris/Tetris.cpp | 368 +++++++++++++++++++++++++++++++++ src/games/tetris/Tetris.hpp | 67 ++++++ src/games/tetris/Tetromino.cpp | 212 +++++++++++++++++++ src/games/tetris/Tetromino.hpp | 59 ++++++ src/main.cpp | 191 +++++++++++++++++ src/renderer/RenderGroup.cpp | 94 +++++++++ src/renderer/RenderGroup.hpp | 83 ++++++++ src/renderer/Renderer.cpp | 18 ++ src/renderer/Renderer.hpp | 26 +++ src/renderer/opengl/GlIndexBuffer.cpp | 44 ++++ src/renderer/opengl/GlIndexBuffer.hpp | 23 +++ src/renderer/opengl/GlRenderer.cpp | 220 ++++++++++++++++++++ src/renderer/opengl/GlRenderer.hpp | 34 +++ src/renderer/opengl/GlShader.cpp | 80 +++++++ src/renderer/opengl/GlShader.hpp | 15 ++ src/renderer/opengl/GlVertexBuffer.cpp | 54 +++++ src/renderer/opengl/GlVertexBuffer.hpp | 27 +++ 29 files changed, 2832 insertions(+) create mode 100644 src/basic/defs.hpp create mode 100644 src/basic/math.cpp create mode 100644 src/basic/math.hpp create mode 100644 src/common/stb_truetype.cpp create mode 100644 src/games/Game.cpp create mode 100644 src/games/Game.hpp create mode 100644 src/games/minesweeper/Minesweeper.cpp create mode 100644 src/games/minesweeper/Minesweeper.hpp create mode 100644 src/games/snake/Snake.cpp create mode 100644 src/games/snake/Snake.hpp create mode 100644 src/games/tetris/Board.cpp create mode 100644 src/games/tetris/Board.hpp create mode 100644 src/games/tetris/Tetris.cpp create mode 100644 src/games/tetris/Tetris.hpp create mode 100644 src/games/tetris/Tetromino.cpp create mode 100644 src/games/tetris/Tetromino.hpp create mode 100644 src/main.cpp create mode 100644 src/renderer/RenderGroup.cpp create mode 100644 src/renderer/RenderGroup.hpp create mode 100644 src/renderer/Renderer.cpp create mode 100644 src/renderer/Renderer.hpp create mode 100644 src/renderer/opengl/GlIndexBuffer.cpp create mode 100644 src/renderer/opengl/GlIndexBuffer.hpp create mode 100644 src/renderer/opengl/GlRenderer.cpp create mode 100644 src/renderer/opengl/GlRenderer.hpp create mode 100644 src/renderer/opengl/GlShader.cpp create mode 100644 src/renderer/opengl/GlShader.hpp create mode 100644 src/renderer/opengl/GlVertexBuffer.cpp create mode 100644 src/renderer/opengl/GlVertexBuffer.hpp (limited to 'src') diff --git a/src/basic/defs.hpp b/src/basic/defs.hpp new file mode 100644 index 0000000..bfd302e --- /dev/null +++ b/src/basic/defs.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include +#include + +#define ARRAY_COUNT(x) (sizeof(x) / sizeof(x[0])) + +#define InvalidDefaultCase assert(0) + +#define KIBIBYTES(x) ((x)*1024) +#define MEBIBYTES(x) ((x)*KIBIBYTES(1024)) +#define GIBIBYTES(x) ((x)*MEBIBYTES(1024)) + diff --git a/src/basic/math.cpp b/src/basic/math.cpp new file mode 100644 index 0000000..3067255 --- /dev/null +++ b/src/basic/math.cpp @@ -0,0 +1,98 @@ +#include + + +V2ST::V2ST(size_t x, size_t y) : x(x), y(y) { +} + +V2ST::V2ST(int32_t x, int32_t y) : x((size_t)x), y((size_t)y) { +} + +bool V2ST::operator==(V2ST &b) { + bool result = this->x == b.x && this->y == b.y; + return result; +} + + +V2F32::V2F32(float x, float y) { + this->x = x; + this->y = y; +} + +V2F32 V2F32::operator/(float scalar) { + V2F32 result = {}; + result.x = this->x / scalar; + result.y = this->y / scalar; + return result; +} + +V2F32 V2F32::operator*(float scalar) { + V2F32 result = {}; + result.x = this->x * scalar; + result.y = this->y * scalar; + return result; +} + +V2F32 V2F32::operator+(V2F32 other) { + V2F32 result = {}; + result.x = this->x + other.x; + result.y = this->y + other.y; + return result; +} + + +V3F32::V3F32(float x, float y, float z) { + this->x = x; + this->y = y; + this->z = z; +} + +V3F32 V3F32::operator/(float scalar) { + V3F32 result = {}; + result.x = this->x / scalar; + result.y = this->y / scalar; + result.z = this->z / scalar; + return result; +} + +V3F32 V3F32::operator*(float scalar) { + V3F32 result = {}; + result.x = this->x * scalar; + result.y = this->y * scalar; + result.z = this->z * scalar; + return result; +} + + +V4F32::V4F32(float x, float y, float z, float w) { + this->x = x; + this->y = y; + this->z = z; + this->w = w; +} + +V4F32 V4F32::operator/(float scalar) { + V4F32 result = {}; + result.x = this->x / scalar; + result.y = this->y / scalar; + result.z = this->z / scalar; + result.w = this->w / scalar; + return result; +} + +V4F32 V4F32::operator*(float scalar) { + V4F32 result = {}; + result.x = this->x * scalar; + result.y = this->y * scalar; + result.z = this->z * scalar; + result.w = this->z * scalar; + return result; +} + +V2I32::V2I32(int32_t x, int32_t y) : x(x), y(y) { +} + +bool V2I32::operator==(V2I32 other) { + bool result = x == other.x && y == other.y; + return result; +} + diff --git a/src/basic/math.hpp b/src/basic/math.hpp new file mode 100644 index 0000000..0f21181 --- /dev/null +++ b/src/basic/math.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include + +struct V2ST { + size_t x; + size_t y; + + V2ST() = default; + V2ST(size_t x, size_t y); + V2ST(int32_t x, int32_t y); + bool operator==(V2ST &b); + bool operator==(const V2ST& other) const { + return x == other.x && y == other.y; + } +}; + +struct V2F32 { + float x; + float y; + + V2F32() = default; + V2F32(float x, float y); + V2F32 operator/(float scalar); + V2F32 operator*(float scalar); + V2F32 operator+(V2F32 other); +}; + +struct V3F32 { + float x; + float y; + float z; + + V3F32() = default; + V3F32(float x, float y, float z); + V3F32 operator/(float scalar); + V3F32 operator*(float scalar); +}; + +struct V4F32 { + float x; + float y; + float z; + float w; + + V4F32() = default; + V4F32 (float x, float y, float z, float w); + V4F32 operator/(float scalar); + V4F32 operator*(float scalar); +}; + +struct V2I32 { + int32_t x; + int32_t y; + + V2I32() = default; + V2I32 (int32_t x, int32_t y); + bool operator==(V2I32 other); +}; diff --git a/src/common/stb_truetype.cpp b/src/common/stb_truetype.cpp new file mode 100644 index 0000000..cc11b20 --- /dev/null +++ b/src/common/stb_truetype.cpp @@ -0,0 +1,3 @@ +#define STB_TRUETYPE_IMPLEMENTATION +#include + diff --git a/src/games/Game.cpp b/src/games/Game.cpp new file mode 100644 index 0000000..628f5d4 --- /dev/null +++ b/src/games/Game.cpp @@ -0,0 +1,39 @@ +#include +#include +#include +#include +#include + +#include +#include + + +std::unique_ptr +Game::Select(GameType type) +{ + switch (type) { + case NO_GAME: { + return nullptr; + } + + case TETRIS: { + return std::make_unique(); + } break; + + case SNAKE: { + return std::make_unique(); + } break; + + case MINESWEEPER: { + return std::make_unique(); + } break; + + InvalidDefaultCase; + } + + return nullptr; +} + + +Game::~Game() {} + diff --git a/src/games/Game.hpp b/src/games/Game.hpp new file mode 100644 index 0000000..9af98b4 --- /dev/null +++ b/src/games/Game.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include +#include + +struct SDL_Window; + +class Game { +public: + enum GameType { + NO_GAME, + TETRIS, + SNAKE, + MINESWEEPER + }; + + Game() = default; + virtual ~Game(); + static std::unique_ptr Select(GameType type); + + virtual bool Update(std::vector &events, RenderGroup &render_group) = 0; +}; + diff --git a/src/games/minesweeper/Minesweeper.cpp b/src/games/minesweeper/Minesweeper.cpp new file mode 100644 index 0000000..60502c6 --- /dev/null +++ b/src/games/minesweeper/Minesweeper.cpp @@ -0,0 +1,321 @@ +#include +#include + +#include +#include + +// Todo: draw a difficulty selection menu at top-mid. + +// Note: How many mines? +// - Beginner: 8x8 or 9x9 grid with 10 mines. +// - Intermediate: 16x16 grid with 40 mines. +// - Expert: 30x16 grid with 99 mines. + +Minesweeper::Minesweeper() +{ + float map_width = static_cast(m_MapWidth); + float map_height = static_cast(m_MapHeight); + float cell_size = 0.8f * std::min(m_WorldHeight / MAX_MAP_HEIGHT, m_WorldWidth / MAX_MAP_WIDTH); + float cell_size_without_border = 0.8f * cell_size; + + m_MapViewPos = { + 0.1f * cell_size_without_border + (m_WorldWidth - cell_size * map_width) / 2, + 0.1f * cell_size_without_border + (m_WorldHeight - cell_size * map_height) / 2 + }; + m_CellOuterViewSize = {cell_size, cell_size}; + m_CellInnerViewSize = {cell_size_without_border, cell_size_without_border}; + + + // Todo: assert various stuff + + Reinit(); +} + +void Minesweeper::Reinit() { + int32_t mine_count = 40; + memset(m_IsCoveredBitmap, 0xff, sizeof(m_IsCoveredBitmap)); + memset(m_IsFlaggedBitmap, 0 , sizeof(m_IsFlaggedBitmap)); + InitIsMineBitmap(mine_count); + InitAdjacentMineCounters(); +} + +void Minesweeper::InitIsMineBitmap(int32_t mine_count) { + assert(mine_count < m_MapWidth * m_MapHeight); + + memset(m_IsMineBitmap, 0 , sizeof(m_IsMineBitmap)); + + std::mt19937 rng((std::random_device()())); + std::uniform_int_distribution dist(0, m_MapWidth * m_MapHeight - 1); + + while (mine_count) { + int32_t random_pos = dist(rng); + int32_t x = random_pos / m_MapWidth; + int32_t y = random_pos % m_MapWidth; + if (!IsMine(x, y)) { + m_IsMineBitmap[y] |= 1 << x; + mine_count--; + } + } +} + +void Minesweeper::InitAdjacentMineCounters() { + for (int32_t y = 0; y < m_MapHeight; y++) { + int32_t y0 = y > 0 ? y-1 : y; + int32_t y1 = y < m_MapHeight-1 ? y+1 : y; + + for (int32_t x = 0; x < m_MapHeight; x++) { + int32_t x0 = x > 0 ? x-1 : x; + int32_t x1 = x < m_MapWidth-1 ? x+1 : x; + + int32_t adjacent_mine_counter = 0; + for (int32_t inner_y = y0; inner_y <= y1; inner_y++) { + for (int32_t inner_x = x0; inner_x <= x1; inner_x++) { + if (IsMine(inner_x, inner_y)) { + adjacent_mine_counter++; + } + } + } + if (IsMine(x, y)) { + adjacent_mine_counter = -1; + } + + m_AdjacentMineCounters[y * m_MapWidth + x] = adjacent_mine_counter; + } + } +} + +bool Minesweeper::Update(std::vector &events, RenderGroup &render_group) { + V3F32 clear_color = {0.3f, 0.2f, 0.3f}; + render_group.SetCameraSize(4.0f, 3.0f); + render_group.Clear(clear_color); + + if (m_RunState == MinesweeperRunState::Restart) { + Reinit(); + m_RunState = MinesweeperRunState::Resume; + } + + for (SDL_Event &event : events) { + if (m_RunState == MinesweeperRunState::Exit) { + return false; + } + else if (m_RunState == MinesweeperRunState::Pause) { + ProcessEventDuringPause(event, render_group); + } + else if (m_RunState == MinesweeperRunState::Resume) { + ProcessEventDuringResume(event, render_group); + } + } + + if (m_RunState == MinesweeperRunState::Pause) { + DrawPauseMenu(render_group); + } + else if (m_RunState == MinesweeperRunState::GameOver) { + DrawGameOverMenu(render_group); + } + + DrawBoard(render_group); + + bool keep_running = m_RunState != MinesweeperRunState::Exit; + return keep_running; +} + +void Minesweeper::ProcessEventDuringPause(SDL_Event &event, RenderGroup &render_group) { + switch (event.type) { + case SDL_EVENT_KEY_DOWN: { + if (event.key.key == SDLK_ESCAPE) { + m_RunState = MinesweeperRunState::Resume; + } + } break; + default:; + } +} + +void Minesweeper::ProcessEventDuringResume(SDL_Event &event, RenderGroup &render_group) { + switch (event.type) { + case SDL_EVENT_KEY_DOWN: { + if (event.key.key == SDLK_ESCAPE) { + m_RunState = MinesweeperRunState::Pause; + } + } break; + + case SDL_EVENT_MOUSE_BUTTON_DOWN: { + V2F32 click_screen_pos = {event.button.x, (float)render_group.m_ScreenHeight -1 - event.button.y}; + V2F32 click_view_pos = ScreenPosToViewPos(click_screen_pos, render_group); + + float x_adjusted = click_view_pos.x - m_MapViewPos.x; + float y_adjusted = click_view_pos.y - m_MapViewPos.y; + if (x_adjusted < 0.0f) { + break; + } + if (y_adjusted < 0.0f) { + break; + } + + int32_t x = (int32_t)(x_adjusted / m_CellOuterViewSize.x); + int32_t y = (int32_t)(y_adjusted / m_CellOuterViewSize.y); + if (x >= m_MapWidth) { + break; + } + if (y >= m_MapHeight) { + break; + } + + if (event.button.button == 1) { + if (IsCovered(x, y)) { + if (IsMine(x, y)) { + m_IsCoveredBitmap[y] &= ~(1 << x); + m_RunState = MinesweeperRunState::GameOver; + } + else { + Uncover(x, y); + } + } + } + else if (event.button.button == 3) { + if (IsCovered(x, y)) { + ToggleFlag(x ,y); + } + } + + } break; + + default:; + } +} + +// Todo: maybe find a more efficient non-naive solution +void Minesweeper::Uncover(int32_t x, int32_t y) { + if (x < 0) return; + if (x >= m_MapWidth) return; + if (y < 0) return; + if (y >= m_MapHeight) return; + if (!IsCovered(x, y)) return; + + m_IsCoveredBitmap[y] &= ~(1 << x); + if (IsFlagged(x, y)) { + ToggleFlag(x, y); + } + + if (m_AdjacentMineCounters[y*m_MapWidth + x] > 0) { + return; + } + Uncover(x-1, y-1); + Uncover(x , y-1); + Uncover(x+1, y-1); + + Uncover(x-1, y); + Uncover(x+1, y); + + Uncover(x-1, y+1); + Uncover(x , y+1); + Uncover(x+1, y+1); +} + +void Minesweeper::ToggleFlag(int32_t x, int32_t y) { + m_IsFlaggedBitmap[y] ^= (1 << x); +} + +bool Minesweeper::IsCovered(int32_t x, int32_t y) { + bool is_covered = m_IsCoveredBitmap[y] & 1 << x; + return is_covered; +} + +bool Minesweeper::IsFlagged(int32_t x, int32_t y) { + bool is_flagged = m_IsFlaggedBitmap[y] & 1 << x; + return is_flagged; +} + +bool Minesweeper::IsMine(int32_t x, int32_t y) { + bool is_mine = m_IsMineBitmap[y] & 1 << x; + return is_mine; +} + +V2F32 Minesweeper::ScreenPosToViewPos(V2F32 screen_pos, RenderGroup &render_group) { + // e.g. [0, 1024] -> [0, 1] -> [0, 4] + // e.g. [0, 768] -> [0, 1] -> [0, 3] + float screen_width = (float)render_group.m_ScreenWidth; + float screen_height = (float)render_group.m_ScreenHeight; + + V2F32 view_pos; + view_pos.x = (screen_pos.x / screen_width) * m_WorldWidth; + view_pos.y = (screen_pos.y / screen_height) * m_WorldHeight; + return view_pos; +} + +void Minesweeper::DrawPauseMenu(RenderGroup &render_group) { + ImGui::Begin("MinesweeperPause"); + if (ImGui::Button("Resume")) { + m_RunState = MinesweeperRunState::Resume; + } + if (ImGui::Button("Exit")) { + m_RunState = MinesweeperRunState::Exit; + } + ImGui::End(); +} + +void Minesweeper::DrawGameOverMenu(RenderGroup &render_group) { + ImGui::Begin("MinesweeperGameOver"); + ImGui::Text("Score = ???"); + if (ImGui::Button("Restart")) { + m_RunState = MinesweeperRunState::Restart; + } + if (ImGui::Button("Exit")) { + m_RunState = MinesweeperRunState::Exit; + } + ImGui::End(); +} + +void Minesweeper::DrawBoard(RenderGroup &render_group) { + V3F32 covered_cell_color = V3F32(0.4f, 0.4f, 0.4f); + V3F32 uncovered_cell_color = V3F32(0.2f, 0.2f, 0.2f); + V3F32 flag_color = {0.6f, 0.3f, 03.f}; + V3F32 mine_color = {0.8f, 0.2f, 0.2f}; + + V2F32 flag_draw_size = {m_CellInnerViewSize.x * 0.5f, m_CellInnerViewSize.y * 0.5f}; + V2F32 flag_draw_offset = { + (m_CellInnerViewSize.x - flag_draw_size.x) / 2, + (m_CellInnerViewSize.y - flag_draw_size.y) / 2 + }; + + + + // Todo: avoid if-statement by having them in separate contiguous locations? + + for (int32_t y = 0; y < m_MapHeight; y++) { + for (int32_t x = 0; x < m_MapWidth; x++) { + V3F32 world_pos = { + m_MapViewPos.x + (float)x * m_CellOuterViewSize.x, + m_MapViewPos.y + (float)y * m_CellOuterViewSize.y, + 0.0f + }; + bool is_covered = IsCovered(x, y); + bool is_flagged = IsFlagged(x, y); + bool is_mine = IsMine(x, y); + + if (is_covered) { + render_group.PushRectangle(world_pos, m_CellInnerViewSize, covered_cell_color); + } + if (is_covered && is_flagged) { + assert(IsCovered(x ,y)); + V3F32 flag_world_pos = { + world_pos.x + flag_draw_offset.x, + world_pos.y + flag_draw_offset.y, + 1.0f + }; + render_group.PushRectangle(flag_world_pos, flag_draw_size, flag_color); + } + if (!is_covered && !is_mine) { + render_group.PushRectangle(world_pos, m_CellInnerViewSize, uncovered_cell_color); + } + if (!is_covered && is_mine) { + V3F32 mine_world_pos = { + world_pos.x, + world_pos.y, + 2.0f + }; + render_group.PushRectangle(mine_world_pos, m_CellInnerViewSize, mine_color); + } + } + } +} + diff --git a/src/games/minesweeper/Minesweeper.hpp b/src/games/minesweeper/Minesweeper.hpp new file mode 100644 index 0000000..4906464 --- /dev/null +++ b/src/games/minesweeper/Minesweeper.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include + +namespace std { + template <> + struct hash { + size_t operator()(const V2ST& v) const { + size_t h1 = hash{}(v.x); + size_t h2 = hash{}(v.y); + return h1 ^ (h2 << 1); + } + }; +} + +enum class MinesweeperRunState { + Resume, + Pause, + GameOver, + Restart, + Exit +}; + +class Minesweeper : public Game { + public: + Minesweeper(); + ~Minesweeper() = default; + + bool Update(std::vector &events, RenderGroup &render_group) override; + + void ProcessEventDuringPause(SDL_Event &event, RenderGroup &render_group); + void ProcessEventDuringResume(SDL_Event &event, RenderGroup &render_group); + + + private: + void Reinit(); + void InitIsMineBitmap(int32_t mine_count); + void InitAdjacentMineCounters(); + + void Uncover(int32_t x, int32_t y); + void ToggleFlag(int32_t x, int32_t y); + + bool IsCovered(int32_t x, int32_t y); + bool IsFlagged(int32_t x, int32_t y); + bool IsMine(int32_t x, int32_t y); + + V2F32 ScreenPosToViewPos(V2F32 screen_pos, RenderGroup &render_group); + + + private: + void DrawPauseMenu(RenderGroup &render_group); + void DrawGameOverMenu(RenderGroup &render_group); + void DrawBoard(RenderGroup &render_group); + + + private: + static constexpr int32_t MAX_MAP_HEIGHT = 32; + static constexpr int32_t MAX_MAP_WIDTH = 32; + static constexpr std::string_view s_FontFilepath = "./fonts/dejavu_ttf/DejaVuSans.ttf"; + + + private: + MinesweeperRunState m_RunState = MinesweeperRunState::Resume; + + float m_WorldWidth = 4.0f; + float m_WorldHeight = 3.0f; + + int32_t m_MapWidth = 16; + int32_t m_MapHeight = 16; + + V2F32 m_MapViewPos; + V2F32 m_CellOuterViewSize; + V2F32 m_CellInnerViewSize; + + uint32_t m_IsCoveredBitmap[MAX_MAP_HEIGHT] {}; + uint32_t m_IsFlaggedBitmap[MAX_MAP_HEIGHT] {}; + uint32_t m_IsMineBitmap[MAX_MAP_HEIGHT] {}; + int32_t m_AdjacentMineCounters[MAX_MAP_WIDTH * MAX_MAP_HEIGHT] {}; +}; + + diff --git a/src/games/snake/Snake.cpp b/src/games/snake/Snake.cpp new file mode 100644 index 0000000..9764a9e --- /dev/null +++ b/src/games/snake/Snake.cpp @@ -0,0 +1,345 @@ +#include +#include + + +Snake::Snake () { + m_IsPaused = false; + m_IsRunning = true; + + m_DtInSecondsRemaining = 0.0f; + m_LastMillisecondsSinceT0 = SDL_GetTicks(); + + m_TilesPerSecond = 4.0f; + m_Direction = DIRECTION_RIGHT; + m_LastAdvancedDirection = DIRECTION_RIGHT; + + m_MapWidth = 16; + m_MapHeight = 16; + assert(MAX_MAP_WIDTH <= 64); // m_BodyBitmap is uint64_t[]. We can't exceed that! + assert(MAX_MAP_HEIGHT <= 64); + assert(m_MapWidth <= MAX_MAP_WIDTH); + assert(m_MapHeight <= MAX_MAP_WIDTH); + + m_Tail = 0; + m_Head = 1; + memset(m_BodyBitmap, 0, sizeof(m_BodyBitmap)); + + int32_t head_x = m_MapWidth / 2; + int32_t head_y = m_MapHeight / 2; + m_BodyPositions[0] = {head_x -1, head_y}; + m_BodyPositions[1] = {head_x, head_y}; + + m_Rng = std::mt19937((std::random_device()())); + m_Dist = std::uniform_int_distribution(0, m_MapWidth * m_MapHeight - 3); + + SpawnFood(); + } + +bool Snake::Update(std::vector &events, RenderGroup &render_group) { + uint64_t milliseconds_since_t0 = SDL_GetTicks(); + uint64_t milliseconds_since_t0_last = m_LastMillisecondsSinceT0; + uint64_t dt_in_milliseconds = milliseconds_since_t0 - milliseconds_since_t0_last; + float dt_in_seconds = (float)dt_in_milliseconds / 1000.0f; + m_LastMillisecondsSinceT0 = milliseconds_since_t0; + + + V3F32 clear_color = V3F32(0.3f, 0.3f, 0.3f); + render_group.SetCameraSize(4.0f, 3.0f); + render_group.Clear(clear_color); + + + for (SDL_Event &event : events) { + if (!m_IsRunning) { + printf("event loop is running just false\n"); + return false; + } + if (m_IsPaused) { + ProcessEventDuringPause(event); + } + else { + ProcessEventDuringResume(event); + } + } + + if (!m_IsPaused) { + MaybeMoveSnake(dt_in_seconds); + } + + + Draw(render_group); + DoImgui(); + + return m_IsRunning; +} + +void Snake::MaybeMoveSnake(float dt_in_seconds) { + float dt_in_seconds_to_use = m_DtInSecondsRemaining + dt_in_seconds; + float tiles_per_second = m_TilesPerSecond; + float seconds_per_tile = 1.0f / tiles_per_second; + while (dt_in_seconds_to_use > seconds_per_tile) { + V2I32 head_pos = m_BodyPositions[m_Head]; + V2I32 tail_pos = m_BodyPositions[m_Tail]; + + + // find head_pos + if (m_Direction == DIRECTION_UP) { + head_pos.y += 1; + } + else if (m_Direction == DIRECTION_DOWN) { + head_pos.y -= 1; + } + else if (m_Direction == DIRECTION_RIGHT) { + head_pos.x += 1; + } + else if (m_Direction == DIRECTION_LEFT) { + head_pos.x -= 1; + } + if ((head_pos.x < 0 || head_pos.x >= m_MapWidth) || + (head_pos.y < 0 || head_pos.y >= m_MapHeight)) + { + m_IsRunning = false; + return; + } + uint64_t head_bit = 1 << head_pos.x; + uint64_t body_bits = m_BodyBitmap[head_pos.y]; + if (head_pos.y == tail_pos.y) { + body_bits &= ~(1 << tail_pos.x); + } + if (head_bit & body_bits) { + m_IsRunning = false; + return; + } + + + // advance head + int32_t max_positions = sizeof(m_BodyPositions) / sizeof(m_BodyPositions[0]); + m_Head += 1; + if (m_Head >= max_positions) { + m_Head = 0; + } + + m_BodyPositions[m_Head] = head_pos; + m_BodyBitmap[head_pos.y] |= (1 << head_pos.x); + + + if (m_BodyPositions[m_Head] == m_FoodPosition) { + SpawnFood(); + } + else { + // advance tail + V2I32 next_tail_pos = m_BodyPositions[m_Tail]; + m_BodyBitmap[next_tail_pos.y] &= ~(1 << next_tail_pos.x); + + m_Tail += 1; + if (m_Tail >= max_positions) { + m_Tail = 0; + } + } + + + m_LastAdvancedDirection = m_Direction; + dt_in_seconds_to_use -= seconds_per_tile; + } + + + m_DtInSecondsRemaining = dt_in_seconds_to_use; +} + +void Snake::ProcessEventDuringPause(SDL_Event &event) { + switch (event.type) { + case SDL_EVENT_KEY_DOWN: { + if (event.key.key == SDLK_ESCAPE) { + m_IsPaused = false; + } + } + default:; + } +} + +void Snake::ProcessEventDuringResume(SDL_Event &event) { + switch (event.type) { + case SDL_EVENT_KEY_DOWN: { + if (event.key.key == SDLK_UP) { + if (m_LastAdvancedDirection == DIRECTION_RIGHT || + m_LastAdvancedDirection == DIRECTION_LEFT) + { + m_Direction = DIRECTION_UP; + } + } + else if (event.key.key == SDLK_DOWN) { + if (m_LastAdvancedDirection == DIRECTION_RIGHT || + m_LastAdvancedDirection == DIRECTION_LEFT) + { + m_Direction = DIRECTION_DOWN; + } + } + else if (event.key.key == SDLK_RIGHT) { + if (m_LastAdvancedDirection == DIRECTION_UP || + m_LastAdvancedDirection == DIRECTION_DOWN) + { + m_Direction = DIRECTION_RIGHT; + } + } + else if (event.key.key == SDLK_LEFT) { + if (m_LastAdvancedDirection == DIRECTION_UP || + m_LastAdvancedDirection == DIRECTION_DOWN) + { + m_Direction = DIRECTION_LEFT; + } + } + else if (event.key.key == SDLK_ESCAPE) { + m_IsPaused = true; + } + } + + default:; + } +} + +void Snake::SpawnFood() { + int32_t bit0_counts[MAX_MAP_HEIGHT]; + int32_t bit0_count_total = 0; + + // count bits + for (int32_t y = 0; y < m_MapHeight; y++) { + int32_t bit1_count = 0; + + uint64_t bitmap_row = m_BodyBitmap[y]; + while (bitmap_row != 0) { + bitmap_row = bitmap_row & (bitmap_row - 1); + bit1_count += 1; + } + + int32_t bit0_count = m_MapWidth - bit1_count; + bit0_counts[y] = bit0_count; + bit0_count_total += bit0_count; + } + + if (bit0_count_total == 0) { + return; + } + + m_Dist.param(std::uniform_int_distribution::param_type(0, bit0_count_total - 1)); + int32_t bit0_index = m_Dist(m_Rng); + int32_t bit0_x = 0; + int32_t bit0_y = 0; + + // find y + for (int32_t y = 0; y < m_MapHeight; y++) { + if (bit0_index < bit0_counts[y]) { + bit0_y = y; + break; + } + bit0_index -= bit0_counts[y]; + } + + // find x + uint64_t bitmap_row_not = ~m_BodyBitmap[bit0_y]; + for (int32_t x = 0; x < m_MapWidth; x++) { + if (bitmap_row_not & 1) { + if (bit0_index == 0) { + bit0_x = x; + break; + } + bit0_index--; + } + bitmap_row_not >>= 1; + } + + m_FoodPosition = {bit0_x, bit0_y}; +} + +void Snake::Draw(RenderGroup &render_group) { + float world_width = 4.0f; + float world_height = 3.0f; + + float tile_size = (world_width / 2) / MAX_MAP_WIDTH; + float bodypart_size = 0.8f * tile_size; + float bodypart_offset = (tile_size - bodypart_size) / 2; + + float map_view_width = tile_size * (float)m_MapWidth; + float map_view_height = tile_size * (float)m_MapHeight; + float map_x = (world_width - map_view_width) / 2; + float map_y = (world_height - map_view_height) / 2; + + int32_t max_positions = sizeof(m_BodyPositions) / sizeof(m_BodyPositions[0]); + + + /* draw map background */ + V3F32 map_world_pos = {map_x, map_y, 0.0f}; + V2F32 map_world_dim = {map_view_width, map_view_height}; + V3F32 bg_color = {0.0f, 0.0f, 0.0f}; + render_group.PushRectangle(map_world_pos, map_world_dim, bg_color); + + + /* draw snake */ + // 1) if tail > head: advance to end first + int32_t tail = m_Tail; + if (tail > m_Head) { + while (tail < max_positions) { + V3F32 local_pos = { + (float)m_BodyPositions[tail].x * tile_size + bodypart_offset, + (float)m_BodyPositions[tail].y * tile_size + bodypart_offset, + 1.0f + }; + V2F32 local_dim = {bodypart_size, bodypart_size}; + + V3F32 world_pos = { + map_world_pos.x + local_pos.x, + map_world_pos.y + local_pos.y, + 1.0f + }; + V2F32 world_dim = local_dim; + + V3F32 color = {0.3f, 0.3f, 0.3f}; + render_group.PushRectangle(world_pos, world_dim, color); + tail++; + } + tail = 0; + } + // 2) advance to head + while (tail <= m_Head) { + V3F32 local_pos = { + (float)m_BodyPositions[tail].x * tile_size + bodypart_offset, + (float)m_BodyPositions[tail].y * tile_size + bodypart_offset, + 1.0f + }; + V2F32 local_dim = {bodypart_size, bodypart_size}; + + V3F32 world_pos = { + map_world_pos.x + local_pos.x, + map_world_pos.y + local_pos.y, + 1.0f + }; + V2F32 world_dim = local_dim; + + V3F32 color = {0.3f, 0.3f, 0.3f}; + render_group.PushRectangle(world_pos, world_dim, color); + tail++; + } + + + /* draw food */ + V3F32 pos = { + map_world_pos.x + (float)m_FoodPosition.x * tile_size + bodypart_offset, + map_world_pos.y + (float)m_FoodPosition.y * tile_size + bodypart_offset, + 1.0f + }; + V2F32 dim = {bodypart_size, bodypart_size}; + V3F32 color = {0.3f, 0.6f, 0.4f}; + render_group.PushRectangle(pos, dim, color); +} + +void Snake::DoImgui() { + if (m_IsPaused) { + ImGui::Begin("SnakePause"); + if (ImGui::Button("Resume")) { + m_IsPaused = false; + } + if (ImGui::Button("Exit")) { + m_IsRunning = false; + } + ImGui::End(); + } +} + diff --git a/src/games/snake/Snake.hpp b/src/games/snake/Snake.hpp new file mode 100644 index 0000000..f04ad16 --- /dev/null +++ b/src/games/snake/Snake.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +#include + + +class Snake : public Game { +public: + enum Direction : int32_t { + DIRECTION_UP, + DIRECTION_DOWN, + DIRECTION_LEFT, + DIRECTION_RIGHT, + }; + + +public: + Snake(); + bool Update(std::vector &events, RenderGroup &render_group) override; + + +private: + void ProcessEventDuringPause(SDL_Event &event); + void ProcessEventDuringResume(SDL_Event &event); + + void MaybeMoveSnake(float dt_in_seconds); + void SpawnFood(); + + void Draw(RenderGroup &render_group); + void DoImgui(); + + + +private: + static constexpr int32_t MAX_MAP_WIDTH = 16; + static constexpr int32_t MAX_MAP_HEIGHT = 16; + + bool m_IsPaused; + bool m_IsRunning; + + float m_DtInSecondsRemaining; + uint64_t m_LastMillisecondsSinceT0; + + float m_TilesPerSecond; + Direction m_Direction; + Direction m_LastAdvancedDirection; + + int32_t m_MapWidth; + int32_t m_MapHeight; + int32_t m_Tail; + int32_t m_Head; + uint64_t m_BodyBitmap[MAX_MAP_HEIGHT]; + V2I32 m_BodyPositions[MAX_MAP_WIDTH * MAX_MAP_HEIGHT]; + V2I32 m_FoodPosition; + + std::mt19937 m_Rng; + std::uniform_int_distribution m_Dist; +}; + + diff --git a/src/games/tetris/Board.cpp b/src/games/tetris/Board.cpp new file mode 100644 index 0000000..9dff135 --- /dev/null +++ b/src/games/tetris/Board.cpp @@ -0,0 +1,139 @@ +#include +#include +#include + +Board::Board() { + for (int y = 0; y < 2; y++) { + m_Bitmap[y] = 0xffff; // 1111111111111111 + } + for (int y = 2; y < 24; y++) { + m_Bitmap[y] = 0xe007; // 1110000000000111 + } + + for (int y = 0; y < 22; y++) { + for (int x = 0; x < 10; x++) { + m_Idmap[y][x] = (uint8_t)TetrominoId::TETROMINO_ID_NONE; + } + } +} + +int32_t Board::PlaceTetromino(Tetromino &tetromino) { + BoardPos pos = tetromino.GetPos(); + TetrominoId id = tetromino.GetId(); + uint16_t tetromino_bitmap[4]; + tetromino.GetBitmap(tetromino_bitmap); + + + // check if Tetromino cannot be placed (Game Over) + if (tetromino.IsCollisionWithBoard()) { + return -1; + } + + + // place in Board's Bitmap + m_Bitmap[pos.y+0] |= tetromino_bitmap[0]; + m_Bitmap[pos.y+1] |= tetromino_bitmap[1]; + m_Bitmap[pos.y+2] |= tetromino_bitmap[2]; + m_Bitmap[pos.y+3] |= tetromino_bitmap[3]; + + + // place in Board's Idmap + for (int32_t y = 0; y < 4; y++) { + for (int32_t x = 0; x < 4; x++) { + int32_t bitmap_x = 0x8000 >> (pos.x + x); + if (tetromino_bitmap[y] & bitmap_x) { + int32_t idmap_x = pos.x + x - 3; + int32_t idmap_y = pos.y + y - 2; + m_Idmap[idmap_y][idmap_x] = static_cast(id); + } + } + } + + int32_t rows_cleared = ClearRows(pos.y); + return rows_cleared; +} + +int32_t Board::ClearRows(int32_t y0) { + int32_t rows_cleared = 0; + int32_t y1 = y0 + 3; + + // ignore for y = {0,1}. Those bitmap rows are all 1's for collision testing + if (y0 < 2) { + y0 += 2 - y0; + } + + for (int32_t y = y0; y <= y1; y++) { + if (m_Bitmap[y] == 0xffff) { + rows_cleared++; + } + else { + m_Bitmap[y-rows_cleared] = m_Bitmap[y]; + std::copy(m_Idmap[y-2], m_Idmap[y-2] + 10, m_Idmap[y-2-rows_cleared]); + } + } + for (int32_t y = y1+1; y < 24; y++) { + m_Bitmap[y-rows_cleared] = m_Bitmap[y]; + std::copy(m_Idmap[y-2], m_Idmap[y-2] + 10, m_Idmap[y-2-rows_cleared]); + } + for (int32_t y = 24-rows_cleared; y < 24; y++) { + m_Bitmap[y] = 0xe007; + std::fill(m_Idmap[y-2], m_Idmap[y-2] + 10, (uint8_t)TetrominoId::TETROMINO_ID_NONE); + } + + + return rows_cleared; +} + +void Board::Draw(int32_t level, RenderGroup& render_group) { + float world_width = 4.0f; + float world_height = 3.0f; + float tetromino_size_with_border = world_height / 20.0f; + float tetromino_size = 0.8f * tetromino_size_with_border; + float tetromino_offset = 0.1f * tetromino_size_with_border; + V2F32 board_world_pos = { + (world_width - tetromino_size_with_border*10) / 2.0f, + 0.0f + }; + + + // background + V3F32 bg_world_pos = { + board_world_pos.x, + board_world_pos.y, + 0.0f + }; + V2F32 bg_world_dim = { + tetromino_size_with_border * 10, + tetromino_size_with_border * 20 + }; + V3F32 bg_color = {0.0f, 0.0f, 0.0f}; + render_group.PushRectangle(bg_world_pos, bg_world_dim, bg_color); + + + // tetromino parts + for (size_t y = 0; y < 20; y++) { + for (size_t x = 0; x < 10; x++) { + uint8_t tetromino_id = m_Idmap[y][x]; + if (tetromino_id < (uint8_t)TetrominoId::TETROMINO_ID_COUNT) { + V2F32 local_pos = { + (float)x * tetromino_size_with_border + tetromino_offset, + (float)y * tetromino_size_with_border + tetromino_offset + }; + V2F32 local_dim = {tetromino_size, tetromino_size}; + + + V3F32 world_pos = { + board_world_pos.x + local_pos.x, + board_world_pos.y + local_pos.y, + 1.0f + }; + V2F32 world_dim = local_dim; + + + V3F32 color = Tetromino::GetColor(static_cast(tetromino_id)); + render_group.PushRectangle(world_pos, world_dim, color); + } + } + } +} + diff --git a/src/games/tetris/Board.hpp b/src/games/tetris/Board.hpp new file mode 100644 index 0000000..0019a7f --- /dev/null +++ b/src/games/tetris/Board.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +class Tetromino; + +struct BoardPos { + int32_t x; + int32_t y; +}; + +class Board { +public: + Board(); + + int32_t PlaceTetromino(Tetromino &tetromino); + void Draw(int32_t level, RenderGroup& render_group); + + +private: + int32_t ClearRows(int32_t y0); + + +private: + friend class Tetromino; + uint16_t m_Bitmap[24]; + uint8_t m_Idmap[22][10]; +}; + diff --git a/src/games/tetris/Tetris.cpp b/src/games/tetris/Tetris.cpp new file mode 100644 index 0000000..e776fb0 --- /dev/null +++ b/src/games/tetris/Tetris.cpp @@ -0,0 +1,368 @@ +#include +#include +#include +#include +#include + +#include + +// Todo: change to new font scaling api in imgui first +// Todo: test text with hardcoded gap + dummy to ensure it gets placed as expected + +Tetris::Tetris() : + m_ActiveTetromino(m_Board), + m_NextTetromino(m_Board) +{ + m_TetrominoCounters[(size_t)m_ActiveTetromino.GetId()] += 1; +} + +void Tetris::Restart() { + m_RunningState = TetrisRunningState::Resume; + m_DtInSecondsRemaining = 0.0f; + m_MillisecondsSinceT0Last = SDL_GetTicks(); + + // Todo: Don't reconstruct! Make reset methods. + m_Board = Board(); + m_ActiveTetromino = Tetromino(m_Board); + m_NextTetromino = Tetromino(m_Board); + + memset(m_TetrominoCounters, 0, sizeof(m_TetrominoCounters)); + m_Score = 0; + m_LineCounter = 0; + m_StartingLevel = 0; + m_Level = 0; + m_SoftdropCounter = 0; +} + +bool Tetris::Update(std::vector &events, RenderGroup &render_group) { + V3F32 clear_color = V3F32(0.2f, 0.2f, 0.2f); + render_group.SetCameraSize(4.0f, 3.0f); + render_group.Clear(clear_color); + + if (m_RunningState == TetrisRunningState::Restart) { + Restart(); + } + + uint64_t milliseconds_since_t0 = SDL_GetTicks(); + uint64_t milliseconds_since_t0_last = m_MillisecondsSinceT0Last; + uint64_t milliseconds_dt = milliseconds_since_t0 - milliseconds_since_t0_last; + float seconds_dt = static_cast(milliseconds_dt) / 1000.0f; + m_MillisecondsSinceT0Last = milliseconds_since_t0; + + + if (m_RunningState == TetrisRunningState::Resume) { + uint32_t harddrop_count = GetHarddropCount(seconds_dt); + while (harddrop_count) { + bool moved_down = m_ActiveTetromino.MaybeMoveDown(); + if (!moved_down) { + HandleTetrominoPlacement(); + } + harddrop_count--; + } + } + + + for (auto &event : events) { + using enum TetrisRunningState; + switch (m_RunningState) { + case Resume: UpdateResumeState(event); break; + case Pause: UpdatePauseState(event); break; + default:; + } + } + + Draw(render_group); + + + bool keep_running = m_RunningState != TetrisRunningState::Exit; + return keep_running; +} + +void Tetris::UpdateResumeState(SDL_Event &event) { + switch (event.type) { + case SDL_EVENT_KEY_DOWN: { + auto key = event.key.key; + if (key == SDLK_RIGHT) { + m_ActiveTetromino.MaybeMoveHorizontally(TetrominoDirection::Right); + } else if (key == SDLK_LEFT) { + m_ActiveTetromino.MaybeMoveHorizontally(TetrominoDirection::Left); + } else if (key == SDLK_DOWN) { + bool moved_down = m_ActiveTetromino.MaybeMoveDown(); + if (!moved_down) { + HandleTetrominoPlacement(); + } + else { + m_SoftdropCounter++; + } + } else if (key == SDLK_X) { + m_ActiveTetromino.MaybeRotate(TetrominoRotation::Clockwise); + } else if (key == SDLK_Z || key == SDLK_Y) { + m_ActiveTetromino.MaybeRotate(TetrominoRotation::CounterClockwise); + } else if (key == SDLK_ESCAPE) { + m_RunningState = TetrisRunningState::Pause; + } + } + default:; + } +} + +void Tetris::UpdatePauseState(SDL_Event &event) { + switch (event.type) { + case SDL_EVENT_KEY_DOWN: { + auto key = event.key.key; + if (key == SDLK_ESCAPE) { + m_RunningState = TetrisRunningState::Resume; + } + } + default:; + } +} + +void Tetris::HandleTetrominoPlacement() { + int32_t rows_cleared = m_Board.PlaceTetromino(m_ActiveTetromino); + + m_ActiveTetromino = m_NextTetromino; + m_NextTetromino = Tetromino(m_Board); + + if (rows_cleared == -1) { + HandleGameOver(); + return; + } + + + m_LineCounter += rows_cleared; + m_TetrominoCounters[(size_t)m_ActiveTetromino.GetId()] += 1; + + if (rows_cleared == 1) { + m_Score += 40 * (m_Level + 1); + } + else if (rows_cleared == 2) { + m_Score += 100 * (m_Level + 1); + } + else if (rows_cleared == 3) { + m_Score += 300 * (m_Level + 1); + } + else if (rows_cleared == 4) { + m_Score += 1200 * (m_Level + 1); + } + + m_Score += m_SoftdropCounter; + m_SoftdropCounter = 0; + + m_Level = m_StartingLevel + m_LineCounter / 10; +} + +uint32_t Tetris::GetHarddropCount(float dt) { + float nes_frame_time = 1.0f / 60; + int32_t nes_frames_per_cell; + if (m_Level <= 8) nes_frames_per_cell = 48 - m_Level * 5; + else if (m_Level == 9) nes_frames_per_cell = 6; + else if (m_Level <= 12) nes_frames_per_cell = 5; + else if (m_Level <= 15) nes_frames_per_cell = 4; + else if (m_Level <= 18) nes_frames_per_cell = 3; + else if (m_Level <= 28) nes_frames_per_cell = 2; + else nes_frames_per_cell = 1; + + + float dt_level = static_cast(nes_frames_per_cell) * nes_frame_time; + float dt_total = m_DtInSecondsRemaining + dt; + + uint32_t harddrop_count = 0; + while (dt_total > dt_level) { + harddrop_count += 1; + dt_total -= dt_level; + } + + m_DtInSecondsRemaining = dt_total; + return harddrop_count; +} + +void Tetris::HandleGameOver() { + m_RunningState = TetrisRunningState::GameOver; + const char *filepath = "tetris_highscore.txt"; + int32_t highscore = 0; + + + std::ifstream highscore_file_in { filepath }; + if (highscore_file_in) { + highscore_file_in >> highscore; + highscore_file_in.close(); + } + else { + SDL_LogInfo(0, "Tetris: cannot open tetris_highscore.txt for reading"); + } + + if (highscore > 0 && highscore > m_HighScore) { + m_HighScore = highscore; + } + + + if (m_Score > m_HighScore) { + m_HighScore = m_Score; + std::ofstream highscore_file_out { filepath }; + if (highscore_file_out) { + highscore_file_out << m_HighScore << std::endl; + highscore_file_out.close(); + } + else { + SDL_LogInfo(0, "Tetris: cannot open tetris_highscore.txt for writing"); + } + } +} + +void Tetris::Draw(RenderGroup &render_group) { + m_Board.Draw(m_Level, render_group); + m_ActiveTetromino.Draw(render_group); + + DrawNextTetromino(render_group); + DrawStatistics(render_group); + DrawLineCounter(render_group); + DrawLevel(render_group); + DrawScore(render_group); + + // Todo: Use transparency + if (m_RunningState == TetrisRunningState::Pause) { + DrawPauseMenu(render_group); + } + else if (m_RunningState == TetrisRunningState::GameOver) { + DrawGameOverMenu(render_group); + } +} + +void Tetris::DrawPauseMenu(RenderGroup &render_group) { + ImGui::Begin("TetrisPause", nullptr, s_MenuImGuiWindowFlags); + if (ImGui::Button("Resume")) { + m_RunningState = TetrisRunningState::Resume; + } + if (ImGui::Button("Restart")) { + m_RunningState = TetrisRunningState::Restart; + } + if (ImGui::Button("Exit")) { + m_RunningState = TetrisRunningState::Exit; + } + ImGui::End(); +} + +void Tetris::DrawGameOverMenu(RenderGroup &render_group) { + ImGui::Begin("TetrisGameOver", nullptr, s_MenuImGuiWindowFlags); + ImGui::Text("Score = %d", m_Score); + ImGui::Text("HighScore = %d", m_HighScore); + if (ImGui::Button("Restart")) { + m_RunningState = TetrisRunningState::Restart; + } + if (ImGui::Button("Exit")) { + m_RunningState = TetrisRunningState::Exit; + } + ImGui::End(); +} + +void Tetris::DrawLineCounter(RenderGroup &render_group) { + V2F32 view_pos = {0.5f, 2.6f}; + ImVec2 screen_pos = render_group.ViewPosToScreenPosImGui(view_pos); + + ImGui::SetNextWindowPos(screen_pos); + ImGui::Begin("TetrisLines", nullptr, s_DefaultImGuiWindowFlags); + ImGui::Text("LINES - %d", m_LineCounter); + ImGui::End(); +} + +void Tetris::DrawStatistics(RenderGroup &render_group) { + V2F32 view_tetrominoes_pos = {0.4f, 1.8f}; + V2F32 view_advance = {0.0f, 0.2f}; + + V2F32 view_text_title_pos = view_tetrominoes_pos + V2F32(0.02f, 0.4f); + ImVec2 screen_text_title_pos = render_group.ViewPosToScreenPosImGui(view_text_title_pos); + + V2F32 view_text_pos = view_tetrominoes_pos + V2F32(0.4f, 0.16f); + V2F32 view_text_gap = {0.0f, 0.124f}; + ImVec2 screen_text_pos = render_group.ViewPosToScreenPosImGui(view_text_pos); + ImVec2 screen_text_gap = render_group.ViewSizeToScreenSizeImGui(view_text_gap); + + + using enum TetrominoId; + + Tetromino::Draw(TETROMINO_T, 0, view_tetrominoes_pos, 0.5f, render_group); + view_tetrominoes_pos.y -= view_advance.y; + + Tetromino::Draw(TETROMINO_J, 0, view_tetrominoes_pos, 0.5f, render_group); + view_tetrominoes_pos.y -= view_advance.y; + + Tetromino::Draw(TETROMINO_Z, 0, view_tetrominoes_pos, 0.5f, render_group); + view_tetrominoes_pos.y -= view_advance.y; + + Tetromino::Draw(TETROMINO_O, 0, view_tetrominoes_pos, 0.5f, render_group); + view_tetrominoes_pos.y -= view_advance.y; + + Tetromino::Draw(TETROMINO_S, 0, view_tetrominoes_pos, 0.5f, render_group); + view_tetrominoes_pos.y -= view_advance.y; + + Tetromino::Draw(TETROMINO_L, 0, view_tetrominoes_pos, 0.5f, render_group); + view_tetrominoes_pos.y -= view_advance.y; + + Tetromino::Draw(TETROMINO_I, 0, view_tetrominoes_pos, 0.5f, render_group); + view_tetrominoes_pos.y -= view_advance.y; + + + ImGui::SetNextWindowPos(screen_text_title_pos); + ImGui::Begin("TetrisStatisticsTitle", nullptr, s_DefaultImGuiWindowFlags); + ImGui::Text("STATISTICS"); + ImGui::End(); + + + ImGui::SetNextWindowPos(screen_text_pos); + ImGui::Begin("TetrisStatistics", nullptr, s_DefaultImGuiWindowFlags); + + ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_T]); + ImGui::Dummy(screen_text_gap); + ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_J]); + ImGui::Dummy(screen_text_gap); + ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_Z]); + ImGui::Dummy(screen_text_gap); + ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_O]); + ImGui::Dummy(screen_text_gap); + ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_S]); + ImGui::Dummy(screen_text_gap); + ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_L]); + ImGui::Dummy(screen_text_gap); + ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_I]); + ImGui::Dummy(screen_text_gap); + + ImGui::End(); +} + +void Tetris::DrawScore(RenderGroup &render_group) { + V2F32 view_pos = {3.0f, 2.2f}; + ImVec2 screen_pos = render_group.ViewPosToScreenPosImGui(view_pos); + + ImGui::SetNextWindowPos(screen_pos); + ImGui::Begin("TetrisScore", nullptr, s_DefaultImGuiWindowFlags); + ImGui::Text("Score"); + ImGui::Text("%d", m_Score); + ImGui::End(); +} + +void Tetris::DrawNextTetromino(RenderGroup &render_group) { + V2F32 text_view_pos = {3.0f, 1.8f}; + ImVec2 text_screen_pos = render_group.ViewPosToScreenPosImGui(text_view_pos); + + ImGui::SetNextWindowPos(text_screen_pos); + ImGui::Begin("TetrisNextTetromino", nullptr, s_DefaultImGuiWindowFlags); + ImGui::Text("Next"); + ImGui::End(); + + + V2F32 tetromino_view_pos = {3.0, 1.4f}; + Tetromino::Draw(m_NextTetromino.GetId(), 0, tetromino_view_pos, 0.5f, render_group); +} + +void Tetris::DrawLevel(RenderGroup &render_group) { + V2F32 view_pos = {3.0f, 1.2f}; + ImVec2 screen_pos = render_group.ViewPosToScreenPosImGui(view_pos); + + ImGui::SetNextWindowPos(screen_pos); + ImGui::Begin("TetrisLevel", nullptr, s_DefaultImGuiWindowFlags); + ImGui::Text("Level"); + ImGui::Text("%d", m_Level); + ImGui::End(); +} + diff --git a/src/games/tetris/Tetris.hpp b/src/games/tetris/Tetris.hpp new file mode 100644 index 0000000..30167d8 --- /dev/null +++ b/src/games/tetris/Tetris.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include +#include + +enum class TetrisRunningState { + Resume, + Pause, + GameOver, + Restart, + Exit +}; + + +class Tetris : public Game { +public: + Tetris(); + bool Update(std::vector &events, RenderGroup& render_group) override; + void HandleTetrominoPlacement(); + +private: + void Restart(); + void UpdateResumeState(SDL_Event &event); + void UpdatePauseState(SDL_Event &event); + + uint32_t GetHarddropCount(float dt); + void HandleGameOver(); + + void Draw(RenderGroup &render_group); + void DrawLineCounter(RenderGroup &render_group); + void DrawStatistics(RenderGroup &render_group); + void DrawScore(RenderGroup &render_group); + void DrawNextTetromino(RenderGroup &render_group); + void DrawLevel(RenderGroup &render_group); + + void DrawPauseMenu(RenderGroup &render_group); + void DrawGameOverMenu(RenderGroup &render_group); + +private: + static constexpr ImGuiWindowFlags s_MenuImGuiWindowFlags = ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_AlwaysAutoResize; + static constexpr ImGuiWindowFlags s_DefaultImGuiWindowFlags = ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoScrollbar; + + +private: + TetrisRunningState m_RunningState = TetrisRunningState::Resume; + + float m_DtInSecondsRemaining = 0.0f; + uint64_t m_MillisecondsSinceT0Last = SDL_GetTicks(); + + Board m_Board; + Tetromino m_ActiveTetromino; + Tetromino m_NextTetromino; + + int32_t m_TetrominoCounters[(size_t)TetrominoId::TETROMINO_ID_COUNT] {}; + int32_t m_Score = 0; + int32_t m_LineCounter = 0; + int32_t m_StartingLevel = 0; + int32_t m_Level = 0; + int32_t m_SoftdropCounter = 0; + + int32_t m_HighScore = 0; +}; + + diff --git a/src/games/tetris/Tetromino.cpp b/src/games/tetris/Tetromino.cpp new file mode 100644 index 0000000..577a869 --- /dev/null +++ b/src/games/tetris/Tetromino.cpp @@ -0,0 +1,212 @@ +#include +#include +#include + +// layout of a left_aligned_bitmap: xxxx000000000000 +// layout of a board_bitmap is 111xxxxxxxxxx111 +static const uint16_t s_left_aligned_bitmaps[7][4][4] = { + { + // O piece + {0x6000, 0x6000, 0x0000, 0x0000}, // orientation 0 + {0x6000, 0x6000, 0x0000, 0x0000}, // orientation 1 + {0x6000, 0x6000, 0x0000, 0x0000}, // orientation 2 + {0x6000, 0x6000, 0x0000, 0x0000}, // orientation 3 + }, + { + // S piece + {0x6000, 0x3000, 0x0000, 0x0000}, // orientation 0 + {0x1000, 0x3000, 0x2000, 0x0000}, // orientation 1 + {0x6000, 0x3000, 0x0000, 0x0000}, // orientation 2 + {0x1000, 0x3000, 0x2000, 0x0000}, // orientation 3 + }, + { + // Z piece + {0x3000, 0x6000, 0x0000, 0x0000}, // orientation 0 + {0x2000, 0x3000, 0x1000, 0x0000}, // orientation 1 + {0x3000, 0x6000, 0x0000, 0x0000}, // orientation 2 + {0x2000, 0x3000, 0x1000, 0x0000}, // orientation 3 + }, + { + // T piece + {0x2000, 0x7000, 0x0000, 0x0000}, // orientation 0 + {0x2000, 0x6000, 0x2000, 0x0000}, // orientation 1 + {0x0000, 0x7000, 0x2000, 0x0000}, // orientation 2 + {0x2000, 0x3000, 0x2000, 0x0000}, // orientation 3 + }, + { + // L piece + {0x4000, 0x7000, 0x0000, 0x0000}, // orientation 0 + {0x2000, 0x2000, 0x6000, 0x0000}, // orientation 1 + {0x0000, 0x7000, 0x1000, 0x0000}, // orientation 2 + {0x3000, 0x2000, 0x2000, 0x0000}, // orientation 3 + }, + { + // J piece + {0x1000, 0x7000, 0x0000, 0x0000}, // orientation 0 + {0x6000, 0x2000, 0x2000, 0x0000}, // orientation 1 + {0x0000, 0x7000, 0x4000, 0x0000}, // orientation 2 + {0x2000, 0x2000, 0x3000, 0x0000}, // orientation 3 + }, + { + // I piece + {0x0000, 0xf000, 0x0000, 0x0000}, // orientation 0 + {0x2000, 0x2000, 0x2000, 0x2000}, // orientation 1 + {0x0000, 0xf000, 0x0000, 0x0000}, // orientation 2 + {0x2000, 0x2000, 0x2000, 0x2000}, // orientation 3 + } +}; + + +TetrominoId Tetromino::GetRandomId() { + static std::uniform_int_distribution s_Dist(0, (int)TetrominoId::TETROMINO_ID_COUNT-1); + static std::mt19937 s_Rng((std::random_device()())); + TetrominoId id = static_cast(s_Dist(s_Rng)); + return id; +} + +Tetromino::Tetromino(uint16_t *board_bitmap) : + m_Id(GetRandomId()), + m_Pos{6, 20}, + m_Ori{0}, + m_BoardBitmap(board_bitmap) +{ +} + +Tetromino::Tetromino(Board &board) : + Tetromino(board.m_Bitmap) +{ +} + +TetrominoId Tetromino::GetId() { + return m_Id; +} + +BoardPos Tetromino::GetPos() { + return m_Pos; +} + +void Tetromino::GetBitmap(uint16_t *bitmap) { + GetBitmap(m_Id, m_Pos, m_Ori, bitmap); +} + +bool Tetromino::IsCollisionWithBoard() { + bool is_collision = IsCollisionWithBoard(m_Id, m_Pos, m_Ori, m_BoardBitmap); + return is_collision; +} + +void Tetromino::MaybeRotate(TetrominoRotation rotation) { + int32_t rot = static_cast(rotation); + int32_t ori = (m_Ori + rot) % 4; + if (!IsCollisionWithBoard(m_Id, m_Pos, ori, m_BoardBitmap)) { + m_Ori = ori; + } +} + +void Tetromino::MaybeMoveHorizontally(TetrominoDirection direction) { + BoardPos pos = m_Pos; + pos.x += static_cast(direction); + if (!IsCollisionWithBoard(m_Id, pos, m_Ori, m_BoardBitmap)) { + m_Pos.x = pos.x; + } +} + +bool Tetromino::MaybeMoveDown() { + BoardPos pos = m_Pos; + pos.y -= 1; + if (!IsCollisionWithBoard(m_Id, pos, m_Ori, m_BoardBitmap)) { + m_Pos.y = pos.y; + return true; + } + return false; +} + +void Tetromino::Draw(RenderGroup &render_group) const { + float world_width = 4.0f; + float world_height = 3.0f; + float tetromino_size_with_border = world_height / 20.0f; + + float x0 = static_cast(m_Pos.x - 3); + float y0 = static_cast(m_Pos.y - 2); + + V2F32 world_pos = { + ((world_width - tetromino_size_with_border*10) / 2.0f) + x0 * tetromino_size_with_border, + y0 * tetromino_size_with_border + }; + + Tetromino::Draw(m_Id, m_Ori, world_pos, 1.0f, render_group); +} + +bool Tetromino::IsCollisionWithBoard(TetrominoId id, BoardPos pos, int32_t ori, uint16_t *board_bitmap) { + uint16_t tetromino_bitmap[16]; + GetBitmap(id, pos, ori, tetromino_bitmap); + + uint64_t tetromino_bits = *(uint64_t*)(tetromino_bitmap); + uint64_t board_bits = *(uint64_t*)(&board_bitmap[pos.y]); + bool is_collision = tetromino_bits & board_bits; + return is_collision; +} + +void Tetromino::GetBitmap(TetrominoId id, BoardPos pos, int32_t ori, uint16_t *bitmap) { + size_t id_ = static_cast(id); + uint64_t *src = (uint64_t*)s_left_aligned_bitmaps[id_][ori]; + uint64_t *dest = (uint64_t*)bitmap; + *dest = *src >> pos.x; +} + +V3F32 Tetromino::GetColor(TetrominoId id) { + using enum TetrominoId; + + V3F32 color; + switch (id) { + case TETROMINO_I: + case TETROMINO_O: + case TETROMINO_T: { + color = V3F32(0.8f, 0.8f, 0.8f); + } break; + + case TETROMINO_J: + case TETROMINO_S: { + color = V3F32(0.8f, 0.2f, 0.2f); + } break; + + default: { + color = V3F32(0.2f, 0.4f, 0.2f); + } + } + return color; +} + +void Tetromino::Draw(TetrominoId id, int32_t ori, V2F32 pos, float scale, RenderGroup &render_group) { + int32_t id_ = static_cast(id); + + float world_height = 3.0f; + float tetromino_size_with_border = scale * world_height / 20.0f; + float tetromino_size = 0.8f * tetromino_size_with_border; + float tetromino_offset = 0.1f * tetromino_size_with_border; + + uint16_t *left_aligned_bitmap = (uint16_t*)s_left_aligned_bitmaps[id_][ori]; + for (int y = 0; y < 4; y++) { + for (int x = 0; x < 4; x++) { + if (left_aligned_bitmap[y] & (0x8000 >> x)) { + V2F32 local_pos = { + (float)x * tetromino_size_with_border + tetromino_offset, + (float)y * tetromino_size_with_border + tetromino_offset + }; + V2F32 local_dim = {tetromino_size, tetromino_size}; + + + V3F32 world_pos = { + pos.x + local_pos.x, + pos.y + local_pos.y, + 1.0f + }; + V2F32 world_dim = local_dim; + + + V3F32 color = GetColor(id); + render_group.PushRectangle(world_pos, world_dim, color); + } + } + } +} + diff --git a/src/games/tetris/Tetromino.hpp b/src/games/tetris/Tetromino.hpp new file mode 100644 index 0000000..c3ceeff --- /dev/null +++ b/src/games/tetris/Tetromino.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include + +enum class TetrominoId : uint8_t { + TETROMINO_O = 0, + TETROMINO_S, + TETROMINO_Z, + TETROMINO_T, + TETROMINO_L, + TETROMINO_J, + TETROMINO_I, + TETROMINO_ID_COUNT, + TETROMINO_ID_NONE, +}; +enum class TetrominoRotation {Clockwise = 1, CounterClockwise = 3}; +enum class TetrominoDirection {Left = -1, Right = 1}; + + +class Tetromino { +public: + Tetromino() = delete; + Tetromino(Board &board); + Tetromino(uint16_t *board_bitmap); + + TetrominoId GetId(); + BoardPos GetPos(); + int32_t GetOri(); + void GetBitmap(uint16_t *bitmap); + bool IsCollisionWithBoard(); // for last tetromino to check game over + + bool MaybeMoveDown(); + void MaybeMoveHorizontally(TetrominoDirection direction); + void MaybeRotate(TetrominoRotation rotation); + + void Draw(RenderGroup &render_group) const; + + +public: + static bool IsCollisionWithBoard(TetrominoId id, BoardPos pos, int32_t ori, uint16_t *board_bitmap); + static void GetBitmap(TetrominoId id, BoardPos pos, int32_t ori, uint16_t *bitmap); + static V3F32 GetColor(TetrominoId id); + static void Draw(TetrominoId id, int32_t ori, V2F32 pos, float scale, RenderGroup &render_group); + + +private: + static TetrominoId GetRandomId(); + + +private: + TetrominoId m_Id; + BoardPos m_Pos; + int32_t m_Ori; + uint16_t *m_BoardBitmap; +}; + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..0a632fa --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,191 @@ +#include "imgui_internal.h" +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + + +Game::GameType +do_menu(RenderGroup &render_group) +{ + Game::GameType type = Game::NO_GAME; + + //ImGuiWindowFlags flags = ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; + ImGuiWindowFlags flags = 0; + + ImGui::Begin("Game Selection", nullptr, flags); + if (ImGui::Button("Tetris")) { + type = Game::TETRIS; + } + if (ImGui::Button("Snake")) { + type = Game::SNAKE; + } + if (ImGui::Button("Minesweeper")) { + type = Game::MINESWEEPER; + } + ImGui::End(); + + + V3F32 clear_color = V3F32(0.4f, 0.4f, 0.4f); + render_group.SetCameraSize(4.0f, 3.0f); + render_group.Clear(clear_color); + + + return type; +} + + +int +main(int argc, char **argv) +{ + if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD)) { + std::cerr << "Failed to init SDL3: " << SDL_GetError() << '\n'; + return EXIT_FAILURE; + } + + const char* glsl_version = "#version 130"; + SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); + + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); + SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); + + SDL_Window *window = SDL_CreateWindow("fsarcade", 1024, 768, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN | SDL_WINDOW_HIGH_PIXEL_DENSITY); + if (!window) { + std::cerr << "Failed to create SDL_window: " << SDL_GetError() << '\n'; + return EXIT_FAILURE; + } + + SDL_GLContext sdl_gl_context = SDL_GL_CreateContext(window); + if (!sdl_gl_context) { + std::cerr << "Failed to create SDL_GLContext: " << SDL_GetError() << '\n'; + SDL_DestroyWindow(window); + return EXIT_FAILURE; + } + + SDL_GL_MakeCurrent(window, sdl_gl_context); + SDL_GL_SetSwapInterval(1); // enable vsync + SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); + SDL_ShowWindow(window); + + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.IniFilename = NULL; + io.LogFilename = NULL; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + + ImGui::StyleColorsDark(); + ImGui_ImplSDL3_InitForOpenGL(window, sdl_gl_context); + ImGui_ImplOpenGL3_Init(glsl_version); + + + std::unique_ptr game = nullptr; + std::unique_ptr renderer = Renderer::Select(Renderer::API_OPENGL, window); + if (!renderer->Init()) { + return EXIT_FAILURE; + } + + RenderGroup render_group; + SDL_GetWindowSize(window, &render_group.m_ScreenWidth, &render_group.m_ScreenHeight); + + + std::vector game_events; + game_events.reserve(32); + for (;;) { + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplSDL3_NewFrame(); + ImGui::NewFrame(); + + + size_t cur_game_events = 0; + size_t max_game_events = game_events.max_size(); + + SDL_Event event; + while (cur_game_events < max_game_events && SDL_PollEvent(&event)) { + if (event.type == SDL_EVENT_KEY_DOWN || + event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) + { + game_events.emplace_back(event); + cur_game_events++; + } + ImGui_ImplSDL3_ProcessEvent(&event); + + if (event.type == SDL_EVENT_QUIT) + goto QUIT; + if (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED && event.window.windowID == SDL_GetWindowID(window)) + goto QUIT; + if (event.type == SDL_EVENT_WINDOW_DESTROYED && event.window.windowID == SDL_GetWindowID(window)) { + goto QUIT; + } + } + + + int w, h; + SDL_GetWindowSize(window, &w, &h); + render_group.m_ScreenWidth = w; + render_group.m_ScreenHeight = h; + + + if (game) { + bool keep_game_running = game->Update(game_events, render_group); + if (!keep_game_running) { + game.reset(); + } + } + else { + Game::GameType type = do_menu(render_group); + if (type != Game::NO_GAME) { + game = Game::Select(type); + } + } + game_events.clear(); + + + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; + + ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x - 200, 0)); + ImGui::Begin("Performance", nullptr, flags); + ImGui::Text("%.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate); + ImGui::End(); + + + renderer->Draw(render_group); + ImGui::Render(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + renderer->Present(); + render_group.Reset(); + } + +QUIT: + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplSDL3_Shutdown(); + ImGui::DestroyContext(); + + SDL_GL_DestroyContext(sdl_gl_context); + SDL_DestroyWindow(window); + SDL_Quit(); + return 0; +} + diff --git a/src/renderer/RenderGroup.cpp b/src/renderer/RenderGroup.cpp new file mode 100644 index 0000000..56e7ec5 --- /dev/null +++ b/src/renderer/RenderGroup.cpp @@ -0,0 +1,94 @@ +#include +#include + +RSortEntry::RSortEntry(float z, size_t entity_index) + : z(z), entity_index(entity_index) { +} + +RenderGroup::RenderGroup() { + m_REntities.reserve(1024); + m_RSortEntries.reserve(1024); +} + +void RenderGroup::Reset() { + m_CameraWidth = 0; + m_CameraHeight = 0; + m_REntities.clear(); + m_RSortEntries.clear(); + m_REntities.reserve(1024); + m_RSortEntries.reserve(1024); +} + +void RenderGroup::SetCameraSize(float width, float height) { + m_CameraWidth = width; + m_CameraHeight = height; +} + +float RenderGroup::GetScale() { + float screen_width = static_cast(m_ScreenWidth); + float screen_height = static_cast(m_ScreenHeight); + float xunits = screen_width / m_CameraWidth; + float yunits = screen_height / m_CameraHeight; + float scale = std::min(xunits, yunits); + return scale; +} + +V2F32 RenderGroup::ViewPosToScreenPos(V2F32 view_pos) { + float scale = GetScale(); + float screen_width = static_cast(m_ScreenWidth); + float screen_height = static_cast(m_ScreenHeight); + float viewport_width = m_CameraWidth * scale; + float viewport_height = m_CameraHeight * scale; + float viewport_x0 = (screen_width - viewport_width) / 2; + float viewport_y0 = (screen_height - viewport_height) / 2; + + V2F32 result; + result.x = viewport_x0 + view_pos.x * scale; + result.y = screen_height - (viewport_y0 + view_pos.y * scale); + + return result; +} + +V2F32 RenderGroup::ViewSizeToScreenSize(V2F32 view_size) { + float scale = GetScale(); + + V2F32 result; + result.x = view_size.x * scale; + result.y = view_size.y * scale; + + return result; +} + +ImVec2 RenderGroup::ViewPosToScreenPosImGui(V2F32 view_pos) { + V2F32 screen_pos = ViewPosToScreenPos(view_pos); + ImVec2 result = {screen_pos.x, screen_pos.y}; + return result; +} + +ImVec2 RenderGroup::ViewSizeToScreenSizeImGui(V2F32 view_size) { + V2F32 screen_size = ViewSizeToScreenSize(view_size); + ImVec2 result = {screen_size.x, screen_size.y}; + return result; +} + +void RenderGroup::Clear(V3F32 color) { + m_ClearColor = color; +} + +void RenderGroup::PushRectangle(V3F32 pos, V2F32 dim, V3F32 color) { + m_REntities.emplace_back(REntity{.rect{REntityType_Rectangle, pos, dim, color}}); + m_RSortEntries.emplace_back(pos.z, m_REntities.size()-1); +} + +void RenderGroup::PushBitmap(V3F32 pos, int w, int h, void *data) { + m_REntities.emplace_back(REntity{.bitmap{REntityType_Bitmap, pos, w, h, data}}); + m_RSortEntries.emplace_back(pos.z, m_REntities.size()-1); +} + +void RenderGroup::Sort() { + std::sort(m_RSortEntries.begin(), m_RSortEntries.end(), + [](const RSortEntry& e1, const RSortEntry& e2) { + return e1.z < e2.z; + }); +} + diff --git a/src/renderer/RenderGroup.hpp b/src/renderer/RenderGroup.hpp new file mode 100644 index 0000000..4904220 --- /dev/null +++ b/src/renderer/RenderGroup.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include +#include + +enum REntityType : int32_t { + REntityType_Rectangle, + REntityType_Bitmap, +}; + +struct REntity_Rectangle { + REntityType type; + V3F32 pos; + V2F32 dim; + V3F32 color; +}; + +struct REntity_Bitmap { + REntityType type; + V3F32 pos; + int32_t width; + int32_t height; + void *data; +}; + +union REntity { + REntityType type; + REntity_Rectangle rect; + REntity_Bitmap bitmap; +}; + +struct RSortEntry { + RSortEntry(float z, size_t entity_index); + float z; + size_t entity_index; +}; + + +class RenderGroup { +public: + RenderGroup(); + void Clear(V3F32 color); + void Reset(); + + void SetCameraSize(float width, float height); + V2F32 ViewPosToScreenPos(V2F32 view_pos); + V2F32 ViewSizeToScreenSize(V2F32 view_size); + ImVec2 ViewPosToScreenPosImGui(V2F32 view_pos); + ImVec2 ViewSizeToScreenSizeImGui(V2F32 view_size); + float GetScale(); + + +public: + void PushRectangle(V3F32 pos, V2F32 dim, V3F32 color); + void PushBitmap(V3F32 pos, int width, int height, void *bitmap); + + +private: + void Sort(); + + +public: + int32_t m_ScreenWidth; + int32_t m_ScreenHeight; + + +private: + friend class GlRenderer; + + + float m_CameraWidth; + float m_CameraHeight; + + + V3F32 m_ClearColor; + + std::vector m_REntities; + std::vector m_RSortEntries; +}; + + diff --git a/src/renderer/Renderer.cpp b/src/renderer/Renderer.cpp new file mode 100644 index 0000000..13f5feb --- /dev/null +++ b/src/renderer/Renderer.cpp @@ -0,0 +1,18 @@ +#include +#include + + +std::unique_ptr +Renderer::Select(Api api, SDL_Window *window) { + switch (api) { + case API_OPENGL: { + return std::make_unique(window); + } + InvalidDefaultCase; + } + + return nullptr; +} + + +Renderer::~Renderer() {} diff --git a/src/renderer/Renderer.hpp b/src/renderer/Renderer.hpp new file mode 100644 index 0000000..a3e0f98 --- /dev/null +++ b/src/renderer/Renderer.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include + +#include + +class Renderer { +public: + enum Api { + API_OPENGL + }; + + Renderer() = default; + static std::unique_ptr Select(Api api, SDL_Window *window); + + +public: + virtual ~Renderer() = 0; + virtual bool Init() = 0; + virtual void Draw(RenderGroup &render_group) = 0; + virtual void Present() = 0; +}; + diff --git a/src/renderer/opengl/GlIndexBuffer.cpp b/src/renderer/opengl/GlIndexBuffer.cpp new file mode 100644 index 0000000..c5bd713 --- /dev/null +++ b/src/renderer/opengl/GlIndexBuffer.cpp @@ -0,0 +1,44 @@ +#include + +#include +#include +#include +#include + +void GlIndexBuffer::Init() { + m_CurrentIndex = 0; + m_Indices.reserve(16384); + glGenBuffers(1, &m_Id); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_Id); +} + +uint32_t *GlIndexBuffer::GetData() { + return m_Indices.data(); +} + +uint32_t GlIndexBuffer::GetCount() { + uint32_t count = static_cast(m_Indices.size()); + return count; +} + +void GlIndexBuffer::Reset() { + m_CurrentIndex = 0; + m_Indices.clear(); +} + +void GlIndexBuffer::PushRectangle() { + uint32_t current_index = m_CurrentIndex; + m_Indices.push_back(current_index + 0); + m_Indices.push_back(current_index + 1); + m_Indices.push_back(current_index + 2); + m_Indices.push_back(current_index + 0); + m_Indices.push_back(current_index + 2); + m_Indices.push_back(current_index + 3); + m_CurrentIndex += 4; +} + +void GlIndexBuffer::TransferData() { + size_t size = m_Indices.size() * sizeof(uint32_t); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, size, m_Indices.data(), GL_STATIC_DRAW); +} + diff --git a/src/renderer/opengl/GlIndexBuffer.hpp b/src/renderer/opengl/GlIndexBuffer.hpp new file mode 100644 index 0000000..7805460 --- /dev/null +++ b/src/renderer/opengl/GlIndexBuffer.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +class GlIndexBuffer { +public: + GlIndexBuffer() = default; + void Init(); + void Reset(); + +public: + uint32_t GetCount(); + uint32_t *GetData(); + + void PushRectangle(); + void TransferData(); + +private: + uint32_t m_Id; + uint32_t m_CurrentIndex; + std::vector m_Indices; +}; + diff --git a/src/renderer/opengl/GlRenderer.cpp b/src/renderer/opengl/GlRenderer.cpp new file mode 100644 index 0000000..5d7c985 --- /dev/null +++ b/src/renderer/opengl/GlRenderer.cpp @@ -0,0 +1,220 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +void GLAPIENTRY +MessageCallback(GLenum source, + GLenum type, + GLuint id, + GLenum severity, + GLsizei length, + const GLchar* message, + const void* userParam) +{ + fprintf(stderr, "GL CALLBACK: %s, type = 0x%x, severity = 0x%x, message = %s\n", + type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "", + type, + severity, + message + ); +} + +GlRenderer::~GlRenderer() { + SDL_GL_DestroyContext(m_Context); +} + +GlRenderer::GlRenderer(SDL_Window *window) + : m_Window(window), m_Context(nullptr) { +} + +bool GlRenderer::Init() { + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); + SDL_GLContext sdl_gl_context = SDL_GL_CreateContext(m_Window); + if (!sdl_gl_context) { + return false; + } + + GLenum err = glewInit(); + if (GLEW_OK != err) + { + fprintf(stderr, "Error: %s\n", glewGetErrorString(err)); + SDL_GL_DestroyContext(sdl_gl_context); + return false; + } + +#if 0 + glEnable(GL_DEBUG_OUTPUT); + glDebugMessageCallback(MessageCallback, 0); +#endif + + GLuint vertex_array; + glGenVertexArrays(1, &vertex_array); + glBindVertexArray(vertex_array); + + + // init rectangle rendering + if (!m_RectangleShader.InitProgram()) { + return false; + } + m_RectangleVertexBuffer.Init(); + m_RectangleIndexBuffer.Init(); + + + // init text rendering + if (!m_TextShader.InitProgram()) { + return false; + } + m_TextVertexBuffer.Init(); + m_TextIndexBuffer.Init(); + + + m_Context = sdl_gl_context; + return true; +} + +void GlRenderer::Draw(RenderGroup& render_group) { + render_group.Sort(); + + float camera_width = render_group.m_CameraWidth; + float camera_height = render_group.m_CameraHeight; + + + glViewport(0, 0, render_group.m_ScreenWidth, render_group.m_ScreenHeight); + glClearColor(render_group.m_ClearColor.x, + render_group.m_ClearColor.y, + render_group.m_ClearColor.z, + 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + + // viewport space + float scale = render_group.GetScale(); + int32_t viewport_width = static_cast(camera_width * scale); + int32_t viewport_height = static_cast(camera_height * scale); + int32_t viewport_x0 = (render_group.m_ScreenWidth - viewport_width) / 2; + int32_t viewport_y0 = (render_group.m_ScreenHeight - viewport_height) / 2; + glViewport(viewport_x0, viewport_y0, viewport_width, viewport_height); + + + // draw batches + float last_z = -1; + for (auto [z, entity_index] : render_group.m_RSortEntries) { + REntity& render_entity = render_group.m_REntities[entity_index]; + switch (render_entity.type) { + case REntityType_Rectangle: { + // clip space (from camera space to [-1, 1] for pos and [0, 2] for dim) + V3F32 pos = render_entity.rect.pos; + V2F32 dim = render_entity.rect.dim; + pos.x = 2*(pos.x / camera_width) - 1; + pos.y = 2*(pos.y / camera_height) - 1; + dim.x = 2*(dim.x / camera_width); + dim.y = 2*(dim.y / camera_height); + + if (render_entity.rect.pos.z > last_z) { + DrawBatch(); + last_z = z; + } + + m_RectangleVertexBuffer.PushRectangle(pos, dim, render_entity.rect.color); + m_RectangleIndexBuffer.PushRectangle(); + } + + case REntityType_Bitmap: { + REntity_Bitmap& bitmap = render_entity.bitmap; + + // clip space (from camera space to [-1, 1] for pos and [0, 2] for dim) + V3F32 pos = render_entity.bitmap.pos; + pos.x = 2*(pos.x / camera_width) - 1; + pos.y = 2*(pos.y / camera_height) - 1; + +#if 0 + + // create setup texture + unsigned int texture_id; + glGenTextures(1, &texture_id); + glBindTexture(GL_TEXTURE_2D, texture_id); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, bitmap.width, bitmap.height, 0, GL_RGB, GL_UNSIGNED_BYTE, bitmap.data); + + glGenerateMipmap(GL_TEXTURE_2D); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + + // apply texture + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); + glEnableVertexAttribArray(2); + + // delete texture + glDeleteTextures(1, &texture_id); + + + + // 1) set vertex buffer + // 2) set texture coordinates + pos.x = 2*(pos.x / camera_width) - 1; + pos.y = 2*(pos.y / camera_height) - 1; + dim.x = 2*(dim.x / camera_width); + dim.y = 2*(dim.y / camera_height); + if (render_entity.rect.pos.z > last_z) { + DrawBatch(); + last_z = z; + } +#endif + + + } break; + + InvalidDefaultCase; + } + } + + DrawBatch(); +} + +void GlRenderer::Present() { + SDL_GL_SwapWindow(m_Window); +} + +void GlRenderer::DrawBatch() { + int32_t rectangle_index_count = static_cast(m_RectangleIndexBuffer.GetCount()); + int32_t text_index_count = static_cast(m_TextIndexBuffer.GetCount()); + + if (rectangle_index_count) { + m_RectangleVertexBuffer.TransferData(); + m_RectangleIndexBuffer.TransferData(); + m_RectangleShader.UseProgram(); + + glDrawElements(GL_TRIANGLES, rectangle_index_count, GL_UNSIGNED_INT, 0); + + m_RectangleVertexBuffer.Reset(); + m_RectangleIndexBuffer.Reset(); + } + + if (text_index_count) { + m_TextVertexBuffer.TransferData(); + m_TextIndexBuffer.TransferData(); + m_TextShader.UseProgram(); + + glDrawElements(GL_TRIANGLES, text_index_count, GL_UNSIGNED_INT, 0); + + m_TextVertexBuffer.Reset(); + m_TextIndexBuffer.Reset(); + } +} + + diff --git a/src/renderer/opengl/GlRenderer.hpp b/src/renderer/opengl/GlRenderer.hpp new file mode 100644 index 0000000..d59ba45 --- /dev/null +++ b/src/renderer/opengl/GlRenderer.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include + +class GlRenderer : public Renderer { +public: + GlRenderer(SDL_Window *window); + ~GlRenderer() override; + + bool Init() override; + void Draw(RenderGroup& render_group) override; + void Present() override; + + void InitTexture(); + +private: + void DrawBatch(); + +private: + SDL_Window *m_Window; + SDL_GLContext m_Context; + + GlVertexBuffer m_RectangleVertexBuffer; + GlIndexBuffer m_RectangleIndexBuffer; + GlShader m_RectangleShader; + + GlVertexBuffer m_TextVertexBuffer; + GlVertexBuffer m_TextIndexBuffer; + GlShader m_TextShader; +}; + diff --git a/src/renderer/opengl/GlShader.cpp b/src/renderer/opengl/GlShader.cpp new file mode 100644 index 0000000..cf7c6c5 --- /dev/null +++ b/src/renderer/opengl/GlShader.cpp @@ -0,0 +1,80 @@ +#include +#include +#include + +static const char *s_vertex_shader_src = +"#version 330 core\n" +"layout (location = 0) in vec3 aPos;\n" +"layout (location = 1) in vec3 aColor;\n" +"\n" +"out vec4 vertexColor;\n" +"\n" +"void main()\n" +"{\n" +" gl_Position = vec4(aPos.xy, 0.0, 1.0);\n" +" vertexColor = vec4(aColor, 1.0);\n" +"}\n" +; + +static const char *s_fragment_shader_src = +"#version 330 core\n" +"in vec4 vertexColor;\n" +"\n" +"out vec4 FragColor;\n" +"\n" +"void main()\n" +"{\n" +" FragColor = vertexColor;\n" +"}\n" +; + +bool GlShader::InitProgram() { + GLuint vertex_shader; + GLuint fragment_shader; + if (!CompileShader(&vertex_shader, s_vertex_shader_src, GL_VERTEX_SHADER) || + !CompileShader(&fragment_shader, s_fragment_shader_src, GL_FRAGMENT_SHADER)) { + return false; + } + + GLuint program = glCreateProgram(); + glAttachShader(program, vertex_shader); + glAttachShader(program, fragment_shader); + glLinkProgram(program); + + glDeleteShader(vertex_shader); + glDeleteShader(fragment_shader); + + int success; + char info_log[512]; + glGetProgramiv(program, GL_LINK_STATUS, &success); + if (!success) { + printf("error linking shader program\n%s\n", info_log); + return false; + } + + m_ProgramId = program; + return true; +} + +void GlShader::UseProgram() { + glUseProgram(m_ProgramId); +} + +bool GlShader::CompileShader(GLuint *id, const char *src, GLenum type) { + GLuint shader = glCreateShader(type); + glShaderSource(shader, 1, &src, 0); + glCompileShader(shader); + + int success; + char info_log[512]; + glGetShaderiv(shader, GL_COMPILE_STATUS, &success); + if(!success) { + glGetShaderInfoLog(shader, 512, 0, info_log); + printf("error %s shader compilation failed\n%s\n", type == GL_VERTEX_SHADER ? "vertex" : "fragment", info_log); + glDeleteShader(shader); + return false; + } + + *id = shader; + return true; +} diff --git a/src/renderer/opengl/GlShader.hpp b/src/renderer/opengl/GlShader.hpp new file mode 100644 index 0000000..d195a8a --- /dev/null +++ b/src/renderer/opengl/GlShader.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +class GlShader { +public: + GlShader() = default; + bool InitProgram(); + void UseProgram(); +private: + bool CompileShader(GLuint *id, const char *src, GLenum type); + +private: + GLuint m_ProgramId; +}; diff --git a/src/renderer/opengl/GlVertexBuffer.cpp b/src/renderer/opengl/GlVertexBuffer.cpp new file mode 100644 index 0000000..f716c50 --- /dev/null +++ b/src/renderer/opengl/GlVertexBuffer.cpp @@ -0,0 +1,54 @@ +#include + +#include +#include + +#include + +void GlVertexBuffer::Init() { + m_Vertices.reserve(16384); + glGenBuffers(1, &m_Id); + glBindBuffer(GL_ARRAY_BUFFER, m_Id); +} + +void GlVertexBuffer::Reset() { + glDisableVertexAttribArray(0); + glDisableVertexAttribArray(1); + m_Vertices.clear(); +} + +float *GlVertexBuffer::GetData() { + return reinterpret_cast(m_Vertices.data()); +} + +uint32_t GlVertexBuffer::GetCount() { + return static_cast(m_Vertices.size()); +} + +void GlVertexBuffer::PushRectangle(V3F32 pos, V2F32 dim, V3F32 color) { + V3F32 positions[4] = { + V3F32(pos.x, pos.y, pos.z), + V3F32(pos.x + dim.x, pos.y, pos.z), + V3F32(pos.x + dim.x, pos.y + dim.y, pos.z), + V3F32(pos.x, pos.y + dim.y, pos.z), + }; + + for (int i = 0; i < 4; i++) { + GlVertex vertex = {}; + vertex.pos = positions[i]; + vertex.color = color; + m_Vertices.push_back(vertex); + } +} + +void GlVertexBuffer::TransferData() { + size_t size = m_Vertices.size() * sizeof(GlVertex); + GLsizei stride = sizeof(GlVertex); + const void *offset_color = (const void*)(3*sizeof(float)); + glEnableVertexAttribArray(0); + glEnableVertexAttribArray(1); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, 0); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, offset_color); + glBufferData(GL_ARRAY_BUFFER, size, m_Vertices.data(), GL_STATIC_DRAW); +} + diff --git a/src/renderer/opengl/GlVertexBuffer.hpp b/src/renderer/opengl/GlVertexBuffer.hpp new file mode 100644 index 0000000..4099ca2 --- /dev/null +++ b/src/renderer/opengl/GlVertexBuffer.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +struct GlVertex { + V3F32 pos; + V3F32 color; +}; + +class GlVertexBuffer { +public: + GlVertexBuffer() = default; + void Init(); + void Reset(); + +public: + float *GetData(); + uint32_t GetCount(); + + void PushRectangle(V3F32 pos, V2F32 dim, V3F32 color); + void TransferData(); + +private: + uint32_t m_Id; + std::vector m_Vertices; +}; + -- cgit v1.2.3