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