89 BreakoutWindow(
const std::string &path,
int width,
int height,
bool fullscreen,
bool enable_vsync)
92 data_root(asset_root +
"/data"),
93 shader_root(data_root) {
95 setFont(data_root +
"/font.ttf", 24);
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();
117 if (
device != VK_NULL_HANDLE) {
125 intro_model->resize(
this);
128 paddle_model->resize(
this);
131 ball_model->resize(
this);
133 for (
Block &block : blocks) {
135 block.model->resize(
this);
141 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
146 if (controller.connectEvent(e)) {
147 if (!controller.active()) {
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;
164 if (screen == Screen::GameOver) {
165 if ((e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_RETURN) || is_gamepad_start_button(e)) {
167 screen = Screen::Intro;
172 if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
173 if (e.key.key == SDLK_SPACE || e.key.key == SDLK_RETURN) {
175 }
else if (e.key.key == SDLK_R) {
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;
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) {
196 }
else if (button == SDL_GAMEPAD_BUTTON_EAST) {
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;
208 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
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) {
217 last_tap_time = current_tap_time;
218 }
else if (e.type == SDL_EVENT_FINGER_MOTION) {
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);
229 background_time += delta_seconds;
230 ensure_background_music_playing();
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});
238 if (screen == Screen::GameOver) {
239 print_centered_game_over(width);
243 update_game(delta_seconds);
244 print_centered_score(width);
250 draw_background(cmd, extent);
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);
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));
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));
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) {
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));
291 std::string asset_root;
292 std::string data_root;
293 std::string shader_root;
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)};
302 std::vector<Block> blocks{};
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;
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;
320 int clear_sound = -1;
323 static constexpr Uint64 DOUBLE_TAP_THRESHOLD_MS = 300;
325#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
326 void ensure_background_music_playing() {
327 if (!mixer || background_music < 0) {
330 if (!mixer->isMusicPlaying(background_music)) {
331 if (mixer->playMusic(background_music, -1) != 0) {
332 throw mxvk::Exception(
"Could not start Breakout background music");
337 void ensure_background_music_playing() {}
343 paddle.position = glm::vec3(0.0f, -3.5f, 0.0f);
344 paddle_current_rotation = 0.0f;
345 paddle_rotating =
false;
347 cleanup_block_models();
349 blocks.reserve(BLOCK_ROWS * BLOCK_COLUMNS);
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));
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;
371 void update_game(
float delta) {
372 if (screen == Screen::Complete || screen == Screen::GameOver) {
376 const bool *keys = SDL_GetKeyboardState(
nullptr);
377 if (keys[SDL_SCANCODE_LEFT] || keys[SDL_SCANCODE_H]) {
380 if (keys[SDL_SCANCODE_RIGHT] || keys[SDL_SCANCODE_L]) {
383 if (keys[SDL_SCANCODE_W]) {
386 if (keys[SDL_SCANCODE_S]) {
389 if (keys[SDL_SCANCODE_A]) {
392 if (keys[SDL_SCANCODE_D]) {
395 if (keys[SDL_SCANCODE_PAGEUP]) {
396 adjust_zoom(ZOOM_SPEED * delta);
398 if (keys[SDL_SCANCODE_PAGEDOWN]) {
399 adjust_zoom(-ZOOM_SPEED * delta);
401 if (keys[SDL_SCANCODE_Q]) {
402 grid_rotation = 0.0f;
403 grid_y_rotation = 0.0f;
406 update_gamepad_controls(delta);
407 if (grid_rotation >= 360.0f) {
408 grid_rotation -= 360.0f;
410 if (grid_rotation <= -360.0f) {
411 grid_rotation += 360.0f;
413 if (grid_y_rotation >= 360.0f) {
414 grid_y_rotation -= 360.0f;
416 if (grid_y_rotation <= -360.0f) {
417 grid_y_rotation += 360.0f;
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;
430 ball.box.position = paddle.position + glm::vec3(0.0f, 0.5f, 0.0f);
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;
445 all_destroyed = all_destroyed && block.destroyed;
448 screen = Screen::Intro;
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;
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);
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);
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);
473 const float distance = (ball.box.position.x - paddle.position.x) / (paddle.size.x * 0.5f);
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;
485 for (Block &block : blocks) {
486 if (block.destroyed || block.rotating || !
check_collision(ball, block.box)) {
489 block.rotating =
true;
490 block.current_rotation = 0.0f;
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;
496 ball.box.position.y -= ball.radius;
502 if (ball.box.position.y <= GAME_BOTTOM) {
505 if (misses >= MAX_MISSES) {
507 screen = Screen::GameOver;
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;
528 void clamp_paddle() {
529 paddle.position.x =
clampf(paddle.position.x, GAME_LEFT, GAME_RIGHT);
532 void adjust_zoom(
float delta) {
533 zoom =
clampf(zoom + delta, MIN_ZOOM, MAX_ZOOM);
536 [[nodiscard]]
static float normalized_gamepad_axis(Sint16 value) {
537 if (std::abs(
static_cast<int>(value)) <= GAMEPAD_DEAD_ZONE) {
541 return static_cast<float>(value) / 32768.0f;
543 return static_cast<float>(value) / 32767.0f;
546 [[nodiscard]]
bool gamepad_button(SDL_GamepadButton button)
const {
547 return controller.active() && controller.getButton(button);
550 void update_gamepad_controls(
float delta) {
551 if (!controller.active()) {
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));
560 if (gamepad_button(SDL_GAMEPAD_BUTTON_DPAD_LEFT)) {
563 if (gamepad_button(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)) {
570 if (gamepad_button(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)) {
571 adjust_zoom(-ZOOM_SPEED * delta);
573 if (gamepad_button(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER)) {
574 adjust_zoom(ZOOM_SPEED * delta);
578 [[nodiscard]]
static bool is_gamepad_start_button(
const SDL_Event &e) {
579 if (e.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
582 const auto button =
static_cast<SDL_GamepadButton
>(e.gbutton.button);
583 return button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START;
586 bool open_controller() {
588 if (controller.open(index)) {
595 void draw_model(VkCommandBuffer cmd,
596 uint32_t image_index,
597 mxvk::VKAbstractModel &model,
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);
609 ubo.
fx = glm::vec4(tint, 1.0f);
611 model.
render(cmd, image_index,
false);
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) {
620 sprite->
drawSpriteRect(0, 0,
static_cast<int>(extent.width),
static_cast<int>(extent.height));
625 void print_centered_score(
int width) {
626 const std::string text = std::format(
"Score: {}", score);
630 printText(text, (width - text_width) / 2, 0, {255, 255, 255, 255});
633 printText(text, width / 2 - 60, 0, {255, 255, 255, 255});
637 const int tries_left = std::max(0, MAX_MISSES - misses);
638 printText(std::format(
"Tries: {}", tries_left), 25, 0, {255, 255, 255, 255});
641 void print_centered_game_over(
int width) {
642 const std::string text =
"Game Over - Press Enter to Restart";
646 printText(text, (width - text_width) / 2, screen_height() / 2 - text_height / 2, {255, 255, 255, 255});
649 printText(text, width / 2 - 200, screen_height() / 2, {255, 255, 255, 255});
652 [[nodiscard]] std::unique_ptr<mxvk::VKAbstractModel> load_model(std::string_view manifest) {
653 return load_model(
"cube.mxmod.z", manifest);
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);
660 shader_root +
"/breakout_model.vert.spv",
661 shader_root +
"/breakout_model.frag.spv");
665 [[nodiscard]]
static std::string_view block_manifest(
int index) {
668 return "texture0_manifest.txt";
670 return "texture1_manifest.txt";
672 return "texture2_manifest.txt";
674 return "texture3_manifest.txt";
678 void cleanup_block_models() {
679 for (Block &block : blocks) {
681 block.model->cleanup(
this);
686 void cleanup_models() {
687 cleanup_block_models();
689 intro_model->cleanup(
this);
692 paddle_model->cleanup(
this);
695 ball_model->cleanup(
this);
699 [[nodiscard]]
int screen_width()
const {
703 [[nodiscard]]
int screen_height()
const {
708#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
709 if (mixer !=
nullptr && ping_sound >= 0) {
710 mixer->playWav(ping_sound, 0, 0);
716#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
717 if (mixer !=
nullptr && clear_sound >= 0) {
718 mixer->playWav(clear_sound, 0, 1);
724#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
725 if (mixer !=
nullptr && die_sound >= 0) {
726 mixer->playWav(die_sound, 0, 2);