MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
puzzle_drop.cpp
Go to the documentation of this file.
1#include <SDL3/SDL.h>
2
3#include <algorithm>
4#include <array>
5#include <chrono>
6#include <cmath>
7#include <cstdlib>
8#include <format>
9#include <iostream>
10#include <memory>
11#include <random>
12#include <string>
13
14#include <glm/ext/matrix_clip_space.hpp>
15#include <glm/ext/matrix_transform.hpp>
16#include <glm/glm.hpp>
17
18#include "mxvk/argz.hpp"
19#include "mxvk/mxvk.hpp"
22#include "rain.hpp"
23
24#ifndef puzzle_drop_ASSET_DIR
25#define puzzle_drop_ASSET_DIR "."
26#endif
27
28namespace {
29 constexpr int BOARD_WIDTH = 20;
30 constexpr int BOARD_HEIGHT = 22;
31 constexpr float CUBE_SCALE = 0.048f;
32 constexpr float CUBE_SPACING = CUBE_SCALE * 1.12f;
33 constexpr std::array<float, 3> FALL_SECONDS{0.86f, 0.68f, 0.50f};
34 constexpr float INTRO_FADE_STEP = 0.01f;
35 constexpr Uint32 INTRO_FADE_INTERVAL_MS = 35U;
36 constexpr float INTRO_START_FADE = 1.0f;
37 constexpr int MATRIX_RAIN_TEXTURE_WIDTH = 1280;
38 constexpr int MATRIX_RAIN_TEXTURE_HEIGHT = 720;
39 constexpr size_t FRAME_TEXTURE_INDEX = 10;
40 constexpr size_t LEVEL_GRAPHIC_COUNT = 8;
41 constexpr size_t PREVIEW_BLOCK_TEXTURE_COUNT = 10;
42
43 enum class BlockType {
44 Null = 0,
45 Clear,
46 Red1,
47 Red2,
48 Red3,
49 Green1,
50 Green2,
51 Green3,
52 Blue1,
53 Blue2,
54 Blue3,
55 Match,
56 };
57
58 enum class ShiftDirection {
59 Down,
60 Up,
61 };
62
63 struct Block {
64 int x = 0;
65 int y = 0;
67 };
68
69 struct Piece {
70 std::array<Block, 3> blocks{};
71 int position = 0;
72
73 void new_piece(int start_x, int start_y, std::mt19937 &rng) {
74 blocks[0] = {start_x, start_y, random_type(rng)};
75 blocks[1] = {start_x, start_y + 1, random_type(rng)};
76 blocks[2] = {start_x, start_y + 2, random_type(rng)};
77 position = 0;
78 }
79
80 void shift(ShiftDirection direction) {
81 const std::array<BlockType, 3> types{blocks[0].type, blocks[1].type, blocks[2].type};
82 if (direction == ShiftDirection::Down) {
83 blocks[0].type = types[2];
84 blocks[1].type = types[0];
85 blocks[2].type = types[1];
86 } else {
87 blocks[0].type = types[1];
88 blocks[1].type = types[2];
89 blocks[2].type = types[0];
90 }
91 }
92
93 void move_left() {
94 for (Block &block : blocks) {
95 --block.x;
96 }
97 }
98
99 void move_right() {
100 for (Block &block : blocks) {
101 ++block.x;
102 }
103 }
104
105 void move_down() {
106 for (Block &block : blocks) {
107 ++block.y;
108 }
109 }
110
111 void rotate_left() {
112 if (position == 0) {
113 blocks[1].y -= 1;
114 blocks[1].x -= 1;
115 blocks[2].x -= 2;
116 blocks[2].y -= 2;
117 position = 1;
118 } else if (position == 1) {
119 blocks[1].y += 1;
120 blocks[1].x += 1;
121 blocks[2].y += 2;
122 blocks[2].x += 2;
123 position = 0;
124 }
125 }
126
128 if (position == 0) {
129 blocks[1].x += 1;
130 blocks[1].y -= 1;
131 blocks[2].x += 2;
132 blocks[2].y -= 2;
133 position = 2;
134 } else if (position == 2) {
135 blocks[1].x -= 1;
136 blocks[1].y += 1;
137 blocks[2].x -= 2;
138 blocks[2].y += 2;
139 position = 0;
140 }
141 }
142
143 private:
144 [[nodiscard]] static BlockType random_type(std::mt19937 &rng) {
145 std::uniform_int_distribution<int> dist(static_cast<int>(BlockType::Red1), static_cast<int>(BlockType::Match));
146 return static_cast<BlockType>(dist(rng));
147 }
148 };
149
155
156 const std::array<glm::vec3, 10> BLOCK_TINTS{{
157 {1.00f, 0.48f, 0.42f},
158 {1.00f, 0.58f, 0.50f},
159 {1.00f, 0.72f, 0.62f},
160 {0.42f, 0.95f, 0.55f},
161 {0.55f, 1.00f, 0.65f},
162 {0.70f, 1.00f, 0.78f},
163 {0.38f, 0.62f, 1.00f},
164 {0.50f, 0.74f, 1.00f},
165 {0.66f, 0.86f, 1.00f},
166 {1.00f, 0.92f, 0.35f},
167 }};
168
169 const std::array<std::string, PREVIEW_BLOCK_TEXTURE_COUNT> PREVIEW_BLOCK_TEXTURE_FILES{{
170 "red1.png",
171 "red2.png",
172 "red3.png",
173 "green1.png",
174 "green2.png",
175 "green3.png",
176 "blue1.png",
177 "blue2.png",
178 "blue3.png",
179 "red3.png",
180 }};
181
182 [[nodiscard]] bool is_play_block(BlockType type) {
183 return type >= BlockType::Red1 && type <= BlockType::Match;
184 }
185
186 [[nodiscard]] int texture_index(BlockType type) {
187 if (!is_play_block(type)) {
188 return 0;
189 }
190 return static_cast<int>(type) - static_cast<int>(BlockType::Red1);
191 }
192
193 [[nodiscard]] bool same_or_match(BlockType actual, BlockType expected) {
194 return actual == expected || actual == BlockType::Match;
195 }
196
197 class PuzzleDropWindow final : public mxvk::VK_Window {
198 public:
199 PuzzleDropWindow(const std::string &path, int width, int height, bool fullscreen, bool enable_vsync)
200 : mxvk::VK_Window("-[ MXVK 3D PuzzleDrop ]-", width, height, fullscreen, MXVK_VALIDATION, enable_vsync),
201 asset_root((path.empty() || path == ".") ? std::string(puzzle_drop_ASSET_DIR) : path),
202 data_root(asset_root + "/data"),
203 shader_root(data_root),
204 tetris_data_root(data_root) {
205 std::random_device rd;
206 rng.seed(rd());
207 setClearColor(0.03f, 0.04f, 0.05f, 1.0f);
208 setFont(data_root + "/font.ttf", 22);
209 std::array<std::string, LEVEL_GRAPHIC_COUNT> level_graphics{};
210 for (size_t i = 0; i < level_graphics.size(); ++i) {
211 level_graphics[i] = std::format("{}/level{}.png", data_root, i + 1);
212 }
213 std::shuffle(level_graphics.begin(), level_graphics.end(), rng);
214 for (size_t i = 0; i < backgrounds.size(); ++i) {
215 backgrounds[i] = createSprite(
216 level_graphics[i],
217 shader_root + "/sprite.vert.spv",
218 shader_root + "/puzzle_drop_background.frag.spv");
219 }
220 intro_sprite = createSprite(
221 std::format("{}/intro1.png", data_root),
222 shader_root + "/sprite.vert.spv",
223 shader_root + "/intro.frag.spv");
224 matrix::RainConfig rain_config = matrix::make_matrix_rain_config(asset_root, false);
225 rain_config.color = "#2f8dff";
228 matrix_rain = std::make_unique<matrix::Rain>(*this, std::move(rain_config));
229 try_open_first_gamepad();
230 preview_border_sprite = createSprite(1, 1);
231 const uint32_t white_pixel = 0xFFFFFFFFu;
232 preview_border_sprite->updateTexture(&white_pixel, 1, 1);
233 for (std::size_t i = 0; i < preview_block_sprites.size(); ++i) {
234 preview_block_sprites[i] = createSprite(data_root + "/" + PREVIEW_BLOCK_TEXTURE_FILES[i]);
235 }
236 init_cube_model();
237 reset_game();
238 reset_intro_screen();
239 }
240
241 ~PuzzleDropWindow() override {
242 if (device != VK_NULL_HANDLE) {
243 vkDeviceWaitIdle(device);
244 }
245 close_gamepad();
246 cleanup_models();
247 }
248
249 void onSwapchainRecreated() override {
250 if (cube_model) {
251 cube_model->resize(this);
252 }
253 if (grid_backdrop_model) {
254 grid_backdrop_model->resize(this);
255 }
256 if (matrix_rain) {
257 matrix_rain->on_swapchain_recreated(*this);
258 }
259 }
260
261 void event(SDL_Event &e) override {
262 if (e.type == SDL_EVENT_QUIT) {
263 exit();
264 return;
265 }
266
267 if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
268 if (!open_gamepad(e.gdevice.which)) {
269 try_open_first_gamepad();
270 }
271 return;
272 }
273
274 if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
275 if (gamepad != nullptr && e.gdevice.which == gamepad_id) {
276 close_gamepad();
277 try_open_first_gamepad();
278 }
279 return;
280 }
281
282 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
283 handle_gamepad_button_down(e.gbutton.button);
284 return;
285 }
286
287 if (e.type != SDL_EVENT_KEY_DOWN || e.key.repeat) {
288 return;
289 }
290
291 if (intro_active && (e.key.key == SDLK_SPACE || e.key.key == SDLK_RETURN || e.key.key == SDLK_KP_ENTER)) {
292 skip_intro();
293 return;
294 }
295
296 switch (e.key.key) {
297 case SDLK_ESCAPE:
298 exit();
299 break;
300 case SDLK_RETURN:
301 case SDLK_KP_ENTER:
302 if (game_over) {
303 reset_game();
304 game_started = true;
305 }
306 break;
307 case SDLK_1:
308 difficulty = 0;
309 reset_game();
310 game_started = true;
311 break;
312 case SDLK_2:
313 difficulty = 1;
314 reset_game();
315 game_started = true;
316 break;
317 case SDLK_3:
318 difficulty = 2;
319 reset_game();
320 game_started = true;
321 break;
322 case SDLK_Z:
323 rotate_left();
324 break;
325 case SDLK_X:
326 rotate_right();
327 break;
328 default:
329 break;
330 }
331 }
332
333 void proc() override {
334 const auto now = std::chrono::steady_clock::now();
335 const float delta_seconds = std::chrono::duration<float>(now - last_input_update).count();
336 last_input_update = now;
337 try_open_first_gamepad();
338 randomize_wildcard_color();
339
340 if (intro_active) {
341 update_intro(now);
342 } else {
343 background_time += delta_seconds;
344 const bool *keys = SDL_GetKeyboardState(nullptr);
345 if (keys != nullptr) {
346 handle_view_controls(keys, delta_seconds);
347 handle_piece_controls(keys, delta_seconds);
348 }
349 handle_gamepad_input(delta_seconds);
350 }
351
352 if (game_started && !game_over) {
353 if (std::chrono::duration<float>(now - last_fall).count() >= FALL_SECONDS[difficulty]) {
354 key_down();
355 last_fall = now;
356 }
357 if (std::chrono::duration<float>(now - last_process).count() >= 0.018f) {
358 proc_blocks();
359 proc_move_down();
360 last_process = now;
361 }
362 }
363
364 const SDL_Color primary{255, 244, 223, 255};
365 const SDL_Color secondary{218, 229, 232, 255};
366 if (intro_active) {
367 printText("Press Enter", 24, 54, secondary);
368 return;
369 }
370
371 if (game_over) {
372 print_game_over_text(secondary);
373 return;
374 }
375
376 printText(std::format("Level {} Lines {} Difficulty {}", level, lines, difficulty + 1), 24, 22, primary);
377 }
378
379 void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t image_index) override {
380 const VkExtent2D extent = getSwapchainExtent();
381 if (intro_active) {
382 draw_game_scene(cmd, image_index, extent);
383 render_intro(cmd, extent);
384 return;
385 }
386
387 draw_game_scene(cmd, image_index, extent);
388 }
389
390 private:
391 std::string asset_root;
392 std::string data_root;
393 std::string shader_root;
394 std::string tetris_data_root;
395 std::mt19937 rng{};
396 std::array<std::array<Cell, BOARD_WIDTH>, BOARD_HEIGHT> board{};
397 Piece piece{};
398 Piece next_piece{};
399 std::unique_ptr<mxvk::VKAbstractModel> cube_model{};
400 std::unique_ptr<mxvk::VKAbstractModel> grid_backdrop_model{};
401 std::array<mxvk::VK_Sprite *, LEVEL_GRAPHIC_COUNT> backgrounds{};
402 mxvk::VK_Sprite *intro_sprite = nullptr;
403 mxvk::VK_Sprite *preview_border_sprite = nullptr;
404 std::array<mxvk::VK_Sprite *, PREVIEW_BLOCK_TEXTURE_COUNT> preview_block_sprites{};
405 std::unique_ptr<matrix::Rain> matrix_rain{};
406 SDL_Gamepad *gamepad = nullptr;
407 SDL_JoystickID gamepad_id = 0;
408 std::chrono::steady_clock::time_point last_fall{std::chrono::steady_clock::now()};
409 std::chrono::steady_clock::time_point last_process{std::chrono::steady_clock::now()};
410 std::chrono::steady_clock::time_point last_input_update{std::chrono::steady_clock::now()};
411 float horizontal_move_timer = 0.0f;
412 float soft_drop_timer = 0.0f;
413 float cycle_timer = 0.0f;
414 float gamepad_move_repeat_timer = 0.0f;
415 float gamepad_soft_drop_repeat_timer = 0.0f;
416 float gamepad_cycle_repeat_timer = 0.0f;
417 float gamepad_move_held_seconds = 0.0f;
418 int horizontal_move_direction = 0;
419 int gamepad_move_direction = 0;
420 bool soft_drop_held = false;
421 bool cycle_held = false;
422 bool gamepad_soft_drop_held = false;
423 bool gamepad_cycle_held = false;
424 int difficulty = 0;
425 int level = 1;
426 int lines = 0;
427 glm::vec3 wildcard_color{1.0f, 0.0f, 1.0f};
428 bool intro_active = true;
429 float intro_fade = INTRO_START_FADE;
430 std::chrono::steady_clock::time_point intro_last_update{std::chrono::steady_clock::now()};
431 std::chrono::steady_clock::time_point intro_start{std::chrono::steady_clock::now()};
432 float background_time = 0.0f;
433 bool game_started = false;
434 bool game_over = false;
435 float grid_yaw = -10.0f;
436 float grid_pitch = -8.0f;
437 float camera_distance = 2.32f;
438 static constexpr Sint16 GAMEPAD_DEADZONE = 10000;
439 static constexpr float GAMEPAD_MOVE_INITIAL_DELAY_SECONDS = 0.22f;
440 static constexpr float GAMEPAD_MOVE_REPEAT_SECONDS = 0.12f;
441 static constexpr float GAMEPAD_SOFT_DROP_INITIAL_DELAY_SECONDS = 0.18f;
442 static constexpr float GAMEPAD_SOFT_DROP_REPEAT_SECONDS = 0.08f;
443 static constexpr float GAMEPAD_CYCLE_INITIAL_DELAY_SECONDS = 0.16f;
444 static constexpr float GAMEPAD_CYCLE_REPEAT_SECONDS = 0.11f;
445 static constexpr float GAMEPAD_STICK_ROTATE_SPEED = 120.0f;
446 static constexpr float GAMEPAD_STICK_PITCH_SPEED = 100.0f;
447 static constexpr float GAMEPAD_STICK_SCALE = 1.0f / 32768.0f;
448
449 void draw_game_scene(VkCommandBuffer cmd, uint32_t image_index, const VkExtent2D &extent) {
450 draw_background(cmd, extent);
451
452 const float aspect = (extent.height > 0U) ? static_cast<float>(extent.width) / static_cast<float>(extent.height) : 1.0f;
453 glm::mat4 view = glm::lookAt(glm::vec3(0.0f, 0.12f, camera_distance), glm::vec3(0.0f, 0.05f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
454 view = glm::rotate(view, glm::radians(grid_pitch), glm::vec3(1.0f, 0.0f, 0.0f));
455 view = glm::rotate(view, glm::radians(grid_yaw), glm::vec3(0.0f, 1.0f, 0.0f));
456
457 glm::mat4 proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f);
458 proj[1][1] *= -1.0f;
459
460 draw_grid_backdrop(cmd, image_index, view, proj);
461 draw_frame(cmd, image_index, view, proj);
462 for (int y = 0; y < BOARD_HEIGHT; ++y) {
463 for (int x = 0; x < BOARD_WIDTH; ++x) {
464 Cell &cell = board[y][x];
465 if (cell.type == BlockType::Null) {
466 continue;
467 }
468 if (cell.type == BlockType::Clear && ((cell.flash_counter / 6) % 2) != 0) {
469 continue;
470 }
471 draw_cube(cmd, image_index, cell.type, x, y, view, proj);
472 }
473 }
474
475 if ((game_started || intro_active) && !game_over) {
476 for (const Block &block : piece.blocks) {
477 draw_cube(cmd, image_index, block.type, block.x, block.y, view, proj);
478 }
479 }
480
481 draw_next_piece_preview(cmd, extent);
482 }
483
484 void randomize_wildcard_color() {
485 std::uniform_int_distribution<int> dist(0, 254);
486 wildcard_color = glm::vec3(
487 static_cast<float>(dist(rng)) / 255.0f,
488 static_cast<float>(dist(rng)) / 255.0f,
489 static_cast<float>(dist(rng)) / 255.0f);
490 }
491
492 void handle_view_controls(const bool *keys, float delta_seconds) {
493 constexpr float YAW_SPEED = 115.0f;
494 constexpr float PITCH_SPEED = 90.0f;
495 constexpr float ZOOM_SPEED = 3.2f;
496 if (keys[SDL_SCANCODE_A]) {
497 grid_yaw -= YAW_SPEED * delta_seconds;
498 }
499 if (keys[SDL_SCANCODE_D]) {
500 grid_yaw += YAW_SPEED * delta_seconds;
501 }
502 if (keys[SDL_SCANCODE_W]) {
503 grid_pitch = std::clamp(grid_pitch + PITCH_SPEED * delta_seconds, -70.0f, 70.0f);
504 }
505 if (keys[SDL_SCANCODE_S]) {
506 grid_pitch = std::clamp(grid_pitch - PITCH_SPEED * delta_seconds, -70.0f, 70.0f);
507 }
508 if (keys[SDL_SCANCODE_PAGEUP]) {
509 camera_distance = std::max(1.35f, camera_distance - ZOOM_SPEED * delta_seconds);
510 }
511 if (keys[SDL_SCANCODE_PAGEDOWN]) {
512 camera_distance = std::min(7.0f, camera_distance + ZOOM_SPEED * delta_seconds);
513 }
514 }
515
516 void handle_piece_controls(const bool *keys, float delta_seconds) {
517 if (!game_started || game_over) {
518 reset_held_piece_input();
519 return;
520 }
521
522 const bool left = keys[SDL_SCANCODE_LEFT];
523 const bool right = keys[SDL_SCANCODE_RIGHT];
524 const int direction = (left == right) ? 0 : (left ? -1 : 1);
525 if (direction == 0) {
526 horizontal_move_direction = 0;
527 horizontal_move_timer = 0.0f;
528 } else {
529 constexpr float INITIAL_DELAY_SECONDS = 0.16f;
530 constexpr float REPEAT_SECONDS = 0.065f;
531 if (horizontal_move_direction != direction) {
532 horizontal_move_direction = direction;
533 horizontal_move_timer = -INITIAL_DELAY_SECONDS;
534 move_piece_horizontal(direction);
535 } else {
536 horizontal_move_timer += delta_seconds;
537 while (horizontal_move_timer >= 0.0f) {
538 horizontal_move_timer -= REPEAT_SECONDS;
539 move_piece_horizontal(direction);
540 }
541 }
542 }
543
544 if (keys[SDL_SCANCODE_DOWN]) {
545 constexpr float SOFT_DROP_REPEAT_SECONDS = 0.045f;
546 if (!soft_drop_held) {
547 soft_drop_held = true;
548 soft_drop_timer = 0.0f;
549 key_down();
550 last_fall = std::chrono::steady_clock::now();
551 } else {
552 soft_drop_timer += delta_seconds;
553 while (soft_drop_timer >= SOFT_DROP_REPEAT_SECONDS) {
554 soft_drop_timer -= SOFT_DROP_REPEAT_SECONDS;
555 key_down();
556 last_fall = std::chrono::steady_clock::now();
557 }
558 }
559 } else {
560 soft_drop_held = false;
561 soft_drop_timer = 0.0f;
562 }
563
564 if (keys[SDL_SCANCODE_UP]) {
565 constexpr float CYCLE_INITIAL_DELAY_SECONDS = 0.16f;
566 constexpr float CYCLE_REPEAT_SECONDS = 0.11f;
567 if (!cycle_held) {
568 cycle_held = true;
569 cycle_timer = -CYCLE_INITIAL_DELAY_SECONDS;
570 cycle_piece_blocks();
571 } else {
572 cycle_timer += delta_seconds;
573 while (cycle_timer >= 0.0f) {
574 cycle_timer -= CYCLE_REPEAT_SECONDS;
575 cycle_piece_blocks();
576 }
577 }
578 } else {
579 cycle_held = false;
580 cycle_timer = 0.0f;
581 }
582 }
583
584 void handle_gamepad_input(float delta_seconds) {
585 if (gamepad == nullptr || !game_started || game_over) {
586 reset_held_gamepad_input();
587 return;
588 }
589
590 const Sint16 left_x = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
591 const Sint16 left_y = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
592 const Sint16 right_x = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX);
593 const Sint16 right_y = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY);
594
595 const bool dpad_left = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
596 const bool dpad_right = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
597 const bool dpad_down = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
598 const bool dpad_up = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_UP);
599
600 const int move_direction = dpad_left == dpad_right
601 ? ((left_x < -GAMEPAD_DEADZONE) ? -1 : (left_x > GAMEPAD_DEADZONE) ? 1
602 : 0)
603 : (dpad_left ? -1 : 1);
604 if (move_direction == 0) {
605 gamepad_move_direction = 0;
606 gamepad_move_held_seconds = 0.0f;
607 gamepad_move_repeat_timer = 0.0f;
608 } else if (move_direction != gamepad_move_direction) {
609 gamepad_move_direction = move_direction;
610 gamepad_move_held_seconds = 0.0f;
611 gamepad_move_repeat_timer = 0.0f;
612 move_piece_horizontal(gamepad_move_direction);
613 } else {
614 gamepad_move_held_seconds += delta_seconds;
615 const float threshold = (gamepad_move_held_seconds < GAMEPAD_MOVE_INITIAL_DELAY_SECONDS)
616 ? GAMEPAD_MOVE_INITIAL_DELAY_SECONDS
617 : GAMEPAD_MOVE_REPEAT_SECONDS;
618 gamepad_move_repeat_timer += delta_seconds;
619 if (gamepad_move_repeat_timer >= threshold) {
620 move_piece_horizontal(gamepad_move_direction);
621 gamepad_move_repeat_timer = 0.0f;
622 }
623 }
624
625 const bool soft_drop_down = dpad_down || left_y > GAMEPAD_DEADZONE;
626 if (!soft_drop_down) {
627 gamepad_soft_drop_held = false;
628 gamepad_soft_drop_repeat_timer = 0.0f;
629 } else {
630 const float threshold = gamepad_soft_drop_held ? GAMEPAD_SOFT_DROP_REPEAT_SECONDS : GAMEPAD_SOFT_DROP_INITIAL_DELAY_SECONDS;
631 gamepad_soft_drop_repeat_timer += delta_seconds;
632 if (gamepad_soft_drop_repeat_timer >= threshold) {
633 key_down();
634 last_fall = std::chrono::steady_clock::now();
635 gamepad_soft_drop_repeat_timer = 0.0f;
636 gamepad_soft_drop_held = true;
637 }
638 }
639
640 if (!dpad_up) {
641 gamepad_cycle_held = false;
642 gamepad_cycle_repeat_timer = 0.0f;
643 } else {
644 const float threshold = gamepad_cycle_held ? GAMEPAD_CYCLE_REPEAT_SECONDS : GAMEPAD_CYCLE_INITIAL_DELAY_SECONDS;
645 gamepad_cycle_repeat_timer += delta_seconds;
646 if (gamepad_cycle_repeat_timer >= threshold) {
647 cycle_piece_blocks();
648 gamepad_cycle_repeat_timer = 0.0f;
649 gamepad_cycle_held = true;
650 }
651 }
652
653 if (std::abs(right_x) > GAMEPAD_DEADZONE) {
654 grid_yaw += static_cast<float>(right_x) * GAMEPAD_STICK_SCALE * GAMEPAD_STICK_ROTATE_SPEED * delta_seconds;
655 }
656 if (std::abs(right_y) > GAMEPAD_DEADZONE) {
657 grid_pitch = std::clamp(grid_pitch - static_cast<float>(right_y) * GAMEPAD_STICK_SCALE * GAMEPAD_STICK_PITCH_SPEED * delta_seconds,
658 -70.0f,
659 70.0f);
660 }
661
662 constexpr float ZOOM_SPEED = 3.2f;
663 if (SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)) {
664 camera_distance = std::min(7.0f, camera_distance + ZOOM_SPEED * delta_seconds);
665 }
666 if (SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER)) {
667 camera_distance = std::max(1.35f, camera_distance - ZOOM_SPEED * delta_seconds);
668 }
669 }
670
671 void handle_gamepad_button_down(Uint8 button) {
672 if (intro_active) {
673 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
674 skip_intro();
675 }
676 return;
677 }
678
679 if (game_over) {
680 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
681 reset_game();
682 game_started = true;
683 } else if (button == SDL_GAMEPAD_BUTTON_BACK) {
684 exit();
685 }
686 return;
687 }
688
689 if (!game_started) {
690 return;
691 }
692
693 if (button == SDL_GAMEPAD_BUTTON_SOUTH) {
694 rotate_right();
695 } else if (button == SDL_GAMEPAD_BUTTON_WEST) {
696 rotate_left();
697 } else if (button == SDL_GAMEPAD_BUTTON_EAST) {
698 hard_drop();
699 } else if (button == SDL_GAMEPAD_BUTTON_BACK) {
700 exit();
701 }
702 }
703
704 void reset_held_piece_input() {
705 horizontal_move_timer = 0.0f;
706 soft_drop_timer = 0.0f;
707 cycle_timer = 0.0f;
708 horizontal_move_direction = 0;
709 soft_drop_held = false;
710 cycle_held = false;
711 }
712
713 void reset_held_gamepad_input() {
714 gamepad_move_repeat_timer = 0.0f;
715 gamepad_soft_drop_repeat_timer = 0.0f;
716 gamepad_cycle_repeat_timer = 0.0f;
717 gamepad_move_held_seconds = 0.0f;
718 gamepad_move_direction = 0;
719 gamepad_soft_drop_held = false;
720 gamepad_cycle_held = false;
721 }
722
723 void move_piece_horizontal(int direction) {
724 if (!check_piece(piece, direction, 0)) {
725 return;
726 }
727 if (direction < 0) {
728 piece.move_left();
729 } else {
730 piece.move_right();
731 }
732 }
733
734 void cycle_piece_blocks() {
735 piece.shift(ShiftDirection::Up);
736 }
737
738 void hard_drop() {
739 if (!game_started || game_over) {
740 return;
741 }
742 while (check_piece(piece, 0, 1)) {
743 piece.move_down();
744 }
745 key_down();
746 last_fall = std::chrono::steady_clock::now();
747 }
748
749 bool open_gamepad(SDL_JoystickID id) {
750 if (gamepad != nullptr && gamepad_id == id) {
751 return true;
752 }
753 close_gamepad();
754 gamepad = SDL_OpenGamepad(id);
755 if (gamepad == nullptr) {
756 return false;
757 }
758 gamepad_id = id;
759 return true;
760 }
761
762 void close_gamepad() {
763 if (gamepad != nullptr) {
764 SDL_CloseGamepad(gamepad);
765 gamepad = nullptr;
766 gamepad_id = 0;
767 }
768 }
769
770 void try_open_first_gamepad() {
771 if (gamepad != nullptr) {
772 return;
773 }
774 int count = 0;
775 SDL_JoystickID *ids = SDL_GetGamepads(&count);
776 if (ids == nullptr || count <= 0) {
777 if (ids != nullptr) {
778 SDL_free(ids);
779 }
780 return;
781 }
782 open_gamepad(ids[0]);
783 SDL_free(ids);
784 }
785
786 void reset_game() {
787 reset_held_piece_input();
788 reset_held_gamepad_input();
789 for (auto &row : board) {
790 for (Cell &cell : row) {
791 cell.type = BlockType::Null;
792 cell.clear_value = 0;
793 cell.flash_counter = 0;
794 }
795 }
796 level = 1;
797 lines = 0;
798 game_over = false;
799 piece.new_piece(BOARD_WIDTH / 2, 0, rng);
800 next_piece.new_piece(BOARD_WIDTH / 2, 0, rng);
801 last_fall = std::chrono::steady_clock::now();
802 last_process = last_fall;
803 }
804
805 void key_down() {
806 if (check_piece(piece, 0, 1)) {
807 piece.move_down();
808 return;
809 }
810 set_piece();
811 piece = next_piece;
812 next_piece.new_piece(BOARD_WIDTH / 2, 0, rng);
813 if (!check_piece(piece, 0, 0)) {
814 game_over = true;
815 }
816 }
817
818 [[nodiscard]] bool check_piece(const Piece &test_piece, int offset_x, int offset_y) const {
819 for (const Block &block : test_piece.blocks) {
820 const int x = block.x + offset_x;
821 const int y = block.y + offset_y;
822 if (x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT) {
823 return false;
824 }
825 const BlockType type = board[y][x].type;
826 if (type != BlockType::Null && type != BlockType::Clear) {
827 return false;
828 }
829 }
830 return true;
831 }
832
833 void set_piece() {
834 for (const Block &block : piece.blocks) {
835 if (block.x < 0 || block.x >= BOARD_WIDTH || block.y < 0 || block.y >= BOARD_HEIGHT) {
836 continue;
837 }
838 Cell &cell = board[block.y][block.x];
839 cell.type = block.type;
840 cell.clear_value = 0;
841 cell.flash_counter = 0;
842 if (block.y == 0) {
843 game_over = true;
844 }
845 }
846 }
847
848 void rotate_left() {
849 if (!game_started || game_over) {
850 return;
851 }
852 Piece test_piece = piece;
853 test_piece.rotate_left();
854 if (check_piece(test_piece, 0, 0)) {
855 piece.rotate_left();
856 }
857 }
858
859 void rotate_right() {
860 if (!game_started || game_over) {
861 return;
862 }
863 Piece test_piece = piece;
864 test_piece.rotate_right();
865 if (check_piece(test_piece, 0, 0)) {
866 piece.rotate_right();
867 }
868 }
869
870 bool proc_blocks() {
871 constexpr std::array<std::array<int, 2>, 4> directions{{
872 {{1, 0}},
873 {{0, 1}},
874 {{1, 1}},
875 {{1, -1}},
876 }};
877 constexpr std::array<BlockType, 3> color_starts{BlockType::Red1, BlockType::Green1, BlockType::Blue1};
878
879 for (int y = 0; y < BOARD_HEIGHT; ++y) {
880 for (int x = 0; x < BOARD_WIDTH; ++x) {
881 for (const auto &direction : directions) {
882 for (BlockType start : color_starts) {
883 const BlockType one = start;
884 const BlockType two = static_cast<BlockType>(static_cast<int>(start) + 1);
885 const BlockType three = static_cast<BlockType>(static_cast<int>(start) + 2);
886 if (check_sequence(x, y, direction[0], direction[1], one, two, three) ||
887 check_sequence(x, y, direction[0], direction[1], three, two, one)) {
888 mark_clear(x, y, direction[0], direction[1]);
889 add_score();
890 return true;
891 }
892 }
893 }
894 }
895 }
896 return false;
897 }
898
899 bool proc_move_down() {
900 for (int y = BOARD_HEIGHT - 2; y >= 0; --y) {
901 for (int x = 0; x < BOARD_WIDTH; ++x) {
902 Cell &source = board[y][x];
903 Cell &target = board[y + 1][x];
904 if (is_play_block(source.type) && target.type == BlockType::Null) {
905 target.type = source.type;
906 target.clear_value = source.clear_value;
907 target.flash_counter = source.flash_counter;
908 source.type = BlockType::Null;
909 source.clear_value = 0;
910 source.flash_counter = 0;
911 return true;
912 }
913 }
914 }
915
916 bool updated = false;
917 for (auto &row : board) {
918 for (Cell &cell : row) {
919 if (cell.type == BlockType::Clear) {
920 ++cell.clear_value;
921 ++cell.flash_counter;
922 if (cell.clear_value > 50) {
923 cell.type = BlockType::Null;
924 cell.clear_value = 0;
925 cell.flash_counter = 0;
926 }
927 updated = true;
928 }
929 }
930 }
931 return updated;
932 }
933
934 [[nodiscard]] bool check_sequence(int x, int y, int dx, int dy, BlockType first, BlockType second, BlockType third) const {
935 return check_block(x, y, first) && check_block(x + dx, y + dy, second) && check_block(x + dx * 2, y + dy * 2, third);
936 }
937
938 [[nodiscard]] bool check_block(int x, int y, BlockType expected) const {
939 if (x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT) {
940 return false;
941 }
942 return same_or_match(board[y][x].type, expected);
943 }
944
945 void mark_clear(int x, int y, int dx, int dy) {
946 for (int i = 0; i < 3; ++i) {
947 Cell &cell = board[y + dy * i][x + dx * i];
948 cell.type = BlockType::Clear;
949 cell.clear_value = 1;
950 cell.flash_counter = 0;
951 }
952 }
953
954 void add_score() {
955 ++lines;
956 if ((lines % 6) == 0 && level < static_cast<int>(backgrounds.size())) {
957 ++level;
958 }
959 }
960
961 void init_cube_model() {
962 cube_model = std::make_unique<mxvk::VKAbstractModel>();
963 cube_model->load(this,
964 tetris_data_root + "/cube.mxmod.z",
965 data_root + "/cube_textures.txt",
966 data_root,
967 1.0f);
968 cube_model->setShaders(this,
969 shader_root + "/puzzle_drop_piece.vert.spv",
970 shader_root + "/puzzle_drop_piece.frag.spv");
971
972 grid_backdrop_model = std::make_unique<mxvk::VKAbstractModel>();
973 grid_backdrop_model->load(this,
974 tetris_data_root + "/cube.mxmod.z",
975 tetris_data_root + "/manifest_gray.txt",
976 tetris_data_root,
977 1.0f);
978 grid_backdrop_model->setShaders(this,
979 shader_root + "/puzzle_drop_piece.vert.spv",
980 shader_root + "/puzzle_drop_piece.frag.spv");
981 grid_backdrop_model->setAlphaBlending(true);
982 }
983
984 void cleanup_models() {
985 if (cube_model) {
986 cube_model->cleanup(this);
987 cube_model.reset();
988 }
989 if (grid_backdrop_model) {
990 grid_backdrop_model->cleanup(this);
991 grid_backdrop_model.reset();
992 }
993 }
994
995 void print_game_over_text(const SDL_Color &color) {
996 const std::string text = std::format("Game Over: Lines cleared: {} [Press Enter to Restart]", lines);
997 const VkExtent2D extent = getSwapchainExtent();
998 int text_width = 0;
999 int text_height = 0;
1000 if (!getTextDimensions(text, text_width, text_height)) {
1001 printText(text, 24, 54, color);
1002 return;
1003 }
1004
1005 const int screen_width = static_cast<int>(extent.width);
1006 const int screen_height = static_cast<int>(extent.height);
1007 const int x = std::max(0, (screen_width - text_width) / 2);
1008 const int y = std::max(0, (screen_height - text_height) / 2);
1009 printText(text, x, y, color);
1010 }
1011
1012 void reset_intro_screen() {
1013 intro_active = true;
1014 intro_fade = INTRO_START_FADE;
1015 intro_last_update = std::chrono::steady_clock::now();
1016 intro_start = intro_last_update;
1017 game_started = false;
1018 if (matrix_rain) {
1019 matrix_rain->set_opacity(1.0f);
1020 matrix_rain->reset();
1021 }
1022 reset_held_piece_input();
1023 reset_held_gamepad_input();
1024 }
1025
1026 void skip_intro() {
1027 intro_fade = std::min(intro_fade, 0.02f);
1028 intro_last_update = std::chrono::steady_clock::now() - std::chrono::milliseconds(INTRO_FADE_INTERVAL_MS);
1029 }
1030
1031 void finish_intro(const std::chrono::steady_clock::time_point &now) {
1032 intro_active = false;
1033 intro_fade = 0.0f;
1034 if (matrix_rain) {
1035 matrix_rain->set_opacity(0.0f);
1036 }
1037 game_started = true;
1038 last_fall = now;
1039 last_process = now;
1040 last_input_update = now;
1041 reset_held_piece_input();
1042 reset_held_gamepad_input();
1043 }
1044
1045 void update_intro(const std::chrono::steady_clock::time_point &now) {
1046 const auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - intro_last_update).count();
1047 if (elapsed_ms > static_cast<long long>(INTRO_FADE_INTERVAL_MS)) {
1048 intro_last_update = now;
1049 intro_fade -= INTRO_FADE_STEP;
1050 }
1051 if (intro_fade <= 0.0f) {
1052 finish_intro(now);
1053 }
1054 }
1055
1056 void render_intro(VkCommandBuffer cmd, const VkExtent2D &extent) {
1057 if (intro_sprite == nullptr || sprite_pipeline == VK_NULL_HANDLE || sprite_pipeline_layout == VK_NULL_HANDLE) {
1058 finish_intro(std::chrono::steady_clock::now());
1059 return;
1060 }
1061
1062 const float elapsed = std::chrono::duration<float>(std::chrono::steady_clock::now() - intro_start).count();
1063 intro_sprite->setShaderParams(elapsed, 0.0f, 0.0f, std::clamp(intro_fade, 0.0f, 1.0f));
1064 intro_sprite->drawSpriteRect(0, 0, static_cast<int>(extent.width), static_cast<int>(extent.height));
1065 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline);
1066 intro_sprite->renderSprites(cmd, sprite_pipeline_layout, extent.width, extent.height);
1067 intro_sprite->clearQueue();
1068 render_matrix_rain(cmd, extent, std::clamp(intro_fade, 0.0f, 1.0f));
1069 }
1070
1071 void draw_background(VkCommandBuffer cmd, const VkExtent2D &extent) {
1072 const int index = std::clamp(level - 1, 0, static_cast<int>(backgrounds.size()) - 1);
1073 mxvk::VK_Sprite *background = backgrounds[index];
1074 if (background == nullptr || sprite_pipeline == VK_NULL_HANDLE || sprite_pipeline_layout == VK_NULL_HANDLE) {
1075 return;
1076 }
1077 background->setShaderParams(background_time, 0.0f, 0.0f, 1.0f);
1078 background->drawSpriteRect(0, 0, static_cast<int>(extent.width), static_cast<int>(extent.height));
1079 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline);
1080 background->renderSprites(cmd, sprite_pipeline_layout, extent.width, extent.height);
1081 background->clearQueue();
1082 }
1083
1084 void draw_sprite_rect(mxvk::VK_Sprite *sprite, VkCommandBuffer cmd, const VkExtent2D &extent, int x, int y, int w, int h) {
1085 if (sprite == nullptr || w <= 0 || h <= 0 || sprite_pipeline == VK_NULL_HANDLE || sprite_pipeline_layout == VK_NULL_HANDLE) {
1086 return;
1087 }
1088 sprite->drawSpriteRect(x, y, w, h);
1089 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline);
1090 sprite->renderSprites(cmd, sprite_pipeline_layout, extent.width, extent.height);
1091 sprite->clearQueue();
1092 }
1093
1094 void draw_next_piece_preview(VkCommandBuffer cmd, const VkExtent2D &extent) {
1095 if (!game_started || intro_active || game_over || preview_border_sprite == nullptr || sprite_pipeline == VK_NULL_HANDLE || sprite_pipeline_layout == VK_NULL_HANDLE) {
1096 return;
1097 }
1098
1099 const int panel_size = std::min({180, static_cast<int>(static_cast<float>(extent.width) * 0.22f), static_cast<int>(static_cast<float>(extent.height) * 0.30f)});
1100 if (panel_size < 72) {
1101 return;
1102 }
1103
1104 const int margin = 24;
1105 const int panel_x = static_cast<int>(extent.width) - panel_size - margin;
1106 const int panel_y = 88;
1107 const int border = 4;
1108
1109 draw_sprite_rect(preview_border_sprite, cmd, extent, panel_x, panel_y, panel_size, border);
1110 draw_sprite_rect(preview_border_sprite, cmd, extent, panel_x, panel_y + panel_size - border, panel_size, border);
1111 draw_sprite_rect(preview_border_sprite, cmd, extent, panel_x, panel_y, border, panel_size);
1112 draw_sprite_rect(preview_border_sprite, cmd, extent, panel_x + panel_size - border, panel_y, border, panel_size);
1113
1114 printText("Next", panel_x + 12, panel_y - 28, SDL_Color{255, 255, 255, 255});
1115
1116 int min_x = next_piece.blocks[0].x;
1117 int max_x = next_piece.blocks[0].x;
1118 int min_y = next_piece.blocks[0].y;
1119 int max_y = next_piece.blocks[0].y;
1120 for (const Block &block : next_piece.blocks) {
1121 min_x = std::min(min_x, block.x);
1122 max_x = std::max(max_x, block.x);
1123 min_y = std::min(min_y, block.y);
1124 max_y = std::max(max_y, block.y);
1125 }
1126
1127 const float inner_padding = 28.0f;
1128 const float inner_size = static_cast<float>(panel_size) - inner_padding * 2.0f;
1129 const int cells_wide = max_x - min_x + 1;
1130 const int cells_high = max_y - min_y + 1;
1131 const int block_size = static_cast<int>(std::min(34.0f, inner_size / static_cast<float>(std::max(cells_wide, cells_high))));
1132 const float piece_w = static_cast<float>(cells_wide * block_size);
1133 const float piece_h = static_cast<float>(cells_high * block_size);
1134 const float center_x = static_cast<float>(panel_x) + static_cast<float>(panel_size) * 0.5f;
1135 const float center_y = static_cast<float>(panel_y) + static_cast<float>(panel_size) * 0.5f;
1136 const float origin_x = center_x - piece_w * 0.5f;
1137 const float origin_y = center_y - piece_h * 0.5f;
1138
1139 for (const Block &block : next_piece.blocks) {
1140 const int index = texture_index(block.type);
1141 if (index < 0 || index >= static_cast<int>(preview_block_sprites.size())) {
1142 continue;
1143 }
1144 mxvk::VK_Sprite *block_sprite = preview_block_sprites[static_cast<std::size_t>(index)];
1145 const float x = origin_x + static_cast<float>(block.x - min_x) * static_cast<float>(block_size);
1146 const float y = origin_y + static_cast<float>(block.y - min_y) * static_cast<float>(block_size);
1147 draw_sprite_rect(block_sprite, cmd, extent, static_cast<int>(x), static_cast<int>(y), block_size, block_size);
1148 }
1149 }
1150
1151 void render_matrix_rain(VkCommandBuffer cmd, const VkExtent2D &extent, float opacity) {
1152 if (matrix_rain == nullptr || sprite_pipeline == VK_NULL_HANDLE || sprite_pipeline_layout == VK_NULL_HANDLE) {
1153 return;
1154 }
1155 matrix_rain->set_opacity(opacity);
1156 matrix_rain->update_and_render(*this, static_cast<int>(extent.width), static_cast<int>(extent.height));
1157 mxvk::VK_Sprite *rain_sprite = matrix_rain->sprite();
1158 if (rain_sprite == nullptr) {
1159 return;
1160 }
1161 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline);
1162 rain_sprite->renderSprites(cmd, sprite_pipeline_layout, extent.width, extent.height);
1163 rain_sprite->clearQueue();
1164 }
1165
1166 [[nodiscard]] glm::vec3 block_position(int x, int y) const {
1167 const float world_x = (static_cast<float>(x) - (static_cast<float>(BOARD_WIDTH) - 1.0f) * 0.5f) * CUBE_SPACING;
1168 const float world_y = ((static_cast<float>(BOARD_HEIGHT) - 1.0f) * 0.5f - static_cast<float>(y)) * CUBE_SPACING;
1169 return glm::vec3(world_x, world_y, 0.0f);
1170 }
1171
1172 void draw_grid_backdrop(VkCommandBuffer cmd, uint32_t image_index, const glm::mat4 &view, const glm::mat4 &proj) {
1173 if (!grid_backdrop_model) {
1174 return;
1175 }
1176
1177 mxvk::UniformBufferObject ubo{};
1178 glm::mat4 model(1.0f);
1179 const float width = static_cast<float>(BOARD_WIDTH) * CUBE_SPACING;
1180 const float height = static_cast<float>(BOARD_HEIGHT) * CUBE_SPACING;
1181 model = glm::translate(model, glm::vec3(0.0f, 0.0f, -CUBE_SCALE * 0.68f));
1182 model = glm::scale(model, glm::vec3(width + CUBE_SPACING * 0.35f, height + CUBE_SPACING * 0.35f, CUBE_SCALE * 0.10f));
1183 ubo.model = model;
1184 ubo.view = view;
1185 ubo.proj = proj;
1186 ubo.fx = glm::vec4(0.16f, 0.16f, 0.17f, 0.62f);
1187 grid_backdrop_model->renderWithPushConstants(cmd, image_index, 0, ubo, false);
1188 }
1189
1190 [[nodiscard]] glm::mat4 block_matrix(int x, int y, float scale = CUBE_SCALE) const {
1191 glm::mat4 model(1.0f);
1192 model = glm::translate(model, block_position(x, y));
1193 model = glm::scale(model, glm::vec3(scale));
1194 return model;
1195 }
1196
1197 void draw_cube(VkCommandBuffer cmd,
1198 uint32_t image_index,
1199 BlockType type,
1200 int x,
1201 int y,
1202 const glm::mat4 &view,
1203 const glm::mat4 &proj) const {
1204 if (!cube_model) {
1205 return;
1206 }
1207
1208 mxvk::UniformBufferObject ubo{};
1209 ubo.model = block_matrix(x, y);
1210 ubo.view = view;
1211 ubo.proj = proj;
1212 const bool use_wildcard_texture = type == BlockType::Match || type == BlockType::Clear;
1213 const int index = texture_index(use_wildcard_texture ? BlockType::Match : type);
1214 glm::vec3 tint = BLOCK_TINTS[index];
1215 if (use_wildcard_texture) {
1216 tint = wildcard_color;
1217 }
1218 ubo.fx = glm::vec4(tint, use_wildcard_texture ? 2.0f : 1.0f);
1219 cube_model->renderWithPushConstants(cmd, image_index, static_cast<size_t>(index), ubo, false);
1220 }
1221
1222 void draw_frame(VkCommandBuffer cmd, uint32_t image_index, const glm::mat4 &view, const glm::mat4 &proj) const {
1223 for (int y = 0; y < BOARD_HEIGHT; ++y) {
1224 draw_frame_cube(cmd, image_index, -1, y, view, proj);
1225 draw_frame_cube(cmd, image_index, BOARD_WIDTH, y, view, proj);
1226 }
1227 for (int x = -1; x <= BOARD_WIDTH; ++x) {
1228 draw_frame_cube(cmd, image_index, x, BOARD_HEIGHT, view, proj);
1229 }
1230 }
1231
1232 void draw_frame_cube(VkCommandBuffer cmd,
1233 uint32_t image_index,
1234 int x,
1235 int y,
1236 const glm::mat4 &view,
1237 const glm::mat4 &proj) const {
1238 if (!cube_model) {
1239 return;
1240 }
1241
1242 mxvk::UniformBufferObject ubo{};
1243 ubo.model = block_matrix(x, y, CUBE_SCALE * 0.82f);
1244 ubo.view = view;
1245 ubo.proj = proj;
1246 ubo.fx = glm::vec4(0.58f, 0.62f, 0.66f, 1.0f);
1247 cube_model->renderWithPushConstants(cmd, image_index, FRAME_TEXTURE_INDEX, ubo, false);
1248 }
1249 };
1250} // namespace
1251
1252int main(int argc, char **argv) {
1253 try {
1254 Arguments args = proc_args(argc, argv);
1255 PuzzleDropWindow window(args.path, args.width, args.height, args.fullscreen, args.enable_vsync);
1256 window.loop();
1257 } catch (const mxvk::Exception &e) {
1258 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
1259 return EXIT_FAILURE;
1260 } catch (const ArgException<std::string> &e) {
1261 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
1262 return EXIT_FAILURE;
1263 } catch (const std::exception &e) {
1264 std::cerr << std::format("puzzle_drop: Exception: {}\n", e.what());
1265 return EXIT_FAILURE;
1266 }
1267 return EXIT_SUCCESS;
1268}
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
PuzzleDropWindow(const std::string &path, int width, int height, bool fullscreen, bool enable_vsync)
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
void proc() override
Execute one processing/update step.
void event(SDL_Event &e) override
Handle one SDL event.
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t image_index) override
Optional hook for derived classes to record extra draw commands.
std::string text() const
void renderSprites(VkCommandBuffer cmdBuffer, VkPipelineLayout pipelineLayout, uint32_t screenWidth, uint32_t screenHeight)
Record all queued draw commands into the given command buffer.
void setShaderParams(float p1=0.0f, float p2=0.0f, float p3=0.0f, float p4=0.0f)
Set up to four custom shader float parameters.
void clearQueue()
Discard all pending draw commands without rendering.
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
VkPipelineLayout sprite_pipeline_layout
Definition mxvk.hpp:543
VkExtent2D getSwapchainExtent() const noexcept
Get the current swapchain extent.
Definition mxvk.hpp:186
VkDevice device
Definition mxvk.hpp:485
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
bool getTextDimensions(const std::string &text, int &width, int &height)
Measure text dimensions in pixels.
Definition mxvk.cpp:3069
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
Definition mxvk.cpp:593
VkPipeline sprite_pipeline
Definition mxvk.hpp:544
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
High-level model wrapper integrated with MXVK dynamic rendering.
int texture_index(BlockType type)
Definition main.cpp:182
constexpr int BOARD_WIDTH
Definition main.cpp:32
bool same_or_match(BlockType actual, BlockType expected)
Definition main.cpp:186
constexpr int BOARD_HEIGHT
Definition main.cpp:33
bool is_play_block(BlockType type)
Definition main.cpp:178
const std::array< glm::vec3, 10 > BLOCK_TINTS
const std::array< std::string, PREVIEW_BLOCK_TEXTURE_COUNT > PREVIEW_BLOCK_TEXTURE_FILES
constexpr std::array< float, 3 > FALL_SECONDS
bool same_or_match(BlockType actual, BlockType expected)
RainConfig make_matrix_rain_config(const std::string &asset_root, bool binary_glyph_mode)
Definition rain.cpp:157
Utilities for loading and saving PNG images.
Definition mxvk.hpp:30
#define puzzle_drop_ASSET_DIR
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
void new_piece(int start_x, int start_y, std::mt19937 &rng)
void shift(ShiftDirection direction)
std::string color
Definition rain.hpp:19