MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
breakout.cpp
Go to the documentation of this file.
1#include <SDL3/SDL.h>
2
3#include <algorithm>
4#include <chrono>
5#include <cmath>
6#include <cstdlib>
7#include <format>
8#include <iostream>
9#include <memory>
10#include <string>
11#include <string_view>
12#include <vector>
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"
20#include "mxvk/mxvk.hpp"
23#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
24#include "mxvk/mxvk_sound.hpp"
25#endif
26
27#ifndef breakout_ASSET_DIR
28#define breakout_ASSET_DIR "."
29#endif
30
31namespace {
32 constexpr float GAME_LEFT = -5.0f;
33 constexpr float GAME_RIGHT = 5.0f;
34 constexpr float GAME_TOP = 3.75f;
35 constexpr float GAME_BOTTOM = -3.75f;
36 constexpr float PADDLE_SPEED = 5.0f;
37 constexpr float BALL_SPEED = 4.0f;
38 constexpr float BALL_RADIUS = 0.25f;
39 constexpr float MODEL_SCALE = 1.0f;
40 constexpr float BALL_MODEL_SCALE = 0.05f;
41 constexpr float PI = 3.14159265358979323846f;
42 constexpr float BALL_ROLL_DEGREES_PER_UNIT = 360.0f / (2.0f * PI * BALL_RADIUS);
43 constexpr float ROTATION_SPEED = 50.0f;
44 constexpr float DEFAULT_ZOOM = 0.8f;
45 constexpr float MIN_ZOOM = 0.45f;
46 constexpr float MAX_ZOOM = 1.6f;
47 constexpr float ZOOM_SPEED = 0.8f;
48 constexpr Sint16 GAMEPAD_DEAD_ZONE = 8000;
49 constexpr int MAX_MISSES = 3;
50 constexpr int BLOCK_ROWS = 5;
51 constexpr int BLOCK_COLUMNS = 11;
52
53 struct Box {
54 glm::vec3 position{0.0f};
55 glm::vec3 size{1.0f};
56 glm::vec3 velocity{0.0f};
57 float rotation = 0.0f;
58 };
59
60 struct Block {
62 std::unique_ptr<mxvk::VKAbstractModel> model{};
63 std::string_view texture_manifest{};
64 bool destroyed = false;
65 bool rotating = false;
66 float current_rotation = 0.0f;
67 };
68
69 struct Ball {
71 float radius = 0.25f;
72 bool stuck = true;
73 };
74
75 [[nodiscard]] float clampf(float value, float low, float high) {
76 return std::max(low, std::min(value, high));
77 }
78
79 [[nodiscard]] bool check_collision(const Ball &ball, const Box &object) {
80 const glm::vec3 half_extents(object.size.x * 0.5f, object.size.y * 0.5f, 0.0f);
81 const glm::vec3 difference = ball.box.position - object.position;
82 const glm::vec3 clamped = glm::clamp(difference, -half_extents, half_extents);
83 const glm::vec3 closest = object.position + clamped;
84 return glm::length(closest - ball.box.position) < ball.radius;
85 }
86
87 class BreakoutWindow final : public mxvk::VK_Window {
88 public:
89 BreakoutWindow(const std::string &path, int width, int height, bool fullscreen, bool enable_vsync)
90 : mxvk::VK_Window("MXVK 3D Breakout", width, height, fullscreen, MXVK_VALIDATION, enable_vsync),
91 asset_root((path.empty() || path == ".") ? std::string(breakout_ASSET_DIR) : path),
92 data_root(asset_root + "/data"),
93 shader_root(data_root) {
94 setClearColor(0.0f, 0.0f, 0.0f, 1.0f);
95 setFont(data_root + "/font.ttf", 24);
96
97 const std::string sprite_vertex = shader_root + "/sprite.vert.spv";
98 const std::string background_fragment = shader_root + "/breakout_background.frag.spv";
99 background = createSprite(data_root + "/intro.png", sprite_vertex, background_fragment);
100 game_background = createSprite(data_root + "/bg.png", sprite_vertex, background_fragment);
101 intro_model = load_model("intro_manifest.txt");
102 paddle_model = load_model("texture_manifest.txt");
103 ball_model = load_model("better_sphere.obj", "moon_manifest.txt");
104#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
105 mixer = std::make_unique<mxvk::VK_Mixer>();
106 background_music = mixer->loadMusic(data_root + "/breakout.ogg");
107 ping_sound = mixer->loadWav(data_root + "/ping.wav");
108 clear_sound = mixer->loadWav(data_root + "/pop.wav");
109 die_sound = mixer->loadWav(data_root + "/die.wav");
110 ensure_background_music_playing();
111#endif
112 open_controller();
113 reset_game();
114 }
115
116 ~BreakoutWindow() override {
117 if (device != VK_NULL_HANDLE) {
118 vkDeviceWaitIdle(device);
119 }
120 cleanup_models();
121 }
122
123 void onSwapchainRecreated() override {
124 if (intro_model) {
125 intro_model->resize(this);
126 }
127 if (paddle_model) {
128 paddle_model->resize(this);
129 }
130 if (ball_model) {
131 ball_model->resize(this);
132 }
133 for (Block &block : blocks) {
134 if (block.model) {
135 block.model->resize(this);
136 }
137 }
138 }
139
140 void event(SDL_Event &e) override {
141 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
142 exit();
143 return;
144 }
145
146 if (controller.connectEvent(e)) {
147 if (!controller.active()) {
148 open_controller();
149 }
150 return;
151 }
152
153 if (screen == Screen::Intro) {
154 if ((e.type == SDL_EVENT_KEY_DOWN && (e.key.key == SDLK_RETURN || e.key.key == SDLK_SPACE)) ||
155 (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) ||
156 e.type == SDL_EVENT_FINGER_DOWN ||
157 is_gamepad_start_button(e)) {
158 screen = Screen::Game;
159 reset_game();
160 }
161 return;
162 }
163
164 if (screen == Screen::GameOver) {
165 if ((e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_RETURN) || is_gamepad_start_button(e)) {
166 reset_game();
167 screen = Screen::Intro;
168 }
169 return;
170 }
171
172 if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
173 if (e.key.key == SDLK_SPACE || e.key.key == SDLK_RETURN) {
174 launch_ball();
175 } else if (e.key.key == SDLK_R) {
176 reset_game();
177 screen = Screen::Game;
178 } else if (e.key.key == SDLK_BACKSPACE) {
179 screen = Screen::Intro;
180 } else if (e.key.key == SDLK_1) {
181 grid_rotation = -33.4f;
182 grid_y_rotation = -18.4f;
183 } else if (e.key.key == SDLK_2) {
184 grid_rotation = -21.4f;
185 grid_y_rotation = 31.15f;
186 } else if (e.key.key == SDLK_3) {
187 grid_rotation = -47.25f;
188 grid_y_rotation = 0.85f;
189 }
190 }
191
192 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
193 const auto button = static_cast<SDL_GamepadButton>(e.gbutton.button);
194 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
195 launch_ball();
196 } else if (button == SDL_GAMEPAD_BUTTON_EAST) {
197 reset_game();
198 screen = Screen::Game;
199 } else if (button == SDL_GAMEPAD_BUTTON_BACK) {
200 screen = Screen::Intro;
201 } else if (button == SDL_GAMEPAD_BUTTON_NORTH) {
202 grid_rotation = 0.0f;
203 grid_y_rotation = 0.0f;
204 zoom = DEFAULT_ZOOM;
205 }
206 }
207
208 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
209 launch_ball();
210 }
211
212 if (e.type == SDL_EVENT_FINGER_DOWN) {
213 const Uint64 current_tap_time = SDL_GetTicks();
214 if (current_tap_time - last_tap_time <= DOUBLE_TAP_THRESHOLD_MS) {
215 launch_ball();
216 }
217 last_tap_time = current_tap_time;
218 } else if (e.type == SDL_EVENT_FINGER_MOTION) {
219 paddle.position.x = (e.tfinger.x * (GAME_RIGHT - GAME_LEFT)) + GAME_LEFT;
220 clamp_paddle();
221 }
222 }
223
224 void proc() override {
225 const auto now = std::chrono::steady_clock::now();
226 delta_seconds = std::chrono::duration<float>(now - last_frame).count();
227 delta_seconds = std::min(delta_seconds, 0.05f);
228 last_frame = now;
229 background_time += delta_seconds;
230 ensure_background_music_playing();
231
232 const int width = screen_width();
233 if (screen == Screen::Intro) {
234 printText("Press Enter or Tap to Start", 25, 25, {255, 255, 255, 255});
235 return;
236 }
237
238 if (screen == Screen::GameOver) {
239 print_centered_game_over(width);
240 return;
241 }
242
243 update_game(delta_seconds);
244 print_centered_score(width);
245 print_tries();
246 }
247
248 void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t image_index) override {
249 const VkExtent2D extent = getSwapchainExtent();
250 draw_background(cmd, extent);
251
252 const float aspect = (extent.height > 0U) ? static_cast<float>(extent.width) / static_cast<float>(extent.height) : 1.0f;
253 if (screen == Screen::Intro) {
254 glm::mat4 view = glm::lookAt(glm::vec3(0.0f, 0.0f, 10.0f), glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
255 glm::mat4 proj = glm::perspective(glm::radians(10.0f), aspect, 0.1f, 100.0f);
256 proj[1][1] *= -1.0f;
257 intro_rotation += delta_seconds * 50.0f;
258 draw_model(cmd, image_index, *intro_model, Box{glm::vec3(0.0f), glm::vec3(1.0f), glm::vec3(0.0f), intro_rotation}, view, proj, glm::vec3(1.0f));
259 return;
260 }
261
262 glm::mat4 view(1.0f);
263 view = glm::rotate(view, glm::radians(grid_rotation), glm::vec3(1.0f, 0.0f, 0.0f));
264 view = glm::rotate(view, glm::radians(grid_y_rotation), glm::vec3(0.0f, 1.0f, 0.0f));
265
266 glm::mat4 proj = glm::orthoRH_ZO(GAME_LEFT / zoom, GAME_RIGHT / zoom, GAME_BOTTOM / zoom, GAME_TOP / zoom, -100.0f, 100.0f);
267 proj[1][1] *= -1.0f;
268
269 Box paddle_draw = paddle;
270 paddle_draw.rotation = paddle_current_rotation;
271 draw_model(cmd, image_index, *paddle_model, paddle_draw, view, proj, glm::vec3(1.0f));
272 draw_model(cmd, image_index, *ball_model, ball.box, view, proj, glm::vec3(1.0f), BALL_MODEL_SCALE);
273 for (const Block &block : blocks) {
274 if (block.destroyed) {
275 continue;
276 }
277 Box draw = block.box;
278 draw.rotation = block.current_rotation;
279 draw_model(cmd, image_index, *block.model, draw, view, proj, glm::vec3(1.0f));
280 }
281 }
282
283 private:
284 enum class Screen {
285 Intro,
286 Game,
287 GameOver,
288 Complete
289 };
290
291 std::string asset_root;
292 std::string data_root;
293 std::string shader_root;
294 Screen screen = Screen::Intro;
295 mxvk::VK_Sprite *background = nullptr;
296 mxvk::VK_Sprite *game_background = nullptr;
297 std::unique_ptr<mxvk::VKAbstractModel> intro_model{};
298 std::unique_ptr<mxvk::VKAbstractModel> paddle_model{};
299 std::unique_ptr<mxvk::VKAbstractModel> ball_model{};
300 Box paddle{glm::vec3(0.0f, -3.5f, 0.0f), glm::vec3(2.0f, 0.5f, 1.0f)};
301 Ball ball{};
302 std::vector<Block> blocks{};
303 int score = 0;
304 int misses = 0;
305 float grid_rotation = 0.0f;
306 float grid_y_rotation = 0.0f;
307 float intro_rotation = 0.0f;
308 float paddle_current_rotation = 0.0f;
309 float background_time = 0.0f;
310 float delta_seconds = 0.0f;
311 float zoom = DEFAULT_ZOOM;
312 bool paddle_rotating = false;
313 mxvk::VK_Controller controller{};
314 Uint64 last_tap_time = 0;
315 std::chrono::steady_clock::time_point last_frame{std::chrono::steady_clock::now()};
316#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
317 std::unique_ptr<mxvk::VK_Mixer> mixer{};
318 int background_music = -1;
319 int ping_sound = -1;
320 int clear_sound = -1;
321 int die_sound = -1;
322#endif
323 static constexpr Uint64 DOUBLE_TAP_THRESHOLD_MS = 300;
324
325#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
326 void ensure_background_music_playing() {
327 if (!mixer || background_music < 0) {
328 return;
329 }
330 if (!mixer->isMusicPlaying(background_music)) {
331 if (mixer->playMusic(background_music, -1) != 0) {
332 throw mxvk::Exception("Could not start Breakout background music");
333 }
334 }
335 }
336#else
337 void ensure_background_music_playing() {}
338#endif
339
340 void reset_game() {
341 score = 0;
342 misses = 0;
343 paddle.position = glm::vec3(0.0f, -3.5f, 0.0f);
344 paddle_current_rotation = 0.0f;
345 paddle_rotating = false;
346 reset_ball();
347 cleanup_block_models();
348 blocks.clear();
349 blocks.reserve(BLOCK_ROWS * BLOCK_COLUMNS);
350 for (int row = 0; row < BLOCK_ROWS; ++row) {
351 for (int column = 0; column < BLOCK_COLUMNS; ++column) {
352 Block block{};
353 block.box.position = glm::vec3(GAME_LEFT + static_cast<float>(column), 3.0f - static_cast<float>(row) * 0.6f, 0.0f);
354 block.box.size = glm::vec3(1.0f, 0.5f, 1.0f);
355 block.texture_manifest = block_manifest(std::rand() % 4);
356 block.model = load_model(block.texture_manifest);
357 blocks.push_back(std::move(block));
358 }
359 }
360 }
361
362 void reset_ball() {
363 ball.radius = BALL_RADIUS;
364 ball.stuck = true;
365 ball.box.size = glm::vec3(ball.radius * 2.0f);
366 ball.box.position = paddle.position + glm::vec3(0.0f, 0.5f, 0.0f);
367 ball.box.velocity = glm::vec3(2.5f, 2.5f, 0.0f);
368 ball.box.rotation = 0.0f;
369 }
370
371 void update_game(float delta) {
372 if (screen == Screen::Complete || screen == Screen::GameOver) {
373 return;
374 }
375
376 const bool *keys = SDL_GetKeyboardState(nullptr);
377 if (keys[SDL_SCANCODE_LEFT] || keys[SDL_SCANCODE_H]) {
378 paddle.position.x -= PADDLE_SPEED * delta;
379 }
380 if (keys[SDL_SCANCODE_RIGHT] || keys[SDL_SCANCODE_L]) {
381 paddle.position.x += PADDLE_SPEED * delta;
382 }
383 if (keys[SDL_SCANCODE_W]) {
384 grid_rotation -= ROTATION_SPEED * delta;
385 }
386 if (keys[SDL_SCANCODE_S]) {
387 grid_rotation += ROTATION_SPEED * delta;
388 }
389 if (keys[SDL_SCANCODE_A]) {
390 grid_y_rotation -= ROTATION_SPEED * delta;
391 }
392 if (keys[SDL_SCANCODE_D]) {
393 grid_y_rotation += ROTATION_SPEED * delta;
394 }
395 if (keys[SDL_SCANCODE_PAGEUP]) {
396 adjust_zoom(ZOOM_SPEED * delta);
397 }
398 if (keys[SDL_SCANCODE_PAGEDOWN]) {
399 adjust_zoom(-ZOOM_SPEED * delta);
400 }
401 if (keys[SDL_SCANCODE_Q]) {
402 grid_rotation = 0.0f;
403 grid_y_rotation = 0.0f;
404 zoom = DEFAULT_ZOOM;
405 }
406 update_gamepad_controls(delta);
407 if (grid_rotation >= 360.0f) {
408 grid_rotation -= 360.0f;
409 }
410 if (grid_rotation <= -360.0f) {
411 grid_rotation += 360.0f;
412 }
413 if (grid_y_rotation >= 360.0f) {
414 grid_y_rotation -= 360.0f;
415 }
416 if (grid_y_rotation <= -360.0f) {
417 grid_y_rotation += 360.0f;
418 }
419 clamp_paddle();
420
421 if (paddle_rotating) {
422 paddle_current_rotation += 360.0f * delta;
423 if (paddle_current_rotation >= 360.0f) {
424 paddle_current_rotation = 0.0f;
425 paddle_rotating = false;
426 }
427 }
428
429 if (ball.stuck) {
430 ball.box.position = paddle.position + glm::vec3(0.0f, 0.5f, 0.0f);
431 } else {
432 move_ball(delta);
433 }
434
435 bool all_destroyed = true;
436 for (Block &block : blocks) {
437 if (block.rotating) {
438 block.current_rotation += 360.0f * delta;
439 if (block.current_rotation >= 360.0f) {
440 block.current_rotation = 0.0f;
441 block.rotating = false;
442 block.destroyed = true;
443 }
444 }
445 all_destroyed = all_destroyed && block.destroyed;
446 }
447 if (all_destroyed) {
448 screen = Screen::Intro;
449 }
450 }
451
452 void move_ball(float delta) {
453 const glm::vec3 distance = ball.box.velocity * delta;
454 ball.box.position += distance;
455 ball.box.rotation = std::fmod(ball.box.rotation + distance.y * BALL_ROLL_DEGREES_PER_UNIT, 360.0f);
456 if (ball.box.rotation < 0.0f) {
457 ball.box.rotation += 360.0f;
458 }
459 if (ball.box.position.x <= GAME_LEFT + ball.radius) {
460 ball.box.position.x = GAME_LEFT + ball.radius;
461 ball.box.velocity.x = std::abs(ball.box.velocity.x);
462 }
463 if (ball.box.position.x >= GAME_RIGHT - ball.radius) {
464 ball.box.position.x = GAME_RIGHT - ball.radius;
465 ball.box.velocity.x = -std::abs(ball.box.velocity.x);
466 }
467 if (ball.box.position.y >= GAME_TOP - ball.radius) {
468 ball.box.position.y = GAME_TOP - ball.radius;
469 ball.box.velocity.y = -std::abs(ball.box.velocity.y);
470 }
471
472 if (check_collision(ball, paddle) && ball.box.velocity.y < 0.0f) {
473 const float distance = (ball.box.position.x - paddle.position.x) / (paddle.size.x * 0.5f);
474 ball.box.velocity.x = clampf(distance, -1.0f, 1.0f) * BALL_SPEED;
475 ball.box.velocity.y = std::abs(ball.box.velocity.y);
476 ball.box.position.y = paddle.position.y + paddle.size.y * 0.5f + ball.radius;
477 normalize_ball_velocity();
478 if (!paddle_rotating) {
479 paddle_current_rotation = 0.0f;
480 paddle_rotating = true;
481 }
482 play_ping();
483 }
484
485 for (Block &block : blocks) {
486 if (block.destroyed || block.rotating || !check_collision(ball, block.box)) {
487 continue;
488 }
489 block.rotating = true;
490 block.current_rotation = 0.0f;
491 score += 10;
492 ball.box.velocity.y = -ball.box.velocity.y;
493 if (ball.box.position.y > block.box.position.y) {
494 ball.box.position.y += ball.radius;
495 } else {
496 ball.box.position.y -= ball.radius;
497 }
498 play_clear();
499 break;
500 }
501
502 if (ball.box.position.y <= GAME_BOTTOM) {
503 ++misses;
504 play_die();
505 if (misses >= MAX_MISSES) {
506 ball.stuck = true;
507 screen = Screen::GameOver;
508 return;
509 }
510 reset_ball();
511 }
512 }
513
514 void launch_ball() {
515 if (!ball.stuck) {
516 return;
517 }
518 ball.stuck = false;
519 }
520
521 void normalize_ball_velocity() {
522 const float speed = glm::length(ball.box.velocity);
523 if (speed > 0.001f) {
524 ball.box.velocity = (ball.box.velocity / speed) * BALL_SPEED;
525 }
526 }
527
528 void clamp_paddle() {
529 paddle.position.x = clampf(paddle.position.x, GAME_LEFT, GAME_RIGHT);
530 }
531
532 void adjust_zoom(float delta) {
533 zoom = clampf(zoom + delta, MIN_ZOOM, MAX_ZOOM);
534 }
535
536 [[nodiscard]] static float normalized_gamepad_axis(Sint16 value) {
537 if (std::abs(static_cast<int>(value)) <= GAMEPAD_DEAD_ZONE) {
538 return 0.0f;
539 }
540 if (value < 0) {
541 return static_cast<float>(value) / 32768.0f;
542 }
543 return static_cast<float>(value) / 32767.0f;
544 }
545
546 [[nodiscard]] bool gamepad_button(SDL_GamepadButton button) const {
547 return controller.active() && controller.getButton(button);
548 }
549
550 void update_gamepad_controls(float delta) {
551 if (!controller.active()) {
552 return;
553 }
554
555 const float left_x = normalized_gamepad_axis(controller.getAxis(SDL_GAMEPAD_AXIS_LEFTX));
556 const float right_x = normalized_gamepad_axis(controller.getAxis(SDL_GAMEPAD_AXIS_RIGHTX));
557 const float right_y = normalized_gamepad_axis(controller.getAxis(SDL_GAMEPAD_AXIS_RIGHTY));
558
559 paddle.position.x += left_x * PADDLE_SPEED * delta;
560 if (gamepad_button(SDL_GAMEPAD_BUTTON_DPAD_LEFT)) {
561 paddle.position.x -= PADDLE_SPEED * delta;
562 }
563 if (gamepad_button(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)) {
564 paddle.position.x += PADDLE_SPEED * delta;
565 }
566
567 grid_y_rotation += right_x * ROTATION_SPEED * delta;
568 grid_rotation += right_y * ROTATION_SPEED * delta;
569
570 if (gamepad_button(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)) {
571 adjust_zoom(-ZOOM_SPEED * delta);
572 }
573 if (gamepad_button(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER)) {
574 adjust_zoom(ZOOM_SPEED * delta);
575 }
576 }
577
578 [[nodiscard]] static bool is_gamepad_start_button(const SDL_Event &e) {
579 if (e.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
580 return false;
581 }
582 const auto button = static_cast<SDL_GamepadButton>(e.gbutton.button);
583 return button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START;
584 }
585
586 bool open_controller() {
587 for (int index = 0; index < mxvk::VK_Controller::joysticks(); ++index) {
588 if (controller.open(index)) {
589 return true;
590 }
591 }
592 return false;
593 }
594
595 void draw_model(VkCommandBuffer cmd,
596 uint32_t image_index,
597 mxvk::VKAbstractModel &model,
598 const Box &box,
599 const glm::mat4 &view,
600 const glm::mat4 &proj,
601 const glm::vec3 &tint,
602 float model_scale = MODEL_SCALE) {
603 mxvk::UniformBufferObject ubo{};
604 ubo.model = glm::translate(glm::mat4(1.0f), box.position);
605 ubo.model = glm::rotate(ubo.model, glm::radians(box.rotation), glm::vec3(1.0f, 0.0f, 0.0f));
606 ubo.model = glm::scale(ubo.model, box.size * model_scale);
607 ubo.view = view;
608 ubo.proj = proj;
609 ubo.fx = glm::vec4(tint, 1.0f);
610 model.updateUBO(image_index, ubo);
611 model.render(cmd, image_index, false);
612 }
613
614 void draw_background(VkCommandBuffer cmd, const VkExtent2D &extent) {
615 mxvk::VK_Sprite *sprite = (screen == Screen::Intro) ? background : game_background;
616 if (sprite == nullptr || extent.width == 0U || extent.height == 0U) {
617 return;
618 }
619 sprite->setShaderParams(background_time, 0.7f, 0.0f, 0.0f);
620 sprite->drawSpriteRect(0, 0, static_cast<int>(extent.width), static_cast<int>(extent.height));
621 renderStandaloneSprite(*sprite, cmd);
622 sprite->clearQueue();
623 }
624
625 void print_centered_score(int width) {
626 const std::string text = std::format("Score: {}", score);
627 int text_width = 0;
628 int text_height = 0;
629 if (getTextDimensions(text, text_width, text_height)) {
630 printText(text, (width - text_width) / 2, 0, {255, 255, 255, 255});
631 return;
632 }
633 printText(text, width / 2 - 60, 0, {255, 255, 255, 255});
634 }
635
636 void print_tries() {
637 const int tries_left = std::max(0, MAX_MISSES - misses);
638 printText(std::format("Tries: {}", tries_left), 25, 0, {255, 255, 255, 255});
639 }
640
641 void print_centered_game_over(int width) {
642 const std::string text = "Game Over - Press Enter to Restart";
643 int text_width = 0;
644 int text_height = 0;
645 if (getTextDimensions(text, text_width, text_height)) {
646 printText(text, (width - text_width) / 2, screen_height() / 2 - text_height / 2, {255, 255, 255, 255});
647 return;
648 }
649 printText(text, width / 2 - 200, screen_height() / 2, {255, 255, 255, 255});
650 }
651
652 [[nodiscard]] std::unique_ptr<mxvk::VKAbstractModel> load_model(std::string_view manifest) {
653 return load_model("cube.mxmod.z", manifest);
654 }
655
656 [[nodiscard]] std::unique_ptr<mxvk::VKAbstractModel> load_model(std::string_view model_path, std::string_view manifest) {
657 auto model = std::make_unique<mxvk::VKAbstractModel>();
658 model->load(this, data_root + "/" + std::string(model_path), data_root + "/" + std::string(manifest), data_root, 1.0f);
659 model->setShaders(this,
660 shader_root + "/breakout_model.vert.spv",
661 shader_root + "/breakout_model.frag.spv");
662 return model;
663 }
664
665 [[nodiscard]] static std::string_view block_manifest(int index) {
666 switch (index) {
667 case 0:
668 return "texture0_manifest.txt";
669 case 1:
670 return "texture1_manifest.txt";
671 case 2:
672 return "texture2_manifest.txt";
673 default:
674 return "texture3_manifest.txt";
675 }
676 }
677
678 void cleanup_block_models() {
679 for (Block &block : blocks) {
680 if (block.model) {
681 block.model->cleanup(this);
682 }
683 }
684 }
685
686 void cleanup_models() {
687 cleanup_block_models();
688 if (intro_model) {
689 intro_model->cleanup(this);
690 }
691 if (paddle_model) {
692 paddle_model->cleanup(this);
693 }
694 if (ball_model) {
695 ball_model->cleanup(this);
696 }
697 }
698
699 [[nodiscard]] int screen_width() const {
700 return swapchain_extent.width > 0U ? static_cast<int>(swapchain_extent.width) : 1280;
701 }
702
703 [[nodiscard]] int screen_height() const {
704 return swapchain_extent.height > 0U ? static_cast<int>(swapchain_extent.height) : 720;
705 }
706
707 void play_ping() {
708#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
709 if (mixer != nullptr && ping_sound >= 0) {
710 mixer->playWav(ping_sound, 0, 0);
711 }
712#endif
713 }
714
715 void play_clear() {
716#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
717 if (mixer != nullptr && clear_sound >= 0) {
718 mixer->playWav(clear_sound, 0, 1);
719 }
720#endif
721 }
722
723 void play_die() {
724#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
725 if (mixer != nullptr && die_sound >= 0) {
726 mixer->playWav(die_sound, 0, 2);
727 }
728#endif
729 }
730 };
731} // namespace
732
733int main(int argc, char **argv) {
734 try {
735 Arguments args = proc_args(argc, argv);
736 BreakoutWindow window(args.path, args.width, args.height, args.fullscreen, args.enable_vsync);
737 window.loop();
738 } catch (const mxvk::Exception &e) {
739 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
740 return EXIT_FAILURE;
741 } catch (const ArgException<std::string> &e) {
742 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
743 return EXIT_FAILURE;
744 }
745 return EXIT_SUCCESS;
746}
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
#define breakout_ASSET_DIR
Definition breakout.cpp:28
Exception thrown by Argz::proc() on unrecognised or malformed options.
Definition argz.hpp:178
void event(SDL_Event &e) override
Handle one SDL event.
Definition breakout.cpp:140
void proc() override
Execute one processing/update step.
Definition breakout.cpp:224
BreakoutWindow(const std::string &path, int width, int height, bool fullscreen, bool enable_vsync)
Definition breakout.cpp:89
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t image_index) override
Optional hook for derived classes to record extra draw commands.
Definition breakout.cpp:248
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
Definition breakout.cpp:123
std::string text() const
void updateUBO(uint32_t imageIndex, const UniformBufferObject &ubo)
Update one per-frame UBO payload.
void load(VK_Window *window, const std::string &modelPath, const std::string &textureManifestPath, const std::string &textureBasePath, float scale=1.0f)
Load mesh/texture resources and build Vulkan state.
void setShaders(VK_Window *window, const std::string &vertSpv, const std::string &fragSpv)
Configure custom shader paths and rebuild pipelines.
void render(VkCommandBuffer cmd, uint32_t imageIndex, bool wireframe=false) const
Record draw commands for this model.
static int joysticks()
Return the number of connected joysticks/controllers.
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
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
VkExtent2D swapchain_extent
Definition mxvk.hpp:495
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
void renderStandaloneSprite(VK_Sprite &sprite, VkCommandBuffer cmd)
Render one standalone sprite using the window's shared sprite pipeline.
Definition mxvk.cpp:1142
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.
SDL3 joystick and gamepad RAII wrappers.
SDL3_mixer audio subsystem wrapper.
constexpr float BALL_ROLL_DEGREES_PER_UNIT
Definition breakout.cpp:42
constexpr Sint16 GAMEPAD_DEAD_ZONE
Definition breakout.cpp:48
bool check_collision(const Ball &ball, const Box &object)
Definition breakout.cpp:79
float clampf(float value, float low, float high)
Definition breakout.cpp:75
constexpr float BALL_RADIUS
Definition main.cpp:35
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
std::unique_ptr< mxvk::VKAbstractModel > model
Definition breakout.cpp:62