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) {
89 setFont(data_root +
"/font.ttf", 24);
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();
111 if (
device != VK_NULL_HANDLE) {
119 intro_model->resize(
this);
122 paddle_model->resize(
this);
125 ball_model->resize(
this);
127 for (
Block &block : blocks) {
129 block.model->resize(
this);
135 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
140 if (controller.connectEvent(e)) {
141 if (!controller.active()) {
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;
155 if (screen == Screen::GameOver) {
156 if ((e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_RETURN) || is_gamepad_start_button(e)) {
158 screen = Screen::Intro;
163 if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
164 if (e.key.key == SDLK_SPACE || e.key.key == SDLK_RETURN) {
166 }
else if (e.key.key == SDLK_R) {
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;
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) {
187 }
else if (button == SDL_GAMEPAD_BUTTON_EAST) {
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;
199 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
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) {
208 last_tap_time = current_tap_time;
209 }
else if (e.type == SDL_EVENT_FINGER_MOTION) {
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);
220 background_time += delta_seconds;
221 ensure_background_music_playing();
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});
229 if (screen == Screen::GameOver) {
230 print_centered_game_over(width);
234 update_game(delta_seconds);
235 print_centered_score(width);
241 draw_background(cmd, extent);
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);
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));
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));
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) {
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));
275 enum class Screen { Intro, Game, GameOver, Complete };
277 std::string asset_root;
278 std::string data_root;
279 std::string shader_root;
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)};
288 std::vector<Block> blocks{};
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;
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;
306 int clear_sound = -1;
309 static constexpr Uint64 DOUBLE_TAP_THRESHOLD_MS = 300;
311#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
312 void ensure_background_music_playing() {
313 if (!mixer || background_music < 0) {
316 if (!mixer->isMusicPlaying(background_music)) {
317 if (mixer->playMusic(background_music, -1) != 0) {
318 throw mxvk::Exception(
"Could not start Breakout background music");
323 void ensure_background_music_playing() {}
329 paddle.position = glm::vec3(0.0f, -3.5f, 0.0f);
330 paddle_current_rotation = 0.0f;
331 paddle_rotating =
false;
333 cleanup_block_models();
335 blocks.reserve(BLOCK_ROWS * BLOCK_COLUMNS);
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));
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;
357 void update_game(
float delta) {
358 if (screen == Screen::Complete || screen == Screen::GameOver) {
362 const bool *keys = SDL_GetKeyboardState(
nullptr);
363 if (keys[SDL_SCANCODE_LEFT] || keys[SDL_SCANCODE_H]) {
366 if (keys[SDL_SCANCODE_RIGHT] || keys[SDL_SCANCODE_L]) {
369 if (keys[SDL_SCANCODE_W]) {
372 if (keys[SDL_SCANCODE_S]) {
375 if (keys[SDL_SCANCODE_A]) {
378 if (keys[SDL_SCANCODE_D]) {
381 if (keys[SDL_SCANCODE_PAGEUP]) {
382 adjust_zoom(ZOOM_SPEED * delta);
384 if (keys[SDL_SCANCODE_PAGEDOWN]) {
385 adjust_zoom(-ZOOM_SPEED * delta);
387 if (keys[SDL_SCANCODE_Q]) {
388 grid_rotation = 0.0f;
389 grid_y_rotation = 0.0f;
392 update_gamepad_controls(delta);
393 if (grid_rotation >= 360.0f) {
394 grid_rotation -= 360.0f;
396 if (grid_rotation <= -360.0f) {
397 grid_rotation += 360.0f;
399 if (grid_y_rotation >= 360.0f) {
400 grid_y_rotation -= 360.0f;
402 if (grid_y_rotation <= -360.0f) {
403 grid_y_rotation += 360.0f;
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;
416 ball.box.position = paddle.position + glm::vec3(0.0f, 0.5f, 0.0f);
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;
431 all_destroyed = all_destroyed && block.destroyed;
434 screen = Screen::Intro;
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;
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);
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);
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);
459 const float distance = (ball.box.position.x - paddle.position.x) / (paddle.size.x * 0.5f);
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;
471 for (Block &block : blocks) {
472 if (block.destroyed || block.rotating || !
check_collision(ball, block.box)) {
475 block.rotating =
true;
476 block.current_rotation = 0.0f;
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;
482 ball.box.position.y -= ball.radius;
488 if (ball.box.position.y <= GAME_BOTTOM) {
491 if (misses >= MAX_MISSES) {
493 screen = Screen::GameOver;
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;
514 void clamp_paddle() { paddle.position.x =
clampf(paddle.position.x, GAME_LEFT, GAME_RIGHT); }
516 void adjust_zoom(
float delta) { zoom =
clampf(zoom + delta, MIN_ZOOM, MAX_ZOOM); }
518 [[nodiscard]]
static float normalized_gamepad_axis(Sint16 value) {
519 if (std::abs(
static_cast<int>(value)) <= GAMEPAD_DEAD_ZONE) {
523 return static_cast<float>(value) / 32768.0f;
525 return static_cast<float>(value) / 32767.0f;
528 [[nodiscard]]
bool gamepad_button(SDL_GamepadButton button)
const {
return controller.active() && controller.getButton(button); }
530 void update_gamepad_controls(
float delta) {
531 if (!controller.active()) {
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));
540 if (gamepad_button(SDL_GAMEPAD_BUTTON_DPAD_LEFT)) {
543 if (gamepad_button(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)) {
550 if (gamepad_button(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)) {
551 adjust_zoom(-ZOOM_SPEED * delta);
553 if (gamepad_button(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER)) {
554 adjust_zoom(ZOOM_SPEED * delta);
558 [[nodiscard]]
static bool is_gamepad_start_button(
const SDL_Event &e) {
559 if (e.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
562 const auto button =
static_cast<SDL_GamepadButton
>(e.gbutton.button);
563 return button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START;
566 bool open_controller() {
568 if (controller.open(index)) {
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);
582 ubo.
fx = glm::vec4(tint, 1.0f);
584 model.
render(cmd, image_index,
false);
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) {
593 sprite->
drawSpriteRect(0, 0,
static_cast<int>(extent.width),
static_cast<int>(extent.height));
598 void print_centered_score(
int width) {
599 const std::string text = std::format(
"Score: {}", score);
603 printText(text, (width - text_width) / 2, 0, {255, 255, 255, 255});
606 printText(text, width / 2 - 60, 0, {255, 255, 255, 255});
610 const int tries_left = std::max(0, MAX_MISSES - misses);
611 printText(std::format(
"Tries: {}", tries_left), 25, 0, {255, 255, 255, 255});
614 void print_centered_game_over(
int width) {
615 const std::string text =
"Game Over - Press Enter to Restart";
619 printText(text, (width - text_width) / 2, screen_height() / 2 - text_height / 2, {255, 255, 255, 255});
622 printText(text, width / 2 - 200, screen_height() / 2, {255, 255, 255, 255});
625 [[nodiscard]] std::unique_ptr<mxvk::VKAbstractModel> load_model(std::string_view manifest) {
return load_model(
"cube.mxmod.z", manifest); }
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");
634 [[nodiscard]]
static std::string_view block_manifest(
int index) {
637 return "texture0_manifest.txt";
639 return "texture1_manifest.txt";
641 return "texture2_manifest.txt";
643 return "texture3_manifest.txt";
647 void cleanup_block_models() {
648 for (Block &block : blocks) {
650 block.model->cleanup(
this);
655 void cleanup_models() {
656 cleanup_block_models();
658 intro_model->cleanup(
this);
661 paddle_model->cleanup(
this);
664 ball_model->cleanup(
this);
673#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
674 if (mixer !=
nullptr && ping_sound >= 0) {
675 mixer->playWav(ping_sound, 0, 0);
681#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
682 if (mixer !=
nullptr && clear_sound >= 0) {
683 mixer->playWav(clear_sound, 0, 1);
689#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
690 if (mixer !=
nullptr && die_sound >= 0) {
691 mixer->playWav(die_sound, 0, 2);