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