MXVK Vulkan Framework 0.35.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1#include "mxvk/argz.hpp"
2#include "mxvk/mxvk.hpp"
4
5#include <SDL3/SDL.h>
6
7#include <algorithm>
8#include <array>
9#include <cmath>
10#include <cstdint>
11#include <cstdlib>
12#include <format>
13#include <iostream>
14#include <random>
15#include <string>
16#include <utility>
17#include <vector>
18
19namespace example {
21 public:
22 TicTacToeWindow(const std::string &assetPath, int width, int height, bool fullscreen, bool enable_vsync) : mxvk::VK_Window("-[ MXVK Tic-Tac-Toe ]-", width, height, fullscreen, MXVK_VALIDATION, enable_vsync), rng(std::random_device{}()) {
23 assetRoot = assetPath.empty() ? std::string(tictactoe_ASSET_DIR) : assetPath;
24 const std::string fontPath = assetPath.empty() ? std::string(tictactoe_FONT_PATH) : assetRoot + "/data/font.ttf";
25 setFont(fontPath, 20);
26 titleFont.reset(fontPath, 30);
27 uiFont.reset(fontPath, 18);
28 setClearColor(0.03f, 0.04f, 0.07f, 1.0f);
29 background = createSprite(assetRoot + "/data/bg.png");
30 makePixelSprite();
31 resetGame();
32 }
33
34 void event(SDL_Event &e) override {
35 if (e.type == SDL_EVENT_KEY_DOWN) {
36 if (e.key.key == SDLK_ESCAPE) {
37 exit();
38 } else if (e.key.key == SDLK_R) {
39 resetGame();
40 }
41 return;
42 }
43
44 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
45 handleClick(e.button.x, e.button.y);
46 }
47 }
48
49 void proc() override {
50 updateBoardLayout();
51 drawBackground();
52 drawBoard();
53 drawMarks();
54 drawText();
55 }
56
57 private:
58 enum class Outcome { Running, UserWon, ComputerWon, Draw };
59
60 static constexpr int gridSize = 360;
61 static constexpr int cellSize = gridSize / 3;
62 static constexpr int lineThickness = 6;
63 static constexpr float pi = 3.14159265358979323846f;
64
65 std::array<char, 9> board{};
66 std::string assetRoot = ".";
67 mxvk::Font titleFont{};
68 mxvk::Font uiFont{};
69 mxvk::VK_Sprite *background = nullptr;
70 mxvk::VK_Sprite *boardPixel = nullptr;
71 mxvk::VK_Sprite *xPixel = nullptr;
72 mxvk::VK_Sprite *oPixel = nullptr;
73 std::mt19937 rng;
74 Outcome outcome = Outcome::Running;
75 int boardX = 0;
76 int boardY = 0;
77
78 void makePixelSprite() {
79 boardPixel = makeSolidPixel(235, 240, 255, 255);
80 xPixel = makeSolidPixel(95, 205, 255, 255);
81 oPixel = makeSolidPixel(255, 140, 140, 255);
82 }
83
84 mxvk::VK_Sprite *makeSolidPixel(std::uint8_t r, std::uint8_t g, std::uint8_t b, std::uint8_t a) {
85 const std::array<std::uint8_t, 4> pixel{r, g, b, a};
86 mxvk::VK_Sprite *sprite = createSprite(1, 1);
87 sprite->updateTexture(pixel.data(), 1, 1, 4);
88 return sprite;
89 }
90
91 void resetGame() {
92 board.fill(' ');
93 outcome = Outcome::Running;
94 }
95
96 void updateBoardLayout() {
97 const int windowWidth = static_cast<int>(swapchain_extent.width);
98 const int windowHeight = static_cast<int>(swapchain_extent.height);
99 boardX = std::max(24, (windowWidth - gridSize) / 2);
100 boardY = std::max(100, (windowHeight - gridSize) / 2 + 20);
101 }
102
103 void handleClick(float windowMouseX, float windowMouseY) {
104 if (outcome != Outcome::Running) {
105 resetGame();
106 return;
107 }
108
109 updateBoardLayout();
110 const auto [mouseX, mouseY] = mouseToRenderCoordinates(windowMouseX, windowMouseY);
111 const int localX = mouseX - boardX;
112 const int localY = mouseY - boardY;
113 if (localX < 0 || localY < 0 || localX >= gridSize || localY >= gridSize) {
114 return;
115 }
116
117 const int col = localX / cellSize;
118 const int row = localY / cellSize;
119 const int index = row * 3 + col;
120 if (board[static_cast<std::size_t>(index)] != ' ') {
121 return;
122 }
123
124 board[static_cast<std::size_t>(index)] = 'X';
125 updateOutcome();
126 if (outcome == Outcome::Running) {
127 makeComputerMove();
128 updateOutcome();
129 }
130 }
131
132 std::pair<int, int> mouseToRenderCoordinates(float mouseX, float mouseY) const {
133 int logicalWidth = 0;
134 int logicalHeight = 0;
135 int pixelWidth = 0;
136 int pixelHeight = 0;
137 SDL_GetWindowSize(getSDLWindow(), &logicalWidth, &logicalHeight);
138 SDL_GetWindowSizeInPixels(getSDLWindow(), &pixelWidth, &pixelHeight);
139
140 if (logicalWidth <= 0 || logicalHeight <= 0 || pixelWidth <= 0 || pixelHeight <= 0 || swapchain_extent.width == 0 || swapchain_extent.height == 0) {
141 return {static_cast<int>(std::lround(mouseX)), static_cast<int>(std::lround(mouseY))};
142 }
143
144 const bool mouseLooksLogical = mouseX >= 0.0f && mouseY >= 0.0f && mouseX <= static_cast<float>(logicalWidth) + 0.5f && mouseY <= static_cast<float>(logicalHeight) + 0.5f;
145
146 if (mouseLooksLogical && (pixelWidth != logicalWidth || pixelHeight != logicalHeight)) {
147 mouseX *= static_cast<float>(swapchain_extent.width) / static_cast<float>(logicalWidth);
148 mouseY *= static_cast<float>(swapchain_extent.height) / static_cast<float>(logicalHeight);
149 }
150
151 return {static_cast<int>(std::lround(mouseX)), static_cast<int>(std::lround(mouseY))};
152 }
153
154 void makeComputerMove() {
155 if (playImmediateMove('O')) {
156 return;
157 }
158 if (playImmediateMove('X')) {
159 return;
160 }
161 if (board[4] == ' ') {
162 board[4] = 'O';
163 return;
164 }
165
166 std::vector<int> openCells;
167 for (int i = 0; i < static_cast<int>(board.size()); ++i) {
168 if (board[static_cast<std::size_t>(i)] == ' ') {
169 openCells.push_back(i);
170 }
171 }
172
173 if (!openCells.empty()) {
174 std::shuffle(openCells.begin(), openCells.end(), rng);
175 board[static_cast<std::size_t>(openCells.front())] = 'O';
176 }
177 }
178
179 bool playImmediateMove(char side) {
180 for (int i = 0; i < static_cast<int>(board.size()); ++i) {
181 if (board[static_cast<std::size_t>(i)] != ' ') {
182 continue;
183 }
184
185 board[static_cast<std::size_t>(i)] = side;
186 const bool completesLine = winner() == side;
187 board[static_cast<std::size_t>(i)] = ' ';
188 if (completesLine) {
189 board[static_cast<std::size_t>(i)] = 'O';
190 return true;
191 }
192 }
193 return false;
194 }
195
196 void updateOutcome() {
197 const char winningSide = winner();
198 if (winningSide == 'X') {
199 outcome = Outcome::UserWon;
200 } else if (winningSide == 'O') {
201 outcome = Outcome::ComputerWon;
202 } else if (std::ranges::none_of(board, [](char c) { return c == ' '; })) {
203 outcome = Outcome::Draw;
204 }
205 }
206
207 char winner() const {
208 static constexpr std::array<std::array<int, 3>, 8> lines{{
209 {{0, 1, 2}},
210 {{3, 4, 5}},
211 {{6, 7, 8}},
212 {{0, 3, 6}},
213 {{1, 4, 7}},
214 {{2, 5, 8}},
215 {{0, 4, 8}},
216 {{2, 4, 6}},
217 }};
218
219 for (const auto &line : lines) {
220 const char first = board[static_cast<std::size_t>(line[0])];
221 if (first != ' ' && first == board[static_cast<std::size_t>(line[1])] && first == board[static_cast<std::size_t>(line[2])]) {
222 return first;
223 }
224 }
225 return ' ';
226 }
227
228 void drawBoard() {
229 if (boardPixel == nullptr) {
230 return;
231 }
232
233 const int boardEnd = boardX + gridSize;
234 for (int i = 1; i <= 2; ++i) {
235 const int pos = boardX + i * cellSize - lineThickness / 2;
236 boardPixel->drawSpriteRect(pos, boardY, lineThickness, gridSize);
237 boardPixel->drawSpriteRect(boardX, boardY + i * cellSize - lineThickness / 2, gridSize, lineThickness);
238 }
239
240 boardPixel->drawSpriteRect(boardX - lineThickness, boardY - lineThickness, gridSize + lineThickness * 2, lineThickness);
241 boardPixel->drawSpriteRect(boardX - lineThickness, boardY + gridSize, gridSize + lineThickness * 2, lineThickness);
242 boardPixel->drawSpriteRect(boardX - lineThickness, boardY, lineThickness, gridSize);
243 boardPixel->drawSpriteRect(boardEnd, boardY, lineThickness, gridSize);
244 }
245
246 void drawBackground() {
247 if (background == nullptr) {
248 return;
249 }
250 background->drawSpriteRect(0, 0, static_cast<int>(swapchain_extent.width), static_cast<int>(swapchain_extent.height));
251 }
252
253 void drawMarks() {
254 if (xPixel == nullptr || oPixel == nullptr) {
255 return;
256 }
257
258 const int markSize = std::max(24, cellSize * 58 / 100);
259 const int thickness = std::max(4, cellSize / 18);
260
261 for (int row = 0; row < 3; ++row) {
262 for (int col = 0; col < 3; ++col) {
263 const int index = row * 3 + col;
264 const char mark = board[static_cast<std::size_t>(index)];
265 if (mark == ' ') {
266 continue;
267 }
268
269 const int centerX = boardX + col * cellSize + cellSize / 2;
270 const int centerY = boardY + row * cellSize + cellSize / 2;
271
272 if (mark == 'X') {
273 drawX(centerX, centerY, markSize, thickness);
274 } else {
275 drawO(centerX, centerY, markSize, thickness);
276 }
277 }
278 }
279 }
280
281 void drawX(int centerX, int centerY, int size, int thickness) {
282 const int half = size / 2;
283 drawLine(*xPixel, centerX - half, centerY - half, centerX + half, centerY + half, thickness);
284 drawLine(*xPixel, centerX + half, centerY - half, centerX - half, centerY + half, thickness);
285 }
286
287 void drawO(int centerX, int centerY, int size, int thickness) {
288 const int radius = size / 2;
289 constexpr int segments = 96;
290 int previousX = centerX + radius;
291 int previousY = centerY;
292
293 for (int i = 1; i <= segments; ++i) {
294 const float angle = (static_cast<float>(i) / static_cast<float>(segments)) * 2.0f * pi;
295 const int x = centerX + static_cast<int>(std::lround(std::cos(angle) * static_cast<float>(radius)));
296 const int y = centerY + static_cast<int>(std::lround(std::sin(angle) * static_cast<float>(radius)));
297 drawLine(*oPixel, previousX, previousY, x, y, thickness);
298 previousX = x;
299 previousY = y;
300 }
301 }
302
303 void drawLine(mxvk::VK_Sprite &sprite, int x0, int y0, int x1, int y1, int thickness) {
304 const int dx = x1 - x0;
305 const int dy = y1 - y0;
306 const int steps = std::max(std::abs(dx), std::abs(dy));
307 if (steps == 0) {
308 drawDot(sprite, x0, y0, thickness);
309 return;
310 }
311
312 for (int i = 0; i <= steps; ++i) {
313 const float t = static_cast<float>(i) / static_cast<float>(steps);
314 const int x = static_cast<int>(std::lround(static_cast<float>(x0) + static_cast<float>(dx) * t));
315 const int y = static_cast<int>(std::lround(static_cast<float>(y0) + static_cast<float>(dy) * t));
316 drawDot(sprite, x, y, thickness);
317 }
318 }
319
320 void drawDot(mxvk::VK_Sprite &sprite, int x, int y, int size) { sprite.drawSpriteRect(x - size / 2, y - size / 2, size, size); }
321
322 void drawText() {
323 printText("Tic-Tac-Toe", boardX, 30, SDL_Color{235, 240, 255, 255}, titleFont);
324 printText(statusText(), boardX, boardY - 135, SDL_Color{190, 210, 255, 255}, uiFont);
325 printText("Click a square. R resets. Esc quits.", boardX, boardY + gridSize + 18, SDL_Color{180, 185, 200, 255}, uiFont);
326 }
327
328 std::string statusText() const {
329 switch (outcome) {
330 case Outcome::Running:
331 return "You are X. The computer is O.";
332 case Outcome::UserWon:
333 return "You won. Click anywhere to play again.";
334 case Outcome::ComputerWon:
335 return "Computer won. Click anywhere to play again.";
336 case Outcome::Draw:
337 return "Draw. Click anywhere to play again.";
338 }
339 return {};
340 }
341 };
342} // namespace example
343
344int main(int argc, char **argv) {
345 try {
346 Arguments args = proc_args(argc, argv);
347 example::TicTacToeWindow window(args.path, args.width, args.height, args.fullscreen, args.enable_vsync);
348 window.loop();
349 } catch (mxvk::Exception &e) {
350 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
351 return EXIT_FAILURE;
352 } catch (ArgException<std::string> &e) {
353 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
354 return EXIT_FAILURE;
355 }
356 return EXIT_SUCCESS;
357}
Lightweight, header-only, template command-line argument parser.
Arguments proc_args(int &argc, char **argv)
Parse standard libmx2 command-line options from main()'s argv.
Definition argz.hpp:854
Exception thrown by Argz::proc() on unrecognised or malformed options.
Definition argz.hpp:169
TicTacToeWindow(const std::string &assetPath, int width, int height, bool fullscreen, bool enable_vsync)
Definition main.cpp:22
void proc() override
Execute one processing/update step.
Definition main.cpp:49
void event(SDL_Event &e) override
Handle one SDL event.
Definition main.cpp:34
std::string text() const
void updateTexture(SDL_Surface *surface)
Replace the sprite texture from an SDL_Surface.
void drawSpriteRect(int x, int y, int w, int h)
Queue a draw into an explicit destination rectangle.
Main Vulkan window wrapper for MXVK.
Definition mxvk.hpp:39
void loop()
Run the main event/render loop.
Definition mxvk.cpp:651
VK_Sprite * createSprite(const std::string &pngPath, const std::string &vertexShaderPath="", const std::string &fragmentShaderPath="")
Create a sprite from a PNG file and register it with this window.
Definition mxvk.cpp:4455
VkExtent2D swapchain_extent
Definition mxvk.hpp:616
SDL_Window * getSDLWindow() const noexcept
Get the underlying SDL window handle.
Definition mxvk.hpp:192
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
Definition mxvk.cpp:637
void exit()
Request loop termination.
Definition mxvk.cpp:1378
VK_Window()=default
Construct an empty window object.
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
Definition mxvk.cpp:3872
void printText(const std::string &text, int x, int y, const SDL_Color &col)
Queue a text string for rendering during the current frame.
Definition mxvk.cpp:3919
#define MXVK_VALIDATION
Definition mxvk.hpp:28
int main()
Definition main.py:165
Utilities for loading and saving PNG images.
Definition mxvk.hpp:31
Plain data structure returned by proc_args() with all common libmx2 CLI options.
Definition argz.hpp:718
bool fullscreen
Whether fullscreen mode was requested.
Definition argz.hpp:724
bool enable_vsync
Enable FIFO present mode / v-sync (--enable-vsync).
Definition argz.hpp:738
int height
Viewport height in pixels (default: 720).
Definition argz.hpp:721
std::string path
Asset root; proc_args() defaults it to the executable directory.
Definition argz.hpp:723
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:720