MXVK Vulkan Framework 0.33.1
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
asteroids3d_window.cpp
Go to the documentation of this file.
3#include "rain.hpp"
4#include "ship.hpp"
5#include "starfield.hpp"
6
7#include "mxvk/argz.hpp"
8#include "mxvk/mxvk.hpp"
10#include "mxvk/mxvk_console.hpp"
13#include "mxvk/mxvk_png.hpp"
14#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
15#include "mxvk/mxvk_sound.hpp"
16#endif
17
18#include <SDL3/SDL.h>
19
20#include <algorithm>
21#include <array>
22#include <atomic>
23#include <chrono>
24#include <cmath>
25#include <cstddef>
26#include <cstdlib>
27#include <cstring>
28#include <exception>
29#include <filesystem>
30#include <format>
31#include <limits>
32#include <memory>
33#include <mutex>
34#include <optional>
35#include <ostream>
36#include <string>
37#include <thread>
38#include <vector>
39
40#include <glm/ext/matrix_clip_space.hpp>
41#include <glm/ext/matrix_transform.hpp>
42#include <glm/glm.hpp>
43
44namespace space {
45
46 namespace {
47
48 [[nodiscard]] std::string resolve_shader_root(const std::string &asset_root) {
49 const std::filesystem::path requested_root = std::filesystem::path(asset_root) / "data";
50 if (std::filesystem::exists(requested_root / "crt.frag.spv")) {
51 return requested_root.lexically_normal().string();
52 }
53
54 if (const char *base_path = SDL_GetBasePath(); base_path != nullptr && base_path[0] != '\0') {
55 const std::filesystem::path runtime_root = std::filesystem::path(base_path) / "data";
56 if (std::filesystem::exists(runtime_root / "crt.frag.spv")) {
57 return runtime_root.lexically_normal().string();
58 }
59 }
60
61 return requested_root.lexically_normal().string();
62 }
63
64 } // namespace
65
66 class Asteroids3DWindow : public mxvk::VK_Window {
67 public:
68 Asteroids3DWindow(const std::string &path, int width, int height, bool fullscreen, bool enable_vsync, bool enable_crt)
69 : mxvk::VK_Window("3D Asteroids", width, height, fullscreen, MXVK_VALIDATION, enable_vsync),
70 asset_root((path.empty() || path == ".") ? std::string(ASTEROIDS3D_ASSET_DIR) : path),
71 shader_root(resolve_shader_root(asset_root)),
72 crt_enabled(enable_crt) {
73 if (asset_root == ".") {
74 asset_root = ASTEROIDS3D_ASSET_DIR;
75 }
76
77 const char *video_driver = SDL_GetCurrentVideoDriver();
78 const bool uses_wayland = video_driver != nullptr && std::strcmp(video_driver, "wayland") == 0;
79 if (!uses_wayland) {
80 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> window_icon(
81 mxvk::LoadPNG((asset_root + "/data/asteroids_icon.png").c_str()), SDL_DestroySurface);
82 if (window_icon != nullptr && !SDL_SetWindowIcon(getSDLWindow(), window_icon.get())) {
83 std::cerr << "asteroids3d: could not set SDL window icon: " << SDL_GetError() << '\n';
84 }
85 }
86
87 setClearColor(0.0f, 0.0f, 0.0f, 1.0f);
88 attachPostProcessingShader(shader_root + "/crt.frag.spv", 0.0f, 3.0f, 0.5f, 0.002f);
90 setPostProcessingEnabled(crt_enabled);
91 load_loading_screen_resources();
92 configure_console();
93 open_controller();
94#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
95 load_sound_effects();
96 ensure_background_music_playing();
97#endif
98 }
99
101 if (mouse_capture_active) {
102 SDL_SetWindowRelativeMouseMode(getSDLWindow(), false);
103 mouse_capture_active = false;
104 }
105 if (device != VK_NULL_HANDLE) {
106 vkDeviceWaitIdle(device);
107 }
108 if (loading_thread.joinable()) {
109 loading_thread.join();
110 }
111#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
112 if (sound_effects) {
113 sound_effects->stopMusic();
114 }
115#endif
116 cleanup_flame_resources();
117 ship_model.cleanup(this);
118 for (auto &model : asteroid_models) {
119 model.cleanup(this);
120 }
121 if (star_sprite != nullptr) {
122 star_sprite->cleanup();
123 }
124 if (projectile_sprite != nullptr) {
125 projectile_sprite->cleanup();
126 }
127 if (effect_sprite != nullptr) {
128 effect_sprite->cleanup();
129 }
130 intro_rain.reset();
131 }
132
133 void event(SDL_Event &e) override {
134 if (e.type == SDL_EVENT_GAMEPAD_ADDED ||
135 e.type == SDL_EVENT_GAMEPAD_REMOVED ||
136 e.type == SDL_EVENT_JOYSTICK_ADDED ||
137 e.type == SDL_EVENT_JOYSTICK_REMOVED) {
138 if (e.type == SDL_EVENT_GAMEPAD_ADDED || e.type == SDL_EVENT_GAMEPAD_REMOVED) {
139 controller.connectEvent(e);
140 }
141 sync_controller_connection();
142 return;
143 }
144
145 const bool was_console_visible = console.isVisible();
146 const bool is_console_toggle = e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_F3;
147 console.handleEvent(e);
148 if (is_console_toggle) {
149 log_game(console.isVisible() ? "Console opened." : "Console closed.");
150 sync_mouse_capture();
151 return;
152 }
153 if (was_console_visible) {
154 return;
155 }
156
157 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
158 if (mode == GameMode::Playing) {
159 log_game("Exit requested while playing.");
160 exit();
161 } else if (mode == GameMode::GameOver || mode == GameMode::GameComplete) {
162 exit();
163 } else {
164 log_game("Exit requested from intro screen.");
165 exit();
166 }
167 return;
168 }
169 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_F8 && !e.key.repeat) {
170 crt_enabled = !crt_enabled;
171 setPostProcessingEnabled(crt_enabled);
172 log_game(std::string("CRT effect ") + (crt_enabled ? "enabled." : "disabled."));
173 return;
174 }
175 if (mode == GameMode::Intro &&
176 e.type == SDL_EVENT_KEY_DOWN &&
177 (e.key.key == SDLK_SPACE || e.key.key == SDLK_RETURN)) {
178 intro_fade = 0.01f;
179 log_game("Intro skipped. Starting game.");
180 return;
181 }
182 if (mode == GameMode::Intro &&
183 e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN &&
184 e.gbutton.button == SDL_GAMEPAD_BUTTON_SOUTH) {
185 intro_fade = 0.01f;
186 log_game("Intro skipped from controller. Starting game.");
187 return;
188 }
189 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
190 if (e.gbutton.button == SDL_GAMEPAD_BUTTON_BACK) {
191 log_game("Exit requested from controller.");
192 exit();
193 return;
194 }
195 if (e.gbutton.button == SDL_GAMEPAD_BUTTON_WEST) {
196 debug_menu = !debug_menu;
197 log_game(std::string("Debug HUD ") + (debug_menu ? "enabled from controller." : "disabled from controller."));
198 return;
199 }
200 if (e.gbutton.button == SDL_GAMEPAD_BUTTON_NORTH) {
201 inverted_controls = !inverted_controls;
202 log_game(std::string("Controls set to ") + (inverted_controls ? "inverted from controller." : "arcade from controller."));
203 return;
204 }
205 if (e.gbutton.button == SDL_GAMEPAD_BUTTON_EAST && mode == GameMode::Playing) {
206 restart_game();
207 log_game("Game restarted from controller.");
208 return;
209 }
210 if ((mode == GameMode::GameOver || mode == GameMode::GameComplete) &&
211 (e.gbutton.button == SDL_GAMEPAD_BUTTON_SOUTH || e.gbutton.button == SDL_GAMEPAD_BUTTON_START)) {
212 prepare_restart_from_game_over();
213 log_game("End screen acknowledged from controller. Returning to intro.");
214 return;
215 }
216 }
217 if ((mode == GameMode::GameOver || mode == GameMode::GameComplete) && e.type == SDL_EVENT_KEY_DOWN) {
218 if (e.key.key == SDLK_RETURN || e.key.key == SDLK_KP_ENTER) {
219 prepare_restart_from_game_over();
220 log_game("End screen acknowledged from keyboard. Returning to intro.");
221 return;
222 }
223 }
224 if (mode == GameMode::Playing && e.type == SDL_EVENT_KEY_DOWN) {
225 if (e.key.key == SDLK_F1) {
226 debug_menu = !debug_menu;
227 log_game(std::string("Debug HUD ") + (debug_menu ? "enabled." : "disabled."));
228 return;
229 }
230 if (e.key.key == SDLK_F2) {
231 inverted_controls = !inverted_controls;
232 log_game(std::string("Controls set to ") + (inverted_controls ? "inverted." : "arcade."));
233 return;
234 }
235 if (e.key.key == SDLK_F5 && !e.key.repeat) {
236 set_mouse_look_controls(!mouse_look_controls);
237 log_game(std::string("Control scheme set to ") + (mouse_look_controls ? "keyboard/mouse." : "classic keyboard."));
238 return;
239 }
240 if (e.key.key == SDLK_F7 && !e.key.repeat) {
241 begin_camera_transition(!first_person_camera);
242 log_game(std::string("Camera set to ") + (first_person_camera ? "first person." : "chase view."));
243 return;
244 }
245 }
246 if (mode == GameMode::Playing && mouse_look_controls && e.type == SDL_EVENT_MOUSE_MOTION) {
247 if (ignore_next_mouse_motion) {
248 ignore_next_mouse_motion = false;
249 return;
250 }
251 apply_mouse_look(e.motion.xrel, e.motion.yrel);
252 return;
253 }
254 if (mode == GameMode::Playing && mouse_look_controls && e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
255 if (can_fire()) {
256 fire_projectile();
257 }
258 return;
259 }
260 }
261
262 void onSwapchainRecreated() override {
263 if (intro_sprite != nullptr) {
264 intro_sprite->rebuildPipeline();
265 }
266 if (intro_rain != nullptr) {
267 intro_rain->resize(*this);
268 }
269 if (!game_resources_loaded.load(std::memory_order_relaxed)) {
270 cleanup_flame_swapchain_resources();
271 return;
272 }
273 ship_model.resize(this);
274 for (auto &model : asteroid_models) {
275 model.resize(this);
276 }
277 if (star_sprite != nullptr) {
278 star_sprite->resize(this);
279 }
280 if (projectile_sprite != nullptr) {
281 projectile_sprite->resize(this);
282 }
283 if (effect_sprite != nullptr) {
284 effect_sprite->resize(this);
285 }
286 cleanup_flame_swapchain_resources();
287 create_flame_swapchain_resources();
288 }
289
290 void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t image_index) override {
291 current_command_buffer = cmd;
292 const auto now = std::chrono::steady_clock::now();
293 const float delta_seconds = std::chrono::duration<float>(now - last_frame_time).count();
294 last_frame_time = now;
295 const float dt = std::min(delta_seconds, 0.1f);
296 last_delta_time = dt;
297 elapsed_seconds += dt;
298
299 sync_controller_connection();
300#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
301 ensure_background_music_playing();
302#endif
303
304 const VkExtent2D extent = getSwapchainExtent();
305 const float aspect = (extent.height > 0U) ? static_cast<float>(extent.width) / static_cast<float>(extent.height) : 1.0f;
306
307 if (mode == GameMode::Intro) {
308 draw_intro(extent);
309 console.draw();
310 return;
311 }
312
313 if (mode == GameMode::Loading) {
314 draw_loading(extent);
315 console.draw();
316 return;
317 }
318
319 if (mode == GameMode::GameOver) {
320 draw_end_screen(image_index, aspect, "Game over", SDL_Color{235, 60, 60, 255});
321 console.draw();
322 return;
323 }
324
325 if (mode == GameMode::GameComplete) {
326 draw_end_screen(image_index, aspect, "Mission complete", SDL_Color{255, 220, 120, 255});
327 console.draw();
328 return;
329 }
330
331 const bool console_visible = console.isVisible();
332 sync_mouse_capture();
333 update_round_timer(dt);
334 if (mode == GameMode::GameOver) {
335 draw_game_over(image_index, aspect);
336 console.draw();
337 return;
338 }
339 if (!console_visible) {
340 handle_input(dt);
341 }
342 update_ship(console_visible ? 0.0f : dt);
343 update_projectiles(dt);
344 update_asteroids(dt);
345 update_particles(dt);
346
347 if (ship.lives <= 0 && !ship.exploding) {
348 mode = GameMode::GameOver;
349 ship.visible = false;
350 if (star_sprite != nullptr) {
351 star_sprite->clearQueue();
352 }
353 if (projectile_sprite != nullptr) {
354 projectile_sprite->clearQueue();
355 }
356 if (effect_sprite != nullptr) {
357 effect_sprite->clearQueue();
358 }
359 draw_game_over(image_index, aspect);
360 console.draw();
361 return;
362 }
363
364 if (mode == GameMode::Playing && active_asteroids() == 0) {
366 ship.visible = false;
367 log_game("All asteroids cleared. Mission complete.", SDL_Color{120, 255, 160, 255});
368 }
369
370 if (mode == GameMode::GameComplete) {
371 if (star_sprite != nullptr) {
372 star_sprite->clearQueue();
373 }
374 if (projectile_sprite != nullptr) {
375 projectile_sprite->clearQueue();
376 }
377 if (effect_sprite != nullptr) {
378 effect_sprite->clearQueue();
379 }
380 draw_end_screen(image_index, aspect, "Mission complete", SDL_Color{255, 220, 120, 255});
381 console.draw();
382 return;
383 }
384
385 update_camera(dt);
386 projection_matrix = glm::perspective(glm::radians(50.0f), aspect, 0.1f, 500.0f);
387 projection_matrix[1][1] *= -1.0f;
388
389 star_field.update(dt, camera_position, elapsed_seconds);
390 star_field.setSprite(star_sprite);
391
392 star_sprite->updateCamera(image_index, view_matrix, projection_matrix);
393 projectile_sprite->updateCamera(image_index, view_matrix, projection_matrix);
394 effect_sprite->updateCamera(image_index, view_matrix, projection_matrix);
395
396 star_field.draw();
397 star_sprite->render(cmd, image_index);
398 star_sprite->clearQueue();
399
400 draw_asteroids(image_index);
401 if (!first_person_camera && !camera_transition_active) {
402 draw_ship(image_index);
403 draw_engine_flame(cmd, extent);
404 }
405 draw_projectiles();
406 draw_particles();
407 projectile_sprite->render(cmd, image_index);
408 projectile_sprite->clearQueue();
409 effect_sprite->render(cmd, image_index);
410 effect_sprite->clearQueue();
411
412 if (!console.isVisible()) {
413 draw_hud(aspect);
414 }
415 console.draw();
416 }
417
418 private:
419 std::string asset_root;
420 std::string shader_root;
421 std::chrono::steady_clock::time_point last_frame_time = std::chrono::steady_clock::now();
422 float elapsed_seconds = 0.0f;
424 float intro_fade = 1.0f;
425 Uint32 intro_last_update_ms = 0;
426 float loading_rain_opacity = 1.0f;
427 static constexpr int INTRO_RAIN_TEXTURE_WIDTH = 1280;
428 static constexpr int INTRO_RAIN_TEXTURE_HEIGHT = 720;
429 bool loading_black_frame_pending = false;
430 bool loading_black_frame_shown = false;
431 bool restart_after_intro = false;
432 bool debug_menu = false;
433 bool inverted_controls = false;
434 bool mouse_look_controls = false;
435 bool mouse_capture_active = false;
436 bool ignore_next_mouse_motion = false;
437 bool first_person_camera = false;
438 bool camera_transition_active = false;
439 bool crt_enabled = false;
440 bool ship_returning_to_field = false;
441 float keyboard_yaw = 0.0f;
442 float keyboard_pitch = 0.0f;
443 float keyboard_roll = 0.0f;
444 float smooth_yaw = 0.0f;
445 float smooth_pitch = 0.0f;
446 float smooth_roll = 0.0f;
447 float return_message_cooldown = 0.0f;
448 float camera_transition_elapsed = 0.0f;
449 static constexpr float CAMERA_TRANSITION_SECONDS = 0.75f;
450 static constexpr float MOUSE_LOOK_SENSITIVITY = 0.04f;
451
452 Ship ship{};
453 std::array<Projectile, MAX_PROJECTILES> projectiles{};
454 std::array<Asteroid, MAX_ASTEROIDS> asteroids{};
455 std::array<Particle, MAX_PARTICLES> particles{};
456 StarField star_field{};
457 glm::vec3 camera_position{0.0f, 1.6f, 6.0f};
458 glm::vec3 camera_target_position{0.0f, 1.6f, 0.0f};
459 glm::vec3 camera_up_vector{0.0f, 1.0f, 0.0f};
460 glm::vec3 camera_transition_start_position{0.0f, 1.6f, 6.0f};
461 glm::vec3 camera_transition_start_target{0.0f, 1.6f, 0.0f};
462 glm::vec3 camera_transition_start_up{0.0f, 1.0f, 0.0f};
463 glm::mat4 view_matrix{1.0f};
464 glm::mat4 projection_matrix{1.0f};
465
466 mxvk::VKAbstractModel ship_model{};
467 std::array<mxvk::VKAbstractModel, MAX_ASTEROIDS> asteroid_models{};
468 mxvk::VK_Sprite3D *star_sprite = nullptr;
469 mxvk::VK_Sprite3D *projectile_sprite = nullptr;
470 mxvk::VK_Sprite3D *effect_sprite = nullptr;
471 mxvk::VK_Sprite *intro_sprite = nullptr;
472 mxvk::VK_Sprite *ui_pixel = nullptr;
473 std::unique_ptr<matrix::Rain> intro_rain{};
474 mxvk::VK_Console console;
475 mxvk::VK_Controller controller;
476#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
477 std::unique_ptr<mxvk::VK_Mixer> sound_effects{};
478 int background_music_track = -1;
479 int crash_sound = -1;
480 int cannon_sound = -1;
481 int asteroid_explosion_sound = -1;
482#endif
483 bool console_ready = false;
484 std::atomic<bool> game_resources_loaded{false};
485 std::atomic<bool> loading_failed{false};
486 std::atomic<bool> model_preload_done{false};
487 std::atomic<bool> model_preload_failed{false};
488 int last_font_size = 0;
489 float round_time_remaining = ROUND_TIME_LIMIT_SECONDS;
490 std::atomic<int> loading_step_index{0};
491 static constexpr int loading_step_count = MAX_ASTEROIDS + 8;
492 glm::mat4 last_ship_model_matrix{1.0f};
493 VkBuffer flame_vertex_buffer = VK_NULL_HANDLE;
494 VkDeviceMemory flame_vertex_buffer_memory = VK_NULL_HANDLE;
495 VkPipeline flame_pipeline = VK_NULL_HANDLE;
496 VkPipelineLayout flame_pipeline_layout = VK_NULL_HANDLE;
497 std::thread loading_thread{};
498 std::string loading_error{};
499 std::mutex prepared_model_mutex{};
500 std::optional<mxvk::MXModel> prepared_ship_model{};
501 std::array<std::optional<mxvk::MXModel>, MAX_ASTEROIDS> prepared_asteroid_models{};
502 std::array<std::string, MAX_ASTEROIDS> prepared_asteroid_texture_paths{};
503 uint32_t flame_vertex_count = 0;
504
505 void log_game(const std::string &message, SDL_Color color = SDL_Color{180, 220, 255, 255}) {
506 if (!console_ready) {
507 return;
508 }
509 console.printLine("[game] " + message, color);
510 }
511
512 void configure_console() {
513 console.attach(*this, asset_root + "/data/font.ttf", 20);
514 console.setSpriteYOriginTopLeft(true);
515 console.setPrompt("asteroids> ");
516 console_ready = true;
517 console.printLine("Press F3 to open/close the console.");
518 console.printLine("Type 'help' for asteroids3d commands.");
519 log_game("Console attached.");
520 log_game("asteroids3d initialized.");
521 console.setCommandCallback([this](mxvk::VK_Window &, const std::vector<std::string> &args, std::ostream &out) {
522 if (args.empty()) {
523 return true;
524 }
525
526 const std::string &cmd = args.front();
527 if (cmd == "help") {
528 out << "asteroids3d commands:\n"
529 << " clear Clear console output\n"
530 << " echo <text> Print text to the console\n"
531 << " status Print score, lives, mode, and asteroid count\n"
532 << " restart Restart the game\n"
533 << " intro Return to the intro screen\n"
534 << " play Start or resume play\n"
535 << " debug Toggle debug HUD\n"
536 << " controls Toggle arcade/inverted pitch controls\n"
537 << " input Toggle classic keyboard versus keyboard/mouse controls\n"
538 << " about Print program banner\n"
539 << " quit / exit Close the window\n";
540 return true;
541 }
542
543 if (cmd == "echo") {
544 for (std::size_t i = 1; i < args.size(); ++i) {
545 if (i > 1) {
546 out << ' ';
547 }
548 out << args[i];
549 }
550 return true;
551 }
552
553 if (cmd == "status") {
554 const char *mode_name = (mode == GameMode::Intro) ? "intro" : (mode == GameMode::Loading) ? "loading"
555 : (mode == GameMode::Playing) ? "playing"
556 : (mode == GameMode::GameComplete) ? "complete"
557 : "gameover";
558 out << "Mode: " << mode_name << '\n'
559 << "Score: " << ship.score << '\n'
560 << "Lives: " << std::max(0, ship.lives) << '\n'
561 << "Asteroids: " << active_asteroids() << '\n'
562 << "Time left: " << format_round_time() << '\n'
563 << "Speed: " << ship.current_speed << " / " << ship.max_speed << '\n'
564 << "Control scheme: " << (mouse_look_controls ? "keyboard/mouse" : "classic keyboard") << '\n'
565 << "Camera: " << (first_person_camera ? "first person" : "chase") << '\n'
566 << "Controls: " << (inverted_controls ? "inverted" : "arcade") << '\n'
567 << "Controller: " << controller_status() << '\n'
568 << "Debug HUD: " << (debug_menu ? "on" : "off") << '\n';
569 return true;
570 }
571
572 if (cmd == "restart") {
573 restart_game();
574 mode = GameMode::Playing;
575 log_game("Game restarted from console.");
576 out << "Game restarted.";
577 return true;
578 }
579
580 if (cmd == "intro") {
581 reset_intro_screen();
582 log_game("Returned to intro screen from console.");
583 out << "Intro screen active.";
584 return true;
585 }
586
587 if (cmd == "play") {
588 mode = GameMode::Playing;
589 log_game("Play mode activated from console.");
590 out << "Playing.";
591 return true;
592 }
593
594 if (cmd == "debug") {
595 debug_menu = !debug_menu;
596 log_game(std::string("Debug HUD ") + (debug_menu ? "enabled from console." : "disabled from console."));
597 out << "Debug HUD " << (debug_menu ? "enabled." : "disabled.");
598 return true;
599 }
600
601 if (cmd == "controls") {
602 inverted_controls = !inverted_controls;
603 log_game(std::string("Controls set to ") + (inverted_controls ? "inverted from console." : "arcade from console."));
604 out << "Controls set to " << (inverted_controls ? "inverted." : "arcade.");
605 return true;
606 }
607
608 if (cmd == "input") {
609 set_mouse_look_controls(!mouse_look_controls);
610 log_game(std::string("Control scheme set to ") + (mouse_look_controls ? "keyboard/mouse from console." : "classic keyboard from console."));
611 out << "Control scheme set to " << (mouse_look_controls ? "keyboard/mouse." : "classic keyboard.");
612 return true;
613 }
614
615 if (cmd == "about") {
616 out << "asteroids3d: MXVK port of gl_asteroids.\n";
617 return true;
618 }
619
620 if (cmd == "quit" || cmd == "exit") {
621 log_game("Exit requested from console.");
622 out << "Closing window...";
623 exit();
624 return true;
625 }
626
627 return false;
628 });
629 }
630
631 bool open_controller() {
632 for (int i = 0; i < mxvk::VK_Controller::joysticks(); ++i) {
633 if (controller.open(i)) {
634 log_game("Controller connected: " + controller.name());
635 return true;
636 }
637 }
638 return false;
639 }
640
641 void sync_controller_connection() {
642 if (!controller.active()) {
643 open_controller();
644 }
645 }
646
647 std::string controller_status() const {
648 return controller.active() ? ("Connected: " + controller.name()) : "Disconnected";
649 }
650
651 float controller_axis(SDL_GamepadAxis axis) const {
652 if (!controller.active()) {
653 return 0.0f;
654 }
655
656 const Sint16 raw_value = controller.getAxis(axis);
657 const float magnitude = static_cast<float>(std::abs(static_cast<int>(raw_value)));
658 if (magnitude <= static_cast<float>(CONTROLLER_DEAD_ZONE)) {
659 return 0.0f;
660 }
661
662 const float normalized = std::clamp((magnitude - static_cast<float>(CONTROLLER_DEAD_ZONE)) /
663 (CONTROLLER_AXIS_MAX - static_cast<float>(CONTROLLER_DEAD_ZONE)),
664 0.0f,
665 1.0f);
666 const float curved = normalized * normalized;
667 return raw_value < 0 ? -curved : curved;
668 }
669
670 void set_mouse_look_controls(bool enabled) {
671 mouse_look_controls = enabled;
672 keyboard_yaw = 0.0f;
673 keyboard_pitch = 0.0f;
674 keyboard_roll = 0.0f;
675 smooth_yaw = 0.0f;
676 smooth_pitch = 0.0f;
677 smooth_roll = 0.0f;
678 sync_mouse_capture();
679 }
680
681 void sync_mouse_capture() {
682 const bool should_capture = mouse_look_controls && mode == GameMode::Playing && !console.isVisible();
683 if (should_capture == mouse_capture_active) {
684 return;
685 }
686
687 SDL_SetWindowRelativeMouseMode(getSDLWindow(), should_capture);
688 mouse_capture_active = should_capture;
689 ignore_next_mouse_motion = should_capture;
690 }
691
692 void apply_mouse_look(float delta_x, float delta_y) {
693 ship.rotation.y -= delta_x * MOUSE_LOOK_SENSITIVITY;
694 const float pitch_delta = inverted_controls ? delta_y * MOUSE_LOOK_SENSITIVITY : -delta_y * MOUSE_LOOK_SENSITIVITY;
695 ship.rotation.x = std::clamp(ship.rotation.x + pitch_delta, -75.0f, 75.0f);
696 }
697
698#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
699 std::string asteroids3d_asset_path(const std::string &filename) const {
700 const std::string local_path = asset_root + "/data/" + filename;
701 if (std::filesystem::exists(local_path)) {
702 return local_path;
703 }
704 return std::string(ASTEROIDS3D_SOURCE_DATA_DIR) + "/" + filename;
705 }
706
707 std::string sound_effect_path(const std::string &filename) const {
708 const std::string local_path = asset_root + "/data/" + filename;
709 if (std::filesystem::exists(local_path)) {
710 return local_path;
711 }
712 return std::string(ASTEROIDS3D_DEFENDER_SOUND_DIR) + "/" + filename;
713 }
714
715 void load_sound_effects() {
716 sound_effects = std::make_unique<mxvk::VK_Mixer>();
717 background_music_track = sound_effects->loadMusic(asteroids3d_asset_path("music.ogg"));
718 crash_sound = sound_effects->loadWav(sound_effect_path("crash.wav"));
719 cannon_sound = sound_effects->loadWav(asteroids3d_asset_path("cannon.wav"));
720 asteroid_explosion_sound = sound_effects->loadWav(sound_effect_path("asteroid.wav"));
721 }
722
723 void ensure_background_music_playing() {
724 if (!sound_effects || background_music_track < 0) {
725 return;
726 }
727 if (!sound_effects->isMusicPlaying(background_music_track)) {
728 if (sound_effects->playMusic(background_music_track, -1) != 0) {
729 throw mxvk::Exception("Could not start asteroids3d background music");
730 }
731 }
732 }
733
734 void play_sound(int sound_id) {
735 if (!sound_effects || sound_id < 0) {
736 return;
737 }
738 sound_effects->playWav(sound_id);
739 }
740#endif
741
742 void load_loading_screen_resources() {
743 set_ui_font_size(18);
744
745 intro_sprite = createSprite(
746 asset_root + "/data/intro.png",
747 asset_root + "/data/sprite.vert.spv",
748 shader_root + "/intro.frag.spv");
749 matrix::RainConfig intro_rain_config = matrix::make_matrix_rain_config(asset_root, false);
750 intro_rain_config.color = "#ff0000";
751 intro_rain_config.surface_width = INTRO_RAIN_TEXTURE_WIDTH;
752 intro_rain_config.surface_height = INTRO_RAIN_TEXTURE_HEIGHT;
753 intro_rain = std::make_unique<matrix::Rain>(*this, std::move(intro_rain_config));
754 reset_intro_screen();
755 loading_step_index.store(0, std::memory_order_relaxed);
756 game_resources_loaded.store(false, std::memory_order_relaxed);
757 loading_failed.store(false, std::memory_order_relaxed);
758 }
759
760 void start_loading_async() {
761 if (loading_thread.joinable()) {
762 loading_thread.join();
763 }
764 loading_thread = std::thread([this]() {
765 try {
766 preload_models();
767 } catch (const std::exception &e) {
768 loading_error = e.what();
769 model_preload_failed.store(true, std::memory_order_release);
770 } catch (...) {
771 loading_error = "unknown loading error";
772 model_preload_failed.store(true, std::memory_order_release);
773 }
774 });
775 }
776
777 void preload_models() {
778 std::optional<mxvk::MXModel> ship_model_cpu;
779 ship_model_cpu.emplace();
780 ship_model_cpu->load(asset_root + "/data/starship.obj", 1.0f);
781
782 std::array<std::optional<mxvk::MXModel>, MAX_ASTEROIDS> asteroid_models_cpu{};
783 std::array<std::string, MAX_ASTEROIDS> asteroid_texture_paths{};
784
785 std::mt19937 rng(std::random_device{}());
786 std::uniform_int_distribution<int> rock_variant_dist(0, 2);
787 for (std::size_t slot_index = 0; slot_index < MAX_ASTEROIDS; ++slot_index) {
788 static constexpr std::array<const char *, 3> asteroid_paths = {
789 "data/asteroid.obj",
790 "data/asteroid2.obj",
791 "data/asteroid3.obj",
792 };
793
794 const std::size_t model_variant = slot_index % asteroid_paths.size();
795 std::string texture_path;
796 if (model_variant == 0) {
797 texture_path = asset_root + "/data/rock.tex";
798 } else if (model_variant == 1) {
799 texture_path = asset_root + "/data/rock2.tex";
800 } else {
801 texture_path = (rock_variant_dist(rng) == 0) ? asset_root + "/data/rock.tex" : asset_root + "/data/rock2.tex";
802 }
803
804 asteroid_models_cpu[slot_index].emplace();
805 asteroid_models_cpu[slot_index]->load(asset_root + "/" + asteroid_paths[model_variant], 1.0f);
806 asteroid_texture_paths[slot_index] = texture_path;
807 }
808
809 {
810 std::lock_guard<std::mutex> lock(prepared_model_mutex);
811 prepared_ship_model = std::move(ship_model_cpu);
812 prepared_asteroid_models = std::move(asteroid_models_cpu);
813 prepared_asteroid_texture_paths = std::move(asteroid_texture_paths);
814 }
815
816 model_preload_done.store(true, std::memory_order_release);
817 }
818
819 void draw_intro(const VkExtent2D &extent) {
820 if (intro_sprite == nullptr) {
821 mode = GameMode::Loading;
822 return;
823 }
824
825 const Uint32 current_ms = SDL_GetTicks();
826 if ((current_ms - intro_last_update_ms) > 35U) {
827 intro_last_update_ms = current_ms;
828 intro_fade -= 0.01f;
829 }
830
831 if (intro_fade <= 0.0f) {
832 intro_fade = 0.0f;
833 if (restart_after_intro) {
834 restart_after_intro = false;
835 restart_game();
836 mode = GameMode::Playing;
837 intro_last_update_ms = SDL_GetTicks();
838 log_game("Intro finished. Restarting game.");
839 } else {
840 mode = GameMode::Loading;
841 loading_step_index.store(0, std::memory_order_relaxed);
842 game_resources_loaded.store(false, std::memory_order_relaxed);
843 loading_failed.store(false, std::memory_order_relaxed);
844 model_preload_done.store(false, std::memory_order_relaxed);
845 model_preload_failed.store(false, std::memory_order_relaxed);
846 loading_black_frame_pending = false;
847 loading_black_frame_shown = false;
848 start_loading_async();
849 log_game("Intro finished. Loading game resources.");
850 draw_loading(extent);
851 }
852 return;
853 }
854
855 intro_sprite->setShaderParams(static_cast<float>(current_ms) / 1000.0f, 0.0f, 0.0f, intro_fade);
856 intro_sprite->drawSpriteRect(0, 0, static_cast<int>(extent.width), static_cast<int>(extent.height));
857 if (intro_rain != nullptr) {
858 intro_rain->update_and_render(*this, static_cast<int>(extent.width), static_cast<int>(extent.height));
859 }
860 }
861
862 void draw_loading(const VkExtent2D &extent) {
863 if (loading_failed.load(std::memory_order_relaxed) || model_preload_failed.load(std::memory_order_relaxed)) {
864 if (intro_rain != nullptr) {
865 intro_rain->set_opacity(0.0f);
866 }
867 if (loading_thread.joinable()) {
868 loading_thread.join();
869 }
870 printText("Loading failed", 25, 25, {255, 100, 100, 255});
871 return;
872 }
873
874 if (!game_resources_loaded.load(std::memory_order_relaxed)) {
875 const int loading_progress_percent = std::clamp((loading_step_index.load(std::memory_order_relaxed) * 100) / loading_step_count, 0, 100);
876 if (intro_rain != nullptr) {
877 const float target_rain_opacity = 1.0f - (static_cast<float>(loading_progress_percent) / 100.0f);
878 loading_rain_opacity = std::lerp(loading_rain_opacity, target_rain_opacity, 0.25f);
879 intro_rain->set_opacity(loading_rain_opacity);
880 intro_rain->update_and_render(*this, static_cast<int>(extent.width), static_cast<int>(extent.height));
881 }
882 set_ui_font_size(40);
883 printText("Loading " + std::to_string(loading_progress_percent) + "%", 25, 25, {255, 255, 255, 255});
884 load_next_game_resource_step();
885 return;
886 }
887
888 if (loading_black_frame_pending) {
889 if (!loading_black_frame_shown) {
890 loading_black_frame_shown = true;
891 if (intro_rain != nullptr) {
892 intro_rain->set_opacity(0.0f);
893 }
894 return;
895 }
896
897 loading_black_frame_pending = false;
898 loading_black_frame_shown = false;
899 }
900
901 if (intro_rain != nullptr) {
902 intro_rain->set_opacity(0.0f);
903 }
904 if (loading_thread.joinable()) {
905 loading_thread.join();
906 }
907 intro_last_update_ms = SDL_GetTicks();
908 mode = GameMode::Playing;
909 log_game("Loading complete. Game is now playing.");
910 }
911
912 bool consume_prepared_ship_model(const std::string &model_vert, const std::string &model_frag) {
913 std::optional<mxvk::MXModel> model_cpu;
914 {
915 std::lock_guard<std::mutex> lock(prepared_model_mutex);
916 if (!prepared_ship_model.has_value()) {
917 return false;
918 }
919 model_cpu = std::move(prepared_ship_model);
920 prepared_ship_model.reset();
921 }
922
923 ship_model.load(this, std::move(*model_cpu), "", asset_root + "/data", 1.0f);
924 ship_model.setShaders(this, model_vert, model_frag);
925 ship_model.setBackfaceCulling(false);
926 return true;
927 }
928
929 bool consume_prepared_asteroid_model(std::size_t slot_index, const std::string &model_vert, const std::string &model_frag) {
930 std::optional<mxvk::MXModel> model_cpu;
931 std::string texture_path;
932 {
933 std::lock_guard<std::mutex> lock(prepared_model_mutex);
934 if (slot_index >= prepared_asteroid_models.size() || !prepared_asteroid_models[slot_index].has_value()) {
935 return false;
936 }
937 model_cpu = std::move(prepared_asteroid_models[slot_index]);
938 prepared_asteroid_models[slot_index].reset();
939 texture_path = prepared_asteroid_texture_paths[slot_index];
940 prepared_asteroid_texture_paths[slot_index].clear();
941 }
942
943 asteroids[slot_index].model_index = static_cast<int>(slot_index % 3U);
944 asteroid_models[slot_index].load(this, std::move(*model_cpu), texture_path, asset_root + "/data", 1.0f);
945 asteroid_models[slot_index].setShaders(this, model_vert, model_frag);
946 asteroid_models[slot_index].setBackfaceCulling(false);
947 return true;
948 }
949
950 void load_next_game_resource_step() {
951 const std::string model_vert = shader_root + "/model.vert.spv";
952 const std::string model_frag = shader_root + "/model.frag.spv";
953
954 const int current_step = loading_step_index.load(std::memory_order_relaxed);
955 if (current_step == 0) {
956 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> ui_surface(SDL_CreateSurface(1, 1, SDL_PIXELFORMAT_RGBA32), SDL_DestroySurface);
957 if (ui_surface == nullptr) {
958 throw mxvk::Exception("Failed to create asteroids3d UI pixel surface");
959 }
960 const SDL_PixelFormatDetails *format_details = SDL_GetPixelFormatDetails(ui_surface->format);
961 if (format_details == nullptr || !SDL_FillSurfaceRect(ui_surface.get(), nullptr, SDL_MapRGBA(format_details, nullptr, 255, 255, 255, 255))) {
962 throw mxvk::Exception("Failed to initialize asteroids3d UI pixel surface");
963 }
964 ui_pixel = createSprite(ui_surface.get(), asset_root + "/data/sprite.vert.spv", shader_root + "/fade_overlay.frag.spv");
965 if (ui_pixel == nullptr) {
966 throw mxvk::Exception("Failed to create asteroids3d UI pixel sprite");
967 }
968 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> star_surface(load_color_keyed_png(asset_root + "/data/particle_star.png", 12), SDL_DestroySurface);
969 star_sprite = createSprite3D(star_surface.get());
970 if (star_sprite == nullptr) {
971 throw mxvk::Exception("Failed to create star sprite batch");
972 }
973 star_sprite->setDepthTestEnabled(false);
974 star_sprite->setDepthWriteEnabled(false);
975 star_sprite->setAlphaDiscardThreshold(0.01f);
976 } else if (current_step == 1) {
977 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> fire_surface(load_color_keyed_png(asset_root + "/data/particle_explosion.png", 12), SDL_DestroySurface);
978 projectile_sprite = createSprite3D(fire_surface.get());
979 if (projectile_sprite == nullptr) {
980 throw mxvk::Exception("Failed to create projectile sprite batch");
981 }
982 projectile_sprite->setDepthTestEnabled(true);
983 projectile_sprite->setDepthWriteEnabled(false);
984 projectile_sprite->setAlphaDiscardThreshold(0.05f);
985 } else if (current_step == 2) {
986 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> explosion_surface(load_color_keyed_png(asset_root + "/data/particle_explosion.png", 12), SDL_DestroySurface);
987 effect_sprite = createSprite3D(explosion_surface.get());
988 if (effect_sprite == nullptr) {
989 throw mxvk::Exception("Failed to create effect sprite batch");
990 }
991 effect_sprite->setDepthTestEnabled(true);
992 effect_sprite->setDepthWriteEnabled(false);
993 effect_sprite->setAlphaDiscardThreshold(0.05f);
994 } else if (current_step == 3) {
995 if (!consume_prepared_ship_model(model_vert, model_frag)) {
996 return;
997 }
998 } else if (current_step == 4) {
999 if (!ship_model.isLoaded()) {
1000 return;
1001 }
1002 } else if (current_step == 5) {
1003 create_flame_resources();
1004 } else if (current_step >= 6 && current_step < 6 + MAX_ASTEROIDS) {
1005 if (!consume_prepared_asteroid_model(static_cast<std::size_t>(current_step - 6), model_vert, model_frag)) {
1006 return;
1007 }
1008 } else if (current_step == 6 + MAX_ASTEROIDS) {
1009 star_field.init(GAME_STARS, 4.0f, 30.0f);
1010 } else if (current_step == 7 + MAX_ASTEROIDS) {
1011 restart_game();
1012 } else {
1013 game_resources_loaded.store(true, std::memory_order_release);
1014 intro_last_update_ms = SDL_GetTicks();
1015 loading_rain_opacity = 0.0f;
1016 loading_black_frame_pending = true;
1017 loading_black_frame_shown = false;
1018 if (intro_rain != nullptr) {
1019 intro_rain->set_opacity(0.0f);
1020 }
1021 return;
1022 }
1023
1024 loading_step_index.store(current_step + 1, std::memory_order_release);
1025 if (loading_step_index.load(std::memory_order_relaxed) >= loading_step_count) {
1026 game_resources_loaded.store(true, std::memory_order_release);
1027 intro_last_update_ms = SDL_GetTicks();
1028 loading_rain_opacity = 0.0f;
1029 loading_black_frame_pending = true;
1030 loading_black_frame_shown = false;
1031 if (intro_rain != nullptr) {
1032 intro_rain->set_opacity(0.0f);
1033 }
1034 return;
1035 }
1036 }
1037
1038 void load_asteroid_model_slot(std::size_t slot_index, const std::string &model_vert, const std::string &model_frag) {
1039 static constexpr std::array<const char *, 3> asteroid_paths = {
1040 "data/asteroid.obj",
1041 "data/asteroid2.obj",
1042 "data/asteroid3.obj",
1043 };
1044
1045 const std::size_t model_variant = slot_index % asteroid_paths.size();
1046 std::string texture_path;
1047 if (model_variant == 0) {
1048 texture_path = asset_root + "/data/rock.tex";
1049 } else if (model_variant == 1) {
1050 texture_path = asset_root + "/data/rock2.tex";
1051 } else {
1052 texture_path = (random_int(0, 1) == 0) ? asset_root + "/data/rock.tex" : asset_root + "/data/rock2.tex";
1053 }
1054
1055 asteroids[slot_index].model_index = static_cast<int>(model_variant);
1056 asteroid_models[slot_index].load(
1057 this,
1058 asset_root + "/" + asteroid_paths[model_variant],
1059 texture_path,
1060 asset_root + "/data",
1061 1.0f);
1062 asteroid_models[slot_index].setShaders(this, model_vert, model_frag);
1063 asteroid_models[slot_index].setBackfaceCulling(false);
1064 }
1065
1066 void restart_game() {
1067 clear_round_state();
1068 spawn_initial_asteroids();
1069 round_time_remaining = ROUND_TIME_LIMIT_SECONDS;
1070 restart_after_intro = false;
1071 log_game("Game state reset: score=0 lives=5.");
1072 }
1073
1074 void clear_round_state() {
1075 ship.position = glm::vec3(0.0f);
1076 ship.prev_position = ship.position;
1077 ship.velocity = glm::vec3(0.0f);
1078 ship.rotation = glm::vec3(0.0f);
1079 ship.current_speed = 1.0f;
1080 ship.visible = true;
1081 ship.exploding = false;
1082 ship.explosion_timer = 0;
1083 ship.lives = 5;
1084 ship.score = 0;
1085 ship.fire_cooldown = 0;
1086 ship.burst_count = 0;
1087 ship.continuous_fire_timer = 0;
1088 ship.overheated = false;
1089 ship.overheat_cooldown = 0;
1090 for (auto &projectile : projectiles) {
1091 projectile.active = false;
1092 }
1093 for (auto &particle : particles) {
1094 particle.active = false;
1095 }
1096 for (auto &asteroid : asteroids) {
1097 asteroid.active = false;
1098 }
1099 keyboard_yaw = 0.0f;
1100 keyboard_pitch = 0.0f;
1101 keyboard_roll = 0.0f;
1102 smooth_yaw = 0.0f;
1103 smooth_pitch = 0.0f;
1104 smooth_roll = 0.0f;
1105 ship_returning_to_field = false;
1106 return_message_cooldown = 0.0f;
1107 set_mouse_look_controls(false);
1108 first_person_camera = false;
1109 camera_transition_active = false;
1110 camera_transition_elapsed = 0.0f;
1111 camera_position = glm::vec3(0.0f, ship.camera_height, ship.camera_distance);
1112 camera_target_position = glm::vec3(0.0f, 0.0f, -6.0f);
1113 camera_up_vector = glm::vec3(0.0f, 1.0f, 0.0f);
1114 round_time_remaining = ROUND_TIME_LIMIT_SECONDS;
1115 }
1116
1117 void spawn_initial_asteroids() {
1118 for (int i = 0; i < 7; ++i) {
1119 glm::vec3 position{0.0f};
1120 do {
1121 position = glm::vec3(
1122 random_float(-90.0f, 90.0f),
1123 random_float(-50.0f, 50.0f),
1124 random_float(-90.0f, 90.0f));
1125 } while (glm::length(position - ship.position) < 28.0f);
1126
1127 spawn_asteroid(position,
1128 glm::vec3(random_float(-0.8f, 0.8f), random_float(-0.8f, 0.8f), random_float(-0.8f, 0.8f)),
1129 random_float(2.8f, 7.0f),
1130 0,
1131 random_int(0, 2));
1132 }
1133 log_game("Initial asteroid field spawned.");
1134 }
1135
1136 void spawn_asteroid(const glm::vec3 &position, const glm::vec3 &velocity, float radius, int generation, int preferred_model_index = -1) {
1137 Asteroid *free_asteroid = find_free_asteroid(preferred_model_index);
1138 if (free_asteroid == nullptr && preferred_model_index >= 0) {
1139 free_asteroid = find_free_asteroid();
1140 }
1141
1142 if (free_asteroid == nullptr) {
1143 log_game("Asteroid spawn skipped: no free asteroid slots.", SDL_Color{255, 190, 90, 255});
1144 return;
1145 }
1146
1147 const int slot_model_index = free_asteroid->model_index;
1148 free_asteroid->position = position;
1149 free_asteroid->velocity = velocity;
1150 free_asteroid->radius = radius;
1151 free_asteroid->generation = generation;
1152 free_asteroid->rotation = glm::vec3(random_float(0.0f, 360.0f), random_float(0.0f, 360.0f), random_float(0.0f, 360.0f));
1153 free_asteroid->rotation_speed = glm::vec3(random_float(-45.0f, 45.0f), random_float(-45.0f, 45.0f), random_float(-45.0f, 45.0f));
1154 free_asteroid->model_index = slot_model_index;
1155 free_asteroid->active = true;
1156 if (generation == 0) {
1157 log_game(std::format("Asteroid spawned at ({:.1f}, {:.1f}, {:.1f}) radius {:.1f}.", position.x, position.y, position.z, radius));
1158 }
1159 }
1160
1161 void handle_input(float dt) {
1162 const bool *keys = SDL_GetKeyboardState(nullptr);
1163 if (keys == nullptr) {
1164 return;
1165 }
1166
1167 if (ship.lives <= 0) {
1168 return;
1169 }
1170
1171 if (controller.getButton(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER) || controller.getButton(SDL_GAMEPAD_BUTTON_DPAD_UP)) {
1172 increase_speed(dt);
1173 } else if (controller.getButton(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER) || controller.getButton(SDL_GAMEPAD_BUTTON_DPAD_DOWN)) {
1174 decrease_speed(dt);
1175 }
1176
1177 auto ramp_axis = [dt](float &value, float target, float rise_rate, float fall_rate) {
1178 const float rate = (std::fabs(target) > std::fabs(value)) ? rise_rate : fall_rate;
1179 const float step = rate * dt;
1180 if (value < target) {
1181 value = std::min(value + step, target);
1182 } else if (value > target) {
1183 value = std::max(value - step, target);
1184 }
1185 };
1186
1187 float yaw_amount = 0.0f;
1188 float pitch_amount = 0.0f;
1189 float roll_amount = 0.0f;
1190 bool manual_roll_input = false;
1191
1192 const float left_x = controller_axis(SDL_GAMEPAD_AXIS_LEFTX);
1193 if (std::fabs(left_x) > 0.001f) {
1194 yaw_amount = -left_x;
1195 }
1196
1197 float keyboard_yaw_target = 0.0f;
1198 if (keys[SDL_SCANCODE_LEFT]) {
1199 keyboard_yaw_target = 1.0f;
1200 } else if (keys[SDL_SCANCODE_RIGHT]) {
1201 keyboard_yaw_target = -1.0f;
1202 }
1203 ramp_axis(keyboard_yaw, keyboard_yaw_target, 3.2f, 8.0f);
1204 if (std::fabs(keyboard_yaw) > 0.001f) {
1205 yaw_amount = keyboard_yaw;
1206 }
1207
1208 float keyboard_pitch_target = 0.0f;
1209 if (!mouse_look_controls) {
1210 if (inverted_controls) {
1211 if (keys[SDL_SCANCODE_W]) {
1212 keyboard_pitch_target = -1.0f;
1213 }
1214 if (keys[SDL_SCANCODE_S]) {
1215 keyboard_pitch_target = 1.0f;
1216 }
1217 } else {
1218 if (keys[SDL_SCANCODE_W]) {
1219 keyboard_pitch_target = 1.0f;
1220 }
1221 if (keys[SDL_SCANCODE_S]) {
1222 keyboard_pitch_target = -1.0f;
1223 }
1224 }
1225 }
1226 ramp_axis(keyboard_pitch, keyboard_pitch_target, 2.8f, 8.0f);
1227 if (std::fabs(keyboard_pitch) > 0.001f) {
1228 pitch_amount = keyboard_pitch;
1229 }
1230
1231 float keyboard_roll_target = 0.0f;
1232 if (keys[SDL_SCANCODE_A]) {
1233 keyboard_roll_target = -1.0f;
1234 } else if (keys[SDL_SCANCODE_D]) {
1235 keyboard_roll_target = 1.0f;
1236 }
1237 ramp_axis(keyboard_roll, keyboard_roll_target, 3.0f, 8.0f);
1238 if (std::fabs(keyboard_roll) > 0.001f) {
1239 roll_amount = keyboard_roll;
1240 manual_roll_input = true;
1241 }
1242
1243 if (controller.getButton(SDL_GAMEPAD_BUTTON_DPAD_LEFT)) {
1244 yaw_amount = 1.0f;
1245 } else if (controller.getButton(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)) {
1246 yaw_amount = -1.0f;
1247 }
1248
1249 const float right_x = controller_axis(SDL_GAMEPAD_AXIS_RIGHTX);
1250 if (std::fabs(right_x) > 0.001f) {
1251 roll_amount = right_x;
1252 manual_roll_input = true;
1253 }
1254
1255 const float right_y = controller_axis(SDL_GAMEPAD_AXIS_RIGHTY);
1256 if (std::fabs(right_y) > 0.001f) {
1257 pitch_amount = inverted_controls ? right_y : -right_y;
1258 }
1259
1260 const float smoothing = std::clamp(dt * 8.0f, 0.0f, 1.0f);
1261 smooth_yaw = glm::mix(smooth_yaw, yaw_amount, smoothing);
1262 smooth_pitch = glm::mix(smooth_pitch, pitch_amount, smoothing);
1263 smooth_roll = glm::mix(smooth_roll, roll_amount, smoothing);
1264
1265 if (std::fabs(smooth_yaw) > 0.01f) {
1266 ship.rotation.y += smooth_yaw * ship.turn_speed * dt;
1267 }
1268 if (std::fabs(smooth_pitch) > 0.01f) {
1269 ship.rotation.x += smooth_pitch * ship.turn_speed * ship.pitch_speed_multiplier * dt;
1270 }
1271 if (manual_roll_input && std::fabs(smooth_roll) > 0.01f) {
1272 ship.rotation.z += smooth_roll * ship.turn_speed * dt;
1273 }
1274 if (!manual_roll_input && std::fabs(smooth_yaw) > 0.01f) {
1275 const float target_roll = -smooth_yaw * 35.0f;
1276 const float roll_diff = target_roll - ship.rotation.z;
1277 ship.rotation.z += roll_diff * 5.0f * dt;
1278 }
1279 if (!manual_roll_input && std::fabs(smooth_yaw) < 0.01f) {
1280 while (ship.rotation.z > 180.0f) {
1281 ship.rotation.z -= 360.0f;
1282 }
1283 while (ship.rotation.z < -180.0f) {
1284 ship.rotation.z += 360.0f;
1285 }
1286 ship.rotation.z = glm::mix(ship.rotation.z, 0.0f, 3.0f * dt);
1287 }
1288
1289 const bool speed_up_key = mouse_look_controls ? keys[SDL_SCANCODE_W] : keys[SDL_SCANCODE_UP];
1290 const bool slow_down_key = mouse_look_controls ? keys[SDL_SCANCODE_S] : keys[SDL_SCANCODE_DOWN];
1291 if (speed_up_key) {
1292 increase_speed(dt);
1293 } else if (slow_down_key) {
1294 decrease_speed(dt);
1295 } else {
1296 if (ship.current_speed > 5.0f) {
1297 decrease_speed(dt * 0.5f);
1298 } else if (ship.current_speed < 5.0f) {
1299 increase_speed(dt * 0.5f);
1300 }
1301 }
1302
1303 const bool firing = keys[SDL_SCANCODE_SPACE] ||
1304 controller.getButton(SDL_GAMEPAD_BUTTON_SOUTH) ||
1305 controller.getAxis(SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > CONTROLLER_DEAD_ZONE;
1306 if (firing) {
1307 if (can_fire()) {
1308 fire_projectile();
1309 }
1310 } else {
1311 update_fire_timer(false);
1312 }
1313 }
1314
1315 void increase_speed(float dt) {
1316 ship.current_speed += ship.turn_speed * dt * 0.2f;
1317 ship.current_speed = std::min(ship.current_speed, ship.max_speed);
1318 }
1319
1320 void decrease_speed(float dt) {
1321 ship.current_speed -= ship.turn_speed * dt * 0.2f;
1322 ship.current_speed = std::max(ship.current_speed, ship.min_speed);
1323 }
1324
1325 bool can_fire() {
1326 if (ship.overheated) {
1327 return false;
1328 }
1329 if (ship.fire_cooldown <= 0) {
1330 if (ship.burst_count < SHOTS_PER_BURST) {
1331 ship.fire_cooldown = FIRE_DELAY;
1332 ship.burst_count++;
1333 return true;
1334 }
1335 ship.fire_cooldown = FIRE_COOLDOWN;
1336 ship.burst_count = 0;
1337 return false;
1338 }
1339 return false;
1340 }
1341
1342 void update_fire_timer(bool firing) {
1343 if (firing && !ship.overheated) {
1344 ship.continuous_fire_timer++;
1345 ship.overheat_cooldown = 0;
1346 if (ship.continuous_fire_timer >= 180) {
1347 ship.overheated = true;
1348 ship.overheat_cooldown = 0;
1349 ship.continuous_fire_timer = 0;
1350 ship.burst_count = 0;
1351 log_game("Weapons overheated.", SDL_Color{255, 150, 80, 255});
1352 }
1353 } else if (firing && ship.overheated) {
1354 ship.overheat_cooldown = 0;
1355 } else {
1356 if (ship.overheated) {
1357 ship.overheat_cooldown++;
1358 if (ship.overheat_cooldown >= 180) {
1359 ship.overheated = false;
1360 ship.overheat_cooldown = 0;
1361 ship.continuous_fire_timer = 0;
1362 log_game("Weapons cooled down.");
1363 }
1364 } else if (ship.continuous_fire_timer > 0) {
1365 ship.continuous_fire_timer--;
1366 }
1367 }
1368 }
1369
1370 static float normalize_degrees(float degrees) {
1371 while (degrees > 180.0f) {
1372 degrees -= 360.0f;
1373 }
1374 while (degrees < -180.0f) {
1375 degrees += 360.0f;
1376 }
1377 return degrees;
1378 }
1379
1380 static float ease_angle_degrees(float current, float target, float blend) {
1381 return current + normalize_degrees(target - current) * std::clamp(blend, 0.0f, 1.0f);
1382 }
1383
1384 glm::vec3 asteroid_field_center() const {
1385 glm::vec3 sum{0.0f};
1386 int count = 0;
1387 for (const auto &asteroid : asteroids) {
1388 if (!asteroid.active) {
1389 continue;
1390 }
1391 sum += asteroid.position;
1392 ++count;
1393 }
1394 if (count == 0) {
1395 return glm::vec3(0.0f);
1396 }
1397 return sum / static_cast<float>(count);
1398 }
1399
1400 bool ship_is_outside_return_volume() const {
1401 constexpr float RETURN_PADDING = 18.0f;
1402 return ship.position.x < BOUNDARY_X_MIN - RETURN_PADDING ||
1403 ship.position.x > BOUNDARY_X_MAX + RETURN_PADDING ||
1404 ship.position.y < BOUNDARY_Y_MIN - RETURN_PADDING ||
1405 ship.position.y > BOUNDARY_Y_MAX + RETURN_PADDING ||
1406 ship.position.z < BOUNDARY_Z_MIN - RETURN_PADDING ||
1407 ship.position.z > BOUNDARY_Z_MAX + RETURN_PADDING;
1408 }
1409
1410 void update_ship_return_to_field(float dt) {
1411 if (active_asteroids() == 0) {
1412 ship_returning_to_field = false;
1413 return;
1414 }
1415
1416 constexpr float RETURN_START_DISTANCE = 145.0f;
1417 constexpr float RETURN_STOP_DISTANCE = 92.0f;
1418 const float nearest_distance = nearest_asteroid_distance();
1419 const bool outside_return_volume = ship_is_outside_return_volume();
1420 if (!ship_returning_to_field && (outside_return_volume || nearest_distance > RETURN_START_DISTANCE)) {
1421 ship_returning_to_field = true;
1422 if (return_message_cooldown <= 0.0f) {
1423 log_game("Return assist engaged: steering back toward the asteroid field.", SDL_Color{120, 220, 255, 255});
1424 return_message_cooldown = 3.0f;
1425 }
1426 } else if (ship_returning_to_field && !outside_return_volume && nearest_distance < RETURN_STOP_DISTANCE) {
1427 ship_returning_to_field = false;
1428 log_game("Return assist disengaged.");
1429 }
1430
1431 if (!ship_returning_to_field) {
1432 return;
1433 }
1434
1435 const glm::vec3 to_field = normalize_or_zero(asteroid_field_center() - ship.position);
1436 const float target_yaw = glm::degrees(std::atan2(-to_field.x, -to_field.z));
1437 const float target_pitch = glm::degrees(std::asin(std::clamp(to_field.y, -1.0f, 1.0f)));
1438 const float blend = 1.0f - std::exp(-dt * 1.8f);
1439 ship.rotation.y = ease_angle_degrees(ship.rotation.y, target_yaw, blend);
1440 ship.rotation.x = ease_angle_degrees(ship.rotation.x, target_pitch, blend);
1441 ship.rotation.z = ease_angle_degrees(ship.rotation.z, 0.0f, blend * 0.8f);
1442 ship.current_speed = std::max(ship.current_speed, 8.0f);
1443 }
1444
1445 void fire_projectile() {
1446 const glm::vec3 forward = ship.forward();
1447 const float muzzle_offset = 0.08f;
1448 const glm::vec3 muzzle = ship.position + forward * muzzle_offset;
1449 for (auto &projectile : projectiles) {
1450 if (projectile.active) {
1451 continue;
1452 }
1453 projectile.position = muzzle;
1454 projectile.prev_position = muzzle;
1455 projectile.velocity = forward * PROJECTILE_SPEED;
1456 projectile.color = PROJECTILE_COLOR;
1457 projectile.lifetime = 0.0f;
1458 projectile.active = true;
1459#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
1460 play_sound(cannon_sound);
1461#endif
1462 log_game(std::format("Projectile fired from ({:.1f}, {:.1f}, {:.1f}).", muzzle.x, muzzle.y, muzzle.z));
1463 return;
1464 }
1465 log_game("Projectile fire skipped: projectile pool full.", SDL_Color{255, 190, 90, 255});
1466 }
1467
1468 void update_ship(float dt) {
1469 if (ship.exploding) {
1470 ship.explosion_timer--;
1471 if (ship.explosion_timer <= 0) {
1472 ship.exploding = false;
1473 ship.visible = true;
1474 ship.position = glm::vec3(0.0f);
1475 ship.prev_position = ship.position;
1476 ship.velocity = glm::vec3(0.0f);
1477 ship.rotation = glm::vec3(0.0f);
1478 ship.current_speed = 1.0f;
1479 ship_returning_to_field = false;
1480 clear_particles();
1481 log_game("Ship respawned at origin.");
1482 }
1483 return;
1484 }
1485
1486 if (dt <= 0.0f) {
1487 ship.prev_position = ship.position;
1488 ship.velocity = glm::vec3(0.0f);
1489 return;
1490 }
1491
1492 if (return_message_cooldown > 0.0f) {
1493 return_message_cooldown = std::max(0.0f, return_message_cooldown - dt);
1494 }
1495 update_ship_return_to_field(dt);
1496 const glm::vec3 forward = ship.forward();
1497 ship.prev_position = ship.position;
1498 ship.velocity = forward * ship.current_speed;
1499 ship.position += ship.velocity * dt;
1500 ship.rotation.x = std::clamp(ship.rotation.x, -75.0f, 75.0f);
1501 if (ship.rotation.z > 180.0f) {
1502 ship.rotation.z -= 360.0f;
1503 } else if (ship.rotation.z < -180.0f) {
1504 ship.rotation.z += 360.0f;
1505 }
1506 if (ship.fire_cooldown > 0) {
1507 ship.fire_cooldown--;
1508 }
1509 }
1510
1511 void update_projectiles(float dt) {
1512 for (auto &projectile : projectiles) {
1513 if (!projectile.active) {
1514 continue;
1515 }
1516 projectile.prev_position = projectile.position;
1517 projectile.position += projectile.velocity * dt;
1518 projectile.lifetime += dt;
1519 if (projectile.lifetime >= PROJECTILE_LIFETIME) {
1520 projectile.active = false;
1521 }
1522 }
1523
1524 for (auto &asteroid : asteroids) {
1525 if (!asteroid.active) {
1526 continue;
1527 }
1528 for (auto &projectile : projectiles) {
1529 if (!projectile.active) {
1530 continue;
1531 }
1532
1533 const glm::vec3 segment = projectile.position - projectile.prev_position;
1534 const float segment_length_sq = glm::dot(segment, segment);
1535
1536 glm::vec3 closest_point = projectile.prev_position;
1537
1538 if (segment_length_sq > 1e-6f) {
1539 const glm::vec3 to_asteroid = asteroid.position - projectile.prev_position;
1540 const float t = std::clamp(glm::dot(to_asteroid, segment) / segment_length_sq, 0.0f, 1.0f);
1541 closest_point = projectile.prev_position + segment * t;
1542 }
1543
1544 const float dist = glm::length(closest_point - asteroid.position);
1545 const float projectile_hit_radius = asteroid.radius * ASTEROID_PROJECTILE_COLLISION_SCALE;
1546
1547 if (dist < projectile_hit_radius) {
1548 projectile.active = false;
1549 log_game(std::format("Projectile hit asteroid at ({:.1f}, {:.1f}, {:.1f}).", asteroid.position.x, asteroid.position.y, asteroid.position.z));
1550 split_asteroid(asteroid);
1551 break;
1552 }
1553 }
1554 }
1555 }
1556
1557 void split_asteroid(Asteroid &asteroid) {
1558 const glm::vec3 hit_position = asteroid.position;
1559 const int generation = asteroid.generation;
1560 const float radius = asteroid.radius * 0.5f;
1561
1562 spawn_asteroid_explosion(hit_position);
1563#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
1564 play_sound(asteroid_explosion_sound);
1565#endif
1566
1567 if (generation >= MAX_GENERATIONS) {
1568 ship.score += SMALL_ASTEROID_POINTS;
1569 asteroid.active = false;
1570 log_game(std::format("Small asteroid destroyed. Score={}.", ship.score));
1571 return;
1572 }
1573
1574 const int child_count = CHILDREN_PER_SPAWN;
1575 const float child_radius = asteroid.radius * 0.18f;
1576 const glm::vec3 view_forward = normalize_or_zero(hit_position - camera_position);
1577 glm::vec3 split_axis = glm::cross(view_forward, glm::vec3(0.0f, 1.0f, 0.0f));
1578 if (glm::length(split_axis) <= 1e-4f) {
1579 split_axis = glm::cross(view_forward, glm::vec3(1.0f, 0.0f, 0.0f));
1580 }
1581 if (glm::length(split_axis) <= 1e-4f) {
1582 split_axis = glm::vec3(1.0f, 0.0f, 0.0f);
1583 }
1584 split_axis = normalize_or_zero(split_axis);
1585 const glm::vec3 toward_camera = normalize_or_zero(camera_position - hit_position);
1586 const float child_separation = std::max(asteroid.radius * 0.45f, child_radius * 2.0f);
1587 const glm::vec3 depth_bias = toward_camera * (child_separation * 0.12f);
1588 std::array<glm::vec3, CHILDREN_PER_SPAWN> child_positions = {
1589 hit_position - split_axis * child_separation + depth_bias,
1590 hit_position + split_axis * child_separation + depth_bias,
1591 };
1592
1593 for (int i = 0; i < child_count; ++i) {
1594 Asteroid *child = find_free_asteroid(asteroid.model_index);
1595 if (child == nullptr) {
1596 log_game("Asteroid split skipped: no free slot for parent asteroid type.", SDL_Color{255, 190, 90, 255});
1597 break;
1598 }
1599
1600 const glm::vec3 child_offset = child_positions[static_cast<std::size_t>(i)] - hit_position;
1601 const glm::vec3 child_velocity = normalize_or_zero(child_offset) * random_float(5.0f, 9.0f);
1602 child->active = true;
1603 child->position = child_positions[static_cast<std::size_t>(i)];
1604 child->radius = child_radius;
1605 child->generation = generation + 1;
1606 child->rotation = glm::vec3(random_float(0.0f, 360.0f), random_float(0.0f, 360.0f), random_float(0.0f, 360.0f));
1607 child->rotation_speed = asteroid.rotation_speed * random_float(0.8f, 1.5f);
1608 child->model_index = asteroid.model_index;
1609 child->velocity = child_velocity;
1610 log_game(std::format(
1611 "Asteroid child {} spawned at ({:.1f}, {:.1f}, {:.1f}) radius {:.1f}.",
1612 i + 1,
1613 child->position.x,
1614 child->position.y,
1615 child->position.z,
1616 child->radius));
1617 }
1618
1619 if (radius >= 25.0f) {
1620 ship.score += LARGE_ASTEROID_POINTS;
1621 log_game(std::format("Large asteroid split into {} pieces. Score={}.", child_count, ship.score));
1622 } else {
1623 ship.score += MEDIUM_ASTEROID_POINTS;
1624 log_game(std::format("Medium asteroid split into {} pieces. Score={}.", child_count, ship.score));
1625 }
1626
1627 asteroid.active = false;
1628 }
1629
1630 Asteroid *find_free_asteroid(int preferred_model_index = -1) {
1631 for (auto &asteroid : asteroids) {
1632 if (!asteroid.active && (preferred_model_index < 0 || asteroid.model_index == preferred_model_index)) {
1633 return &asteroid;
1634 }
1635 }
1636 return nullptr;
1637 }
1638
1639 void update_asteroids(float dt) {
1640 for (auto &asteroid : asteroids) {
1641 if (!asteroid.active) {
1642 continue;
1643 }
1644 asteroid.position += asteroid.velocity * dt;
1645 asteroid.rotation += asteroid.rotation_speed * dt;
1646 bool bounced = false;
1647 if (asteroid.position.x < BOUNDARY_X_MIN) {
1648 asteroid.position.x = BOUNDARY_X_MIN;
1649 asteroid.velocity.x = -asteroid.velocity.x * BOUNDARY_BOUNCE_FACTOR;
1650 bounced = true;
1651 } else if (asteroid.position.x > BOUNDARY_X_MAX) {
1652 asteroid.position.x = BOUNDARY_X_MAX;
1653 asteroid.velocity.x = -asteroid.velocity.x * BOUNDARY_BOUNCE_FACTOR;
1654 bounced = true;
1655 }
1656 if (asteroid.position.y < BOUNDARY_Y_MIN) {
1657 asteroid.position.y = BOUNDARY_Y_MIN;
1658 asteroid.velocity.y = -asteroid.velocity.y * BOUNDARY_BOUNCE_FACTOR;
1659 bounced = true;
1660 } else if (asteroid.position.y > BOUNDARY_Y_MAX) {
1661 asteroid.position.y = BOUNDARY_Y_MAX;
1662 asteroid.velocity.y = -asteroid.velocity.y * BOUNDARY_BOUNCE_FACTOR;
1663 bounced = true;
1664 }
1665 if (asteroid.position.z < BOUNDARY_Z_MIN) {
1666 asteroid.position.z = BOUNDARY_Z_MIN;
1667 asteroid.velocity.z = -asteroid.velocity.z * BOUNDARY_BOUNCE_FACTOR;
1668 bounced = true;
1669 } else if (asteroid.position.z > BOUNDARY_Z_MAX) {
1670 asteroid.position.z = BOUNDARY_Z_MAX;
1671 asteroid.velocity.z = -asteroid.velocity.z * BOUNDARY_BOUNCE_FACTOR;
1672 bounced = true;
1673 }
1674 if (bounced) {
1675 asteroid.velocity += glm::vec3(random_float(-0.5f, 0.5f), random_float(-0.5f, 0.5f), random_float(-0.5f, 0.5f));
1676 }
1677 if (glm::length(asteroid.velocity) > 0.01f) {
1678 asteroid.velocity *= 0.995f;
1679 }
1680 if (asteroid.rotation.x > 360.0f)
1681 asteroid.rotation.x -= 360.0f;
1682 if (asteroid.rotation.y > 360.0f)
1683 asteroid.rotation.y -= 360.0f;
1684 if (asteroid.rotation.z > 360.0f)
1685 asteroid.rotation.z -= 360.0f;
1686 }
1687
1688 for (auto &asteroid : asteroids) {
1689 if (!asteroid.active) {
1690 continue;
1691 }
1692 const float ship_distance = ship_asteroid_collision_distance(asteroid);
1693 if (ship_distance <= 0.0f) {
1694 log_game(std::format("Ship collision with asteroid overlap {:.2f}.", -ship_distance), SDL_Color{255, 130, 90, 255});
1695 start_ship_explosion();
1696 break;
1697 }
1698 }
1699 }
1700
1701 float ship_asteroid_collision_distance(const Asteroid &asteroid) const {
1702 static constexpr std::array<ShipCollisionSample, 5> ship_samples = {
1703 ShipCollisionSample{{0.0f, 0.0f, -0.55f}, 0.055f},
1704 ShipCollisionSample{{0.0f, 0.06f, -0.16f}, 0.160f},
1705 ShipCollisionSample{{0.0f, 0.08f, 0.34f}, 0.125f},
1706 ShipCollisionSample{{-0.42f, 0.03f, -0.02f}, 0.085f},
1707 ShipCollisionSample{{0.42f, 0.03f, -0.02f}, 0.085f},
1708 };
1709
1710 const float asteroid_collision_radius = asteroid.radius * ASTEROID_SHIP_COLLISION_SCALE;
1711 float nearest_surface_distance = std::numeric_limits<float>::max();
1712
1713 for (const ShipCollisionSample &sample : ship_samples) {
1714 const float ship_scale = rendered_ship_scale();
1715 const glm::vec3 offset = transform_ship_collision_offset(sample.local_position);
1716 const glm::vec3 previous_position = ship.prev_position + offset;
1717 const glm::vec3 current_position = ship.position + offset;
1718 const float center_distance = swept_point_distance_to_asteroid(previous_position, current_position, asteroid.position);
1719 nearest_surface_distance = std::min(nearest_surface_distance, center_distance - asteroid_collision_radius - (sample.radius * ship_scale));
1720 }
1721
1722 return nearest_surface_distance;
1723 }
1724
1725 glm::vec3 transform_ship_collision_offset(const glm::vec3 &local_position) const {
1726 const glm::mat4 model = build_model_matrix(
1727 glm::vec3(0.0f),
1728 ship.rotation,
1729 rendered_ship_scale(),
1730 ship_model.modelCenterOffset());
1731 return glm::vec3(model * glm::vec4(local_position, 1.0f));
1732 }
1733
1734 float rendered_ship_scale() const {
1735 return SHIP_MODEL_SCALE * ship_model.modelRenderScale();
1736 }
1737
1738 float swept_point_distance_to_asteroid(const glm::vec3 &previous_position,
1739 const glm::vec3 &current_position,
1740 const glm::vec3 &asteroid_position) const {
1741 const glm::vec3 segment = current_position - previous_position;
1742 const float segment_length_sq = glm::dot(segment, segment);
1743 glm::vec3 closest_point = current_position;
1744
1745 if (segment_length_sq > 1e-6f) {
1746 const glm::vec3 to_asteroid = asteroid_position - previous_position;
1747 const float t = std::clamp(glm::dot(to_asteroid, segment) / segment_length_sq, 0.0f, 1.0f);
1748 closest_point = previous_position + segment * t;
1749 }
1750
1751 return glm::length(closest_point - asteroid_position);
1752 }
1753
1754 void start_ship_explosion() {
1755 if (ship.exploding) {
1756 return;
1757 }
1758 ship.exploding = true;
1759 ship.visible = false;
1760 ship.explosion_timer = EXPLOSION_DURATION_FRAMES;
1761 ship.lives--;
1762 ship.overheated = false;
1763 ship.overheat_cooldown = 0;
1764 ship.continuous_fire_timer = 0;
1765 ship.burst_count = 0;
1766#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
1767 play_sound(crash_sound);
1768#endif
1769 log_game(std::format("Ship destroyed. Lives remaining: {}.", std::max(0, ship.lives)), SDL_Color{255, 120, 80, 255});
1770 if (ship.lives <= 0) {
1771 log_game(std::format("Game over. Final score: {}.", ship.score), SDL_Color{255, 90, 90, 255});
1772 }
1773 spawn_ship_explosion(ship.position);
1774 }
1775
1776 void spawn_asteroid_explosion(const glm::vec3 &position) {
1777 spawn_gl_explosion(position);
1778 }
1779
1780 void spawn_ship_explosion(const glm::vec3 &position) {
1781 spawn_gl_explosion(position);
1782 }
1783
1784 void spawn_gl_explosion(const glm::vec3 &position) {
1785 struct ExplosionWave {
1786 float min_speed;
1787 float max_speed;
1788 float min_size;
1789 float max_size;
1790 float min_lifetime;
1791 float max_lifetime;
1792 glm::vec3 color;
1793 };
1794
1795 constexpr int WAVE_COUNT = 4;
1796 constexpr int MAX_GL_EXPLOSIONS = 5;
1797 constexpr std::array<ExplosionWave, WAVE_COUNT> waves = {
1798 ExplosionWave{40.0f, 60.0f, 0.72f, 1.14f, 1.5f, 2.5f, {1.0f, 1.0f, 1.0f}},
1799 ExplosionWave{30.0f, 45.0f, 0.58f, 0.86f, 2.0f, 3.0f, {1.0f, 1.0f, 1.0f}},
1800 ExplosionWave{20.0f, 35.0f, 0.43f, 0.72f, 2.5f, 3.5f, {1.0f, 1.0f, 1.0f}},
1801 ExplosionWave{10.0f, 25.0f, 0.14f, 0.43f, 3.0f, 4.0f, {1.0f, 1.0f, 1.0f}},
1802 };
1803
1804 const int particles_per_wave = MAX_PARTICLES / (WAVE_COUNT * MAX_GL_EXPLOSIONS);
1805 int spawned = 0;
1806 for (int wave_index = 0; wave_index < WAVE_COUNT; ++wave_index) {
1807 const ExplosionWave &wave = waves[static_cast<std::size_t>(wave_index)];
1808 for (int i = 0; i < particles_per_wave; ++i) {
1809 Particle *particle = find_free_particle();
1810 if (particle == nullptr) {
1811 return;
1812 }
1813
1814 const float theta = random_float(0.0f, 2.0f * PI);
1815 const float phi = random_float(0.0f, PI);
1816 const glm::vec3 dir{
1817 std::sin(phi) * std::cos(theta),
1818 std::sin(phi) * std::sin(theta),
1819 std::cos(phi),
1820 };
1821
1822 const float offset = 0.8f + 0.2f * static_cast<float>(wave_index) / static_cast<float>(WAVE_COUNT);
1823 const float speed = random_float(wave.min_speed, wave.max_speed);
1824 particle->position = position + dir * offset;
1825 particle->velocity = dir * speed + glm::vec3(
1826 random_float(-5.0f, 5.0f),
1827 random_float(-5.0f, 5.0f),
1828 random_float(-5.0f, 5.0f));
1829 particle->color = glm::vec4(
1830 wave.color.r * random_float(0.9f, 1.1f),
1831 wave.color.g * random_float(0.9f, 1.1f),
1832 wave.color.b * random_float(0.9f, 1.1f),
1833 0.1f);
1834 particle->size = random_float(wave.min_size, wave.max_size);
1835 particle->lifetime = 0.0f;
1836 particle->max_lifetime = random_float(wave.min_lifetime, wave.max_lifetime);
1837 particle->active = true;
1838 ++spawned;
1839 }
1840 }
1841 log_game(std::format("Explosion spawned {} particles.", spawned));
1842 }
1843
1844 void spawn_particles(const glm::vec3 &position,
1845 const glm::vec4 &color,
1846 int count,
1847 float min_speed,
1848 float max_speed,
1849 float min_size,
1850 float max_size,
1851 float min_lifetime,
1852 float max_lifetime) {
1853 for (int i = 0; i < count; ++i) {
1854 Particle *particle = find_free_particle();
1855 if (particle == nullptr) {
1856 return;
1857 }
1858 const glm::vec3 dir = normalize_or_zero(glm::vec3(
1859 random_float(-1.0f, 1.0f),
1860 random_float(-1.0f, 1.0f),
1861 random_float(-1.0f, 1.0f)));
1862 particle->position = position;
1863 particle->velocity = dir * random_float(min_speed, max_speed);
1864 particle->color = color;
1865 particle->size = random_float(min_size, max_size);
1866 particle->lifetime = 0.0f;
1867 particle->max_lifetime = random_float(min_lifetime, max_lifetime);
1868 particle->active = true;
1869 }
1870 }
1871
1872 Particle *find_free_particle() {
1873 for (auto &particle : particles) {
1874 if (!particle.active) {
1875 return &particle;
1876 }
1877 }
1878 return nullptr;
1879 }
1880
1881 void update_particles(float dt) {
1882 for (auto &particle : particles) {
1883 if (!particle.active) {
1884 continue;
1885 }
1886 particle.position += particle.velocity * dt;
1887 particle.velocity *= 0.98f;
1888 particle.velocity.y -= 0.5f * dt;
1889 particle.lifetime += dt;
1890
1891 const float life_ratio = particle.lifetime / particle.max_lifetime;
1892 if (life_ratio >= 1.0f) {
1893 particle.active = false;
1894 continue;
1895 }
1896 if (life_ratio < 0.2f) {
1897 particle.color.a = life_ratio / 0.2f;
1898 } else if (life_ratio > 0.8f) {
1899 particle.color.a = (1.0f - life_ratio) / 0.2f;
1900 } else {
1901 particle.color.a = 1.0f;
1902 }
1903 if (life_ratio < 0.3f) {
1904 particle.size *= 1.01f;
1905 } else {
1906 particle.size *= 0.99f;
1907 }
1908 if (particle.color.a < 0.01f) {
1909 particle.active = false;
1910 }
1911 }
1912 }
1913
1914 void clear_particles() {
1915 for (auto &particle : particles) {
1916 particle.active = false;
1917 }
1918 }
1919
1920 void prepare_restart_from_game_over() {
1921 clear_round_state();
1922 restart_after_intro = true;
1923 reset_intro_screen();
1924 }
1925
1926 void reset_intro_screen() {
1927 mode = GameMode::Intro;
1928 intro_fade = 1.0f;
1929 intro_last_update_ms = SDL_GetTicks();
1930 loading_rain_opacity = 1.0f;
1931 if (intro_rain != nullptr) {
1932 intro_rain->set_opacity(1.0f);
1933 intro_rain->reset();
1934 }
1935 }
1936
1937 void update_round_timer(float dt) {
1938 if (mode != GameMode::Playing) {
1939 return;
1940 }
1941
1942 if (round_time_remaining <= 0.0f) {
1943 mode = GameMode::GameOver;
1944 return;
1945 }
1946
1947 round_time_remaining = std::max(0.0f, round_time_remaining - dt);
1948 if (round_time_remaining <= 0.0f) {
1949 mode = GameMode::GameOver;
1950 ship.exploding = false;
1951 ship.visible = false;
1952 ship.fire_cooldown = 0;
1953 ship.burst_count = 0;
1954 ship.continuous_fire_timer = 0;
1955 ship.overheated = false;
1956 ship.overheat_cooldown = 0;
1957 log_game("Time expired. Game over.", SDL_Color{255, 90, 90, 255});
1958 }
1959 }
1960
1961 void set_ui_font_size(int font_size) {
1962 if (font_size == last_font_size) {
1963 return;
1964 }
1965
1966 last_font_size = font_size;
1967 setFont(asset_root + "/data/font.ttf", font_size);
1969 }
1970
1971 std::string format_round_time() const {
1972 const int total_seconds = std::max(0, static_cast<int>(std::ceil(round_time_remaining)));
1973 const int minutes = total_seconds / 60;
1974 const int seconds = total_seconds % 60;
1975 return std::format("{:02d}:{:02d}", minutes, seconds);
1976 }
1977
1978 glm::mat4 ship_rotation_matrix() const {
1979 glm::mat4 rotation(1.0f);
1980 rotation = glm::rotate(rotation, glm::radians(ship.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
1981 rotation = glm::rotate(rotation, glm::radians(ship.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
1982 rotation = glm::rotate(rotation, glm::radians(ship.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
1983 return rotation;
1984 }
1985
1986 struct CameraPose {
1987 glm::vec3 position{0.0f};
1988 glm::vec3 target{0.0f};
1989 glm::vec3 up{0.0f, 1.0f, 0.0f};
1990 };
1991
1992 CameraPose chase_camera_pose(const glm::vec3 &ship_forward) const {
1993 CameraPose pose{};
1994 pose.position = ship.position - ship_forward * ship.camera_distance + glm::vec3(0.0f, ship.camera_height, 0.0f);
1995 pose.target = ship.position + ship_forward * 6.0f;
1996 pose.up = glm::vec3(0.0f, 1.0f, 0.0f);
1997 return pose;
1998 }
1999
2000 CameraPose first_person_camera_pose(const glm::mat4 &ship_rotation_matrix, const glm::vec3 &ship_forward) const {
2001 constexpr glm::vec3 FIRST_PERSON_CAMERA_OFFSET{0.0f, 0.16f, -0.30f};
2002
2003 CameraPose pose{};
2004 const glm::vec3 cockpit_offset = glm::vec3(ship_rotation_matrix * glm::vec4(FIRST_PERSON_CAMERA_OFFSET * rendered_ship_scale(), 0.0f));
2005 pose.position = ship.position + cockpit_offset;
2006 pose.target = pose.position + ship_forward * 8.0f;
2007 pose.up = normalize_or_zero(glm::vec3(ship_rotation_matrix * glm::vec4(0.0f, 1.0f, 0.0f, 0.0f)));
2008 return pose;
2009 }
2010
2011 static float smooth_camera_transition(float value) {
2012 value = std::clamp(value, 0.0f, 1.0f);
2013 return value * value * (3.0f - 2.0f * value);
2014 }
2015
2016 void begin_camera_transition(bool target_first_person_camera) {
2017 first_person_camera = target_first_person_camera;
2018 camera_transition_active = true;
2019 camera_transition_elapsed = 0.0f;
2020 camera_transition_start_position = camera_position;
2021 camera_transition_start_target = camera_target_position;
2022 camera_transition_start_up = camera_up_vector;
2023 }
2024
2025 void update_camera(float dt) {
2026 const glm::mat4 ship_rotation_matrix = this->ship_rotation_matrix();
2027 const glm::vec3 ship_forward = normalize_or_zero(glm::vec3(ship_rotation_matrix * glm::vec4(0.0f, 0.0f, -1.0f, 0.0f)));
2028 const CameraPose target_pose = first_person_camera ? first_person_camera_pose(ship_rotation_matrix, ship_forward) : chase_camera_pose(ship_forward);
2029
2030 if (camera_transition_active) {
2031 camera_transition_elapsed += dt;
2032 const float blend = smooth_camera_transition(camera_transition_elapsed / CAMERA_TRANSITION_SECONDS);
2033 camera_position = glm::mix(camera_transition_start_position, target_pose.position, blend);
2034 camera_target_position = glm::mix(camera_transition_start_target, target_pose.target, blend);
2035 camera_up_vector = normalize_or_zero(glm::mix(camera_transition_start_up, target_pose.up, blend));
2036 if (camera_transition_elapsed >= CAMERA_TRANSITION_SECONDS) {
2037 camera_transition_active = false;
2038 camera_position = target_pose.position;
2039 camera_target_position = target_pose.target;
2040 camera_up_vector = target_pose.up;
2041 }
2042 } else if (first_person_camera) {
2043 camera_position = target_pose.position;
2044 camera_target_position = target_pose.target;
2045 camera_up_vector = target_pose.up;
2046 } else {
2047 camera_position = glm::mix(camera_position, target_pose.position, 1.0f - std::exp(-dt * 10.0f));
2048 camera_target_position = target_pose.target;
2049 camera_up_vector = target_pose.up;
2050 }
2051
2052 view_matrix = glm::lookAt(camera_position, camera_target_position, camera_up_vector);
2053 }
2054
2055 void draw_ship(uint32_t image_index) {
2056 if (!ship.visible) {
2057 return;
2058 }
2059
2060 mxvk::UniformBufferObject ubo{};
2061 ubo.model = build_model_matrix(ship.position, ship.rotation, rendered_ship_scale(), ship_model.modelCenterOffset());
2062 last_ship_model_matrix = ubo.model;
2063 ubo.view = view_matrix;
2064 ubo.proj = projection_matrix;
2065 ubo.fx = glm::vec4(camera_position, elapsed_seconds);
2066 ship_model.updateUBO(image_index, ubo);
2067 ship_model.render(current_command_buffer, image_index, false);
2068 }
2069
2070 void draw_asteroids(uint32_t image_index) {
2071 for (std::size_t i = 0; i < asteroids.size(); ++i) {
2072 const Asteroid &asteroid = asteroids[i];
2073 if (!asteroid.active) {
2074 continue;
2075 }
2076 mxvk::UniformBufferObject ubo{};
2077 const float scale = asteroid.radius;
2078 mxvk::VKAbstractModel &asteroid_model = asteroid_models[i];
2079 ubo.model = build_model_matrix(asteroid.position, asteroid.rotation, scale * asteroid_model.modelRenderScale(),
2080 asteroid_model.modelCenterOffset());
2081 ubo.view = view_matrix;
2082 ubo.proj = projection_matrix;
2083 ubo.fx = glm::vec4(camera_position, elapsed_seconds);
2084 asteroid_model.updateUBO(image_index, ubo);
2085 asteroid_model.render(current_command_buffer, image_index, false);
2086 }
2087 }
2088
2089 void draw_projectiles() {
2090 for (const auto &projectile : projectiles) {
2091 if (!projectile.active) {
2092 continue;
2093 }
2094 const float life_factor = 1.0f - (projectile.lifetime / PROJECTILE_LIFETIME);
2095 const float pulse = (0.55f + 0.22f * (1.0f - life_factor)) * (0.9f + 0.1f * std::sin(elapsed_seconds * 12.0f));
2096 const glm::vec4 color = glm::vec4(
2097 std::clamp(projectile.color.r * (0.9f + 0.1f * life_factor), 0.0f, 1.0f),
2098 std::clamp(projectile.color.g * (0.9f + 0.1f * life_factor), 0.0f, 1.0f),
2099 std::clamp(projectile.color.b * (0.9f + 0.1f * life_factor), 0.0f, 1.0f),
2100 std::clamp(projectile.color.a * (0.65f + 0.35f * life_factor), 0.0f, 1.0f));
2101 projectile_sprite->drawSprite(projectile.position, glm::vec2(pulse), color);
2102 }
2103 }
2104
2105 void draw_particles() {
2106 for (const auto &particle : particles) {
2107 if (!particle.active) {
2108 continue;
2109 }
2110 effect_sprite->drawSprite(particle.position,
2111 glm::vec2(particle.size),
2112 particle.color);
2113 }
2114 }
2115
2116 void create_flame_resources() {
2117 create_flame_mesh();
2118 create_flame_swapchain_resources();
2119 }
2120
2121 void cleanup_flame_resources() {
2122 cleanup_flame_swapchain_resources();
2123 if (flame_vertex_buffer != VK_NULL_HANDLE) {
2124 vkDestroyBuffer(device, flame_vertex_buffer, nullptr);
2125 flame_vertex_buffer = VK_NULL_HANDLE;
2126 }
2127 if (flame_vertex_buffer_memory != VK_NULL_HANDLE) {
2128 vkFreeMemory(device, flame_vertex_buffer_memory, nullptr);
2129 flame_vertex_buffer_memory = VK_NULL_HANDLE;
2130 }
2131 flame_vertex_count = 0;
2132 }
2133
2134 void cleanup_flame_swapchain_resources() {
2135 if (flame_pipeline != VK_NULL_HANDLE) {
2136 vkDestroyPipeline(device, flame_pipeline, nullptr);
2137 flame_pipeline = VK_NULL_HANDLE;
2138 }
2139 if (flame_pipeline_layout != VK_NULL_HANDLE) {
2140 vkDestroyPipelineLayout(device, flame_pipeline_layout, nullptr);
2141 flame_pipeline_layout = VK_NULL_HANDLE;
2142 }
2143 }
2144
2145 void create_flame_swapchain_resources() {
2146 if (flame_vertex_count == 0 || device == VK_NULL_HANDLE) {
2147 return;
2148 }
2149 create_flame_pipeline();
2150 }
2151
2152 void create_flame_mesh() {
2153 constexpr int segments = 40;
2154 constexpr float base_z = 0.555f;
2155 constexpr float tip_z = 1.02f;
2156 constexpr float base_y = 0.040f;
2157 constexpr float outer_radius = 0.052f;
2158 constexpr float inner_radius = 0.026f;
2159
2160 std::vector<FlameVertex> vertices{};
2161 vertices.reserve(static_cast<std::size_t>(segments) * 6U);
2162
2163 const glm::vec4 outer_base_color{1.0f, 0.42f, 0.08f, 0.50f};
2164 const glm::vec4 outer_tip_color{0.7f, 0.08f, 0.0f, 0.0f};
2165 const glm::vec4 inner_base_color{1.0f, 0.92f, 0.45f, 0.72f};
2166 const glm::vec4 inner_tip_color{1.0f, 0.32f, 0.04f, 0.0f};
2167
2168 auto add_cone = [&](float radius, const glm::vec4 &base_color, const glm::vec4 &tip_color) {
2169 const glm::vec3 tip{0.0f, base_y, tip_z};
2170 for (int i = 0; i < segments; ++i) {
2171 const float a0 = (static_cast<float>(i) / static_cast<float>(segments)) * 2.0f * PI;
2172 const float a1 = (static_cast<float>(i + 1) / static_cast<float>(segments)) * 2.0f * PI;
2173 const glm::vec3 p0{std::cos(a0) * radius, base_y + std::sin(a0) * radius, base_z};
2174 const glm::vec3 p1{std::cos(a1) * radius, base_y + std::sin(a1) * radius, base_z};
2175 vertices.push_back({p0, base_color});
2176 vertices.push_back({p1, base_color});
2177 vertices.push_back({tip, tip_color});
2178 }
2179 };
2180
2181 add_cone(outer_radius, outer_base_color, outer_tip_color);
2182 add_cone(inner_radius, inner_base_color, inner_tip_color);
2183
2184 flame_vertex_count = static_cast<uint32_t>(vertices.size());
2185 const VkDeviceSize buffer_size = sizeof(FlameVertex) * static_cast<VkDeviceSize>(vertices.size());
2186 create_buffer(buffer_size,
2187 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
2188 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
2189 flame_vertex_buffer,
2190 flame_vertex_buffer_memory);
2191
2192 void *data = nullptr;
2193 if (vkMapMemory(device, flame_vertex_buffer_memory, 0, buffer_size, 0, &data) != VK_SUCCESS || data == nullptr) {
2194 throw mxvk::Exception("Failed to map asteroids3d flame vertex buffer");
2195 }
2196 std::memcpy(data, vertices.data(), static_cast<std::size_t>(buffer_size));
2197 vkUnmapMemory(device, flame_vertex_buffer_memory);
2198 }
2199
2200 void create_flame_pipeline() {
2201 cleanup_flame_swapchain_resources();
2202
2203 const std::vector<char> vert_shader_code = loadSpv(shader_root + "/flame.vert.spv");
2204 const std::vector<char> frag_shader_code = loadSpv(shader_root + "/flame.frag.spv");
2205
2206 VkShaderModule vert_shader_module = createShaderModule(device, vert_shader_code);
2207 VkShaderModule frag_shader_module = VK_NULL_HANDLE;
2208
2209 try {
2210 frag_shader_module = createShaderModule(device, frag_shader_code);
2211
2212 VkPipelineShaderStageCreateInfo vert_stage{};
2213 vert_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
2214 vert_stage.stage = VK_SHADER_STAGE_VERTEX_BIT;
2215 vert_stage.module = vert_shader_module;
2216 vert_stage.pName = "main";
2217
2218 VkPipelineShaderStageCreateInfo frag_stage{};
2219 frag_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
2220 frag_stage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
2221 frag_stage.module = frag_shader_module;
2222 frag_stage.pName = "main";
2223
2224 std::array<VkPipelineShaderStageCreateInfo, 2> shader_stages = {vert_stage, frag_stage};
2225
2226 VkVertexInputBindingDescription binding_description{};
2227 binding_description.binding = 0;
2228 binding_description.stride = sizeof(FlameVertex);
2229 binding_description.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
2230
2231 std::array<VkVertexInputAttributeDescription, 2> attributes{};
2232 attributes[0].binding = 0;
2233 attributes[0].location = 0;
2234 attributes[0].format = VK_FORMAT_R32G32B32_SFLOAT;
2235 attributes[0].offset = offsetof(FlameVertex, pos);
2236 attributes[1].binding = 0;
2237 attributes[1].location = 1;
2238 attributes[1].format = VK_FORMAT_R32G32B32A32_SFLOAT;
2239 attributes[1].offset = offsetof(FlameVertex, color);
2240
2241 VkPipelineVertexInputStateCreateInfo vertex_input{};
2242 vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
2243 vertex_input.vertexBindingDescriptionCount = 1;
2244 vertex_input.pVertexBindingDescriptions = &binding_description;
2245 vertex_input.vertexAttributeDescriptionCount = static_cast<uint32_t>(attributes.size());
2246 vertex_input.pVertexAttributeDescriptions = attributes.data();
2247
2248 VkPipelineInputAssemblyStateCreateInfo input_assembly{};
2249 input_assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
2250 input_assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
2251 input_assembly.primitiveRestartEnable = VK_FALSE;
2252
2253 VkPipelineViewportStateCreateInfo viewport_state{};
2254 viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
2255 viewport_state.viewportCount = 1;
2256 viewport_state.scissorCount = 1;
2257
2258 const std::array<VkDynamicState, 2> dynamic_states = {
2259 VK_DYNAMIC_STATE_VIEWPORT,
2260 VK_DYNAMIC_STATE_SCISSOR,
2261 };
2262 VkPipelineDynamicStateCreateInfo dynamic_info{};
2263 dynamic_info.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
2264 dynamic_info.dynamicStateCount = static_cast<uint32_t>(dynamic_states.size());
2265 dynamic_info.pDynamicStates = dynamic_states.data();
2266
2267 VkPipelineRasterizationStateCreateInfo rasterizer{};
2268 rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
2269 rasterizer.depthClampEnable = VK_FALSE;
2270 rasterizer.rasterizerDiscardEnable = VK_FALSE;
2271 rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
2272 rasterizer.lineWidth = 1.0f;
2273 rasterizer.cullMode = VK_CULL_MODE_NONE;
2274 rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
2275 rasterizer.depthBiasEnable = VK_FALSE;
2276
2277 VkPipelineMultisampleStateCreateInfo multisampling{};
2278 multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
2279 multisampling.sampleShadingEnable = VK_FALSE;
2280 multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
2281
2282 VkPipelineDepthStencilStateCreateInfo depth_stencil{};
2283 depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
2284 depth_stencil.depthTestEnable = VK_TRUE;
2285 depth_stencil.depthWriteEnable = VK_FALSE;
2286 depth_stencil.depthCompareOp = VK_COMPARE_OP_LESS;
2287 depth_stencil.depthBoundsTestEnable = VK_FALSE;
2288 depth_stencil.stencilTestEnable = VK_FALSE;
2289
2290 VkPipelineColorBlendAttachmentState color_blend_attachment{};
2291 color_blend_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
2292 VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
2293 color_blend_attachment.blendEnable = VK_TRUE;
2294 color_blend_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
2295 color_blend_attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE;
2296 color_blend_attachment.colorBlendOp = VK_BLEND_OP_ADD;
2297 color_blend_attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
2298 color_blend_attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
2299 color_blend_attachment.alphaBlendOp = VK_BLEND_OP_ADD;
2300
2301 VkPipelineColorBlendStateCreateInfo color_blending{};
2302 color_blending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
2303 color_blending.logicOpEnable = VK_FALSE;
2304 color_blending.attachmentCount = 1;
2305 color_blending.pAttachments = &color_blend_attachment;
2306
2307 VkPushConstantRange push_range{};
2308 push_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
2309 push_range.offset = 0;
2310 push_range.size = sizeof(FlamePushConstants);
2311
2312 VkPipelineLayoutCreateInfo layout_info{};
2313 layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
2314 layout_info.pushConstantRangeCount = 1;
2315 layout_info.pPushConstantRanges = &push_range;
2316
2317 if (vkCreatePipelineLayout(device, &layout_info, nullptr, &flame_pipeline_layout) != VK_SUCCESS) {
2318 throw mxvk::Exception("Failed to create asteroids3d flame pipeline layout");
2319 }
2320
2321 const VkFormat color_format = getSwapchainFormat();
2322 const VkFormat depth_format = getDepthFormat();
2323
2324 VkPipelineRenderingCreateInfo rendering_info{};
2325 rendering_info.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
2326 rendering_info.colorAttachmentCount = 1;
2327 rendering_info.pColorAttachmentFormats = &color_format;
2328 if (depth_format != VK_FORMAT_UNDEFINED) {
2329 rendering_info.depthAttachmentFormat = depth_format;
2330 }
2331
2332 VkGraphicsPipelineCreateInfo pipeline_info{};
2333 pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
2334 pipeline_info.pNext = &rendering_info;
2335 pipeline_info.stageCount = static_cast<uint32_t>(shader_stages.size());
2336 pipeline_info.pStages = shader_stages.data();
2337 pipeline_info.pVertexInputState = &vertex_input;
2338 pipeline_info.pInputAssemblyState = &input_assembly;
2339 pipeline_info.pViewportState = &viewport_state;
2340 pipeline_info.pRasterizationState = &rasterizer;
2341 pipeline_info.pMultisampleState = &multisampling;
2342 pipeline_info.pDepthStencilState = &depth_stencil;
2343 pipeline_info.pColorBlendState = &color_blending;
2344 pipeline_info.pDynamicState = &dynamic_info;
2345 pipeline_info.layout = flame_pipeline_layout;
2346 pipeline_info.renderPass = VK_NULL_HANDLE;
2347 pipeline_info.subpass = 0;
2348
2349 if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &flame_pipeline) != VK_SUCCESS) {
2350 throw mxvk::Exception("Failed to create asteroids3d flame pipeline");
2351 }
2352 } catch (...) {
2353 if (frag_shader_module != VK_NULL_HANDLE) {
2354 vkDestroyShaderModule(device, frag_shader_module, nullptr);
2355 }
2356 vkDestroyShaderModule(device, vert_shader_module, nullptr);
2357 cleanup_flame_swapchain_resources();
2358 throw;
2359 }
2360
2361 vkDestroyShaderModule(device, frag_shader_module, nullptr);
2362 vkDestroyShaderModule(device, vert_shader_module, nullptr);
2363 }
2364
2365 void draw_engine_flame(VkCommandBuffer cmd, const VkExtent2D &extent) {
2366 if (!ship.visible || ship.current_speed <= ship.min_speed * 1.2f) {
2367 return;
2368 }
2369 if (flame_pipeline == VK_NULL_HANDLE || flame_vertex_buffer == VK_NULL_HANDLE || flame_vertex_count == 0) {
2370 return;
2371 }
2372
2373 VkViewport viewport{};
2374 viewport.x = 0.0f;
2375 viewport.y = 0.0f;
2376 viewport.width = static_cast<float>(extent.width);
2377 viewport.height = static_cast<float>(extent.height);
2378 viewport.minDepth = 0.0f;
2379 viewport.maxDepth = 1.0f;
2380 vkCmdSetViewport(cmd, 0, 1, &viewport);
2381
2382 VkRect2D scissor{};
2383 scissor.offset = {0, 0};
2384 scissor.extent = extent;
2385 vkCmdSetScissor(cmd, 0, 1, &scissor);
2386
2387 FlamePushConstants pc{};
2388 pc.mvp = projection_matrix * view_matrix * last_ship_model_matrix;
2389 pc.params = glm::vec4(elapsed_seconds, ship.current_speed / ship.max_speed, 0.0f, 0.0f);
2390
2391 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, flame_pipeline);
2392 vkCmdPushConstants(cmd,
2393 flame_pipeline_layout,
2394 VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
2395 0,
2396 sizeof(pc),
2397 &pc);
2398
2399 VkBuffer vertex_buffers[] = {flame_vertex_buffer};
2400 VkDeviceSize offsets[] = {0};
2401 vkCmdBindVertexBuffers(cmd, 0, 1, vertex_buffers, offsets);
2402 vkCmdDraw(cmd, flame_vertex_count, 1, 0, 0);
2403 }
2404
2405 void create_buffer(VkDeviceSize size,
2406 VkBufferUsageFlags usage,
2407 VkMemoryPropertyFlags properties,
2408 VkBuffer &buffer,
2409 VkDeviceMemory &buffer_memory) const {
2410 VkBufferCreateInfo buffer_info{};
2411 buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
2412 buffer_info.size = size;
2413 buffer_info.usage = usage;
2414 buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
2415
2416 if (vkCreateBuffer(device, &buffer_info, nullptr, &buffer) != VK_SUCCESS) {
2417 throw mxvk::Exception("Failed to create asteroids3d buffer");
2418 }
2419
2420 VkMemoryRequirements mem_requirements{};
2421 vkGetBufferMemoryRequirements(device, buffer, &mem_requirements);
2422
2423 VkMemoryAllocateInfo alloc_info{};
2424 alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
2425 alloc_info.allocationSize = mem_requirements.size;
2426
2427 try {
2428 alloc_info.memoryTypeIndex = find_memory_type(mem_requirements.memoryTypeBits, properties);
2429 if (vkAllocateMemory(device, &alloc_info, nullptr, &buffer_memory) != VK_SUCCESS) {
2430 throw mxvk::Exception("Failed to allocate asteroids3d buffer memory");
2431 }
2432 if (vkBindBufferMemory(device, buffer, buffer_memory, 0) != VK_SUCCESS) {
2433 throw mxvk::Exception("Failed to bind asteroids3d buffer memory");
2434 }
2435 } catch (...) {
2436 if (buffer_memory != VK_NULL_HANDLE) {
2437 vkFreeMemory(device, buffer_memory, nullptr);
2438 buffer_memory = VK_NULL_HANDLE;
2439 }
2440 if (buffer != VK_NULL_HANDLE) {
2441 vkDestroyBuffer(device, buffer, nullptr);
2442 buffer = VK_NULL_HANDLE;
2443 }
2444 throw;
2445 }
2446 }
2447
2448 [[nodiscard]] uint32_t find_memory_type(uint32_t type_filter, VkMemoryPropertyFlags properties) const {
2449 VkPhysicalDeviceMemoryProperties mem_properties{};
2450 vkGetPhysicalDeviceMemoryProperties(physical_device, &mem_properties);
2451
2452 for (uint32_t i = 0; i < mem_properties.memoryTypeCount; ++i) {
2453 if ((type_filter & (1U << i)) && (mem_properties.memoryTypes[i].propertyFlags & properties) == properties) {
2454 return i;
2455 }
2456 }
2457
2458 throw mxvk::Exception("Failed to find asteroids3d memory type");
2459 }
2460
2461 void draw_ui_rect(int x, int y, int width, int height, const glm::vec4 &color) {
2462 if (ui_pixel == nullptr || width <= 0 || height <= 0) {
2463 return;
2464 }
2465 ui_pixel->setShaderParams(color.r, color.g, color.b, color.a);
2466 ui_pixel->drawSpriteRect(x, y, width, height);
2467 }
2468
2469 std::optional<glm::ivec2> project_world_to_screen(const glm::vec3 &world_position, const VkExtent2D &extent) const {
2470 const glm::vec4 clip = projection_matrix * view_matrix * glm::vec4(world_position, 1.0f);
2471 if (clip.w <= 0.0001f) {
2472 return std::nullopt;
2473 }
2474
2475 const glm::vec3 ndc = glm::vec3(clip) / clip.w;
2476 if (ndc.z < 0.0f || ndc.z > 1.0f) {
2477 return std::nullopt;
2478 }
2479
2480 const int x = static_cast<int>((ndc.x * 0.5f + 0.5f) * static_cast<float>(extent.width));
2481 const int y = static_cast<int>((ndc.y * 0.5f + 0.5f) * static_cast<float>(extent.height));
2482 return glm::ivec2{x, y};
2483 }
2484
2485 bool cannon_has_asteroid_target(const glm::vec3 &muzzle, const glm::vec3 &forward) const {
2486 constexpr float AIM_ASSIST_SCALE = 1.04f;
2487 const float max_range = PROJECTILE_SPEED * PROJECTILE_LIFETIME;
2488 for (const auto &asteroid : asteroids) {
2489 if (!asteroid.active) {
2490 continue;
2491 }
2492
2493 const glm::vec3 to_asteroid = asteroid.position - muzzle;
2494 const float along_ray = glm::dot(to_asteroid, forward);
2495 if (along_ray < 0.0f || along_ray > max_range) {
2496 continue;
2497 }
2498
2499 const glm::vec3 closest_point = muzzle + forward * along_ray;
2500 const float hit_radius = asteroid.radius * ASTEROID_PROJECTILE_COLLISION_SCALE * AIM_ASSIST_SCALE;
2501 if (glm::length(closest_point - asteroid.position) <= hit_radius) {
2502 return true;
2503 }
2504 }
2505 return false;
2506 }
2507
2508 void draw_cannon_crosshair(const VkExtent2D &extent) {
2509 if (ui_pixel == nullptr || !ship.visible || ship.exploding || extent.width < 160U || extent.height < 120U) {
2510 return;
2511 }
2512
2513 constexpr float AIM_DISTANCE = 120.0f;
2514 constexpr float MUZZLE_OFFSET = 0.08f;
2515 constexpr int ARM_LENGTH = 18;
2516 constexpr int GAP = 6;
2517 constexpr int THICKNESS = 2;
2518 const glm::vec3 forward = ship.forward();
2519 const glm::vec3 muzzle = ship.position + forward * MUZZLE_OFFSET;
2520 const glm::vec3 aim_position = muzzle + forward * AIM_DISTANCE;
2521 const std::optional<glm::ivec2> screen_position = project_world_to_screen(aim_position, extent);
2522 if (!screen_position.has_value()) {
2523 return;
2524 }
2525
2526 const int x = std::clamp(screen_position->x, ARM_LENGTH + 2, static_cast<int>(extent.width) - ARM_LENGTH - 2);
2527 const int y = std::clamp(screen_position->y, ARM_LENGTH + 2, static_cast<int>(extent.height) - ARM_LENGTH - 2);
2528 const bool target_locked = cannon_has_asteroid_target(muzzle, forward);
2529 const glm::vec4 shadow = target_locked ? glm::vec4{0.0f, 0.02f, 0.07f, 0.7f} : glm::vec4{0.05f, 0.0f, 0.0f, 0.7f};
2530 const glm::vec4 crosshair_color = target_locked ? glm::vec4{0.12f, 0.58f, 1.0f, 0.98f} : glm::vec4{1.0f, 0.03f, 0.02f, 0.96f};
2531
2532 draw_ui_rect(x - ARM_LENGTH - 1, y - THICKNESS / 2 - 1, ARM_LENGTH - GAP + 2, THICKNESS + 2, shadow);
2533 draw_ui_rect(x + GAP - 1, y - THICKNESS / 2 - 1, ARM_LENGTH - GAP + 2, THICKNESS + 2, shadow);
2534 draw_ui_rect(x - THICKNESS / 2 - 1, y - ARM_LENGTH - 1, THICKNESS + 2, ARM_LENGTH - GAP + 2, shadow);
2535 draw_ui_rect(x - THICKNESS / 2 - 1, y + GAP - 1, THICKNESS + 2, ARM_LENGTH - GAP + 2, shadow);
2536
2537 draw_ui_rect(x - ARM_LENGTH, y - THICKNESS / 2, ARM_LENGTH - GAP, THICKNESS, crosshair_color);
2538 draw_ui_rect(x + GAP, y - THICKNESS / 2, ARM_LENGTH - GAP, THICKNESS, crosshair_color);
2539 draw_ui_rect(x - THICKNESS / 2, y - ARM_LENGTH, THICKNESS, ARM_LENGTH - GAP, crosshair_color);
2540 draw_ui_rect(x - THICKNESS / 2, y + GAP, THICKNESS, ARM_LENGTH - GAP, crosshair_color);
2541 draw_ui_rect(x - 1, y - 1, 3, 3, crosshair_color);
2542 }
2543
2544 void draw_radar(const VkExtent2D &extent) {
2545 if (ui_pixel == nullptr || extent.width < 360U || extent.height < 280U) {
2546 return;
2547 }
2548
2549 constexpr int BORDER = 2;
2550 constexpr float RADAR_RANGE = 180.0f;
2551 const int radar_size = std::clamp(static_cast<int>(std::min(extent.width, extent.height)) / 4, 150, 220);
2552 const int radar_x = 24;
2553 const int radar_y = std::max(230, static_cast<int>(extent.height) - radar_size - 24);
2554 const int inner_x = radar_x + BORDER;
2555 const int inner_y = radar_y + BORDER;
2556 const int inner_size = radar_size - BORDER * 2;
2557 const int center_x = inner_x + inner_size / 2;
2558 const int center_y = inner_y + inner_size / 2;
2559 const float half_size = static_cast<float>(inner_size) * 0.5f;
2560
2561 draw_ui_rect(radar_x, radar_y, radar_size, radar_size, {0.01f, 0.025f, 0.045f, 0.74f});
2562 const glm::vec4 border_color = ship_returning_to_field ? glm::vec4{1.0f, 0.64f, 0.18f, 0.94f} : glm::vec4{0.16f, 0.66f, 0.82f, 0.9f};
2563 draw_ui_rect(radar_x, radar_y, radar_size, BORDER, border_color);
2564 draw_ui_rect(radar_x, radar_y + radar_size - BORDER, radar_size, BORDER, border_color);
2565 draw_ui_rect(radar_x, radar_y, BORDER, radar_size, border_color);
2566 draw_ui_rect(radar_x + radar_size - BORDER, radar_y, BORDER, radar_size, border_color);
2567
2568 draw_ui_rect(center_x, inner_y, 1, inner_size, {0.10f, 0.30f, 0.38f, 0.7f});
2569 draw_ui_rect(inner_x, center_y, inner_size, 1, {0.10f, 0.30f, 0.38f, 0.7f});
2570 draw_ui_rect(center_x - inner_size / 4, inner_y, 1, inner_size, {0.08f, 0.22f, 0.28f, 0.45f});
2571 draw_ui_rect(center_x + inner_size / 4, inner_y, 1, inner_size, {0.08f, 0.22f, 0.28f, 0.45f});
2572 draw_ui_rect(inner_x, center_y - inner_size / 4, inner_size, 1, {0.08f, 0.22f, 0.28f, 0.45f});
2573 draw_ui_rect(inner_x, center_y + inner_size / 4, inner_size, 1, {0.08f, 0.22f, 0.28f, 0.45f});
2574
2575 for (const auto &asteroid : asteroids) {
2576 if (!asteroid.active) {
2577 continue;
2578 }
2579 glm::vec2 relative{asteroid.position.x - ship.position.x, asteroid.position.z - ship.position.z};
2580 const float distance = glm::length(relative);
2581 const bool clamped_to_edge = distance > RADAR_RANGE;
2582 if (clamped_to_edge && distance > 1e-4f) {
2583 relative *= RADAR_RANGE / distance;
2584 }
2585 const int dot_x = center_x + static_cast<int>((relative.x / RADAR_RANGE) * half_size);
2586 const int dot_y = center_y + static_cast<int>((relative.y / RADAR_RANGE) * half_size);
2587 const int dot_size = std::clamp(static_cast<int>(asteroid.radius * 0.55f), 3, 8);
2588 const float altitude = std::clamp((asteroid.position.y - BOUNDARY_Y_MIN) / (BOUNDARY_Y_MAX - BOUNDARY_Y_MIN), 0.0f, 1.0f);
2589 const glm::vec4 dot_color = clamped_to_edge
2590 ? glm::vec4{1.0f, 0.38f, 0.16f, 0.9f}
2591 : glm::vec4{1.0f, 0.55f + altitude * 0.28f, 0.18f, 1.0f};
2592 draw_ui_rect(dot_x - dot_size / 2, dot_y - dot_size / 2, dot_size, dot_size, dot_color);
2593 }
2594
2595 draw_ui_rect(center_x - 5, center_y, 11, 2, {0.95f, 1.0f, 1.0f, 1.0f});
2596 draw_ui_rect(center_x, center_y - 5, 2, 11, {0.95f, 1.0f, 1.0f, 1.0f});
2597
2598 const SDL_Color label_color = ship_returning_to_field ? SDL_Color{255, 180, 80, 255} : SDL_Color{120, 220, 255, 255};
2599 printText(ship_returning_to_field ? "RADAR RETURN" : "RADAR", radar_x, std::max(4, radar_y - 22), label_color);
2600 }
2601
2602 void draw_hud([[maybe_unused]] float aspect) {
2603 set_ui_font_size(18);
2604 const SDL_Color white{255, 255, 255, 255};
2605 const SDL_Color red{220, 60, 60, 255};
2606 const SDL_Color yellow{255, 220, 120, 255};
2607 const VkExtent2D extent = getSwapchainExtent();
2608 draw_cannon_crosshair(extent);
2609 draw_radar(extent);
2610 const int right_x = std::max(25, static_cast<int>(extent.width) - 250);
2611 printText("MXVK Asteroids v1.0", right_x, 25, red);
2612 printText("Score: " + std::to_string(ship.score), right_x, 50, white);
2613 printText("Lives: " + std::to_string(std::max(0, ship.lives)), right_x, 75, white);
2614 printText("Asteroids: " + std::to_string(active_asteroids()), right_x, 100, white);
2615 printText("Time Left: " + format_round_time(), right_x, 125, round_time_remaining <= 30.0f ? yellow : white);
2616 printText("[F1 for Debug]", right_x, 150, white);
2617 printText(inverted_controls ? "[Inverted] F2/Y" : "[Arcade] F2/Y", right_x, 175, white);
2618 printText("[F3 for Console]", right_x, 200, white);
2619 printText(mouse_look_controls ? "[Mouse Look] F5" : "[Classic Keys] F5", right_x, 225, white);
2620 printText(first_person_camera ? "[First Person] F7" : "[Chase View] F7", right_x, 250, white);
2621
2622 if (!debug_menu) {
2623 return;
2624 }
2625
2626 const float fps = (last_delta_time > 0.0001f) ? (1.0f / last_delta_time) : 0.0f;
2627 printText("Ship X,Y,Z: " + vec3_string(ship.position), 25, 25, white);
2628 printText("Velocity X,Y,Z: " + vec3_string(ship.velocity), 25, 50, white);
2629 printText("FPS: " + std::to_string(fps), 25, 75, white);
2630 printText("Aseroids destroyed: " + std::to_string(MAX_ASTEROIDS - active_asteroids()), 25, 100, white);
2631 printText(mouse_look_controls ? "Controls: Mouse look, W/S speed, A/D roll, click/SPACE to shoot" : "Controls: Arrows to Move, W,S Tilt Up/Down - SPACE to shoot", 25, 125, white);
2632 printText("Nearest Object: " + std::to_string(nearest_asteroid_distance()), 25, 150, white);
2633 printText("Farthest Object: " + std::to_string(farthest_asteroid_distance()), 25, 175, white);
2634 printText("Speed: " + std::to_string(ship.current_speed) + " / " + std::to_string(ship.max_speed), 25, 200, white);
2635 printText("Controller: " + controller_status(), 25, 225, white);
2636 printText(std::string("Input: ") + (mouse_look_controls ? "Keyboard/mouse" : "Classic keyboard"), 25, 250, white);
2637 printText(std::string("Camera: ") + (first_person_camera ? "First person" : "Chase"), 25, 275, white);
2638 printText("Press ENTER to randomize asteroids", 25, 300, white);
2639 }
2640
2641 int active_asteroids() const {
2642 int count = 0;
2643 for (const auto &asteroid : asteroids) {
2644 if (asteroid.active) {
2645 ++count;
2646 }
2647 }
2648 return count;
2649 }
2650
2651 void draw_end_screen([[maybe_unused]] uint32_t image_index,
2652 [[maybe_unused]] float aspect,
2653 const std::string &title,
2654 const SDL_Color &title_color) {
2655 set_ui_font_size(32);
2656 const SDL_Color white{255, 255, 255, 255};
2657 const SDL_Color yellow{255, 220, 120, 255};
2658 const VkExtent2D extent = getSwapchainExtent();
2659
2660 const std::string score_text = "Final Score: " + std::to_string(ship.score);
2661 const std::string prompt = "Press ENTER to start over";
2662
2663 int title_w = 0;
2664 int title_h = 0;
2665 if (getTextDimensions(title.c_str(), title_w, title_h)) {
2666 printText(title.c_str(),
2667 static_cast<int>(extent.width) / 2 - title_w / 2,
2668 static_cast<int>(extent.height) / 2 - title_h,
2669 title_color);
2670 } else {
2671 printText(title.c_str(), 24, 20, title_color);
2672 }
2673
2674 int score_w = 0;
2675 int score_h = 0;
2676 if (getTextDimensions(score_text.c_str(), score_w, score_h)) {
2677 printText(score_text.c_str(),
2678 static_cast<int>(extent.width) / 2 - score_w / 2,
2679 static_cast<int>(extent.height) / 2 + 10,
2680 white);
2681 } else {
2682 printText(score_text.c_str(), 24, 70, white);
2683 }
2684
2685 int prompt_w = 0;
2686 int prompt_h = 0;
2687 if (getTextDimensions(prompt.c_str(), prompt_w, prompt_h)) {
2688 printText(prompt.c_str(),
2689 static_cast<int>(extent.width) / 2 - prompt_w / 2,
2690 static_cast<int>(extent.height) / 2 + score_h + 28,
2691 yellow);
2692 } else {
2693 printText(prompt.c_str(), 24, 100, yellow);
2694 }
2695 }
2696
2697 void draw_game_over([[maybe_unused]] uint32_t image_index, [[maybe_unused]] float aspect) {
2698 draw_end_screen(image_index, aspect, "Game over", SDL_Color{235, 60, 60, 255});
2699 }
2700
2701 VkCommandBuffer current_command_buffer = VK_NULL_HANDLE;
2702 float last_delta_time = 1.0f / 60.0f;
2703
2704 std::string vec3_string(const glm::vec3 &value) const {
2705 return std::to_string(value.x) + ", " + std::to_string(value.y) + ", " + std::to_string(value.z);
2706 }
2707
2708 float nearest_asteroid_distance() const {
2709 float nearest = 999999.0f;
2710 for (const auto &asteroid : asteroids) {
2711 if (asteroid.active) {
2712 nearest = std::min(nearest, glm::length(ship.position - asteroid.position));
2713 }
2714 }
2715 return nearest;
2716 }
2717
2718 float farthest_asteroid_distance() const {
2719 float farthest = 0.0f;
2720 for (const auto &asteroid : asteroids) {
2721 if (asteroid.active) {
2722 farthest = std::max(farthest, glm::length(ship.position - asteroid.position));
2723 }
2724 }
2725 return farthest;
2726 }
2727 };
2728
2729} // namespace space
2730
2731void space::run_asteroids3d(const Arguments &args) {
2732 Asteroids3DWindow window(args.path, args.width, args.height, args.fullscreen, args.enable_vsync, args.enable_crt);
2733 window.loop();
2734}
Lightweight, header-only, template command-line argument parser.
void updateUBO(uint32_t imageIndex, const UniformBufferObject &ubo)
Update one per-frame UBO payload.
float modelRenderScale() const
Access the computed render scale used for normalization.
glm::vec3 modelCenterOffset() const
Access the computed center offset used for normalization.
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.
VkExtent2D getSwapchainExtent() const noexcept
Get the current swapchain extent.
Definition mxvk.hpp:210
VK_Sprite3D * createSprite3D(const std::string &pngPath, const std::string &vertexShaderPath="", const std::string &fragmentShaderPath="")
Create a world-space billboard sprite from a PNG file.
Definition mxvk.cpp:4735
VkDevice device
Definition mxvk.hpp:596
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:4608
VkFormat depth_format
Definition mxvk.hpp:605
static VkShaderModule createShaderModule(VkDevice device, const std::vector< char > &spv_bytes)
Create a shader module from SPIR-V bytecode.
Definition mxvk.cpp:145
bool getTextDimensions(const std::string &text, int &width, int &height)
Measure text dimensions in pixels.
Definition mxvk.cpp:4142
void clearTextQueue()
Clear all queued text draw calls for the current frame.
Definition mxvk.cpp:4096
SDL_Window * getSDLWindow() const noexcept
Get the underlying SDL window handle.
Definition mxvk.hpp:189
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
Definition mxvk.cpp:613
void exit()
Request loop termination.
Definition mxvk.cpp:1429
VK_Sprite * attachPostProcessingShader(const std::string &fragmentShaderPath, float p1=0.0f, float p2=0.0f, float p3=0.0f, float p4=0.0f)
Attach a full-screen post-processing fragment shader.
Definition mxvk.cpp:1499
VK_Window()=default
Construct an empty window object.
VkPhysicalDevice physical_device
Definition mxvk.hpp:595
VkFormat getDepthFormat() const noexcept
Get the depth format used for dynamic rendering attachments.
Definition mxvk.hpp:224
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
Definition mxvk.cpp:4002
void setPostProcessingEnabled(bool enabled)
Definition mxvk.hpp:339
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:4051
static std::vector< char > loadSpv(const std::string &path)
Load a SPIR-V file from disk.
Definition mxvk.cpp:141
void setPostProcessingShaderTimeEnabled(bool enabled)
Keep shader param 1 updated with elapsed render time in seconds.
Definition mxvk.cpp:1645
VkFormat getSwapchainFormat() const noexcept
Get the swapchain color format.
Definition mxvk.hpp:207
void event(SDL_Event &e) override
Handle one SDL event.
Asteroids3DWindow(const std::string &path, int width, int height, bool fullscreen, bool enable_vsync, bool enable_crt)
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t image_index) override
Optional hook for derived classes to record extra draw commands.
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
Mutable gameplay and movement state for a player ship.
Definition ship.hpp:17
#define MXVK_VALIDATION
Definition mxvk.hpp:28
High-level model wrapper integrated with MXVK dynamic rendering.
In-game command console for MXVK / Vulkan windows.
SDL3 joystick and gamepad RAII wrappers.
PNG image loading and saving utilities via SDL3.
SDL3_mixer audio subsystem wrapper.
std::filesystem::path texture_path(const std::string &filename, const std::string &asset_path)
Definition main.cpp:147
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:31
SDL_Surface * LoadPNG(const char *file)
Load a PNG file into an SDL_Surface.
Definition mxvk_png.cpp:103
std::string resolve_shader_root(const std::string &asset_root)
GameMode
High-level state of the Asteroids application.
@ Loading
Asset-loading screen.
@ Intro
Introductory screen.
@ GameComplete
Completed game.
@ GameOver
Player has no remaining lives.
@ Playing
Active gameplay.
glm::vec3 normalize_or_zero(const glm::vec3 &value)
Normalizes a direction with a stable fallback.
constexpr float ROUND_TIME_LIMIT_SECONDS
Multiplayer round limit in seconds.
glm::mat4 build_model_matrix(const glm::vec3 &position, const glm::vec3 &rotation_degrees, float scale, const glm::vec3 &center_offset)
Builds a translated, rotated, scaled model matrix.
int random_int(int min_value, int max_value)
Generates a uniformly distributed integer.
std::default_random_engine & rng()
Returns the thread-local random number engine used by simulation helpers.
constexpr int SMALL_ASTEROID_POINTS
Score awarded for a small asteroid.
constexpr int FIRE_COOLDOWN
Frames between firing bursts.
constexpr float BOUNDARY_X_MAX
Maximum simulation x-coordinate.
constexpr int MAX_GENERATIONS
Maximum asteroid split generation.
constexpr int CHILDREN_PER_SPAWN
Child asteroids created by a split.
SDL_Surface * load_color_keyed_png(const std::string &path, std::uint8_t threshold=12, std::uint8_t softness=48)
Loads a PNG and fades dark color-key pixels to transparency.
constexpr float ASTEROID_PROJECTILE_COLLISION_SCALE
Projectile collision-radius adjustment.
constexpr float BOUNDARY_Y_MAX
Maximum simulation y-coordinate.
constexpr int MAX_ASTEROIDS
Maximum locally simulated asteroids.
constexpr int MAX_PARTICLES
Maximum particles in the shared particle pool.
constexpr float BOUNDARY_Y_MIN
Minimum simulation y-coordinate.
constexpr float ASTEROID_SHIP_COLLISION_SCALE
Ship collision-radius adjustment.
constexpr glm::vec4 PROJECTILE_COLOR
Default projectile color.
constexpr int SHOTS_PER_BURST
Projectiles fired in one burst.
constexpr int EXPLOSION_DURATION_FRAMES
Ship explosion duration in frames.
constexpr int FIRE_DELAY
Frames between shots within a burst.
constexpr float BOUNDARY_X_MIN
Minimum simulation x-coordinate.
constexpr int LARGE_ASTEROID_POINTS
Score awarded for a large asteroid.
constexpr float BOUNDARY_BOUNCE_FACTOR
Boundary collision response multiplier.
constexpr float PROJECTILE_SPEED
Projectile travel speed in world units per second.
constexpr float BOUNDARY_Z_MIN
Minimum simulation z-coordinate.
void run_asteroids3d(const Arguments &args)
Runs the Asteroids application.
constexpr int MEDIUM_ASTEROID_POINTS
Score awarded for a medium asteroid.
constexpr float PI
Single-precision value of pi.
constexpr float SHIP_MODEL_SCALE
Uniform ship model scale.
constexpr float PROJECTILE_LIFETIME
Projectile lifetime in seconds.
constexpr Sint16 CONTROLLER_DEAD_ZONE
Controller axis dead-zone magnitude.
constexpr float CONTROLLER_AXIS_MAX
Maximum signed controller axis magnitude.
float random_float(float min_value, float max_value)
Generates a uniformly distributed floating-point value.
constexpr int GAME_STARS
Number of stars in the background field.
constexpr float BOUNDARY_Z_MAX
Maximum simulation z-coordinate.
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
bool enable_crt
Enable CRT post-processing at startup (--enable-crt).
Definition argz.hpp:749
bool active
Definition space.cpp:69
float x
Definition space.cpp:66
float y
Definition space.cpp:66
float rotation_speed
Definition space.cpp:72
float radius
Definition space.cpp:68
float y
Definition space.cpp:76
int lifetime
Definition space.cpp:78
bool active
Definition space.cpp:79
std::string color
Definition rain.hpp:19