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