MXVK Vulkan Framework 0.24.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.
1#ifdef _WIN32
2#include <winsock2.h>
3#endif
4
7#include "multiplayer.hpp"
8#include "rain.hpp"
9#include "ship.hpp"
10#include "starfield.hpp"
11
12#include "mxvk/argz.hpp"
13#include "mxvk/mxvk.hpp"
15#include "mxvk/mxvk_console.hpp"
18#include "mxvk/mxvk_png.hpp"
19#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
20#include "mxvk/mxvk_sound.hpp"
21#endif
22
23#include <SDL3/SDL.h>
24
25#include <algorithm>
26#include <array>
27#include <atomic>
28#include <cctype>
29#include <chrono>
30#include <cmath>
31#include <cstddef>
32#include <cstdlib>
33#include <cstring>
34#include <exception>
35#include <filesystem>
36#include <format>
37#include <iostream>
38#include <limits>
39#include <memory>
40#include <mutex>
41#include <optional>
42#include <ostream>
43#include <sstream>
44#include <string>
45#include <thread>
46#include <unordered_set>
47#include <vector>
48
49#include <glm/ext/matrix_clip_space.hpp>
50#include <glm/ext/matrix_transform.hpp>
51#include <glm/glm.hpp>
52
53namespace space {
54
55 namespace {
56
57 [[nodiscard]] std::string resolve_asset_root(const std::string &path) {
58 if (!path.empty() && path != ".") {
59 return path;
60 }
61
62 if (const char *base_path = SDL_GetBasePath(); base_path != nullptr && base_path[0] != '\0') {
63 return std::filesystem::path(base_path).lexically_normal().string();
64 }
65
66 return ".";
67 }
68
69 [[nodiscard]] std::string resolve_shader_root(const std::string &asset_root) {
70 const std::filesystem::path requested_root = std::filesystem::path(asset_root) / "data";
71 if (std::filesystem::exists(requested_root / "crt.frag.spv")) {
72 return requested_root.lexically_normal().string();
73 }
74
75 if (const char *base_path = SDL_GetBasePath(); base_path != nullptr && base_path[0] != '\0') {
76 const std::filesystem::path runtime_root = std::filesystem::path(base_path) / "data";
77 if (std::filesystem::exists(runtime_root / "crt.frag.spv")) {
78 return runtime_root.lexically_normal().string();
79 }
80 }
81
82 return requested_root.lexically_normal().string();
83 }
84
85 constexpr std::uint32_t MULTIPLAYER_KILLS_TO_WIN = 10U;
86 constexpr std::array<glm::vec3, NETWORK_PLAYER_COUNT> MULTIPLAYER_SPAWNS = {
87 glm::vec3{-75.0f, 0.0f, -75.0f}, glm::vec3{75.0f, 0.0f, 75.0f},
88 glm::vec3{-75.0f, 0.0f, 75.0f}, glm::vec3{75.0f, 0.0f, -75.0f}};
89 constexpr std::array<float, NETWORK_PLAYER_COUNT> MULTIPLAYER_SPAWN_YAWS = {-135.0f, 45.0f, -45.0f, 135.0f};
90
91 } // namespace
92
94 public:
95 Asteroids3DWindow(const std::string &path, int width, int height, bool fullscreen, bool enable_vsync, bool enable_crt, bool disable_sound)
96 : mxvk::VK_Window("3D Asteroids", width, height, fullscreen, MXVK_VALIDATION, enable_vsync),
97 asset_root(resolve_asset_root(path)),
98 shader_root(resolve_shader_root(asset_root)),
99 crt_enabled(enable_crt),
100 background_music_disabled(disable_sound) {
101 const char *video_driver = SDL_GetCurrentVideoDriver();
102 const bool uses_wayland = video_driver != nullptr && std::strcmp(video_driver, "wayland") == 0;
103 if (!uses_wayland) {
104 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> window_icon(
105 mxvk::LoadPNG((asset_root + "/data/asteroids_icon.png").c_str()), SDL_DestroySurface);
106 if (window_icon != nullptr && !SDL_SetWindowIcon(getSDLWindow(), window_icon.get())) {
107 std::cerr << "asteroids-net: could not set SDL window icon: " << SDL_GetError() << '\n';
108 }
109 }
110
111 setClearColor(0.0f, 0.0f, 0.0f, 1.0f);
112 attachPostProcessingShader(shader_root + "/crt.frag.spv", 0.0f, 3.0f, 0.5f, 0.002f);
114 setPostProcessingEnabled(crt_enabled);
115 load_loading_screen_resources();
116 configure_console();
117 open_controller();
118#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
119 load_sound_effects();
120 ensure_background_music_playing();
121#endif
122 }
123
125 if (mouse_capture_active) {
126 SDL_SetWindowRelativeMouseMode(getSDLWindow(), false);
127 mouse_capture_active = false;
128 }
129 if (device != VK_NULL_HANDLE) {
130 vkDeviceWaitIdle(device);
131 }
132 if (loading_thread.joinable()) {
133 loading_thread.join();
134 }
135#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
136 if (sound_effects) {
137 sound_effects->stopMusic();
138 }
139#endif
140 cleanup_flame_resources();
141 ship_model.cleanup(this);
142 for (auto &model : asteroid_models) {
143 model.cleanup(this);
144 }
145 for (auto &model : remote_ship_models) {
146 model.cleanup(this);
147 }
148 if (star_sprite != nullptr) {
149 star_sprite->cleanup();
150 }
151 if (projectile_sprite != nullptr) {
152 projectile_sprite->cleanup();
153 }
154 if (effect_sprite != nullptr) {
155 effect_sprite->cleanup();
156 }
157 intro_rain.reset();
158 }
159
160 void event(SDL_Event &e) override {
161 if (e.type == SDL_EVENT_GAMEPAD_ADDED ||
162 e.type == SDL_EVENT_GAMEPAD_REMOVED ||
163 e.type == SDL_EVENT_JOYSTICK_ADDED ||
164 e.type == SDL_EVENT_JOYSTICK_REMOVED) {
165 if (e.type == SDL_EVENT_GAMEPAD_ADDED || e.type == SDL_EVENT_GAMEPAD_REMOVED) {
166 controller.connectEvent(e);
167 }
168 sync_controller_connection();
169 return;
170 }
171
172 const bool was_console_visible = console.isVisible();
173 const bool is_console_toggle = e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_F3;
174 console.handleEvent(e);
175 if (is_console_toggle) {
176 log_game(console.isVisible() ? "Console opened." : "Console closed.");
177 sync_mouse_capture();
178 return;
179 }
180 if (was_console_visible) {
181 return;
182 }
183
184 if (mode == GameMode::Lobby) {
185 handle_lobby_event(e);
186 return;
187 }
188 if (mode == GameMode::MatchOver && e.type == SDL_EVENT_KEY_DOWN &&
189 (e.key.key == SDLK_RETURN || e.key.key == SDLK_KP_ENTER || e.key.key == SDLK_ESCAPE)) {
190 multiplayer.stop();
191 multiplayer_match = false;
192 mode = GameMode::Lobby;
193 lobby_page = LobbyPage::Main;
194 lobby_selection = 0;
195 lobby_status = "Choose how you want to play.";
196 return;
197 }
198
199 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
200 if (mode == GameMode::Playing) {
201 log_game("Exit requested while playing.");
202 exit();
203 } else if (mode == GameMode::GameOver || mode == GameMode::GameComplete) {
204 exit();
205 } else {
206 log_game("Exit requested from intro screen.");
207 exit();
208 }
209 return;
210 }
211 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_F8 && !e.key.repeat) {
212 crt_enabled = !crt_enabled;
213 setPostProcessingEnabled(crt_enabled);
214 log_game(std::string("CRT effect ") + (crt_enabled ? "enabled." : "disabled."));
215 return;
216 }
217 if (mode == GameMode::Intro &&
218 e.type == SDL_EVENT_KEY_DOWN &&
219 (e.key.key == SDLK_SPACE || e.key.key == SDLK_RETURN)) {
220 intro_fade = 0.01f;
221 log_game("Intro skipped. Starting game.");
222 return;
223 }
224 if (mode == GameMode::Intro &&
225 e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN &&
226 e.gbutton.button == SDL_GAMEPAD_BUTTON_SOUTH) {
227 intro_fade = 0.01f;
228 log_game("Intro skipped from controller. Starting game.");
229 return;
230 }
231 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
232 if (e.gbutton.button == SDL_GAMEPAD_BUTTON_BACK) {
233 log_game("Exit requested from controller.");
234 exit();
235 return;
236 }
237 if (e.gbutton.button == SDL_GAMEPAD_BUTTON_WEST) {
238 debug_menu = !debug_menu;
239 log_game(std::string("Debug HUD ") + (debug_menu ? "enabled from controller." : "disabled from controller."));
240 return;
241 }
242 if (e.gbutton.button == SDL_GAMEPAD_BUTTON_NORTH) {
243 inverted_controls = !inverted_controls;
244 log_game(std::string("Controls set to ") + (inverted_controls ? "inverted from controller." : "arcade from controller."));
245 return;
246 }
247 if (e.gbutton.button == SDL_GAMEPAD_BUTTON_EAST && mode == GameMode::Playing) {
248 restart_game();
249 log_game("Game restarted from controller.");
250 return;
251 }
252 if ((mode == GameMode::GameOver || mode == GameMode::GameComplete) &&
253 (e.gbutton.button == SDL_GAMEPAD_BUTTON_SOUTH || e.gbutton.button == SDL_GAMEPAD_BUTTON_START)) {
254 prepare_restart_from_game_over();
255 log_game("End screen acknowledged from controller. Returning to intro.");
256 return;
257 }
258 }
259 if ((mode == GameMode::GameOver || mode == GameMode::GameComplete) && e.type == SDL_EVENT_KEY_DOWN) {
260 if (e.key.key == SDLK_RETURN || e.key.key == SDLK_KP_ENTER) {
261 prepare_restart_from_game_over();
262 log_game("End screen acknowledged from keyboard. Returning to intro.");
263 return;
264 }
265 }
266 if (mode == GameMode::Playing && e.type == SDL_EVENT_KEY_DOWN) {
267 if (e.key.key == SDLK_F1) {
268 debug_menu = !debug_menu;
269 log_game(std::string("Debug HUD ") + (debug_menu ? "enabled." : "disabled."));
270 return;
271 }
272 if (e.key.key == SDLK_F2) {
273 inverted_controls = !inverted_controls;
274 log_game(std::string("Controls set to ") + (inverted_controls ? "inverted." : "arcade."));
275 return;
276 }
277 if (e.key.key == SDLK_F5 && !e.key.repeat) {
278 set_mouse_look_controls(!mouse_look_controls);
279 log_game(std::string("Control scheme set to ") + (mouse_look_controls ? "keyboard/mouse." : "classic keyboard."));
280 return;
281 }
282 if (e.key.key == SDLK_F7 && !e.key.repeat) {
283 begin_camera_transition(!first_person_camera);
284 log_game(std::string("Camera set to ") + (first_person_camera ? "first person." : "chase view."));
285 return;
286 }
287 }
288 if (mode == GameMode::Playing && mouse_look_controls && e.type == SDL_EVENT_MOUSE_MOTION) {
289 if (ignore_next_mouse_motion) {
290 ignore_next_mouse_motion = false;
291 return;
292 }
293 apply_mouse_look(e.motion.xrel, e.motion.yrel);
294 return;
295 }
296 if (mode == GameMode::Playing && mouse_look_controls && e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
297 if (can_fire()) {
298 fire_projectile();
299 }
300 return;
301 }
302 }
303
304 void onSwapchainRecreated() override {
305 if (intro_sprite != nullptr) {
306 intro_sprite->rebuildPipeline();
307 }
308 if (intro_rain != nullptr) {
309 intro_rain->resize(*this);
310 }
311 if (!game_resources_loaded.load(std::memory_order_relaxed)) {
312 cleanup_flame_swapchain_resources();
313 return;
314 }
315 ship_model.resize(this);
316 for (auto &model : remote_ship_models) {
317 model.resize(this);
318 }
319 for (auto &model : asteroid_models) {
320 model.resize(this);
321 }
322 if (star_sprite != nullptr) {
323 star_sprite->resize(this);
324 }
325 if (projectile_sprite != nullptr) {
326 projectile_sprite->resize(this);
327 }
328 if (effect_sprite != nullptr) {
329 effect_sprite->resize(this);
330 }
331 cleanup_flame_swapchain_resources();
332 create_flame_swapchain_resources();
333 }
334
335 void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t image_index) override {
336 current_command_buffer = cmd;
337 const auto now = std::chrono::steady_clock::now();
338 const float delta_seconds = std::chrono::duration<float>(now - last_frame_time).count();
339 last_frame_time = now;
340 const float dt = std::min(delta_seconds, 0.1f);
341 last_delta_time = dt;
342 elapsed_seconds += dt;
343
344 sync_controller_connection();
345#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
346 ensure_background_music_playing();
347#endif
348
349 const VkExtent2D extent = getSwapchainExtent();
350 const float aspect = (extent.height > 0U) ? static_cast<float>(extent.width) / static_cast<float>(extent.height) : 1.0f;
351
352 if (mode == GameMode::Intro) {
353 draw_intro(extent);
354 console.draw();
355 return;
356 }
357
358 if (mode == GameMode::Loading) {
359 draw_loading(extent);
360 console.draw();
361 return;
362 }
363
364 if (mode == GameMode::Lobby) {
365 draw_lobby(cmd, image_index, extent, aspect);
366 console.draw();
367 return;
368 }
369
370 if (mode == GameMode::MatchOver) {
371 draw_multiplayer_end(extent);
372 console.draw();
373 return;
374 }
375
376 if (mode == GameMode::GameOver) {
377 draw_end_screen(image_index, aspect, "Game over", SDL_Color{235, 60, 60, 255});
378 console.draw();
379 return;
380 }
381
382 if (mode == GameMode::GameComplete) {
383 draw_end_screen(image_index, aspect, "Mission complete", SDL_Color{255, 220, 120, 255});
384 console.draw();
385 return;
386 }
387
388 const bool console_visible = console.isVisible();
389 sync_mouse_capture();
390 if (!multiplayer_match) {
391 update_round_timer(dt);
392 }
393 if (mode == GameMode::GameOver) {
394 draw_game_over(image_index, aspect);
395 console.draw();
396 return;
397 }
398 if (!console_visible) {
399 handle_input(dt);
400 }
401 update_ship(console_visible ? 0.0f : dt);
402 update_projectiles(dt);
403 if (multiplayer_match) {
404 if (multiplayer.is_host()) {
405 update_asteroids(dt);
406 }
407 update_multiplayer(dt);
408 } else {
409 update_asteroids(dt);
410 }
411 update_particles(dt);
412
413 if (ship.lives <= 0 && !ship.exploding) {
414 mode = GameMode::GameOver;
415 ship.visible = false;
416 if (star_sprite != nullptr) {
417 star_sprite->clearQueue();
418 }
419 if (projectile_sprite != nullptr) {
420 projectile_sprite->clearQueue();
421 }
422 if (effect_sprite != nullptr) {
423 effect_sprite->clearQueue();
424 }
425 draw_game_over(image_index, aspect);
426 console.draw();
427 return;
428 }
429
430 if (!multiplayer_match && mode == GameMode::Playing && active_asteroids() == 0) {
432 ship.visible = false;
433 log_game("All asteroids cleared. Mission complete.", SDL_Color{120, 255, 160, 255});
434 }
435
436 if (mode == GameMode::GameComplete) {
437 if (star_sprite != nullptr) {
438 star_sprite->clearQueue();
439 }
440 if (projectile_sprite != nullptr) {
441 projectile_sprite->clearQueue();
442 }
443 if (effect_sprite != nullptr) {
444 effect_sprite->clearQueue();
445 }
446 draw_end_screen(image_index, aspect, "Mission complete", SDL_Color{255, 220, 120, 255});
447 console.draw();
448 return;
449 }
450
451 update_camera(dt);
452 projection_matrix = glm::perspective(glm::radians(50.0f), aspect, 0.1f, 500.0f);
453 projection_matrix[1][1] *= -1.0f;
454
455 star_field.update(dt, camera_position, elapsed_seconds);
456 star_field.setSprite(star_sprite);
457
458 star_sprite->updateCamera(image_index, view_matrix, projection_matrix);
459 projectile_sprite->updateCamera(image_index, view_matrix, projection_matrix);
460 effect_sprite->updateCamera(image_index, view_matrix, projection_matrix);
461
462 star_field.draw();
463 star_sprite->render(cmd, image_index);
464 star_sprite->clearQueue();
465
466 draw_asteroids(image_index);
467 if (!first_person_camera && !camera_transition_active) {
468 draw_ship(image_index);
469 draw_engine_flame(cmd, extent, last_ship_model_matrix, ship.current_speed, ship.visible && !ship.exploding);
470 }
471 if (multiplayer_match) {
472 for (std::uint8_t player = 0; player < NETWORK_PLAYER_COUNT; ++player) {
473 if (player == multiplayer.local_player_id() || !multiplayer.player_connected()[player]) {
474 continue;
475 }
476 draw_remote_ship(image_index, player);
477 draw_engine_flame(cmd, extent, last_remote_ship_model_matrices[player], std::max(remote_ships[player].current_speed, 1.0f),
478 remote_ships[player].visible && !remote_ship_exploding[player]);
479 }
480 }
481 draw_projectiles();
482 if (multiplayer_match) {
483 draw_remote_projectiles();
484 }
485 draw_particles();
486 projectile_sprite->render(cmd, image_index);
487 projectile_sprite->clearQueue();
488 effect_sprite->render(cmd, image_index);
489 effect_sprite->clearQueue();
490
491 if (!console.isVisible()) {
492 draw_hud(aspect);
493 }
494 console.draw();
495 }
496
497 private:
498 std::string asset_root;
499 std::string shader_root;
500 std::chrono::steady_clock::time_point last_frame_time = std::chrono::steady_clock::now();
501 float elapsed_seconds = 0.0f;
503 float intro_fade = 1.0f;
504 Uint32 intro_last_update_ms = 0;
505 float loading_rain_opacity = 1.0f;
506 static constexpr int INTRO_RAIN_TEXTURE_WIDTH = 1280;
507 static constexpr int INTRO_RAIN_TEXTURE_HEIGHT = 720;
508 bool loading_black_frame_pending = false;
509 bool loading_black_frame_shown = false;
510 bool restart_after_intro = false;
511 enum class LobbyPage {
512 Main,
513 Host,
514 Join
515 };
516 LobbyPage lobby_page = LobbyPage::Main;
517 int lobby_selection = 0;
518 int lobby_edit_field = -1;
519 std::string lobby_player_name = "Pilot 1";
520 std::string lobby_host_address = "127.0.0.1";
521 std::string lobby_port = "48120";
522 std::string lobby_join_code{};
523 std::string lobby_status = "Choose how you want to play.";
524 float lobby_camera_distance = 0.0f;
525 bool debug_menu = false;
526 bool inverted_controls = false;
527 bool mouse_look_controls = false;
528 bool mouse_capture_active = false;
529 bool ignore_next_mouse_motion = false;
530 bool first_person_camera = false;
531 bool camera_transition_active = false;
532 bool crt_enabled = false;
533 bool background_music_disabled = false;
534 bool ship_returning_to_field = false;
535 float keyboard_yaw = 0.0f;
536 float keyboard_pitch = 0.0f;
537 float keyboard_roll = 0.0f;
538 float smooth_yaw = 0.0f;
539 float smooth_pitch = 0.0f;
540 float smooth_roll = 0.0f;
541 float return_message_cooldown = 0.0f;
542 float camera_transition_elapsed = 0.0f;
543 static constexpr float CAMERA_TRANSITION_SECONDS = 0.75f;
544 static constexpr float MOUSE_LOOK_SENSITIVITY = 0.04f;
545
546 Ship ship{};
547 std::array<Ship, NETWORK_PLAYER_COUNT> remote_ships{};
548 std::array<Projectile, MAX_PROJECTILES> projectiles{};
549 std::array<Asteroid, MAX_ASTEROIDS> asteroids{};
550 std::array<Particle, MAX_PARTICLES> particles{};
551 StarField star_field{};
552 glm::vec3 camera_position{0.0f, 1.6f, 6.0f};
553 glm::vec3 camera_target_position{0.0f, 1.6f, 0.0f};
554 glm::vec3 camera_up_vector{0.0f, 1.0f, 0.0f};
555 glm::vec3 camera_transition_start_position{0.0f, 1.6f, 6.0f};
556 glm::vec3 camera_transition_start_target{0.0f, 1.6f, 0.0f};
557 glm::vec3 camera_transition_start_up{0.0f, 1.0f, 0.0f};
558 glm::mat4 view_matrix{1.0f};
559 glm::mat4 projection_matrix{1.0f};
560
561 mxvk::VKAbstractModel ship_model{};
562 std::array<mxvk::VKAbstractModel, NETWORK_PLAYER_COUNT> remote_ship_models{};
563 std::array<mxvk::VKAbstractModel, MAX_ASTEROIDS> asteroid_models{};
564 mxvk::VK_Sprite3D *star_sprite = nullptr;
565 mxvk::VK_Sprite3D *projectile_sprite = nullptr;
566 mxvk::VK_Sprite3D *effect_sprite = nullptr;
567 mxvk::VK_Sprite *intro_sprite = nullptr;
568 mxvk::VK_Sprite *ui_pixel = nullptr;
569 std::unique_ptr<matrix::Rain> intro_rain{};
570 mxvk::Font lobby_status_font{};
571 mxvk::Font lobby_roster_font{};
572 mxvk::VK_Console console;
573 mxvk::VK_Controller controller;
574#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
575 std::unique_ptr<mxvk::VK_Mixer> sound_effects{};
576 int background_music_track = -1;
577 int crash_sound = -1;
578 int cannon_sound = -1;
579 int asteroid_explosion_sound = -1;
580#endif
581 bool console_ready = false;
582 std::atomic<bool> game_resources_loaded{false};
583 std::atomic<bool> loading_failed{false};
584 std::atomic<bool> model_preload_done{false};
585 std::atomic<bool> model_preload_failed{false};
586 int last_font_size = 0;
587 int lobby_status_font_size = 16;
588 int lobby_roster_font_size = 16;
589 float round_time_remaining = ROUND_TIME_LIMIT_SECONDS;
590 std::atomic<int> loading_step_index{0};
591 static constexpr int loading_step_count = MAX_ASTEROIDS + 8;
592 glm::mat4 last_ship_model_matrix{1.0f};
593 std::array<glm::mat4, NETWORK_PLAYER_COUNT> last_remote_ship_model_matrices{};
594 VkBuffer flame_vertex_buffer = VK_NULL_HANDLE;
595 VkDeviceMemory flame_vertex_buffer_memory = VK_NULL_HANDLE;
596 VkPipeline flame_pipeline = VK_NULL_HANDLE;
597 VkPipelineLayout flame_pipeline_layout = VK_NULL_HANDLE;
598 std::thread loading_thread{};
599 std::string loading_error{};
600 std::mutex prepared_model_mutex{};
601 std::optional<mxvk::MXModel> prepared_ship_model{};
602 std::array<std::optional<mxvk::MXModel>, MAX_ASTEROIDS> prepared_asteroid_models{};
603 std::array<std::string, MAX_ASTEROIDS> prepared_asteroid_texture_paths{};
604 uint32_t flame_vertex_count = 0;
605 MultiplayerSession multiplayer{};
606 bool multiplayer_match = false;
607 float multiplayer_collision_grace = 0.0f;
608 std::array<std::uint32_t, NETWORK_PLAYER_COUNT> multiplayer_kills{};
609 std::array<std::string, NETWORK_PLAYER_COUNT> multiplayer_player_names{};
610 std::array<std::uint32_t, NETWORK_PLAYER_COUNT> multiplayer_death_serials{};
611 std::array<std::uint32_t, NETWORK_PLAYER_COUNT> received_death_serials{};
612 std::uint8_t multiplayer_winner = 0;
613 std::array<bool, NETWORK_PLAYER_COUNT> remote_ship_exploding{};
614 std::array<float, NETWORK_PLAYER_COUNT> remote_explosion_timers{};
615 std::array<std::array<NetworkProjectile, NETWORK_PROJECTILE_COUNT>, NETWORK_PLAYER_COUNT> remote_projectiles{};
616 std::array<std::uint32_t, MAX_PROJECTILES> projectile_ids{};
617 std::uint32_t next_projectile_id = 1;
618 std::array<std::unordered_set<std::uint32_t>, NETWORK_PLAYER_COUNT> consumed_remote_projectiles{};
619 std::array<std::uint32_t, NETWORK_PLAYER_COUNT> consumed_projectile_ids{};
620 bool host_requested_start = false;
621
622 void log_game(const std::string &message, SDL_Color color = SDL_Color{180, 220, 255, 255}) {
623 if (!console_ready) {
624 return;
625 }
626 console.printLine("[game] " + message, color);
627 }
628
629 void configure_console() {
630 console.attach(*this, asset_root + "/data/font.ttf", 20);
631 console.setSpriteYOriginTopLeft(true);
632 console.setPrompt("asteroids> ");
633 console_ready = true;
634 console.printLine("Press F3 to open/close the console.");
635 console.printLine("Type 'help' for asteroids3d commands.");
636 log_game("Console attached.");
637 log_game("asteroids3d initialized.");
638 console.setCommandCallback([this](mxvk::VK_Window &, const std::vector<std::string> &args, std::ostream &out) {
639 if (args.empty()) {
640 return true;
641 }
642
643 const std::string &cmd = args.front();
644 if (cmd == "help") {
645 out << "asteroids3d commands:\n"
646 << " clear Clear console output\n"
647 << " echo <text> Print text to the console\n"
648 << " status Print score, lives, mode, and asteroid count\n"
649 << " restart Restart the game\n"
650 << " intro Return to the intro screen\n"
651 << " play Start or resume play\n"
652 << " debug Toggle debug HUD\n"
653 << " controls Toggle arcade/inverted pitch controls\n"
654 << " input Toggle classic keyboard versus keyboard/mouse controls\n"
655 << " about Print program banner\n"
656 << " quit / exit Close the window\n";
657 return true;
658 }
659
660 if (cmd == "echo") {
661 for (std::size_t i = 1; i < args.size(); ++i) {
662 if (i > 1) {
663 out << ' ';
664 }
665 out << args[i];
666 }
667 return true;
668 }
669
670 if (cmd == "status") {
671 const char *mode_name = (mode == GameMode::Intro) ? "intro" : (mode == GameMode::Loading) ? "loading"
672 : (mode == GameMode::Lobby) ? "lobby"
673 : (mode == GameMode::Playing) ? "playing"
674 : (mode == GameMode::GameComplete) ? "complete"
675 : "gameover";
676 out << "Mode: " << mode_name << '\n'
677 << "Score: " << ship.score << '\n'
678 << "Lives: " << std::max(0, ship.lives) << '\n'
679 << "Asteroids: " << active_asteroids() << '\n'
680 << "Time left: " << format_round_time() << '\n'
681 << "Speed: " << ship.current_speed << " / " << ship.max_speed << '\n'
682 << "Control scheme: " << (mouse_look_controls ? "keyboard/mouse" : "classic keyboard") << '\n'
683 << "Camera: " << (first_person_camera ? "first person" : "chase") << '\n'
684 << "Controls: " << (inverted_controls ? "inverted" : "arcade") << '\n'
685 << "Controller: " << controller_status() << '\n'
686 << "Debug HUD: " << (debug_menu ? "on" : "off") << '\n';
687 return true;
688 }
689
690 if (cmd == "restart") {
691 restart_game();
692 mode = GameMode::Playing;
693 log_game("Game restarted from console.");
694 out << "Game restarted.";
695 return true;
696 }
697
698 if (cmd == "intro") {
699 reset_intro_screen();
700 log_game("Returned to intro screen from console.");
701 out << "Intro screen active.";
702 return true;
703 }
704
705 if (cmd == "play") {
706 mode = GameMode::Playing;
707 log_game("Play mode activated from console.");
708 out << "Playing.";
709 return true;
710 }
711
712 if (cmd == "debug") {
713 debug_menu = !debug_menu;
714 log_game(std::string("Debug HUD ") + (debug_menu ? "enabled from console." : "disabled from console."));
715 out << "Debug HUD " << (debug_menu ? "enabled." : "disabled.");
716 return true;
717 }
718
719 if (cmd == "controls") {
720 inverted_controls = !inverted_controls;
721 log_game(std::string("Controls set to ") + (inverted_controls ? "inverted from console." : "arcade from console."));
722 out << "Controls set to " << (inverted_controls ? "inverted." : "arcade.");
723 return true;
724 }
725
726 if (cmd == "input") {
727 set_mouse_look_controls(!mouse_look_controls);
728 log_game(std::string("Control scheme set to ") + (mouse_look_controls ? "keyboard/mouse from console." : "classic keyboard from console."));
729 out << "Control scheme set to " << (mouse_look_controls ? "keyboard/mouse." : "classic keyboard.");
730 return true;
731 }
732
733 if (cmd == "about") {
734 out << "asteroids3d: MXVK port of gl_asteroids.\n";
735 return true;
736 }
737
738 if (cmd == "quit" || cmd == "exit") {
739 log_game("Exit requested from console.");
740 out << "Closing window...";
741 exit();
742 return true;
743 }
744
745 return false;
746 });
747 }
748
749 bool open_controller() {
750 for (int i = 0; i < mxvk::VK_Controller::joysticks(); ++i) {
751 if (controller.open(i)) {
752 log_game("Controller connected: " + controller.name());
753 return true;
754 }
755 }
756 return false;
757 }
758
759 void sync_controller_connection() {
760 if (!controller.active()) {
761 open_controller();
762 }
763 }
764
765 std::string controller_status() const {
766 return controller.active() ? ("Connected: " + controller.name()) : "Disconnected";
767 }
768
769 float controller_axis(SDL_GamepadAxis axis) const {
770 if (!controller.active()) {
771 return 0.0f;
772 }
773
774 const Sint16 raw_value = controller.getAxis(axis);
775 const float magnitude = static_cast<float>(std::abs(static_cast<int>(raw_value)));
776 if (magnitude <= static_cast<float>(CONTROLLER_DEAD_ZONE)) {
777 return 0.0f;
778 }
779
780 const float normalized = std::clamp((magnitude - static_cast<float>(CONTROLLER_DEAD_ZONE)) /
781 (CONTROLLER_AXIS_MAX - static_cast<float>(CONTROLLER_DEAD_ZONE)),
782 0.0f,
783 1.0f);
784 const float curved = normalized * normalized;
785 return raw_value < 0 ? -curved : curved;
786 }
787
788 void set_mouse_look_controls(bool enabled) {
789 mouse_look_controls = enabled;
790 keyboard_yaw = 0.0f;
791 keyboard_pitch = 0.0f;
792 keyboard_roll = 0.0f;
793 smooth_yaw = 0.0f;
794 smooth_pitch = 0.0f;
795 smooth_roll = 0.0f;
796 sync_mouse_capture();
797 }
798
799 void sync_mouse_capture() {
800 const bool should_capture = mouse_look_controls && mode == GameMode::Playing && !console.isVisible();
801 if (should_capture == mouse_capture_active) {
802 return;
803 }
804
805 SDL_SetWindowRelativeMouseMode(getSDLWindow(), should_capture);
806 mouse_capture_active = should_capture;
807 ignore_next_mouse_motion = should_capture;
808 }
809
810 void apply_mouse_look(float delta_x, float delta_y) {
811 ship.rotation.y -= delta_x * MOUSE_LOOK_SENSITIVITY;
812 const float pitch_delta = inverted_controls ? delta_y * MOUSE_LOOK_SENSITIVITY : -delta_y * MOUSE_LOOK_SENSITIVITY;
813 ship.rotation.x = std::clamp(ship.rotation.x + pitch_delta, -75.0f, 75.0f);
814 }
815
816#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
817 std::string asteroids3d_asset_path(const std::string &filename) const {
818 return asset_root + "/data/" + filename;
819 }
820
821 std::string sound_effect_path(const std::string &filename) const {
822 return asset_root + "/data/" + filename;
823 }
824
825 void load_sound_effects() {
826 sound_effects = std::make_unique<mxvk::VK_Mixer>();
827 if (!background_music_disabled) {
828 background_music_track = sound_effects->loadMusic(asteroids3d_asset_path("music.ogg"));
829 }
830 crash_sound = sound_effects->loadWav(sound_effect_path("crash.wav"));
831 cannon_sound = sound_effects->loadWav(asteroids3d_asset_path("cannon.wav"));
832 asteroid_explosion_sound = sound_effects->loadWav(sound_effect_path("asteroid.wav"));
833 }
834
835 void ensure_background_music_playing() {
836 if (background_music_disabled || !sound_effects || background_music_track < 0) {
837 return;
838 }
839 if (!sound_effects->isMusicPlaying(background_music_track)) {
840 if (sound_effects->playMusic(background_music_track, -1) != 0) {
841 throw mxvk::Exception("Could not start asteroids3d background music");
842 }
843 }
844 }
845
846 void play_sound(int sound_id) {
847 if (!sound_effects || sound_id < 0) {
848 return;
849 }
850 sound_effects->playWav(sound_id);
851 }
852#endif
853
854 void load_loading_screen_resources() {
855 set_ui_font_size(18);
856 lobby_status_font.reset(asset_root + "/data/font.ttf", lobby_status_font_size);
857 lobby_roster_font.reset(asset_root + "/data/font.ttf", lobby_roster_font_size);
858
859 intro_sprite = createSprite(
860 asset_root + "/data/intro.png",
861 asset_root + "/data/sprite.vert.spv",
862 shader_root + "/intro.frag.spv");
863 matrix::RainConfig intro_rain_config = matrix::make_matrix_rain_config(asset_root, false);
864 intro_rain_config.color = "#ff0000";
865 intro_rain_config.surface_width = INTRO_RAIN_TEXTURE_WIDTH;
866 intro_rain_config.surface_height = INTRO_RAIN_TEXTURE_HEIGHT;
867 intro_rain = std::make_unique<matrix::Rain>(*this, std::move(intro_rain_config));
868 reset_intro_screen();
869 loading_step_index.store(0, std::memory_order_relaxed);
870 game_resources_loaded.store(false, std::memory_order_relaxed);
871 loading_failed.store(false, std::memory_order_relaxed);
872 }
873
874 void start_loading_async() {
875 if (loading_thread.joinable()) {
876 loading_thread.join();
877 }
878 loading_thread = std::thread([this]() {
879 try {
880 preload_models();
881 } catch (const std::exception &e) {
882 loading_error = e.what();
883 model_preload_failed.store(true, std::memory_order_release);
884 } catch (...) {
885 loading_error = "unknown loading error";
886 model_preload_failed.store(true, std::memory_order_release);
887 }
888 });
889 }
890
891 void preload_models() {
892 std::optional<mxvk::MXModel> ship_model_cpu;
893 ship_model_cpu.emplace();
894 ship_model_cpu->load(asset_root + "/data/starship.obj", 1.0f);
895
896 std::array<std::optional<mxvk::MXModel>, MAX_ASTEROIDS> asteroid_models_cpu{};
897 std::array<std::string, MAX_ASTEROIDS> asteroid_texture_paths{};
898
899 std::mt19937 rng(std::random_device{}());
900 std::uniform_int_distribution<int> rock_variant_dist(0, 2);
901 for (std::size_t slot_index = 0; slot_index < MAX_ASTEROIDS; ++slot_index) {
902 static constexpr std::array<const char *, 3> asteroid_paths = {
903 "data/asteroid.obj",
904 "data/asteroid2.obj",
905 "data/asteroid3.obj",
906 };
907
908 const std::size_t model_variant = slot_index % asteroid_paths.size();
909 std::string texture_path;
910 if (model_variant == 0) {
911 texture_path = asset_root + "/data/rock.tex";
912 } else if (model_variant == 1) {
913 texture_path = asset_root + "/data/rock2.tex";
914 } else {
915 texture_path = (rock_variant_dist(rng) == 0) ? asset_root + "/data/rock.tex" : asset_root + "/data/rock2.tex";
916 }
917
918 asteroid_models_cpu[slot_index].emplace();
919 asteroid_models_cpu[slot_index]->load(asset_root + "/" + asteroid_paths[model_variant], 1.0f);
920 asteroid_texture_paths[slot_index] = texture_path;
921 }
922
923 {
924 std::lock_guard<std::mutex> lock(prepared_model_mutex);
925 prepared_ship_model = std::move(ship_model_cpu);
926 prepared_asteroid_models = std::move(asteroid_models_cpu);
927 prepared_asteroid_texture_paths = std::move(asteroid_texture_paths);
928 }
929
930 model_preload_done.store(true, std::memory_order_release);
931 }
932
933 int lobby_item_count() const {
934 if (lobby_page == LobbyPage::Host) {
935 return 4;
936 }
937 if (lobby_page == LobbyPage::Join) {
938 return 6;
939 }
940 return 3;
941 }
942
943 void stop_lobby_editing() {
944 lobby_edit_field = -1;
945 SDL_StopTextInput(getSDLWindow());
946 }
947
948 void activate_lobby_item() {
949 if (lobby_page == LobbyPage::Main) {
950 if (lobby_selection == 0) {
951 lobby_page = LobbyPage::Host;
952 lobby_selection = 0;
953 lobby_status = "Set your pilot name and listen port.";
954 } else if (lobby_selection == 1) {
955 lobby_page = LobbyPage::Join;
956 lobby_selection = 0;
957 lobby_status = "Enter the host address to join the match.";
958 } else {
959 exit();
960 }
961 return;
962 }
963
964 const bool host_page = lobby_page == LobbyPage::Host;
965 const int action_item = host_page ? 2 : 4;
966 const int back_item = host_page ? 3 : 5;
967 if (lobby_selection < action_item) {
968 lobby_edit_field = lobby_selection;
969 SDL_StartTextInput(getSDLWindow());
970 lobby_status = "Editing field. Press Enter when finished.";
971 } else if (lobby_selection == action_item) {
972 stop_lobby_editing();
973 if (lobby_player_name.empty() || lobby_port.empty() ||
974 (!host_page && (lobby_host_address.empty() || lobby_join_code.size() != 8U))) {
975 lobby_status = "Complete every field before continuing.";
976 return;
977 }
978 try {
979 if (host_page) {
980 if (multiplayer.active() && multiplayer.is_host()) {
981 if (multiplayer.player_count() < 2U) {
982 lobby_status = "At least one other player must join before starting.";
983 } else {
984 host_requested_start = true;
985 lobby_status = "Starting match...";
986 }
987 } else {
988 multiplayer.host(lobby_port, lobby_player_name);
989 host_requested_start = false;
990 lobby_status = multiplayer.port_mapping_status();
991 }
992 } else {
993 if (multiplayer.active()) {
994 lobby_status = "Connected. Waiting for the host to start the match.";
995 } else {
996 multiplayer.join(lobby_host_address, lobby_port, lobby_player_name, lobby_join_code);
997 lobby_status = "Connecting to " + lobby_host_address + ":" + lobby_port + "...";
998 }
999 }
1000 } catch (const std::exception &error) {
1001 multiplayer.stop();
1002 lobby_status = std::string("Network error: ") + error.what();
1003 }
1004 } else if (lobby_selection == back_item) {
1005 stop_lobby_editing();
1006 multiplayer.stop();
1007 host_requested_start = false;
1008 lobby_page = LobbyPage::Main;
1009 lobby_selection = 0;
1010 lobby_status = "Choose how you want to play.";
1011 }
1012 }
1013
1014 void handle_lobby_event(const SDL_Event &event) {
1015 if (event.type == SDL_EVENT_TEXT_INPUT && lobby_edit_field >= 0) {
1016 std::string *field = &lobby_player_name;
1017 if (lobby_page == LobbyPage::Join && lobby_edit_field == 1) {
1018 field = &lobby_host_address;
1019 } else if ((lobby_page == LobbyPage::Host && lobby_edit_field == 1) ||
1020 (lobby_page == LobbyPage::Join && lobby_edit_field == 2)) {
1021 field = &lobby_port;
1022 } else if (lobby_page == LobbyPage::Join && lobby_edit_field == 3) {
1023 field = &lobby_join_code;
1024 }
1025 if (field == &lobby_join_code) {
1026 for (const char character : std::string(event.text.text)) {
1027 if (field->size() < 8U && std::isalnum(static_cast<unsigned char>(character))) {
1028 field->push_back(static_cast<char>(std::toupper(static_cast<unsigned char>(character))));
1029 }
1030 }
1031 } else if (field->size() < 32U) {
1032 *field += event.text.text;
1033 }
1034 return;
1035 }
1036
1037 if (event.type == SDL_EVENT_KEY_DOWN) {
1038 if (lobby_edit_field >= 0) {
1039 std::string *field = &lobby_player_name;
1040 if (lobby_page == LobbyPage::Join && lobby_edit_field == 1) {
1041 field = &lobby_host_address;
1042 } else if ((lobby_page == LobbyPage::Host && lobby_edit_field == 1) ||
1043 (lobby_page == LobbyPage::Join && lobby_edit_field == 2)) {
1044 field = &lobby_port;
1045 } else if (lobby_page == LobbyPage::Join && lobby_edit_field == 3) {
1046 field = &lobby_join_code;
1047 }
1048 if (event.key.key == SDLK_BACKSPACE && !field->empty()) {
1049 field->pop_back();
1050 } else if (event.key.key == SDLK_RETURN || event.key.key == SDLK_KP_ENTER || event.key.key == SDLK_ESCAPE) {
1051 stop_lobby_editing();
1052 lobby_status = "Setup updated.";
1053 }
1054 return;
1055 }
1056 if (event.key.key == SDLK_UP || event.key.key == SDLK_W) {
1057 lobby_selection = (lobby_selection + lobby_item_count() - 1) % lobby_item_count();
1058 } else if (event.key.key == SDLK_DOWN || event.key.key == SDLK_S) {
1059 lobby_selection = (lobby_selection + 1) % lobby_item_count();
1060 } else if (event.key.key == SDLK_RETURN || event.key.key == SDLK_KP_ENTER || event.key.key == SDLK_SPACE) {
1061 activate_lobby_item();
1062 } else if (event.key.key == SDLK_ESCAPE) {
1063 if (lobby_page == LobbyPage::Main) {
1064 exit();
1065 } else {
1066 multiplayer.stop();
1067 host_requested_start = false;
1068 lobby_page = LobbyPage::Main;
1069 lobby_selection = 0;
1070 lobby_status = "Choose how you want to play.";
1071 }
1072 }
1073 return;
1074 }
1075
1076 if (event.type == SDL_EVENT_MOUSE_MOTION || event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
1077 const VkExtent2D extent = getSwapchainExtent();
1078 const int panel_width = 760;
1079 const int panel_x = static_cast<int>(extent.width) / 2 - panel_width / 2;
1080 const int first_y = 250;
1081 const int item_height = 50;
1082 const float mouse_x = event.type == SDL_EVENT_MOUSE_MOTION ? event.motion.x : event.button.x;
1083 const float mouse_y = event.type == SDL_EVENT_MOUSE_MOTION ? event.motion.y : event.button.y;
1084 const int item = (static_cast<int>(mouse_y) - first_y) / item_height;
1085 if (mouse_x >= panel_x + 20 && mouse_x <= panel_x + panel_width - 20 &&
1086 mouse_y >= first_y && item >= 0 && item < lobby_item_count()) {
1087 lobby_selection = item;
1088 if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT) {
1089 activate_lobby_item();
1090 }
1091 }
1092 }
1093 }
1094
1095 NetworkState make_network_state(bool match_started) const {
1096 NetworkState state{};
1097 state.position = {ship.position.x, ship.position.y, ship.position.z};
1098 state.rotation = {ship.rotation.x, ship.rotation.y, ship.rotation.z};
1099 state.current_speed = ship.current_speed;
1100 state.exploding = ship.exploding ? 1U : 0U;
1101 state.match_started = match_started ? 1U : 0U;
1102 state.kills = multiplayer_kills;
1103 state.death_serials = multiplayer_death_serials;
1104 state.consumed_projectile_ids = consumed_projectile_ids;
1105 state.winner = multiplayer_winner;
1106 std::size_t output_index = 0;
1107 for (std::size_t projectile_index = 0; projectile_index < projectiles.size(); ++projectile_index) {
1108 const Projectile &projectile = projectiles[projectile_index];
1109 if (!projectile.active || output_index >= state.projectiles.size()) {
1110 continue;
1111 }
1112 NetworkProjectile &output = state.projectiles[output_index++];
1113 output.id = projectile_ids[projectile_index];
1114 output.position = {projectile.position.x, projectile.position.y, projectile.position.z};
1115 output.velocity = {projectile.velocity.x, projectile.velocity.y, projectile.velocity.z};
1116 output.lifetime = projectile.lifetime;
1117 output.active = 1U;
1118 }
1119 if (multiplayer.is_host()) {
1120 std::size_t asteroid_output_index = 0;
1121 for (std::size_t asteroid_index = 0; asteroid_index < asteroids.size(); ++asteroid_index) {
1122 const Asteroid &asteroid = asteroids[asteroid_index];
1123 if (!asteroid.active || asteroid_output_index >= state.asteroids.size()) {
1124 continue;
1125 }
1126 NetworkAsteroid &output = state.asteroids[asteroid_output_index++];
1127 output.position = {asteroid.position.x, asteroid.position.y, asteroid.position.z};
1128 output.rotation = {asteroid.rotation.x, asteroid.rotation.y, asteroid.rotation.z};
1129 output.radius = asteroid.radius;
1130 output.slot = static_cast<std::uint8_t>(asteroid_index);
1131 output.active = 1U;
1132 }
1133 }
1134 return state;
1135 }
1136
1137 void apply_remote_state(std::uint8_t player, const NetworkState &state) {
1138 if (player >= NETWORK_PLAYER_COUNT || player == multiplayer.local_player_id()) {
1139 return;
1140 }
1141 Ship &remote_ship = remote_ships[player];
1142 remote_ship.prev_position = remote_ship.position;
1143 remote_ship.position = {state.position[0], state.position[1], state.position[2]};
1144 remote_ship.rotation = {state.rotation[0], state.rotation[1], state.rotation[2]};
1145 remote_ship.current_speed = state.current_speed;
1146 remote_ship.visible = state.exploding == 0U;
1147 remote_projectiles[player] = state.projectiles;
1148 if (!multiplayer.is_host() && player == 0) {
1149 for (Asteroid &asteroid : asteroids) {
1150 asteroid.active = false;
1151 }
1152 for (const NetworkAsteroid &network_asteroid : state.asteroids) {
1153 if (network_asteroid.active == 0U || network_asteroid.slot >= asteroids.size()) {
1154 continue;
1155 }
1156 Asteroid &asteroid = asteroids[network_asteroid.slot];
1157 asteroid.position = {network_asteroid.position[0], network_asteroid.position[1], network_asteroid.position[2]};
1158 asteroid.rotation = {network_asteroid.rotation[0], network_asteroid.rotation[1], network_asteroid.rotation[2]};
1159 asteroid.radius = network_asteroid.radius;
1160 asteroid.active = true;
1161 }
1162 }
1163 }
1164
1165 void begin_multiplayer_match() {
1166 multiplayer_match = true;
1167 multiplayer_collision_grace = 2.5f;
1168 multiplayer_kills = {};
1169 multiplayer_player_names = multiplayer.player_names();
1170 multiplayer_death_serials = {};
1171 received_death_serials = {};
1172 multiplayer_winner = 0;
1173 remote_ship_exploding = {};
1174 remote_explosion_timers = {};
1175 for (auto &consumed : consumed_remote_projectiles)
1176 consumed.clear();
1177 consumed_projectile_ids = {};
1178 clear_round_state();
1179 ship.lives = 99;
1180 const std::uint8_t local_id = std::min<std::uint8_t>(multiplayer.local_player_id(), 3U);
1181 ship.position = MULTIPLAYER_SPAWNS[local_id];
1182 ship.rotation.y = MULTIPLAYER_SPAWN_YAWS[local_id];
1183 ship.current_speed = 6.0f;
1184 ship.prev_position = ship.position;
1185 for (std::uint8_t player = 0; player < NETWORK_PLAYER_COUNT; ++player) {
1186 remote_ships[player].position = MULTIPLAYER_SPAWNS[player];
1187 remote_ships[player].rotation = {0.0f, MULTIPLAYER_SPAWN_YAWS[player], 0.0f};
1188 remote_ships[player].current_speed = 6.0f;
1189 remote_ships[player].visible = multiplayer.player_connected()[player];
1190 }
1191 if (multiplayer.is_host()) {
1192 spawn_initial_asteroids();
1193 } else {
1194 for (Asteroid &asteroid : asteroids) {
1195 asteroid.active = false;
1196 }
1197 }
1198 mode = GameMode::Playing;
1199 log_game(std::format("UDP match started with {} players.", multiplayer.player_count()));
1200 }
1201
1202 void update_lobby_network() {
1203 if (!multiplayer.active()) {
1204 return;
1205 }
1206 const NetworkExchange exchange = multiplayer.exchange(make_network_state(host_requested_start), last_delta_time);
1207 for (const NetworkPlayerUpdate &update : exchange.players) {
1208 apply_remote_state(update.player_id, update.state);
1209 if (!multiplayer.is_host() && update.player_id == 0 && update.state.match_started != 0U) {
1210 begin_multiplayer_match();
1211 return;
1212 }
1213 }
1214 if (multiplayer.is_host() && host_requested_start) {
1215 multiplayer.exchange(make_network_state(true), 1.0f / 20.0f);
1216 begin_multiplayer_match();
1217 }
1218 }
1219
1220 static float projectile_distance_to_ship(const Projectile &projectile, const glm::vec3 &target) {
1221 const glm::vec3 segment = projectile.position - projectile.prev_position;
1222 const float length_squared = glm::dot(segment, segment);
1223 if (length_squared <= 1e-6f) {
1224 return glm::length(projectile.position - target);
1225 }
1226 const float amount = std::clamp(glm::dot(target - projectile.prev_position, segment) / length_squared, 0.0f, 1.0f);
1227 return glm::length(projectile.prev_position + segment * amount - target);
1228 }
1229
1230 void start_multiplayer_local_explosion() {
1231 if (ship.exploding) {
1232 return;
1233 }
1234 start_ship_explosion();
1235 ship.lives = 99;
1236 }
1237
1238 void finish_multiplayer_match(std::uint8_t winner) {
1239 multiplayer_winner = winner;
1240 multiplayer.exchange(make_network_state(true), 1.0f / 20.0f);
1241 mode = GameMode::MatchOver;
1242 }
1243
1244 void finish_abandoned_multiplayer_match() {
1245 const auto winner = std::max_element(multiplayer_kills.begin(), multiplayer_kills.end());
1246 const std::uint8_t winner_id = static_cast<std::uint8_t>(std::distance(multiplayer_kills.begin(), winner) + 1);
1247 log_game(std::format("All opponents disconnected. Player {} wins with {} kills.", winner_id, *winner));
1248 finish_multiplayer_match(winner_id);
1249 }
1250
1251#if 0
1252 void update_multiplayer_legacy(float dt) {
1253 const std::optional<NetworkState> state = multiplayer.exchange(make_network_state(true), dt);
1254 if (state.has_value()) {
1255 apply_remote_state(*state);
1256 if (!multiplayer.is_host()) {
1257 host_kills = state->host_kills;
1258 client_kills = state->client_kills;
1259 multiplayer_winner = state->winner;
1260 if (state->consumed_client_projectile_id != 0U) {
1261 for (std::size_t projectile_index = 0; projectile_index < projectile_ids.size(); ++projectile_index) {
1262 if (projectile_ids[projectile_index] == state->consumed_client_projectile_id) {
1263 projectiles[projectile_index].active = false;
1264 }
1265 }
1266 }
1267 if (state->client_death_serial != received_client_death_serial) {
1268 received_client_death_serial = state->client_death_serial;
1269 start_multiplayer_local_explosion();
1270 }
1271 if (state->host_death_serial != received_host_death_serial) {
1272 received_host_death_serial = state->host_death_serial;
1273 remote_ship_exploding = true;
1274 remote_explosion_timer = 1.5f;
1275 spawn_ship_explosion(remote_ship.position);
1276 }
1277 if (multiplayer_winner != 0U) {
1278 mode = GameMode::MatchOver;
1279 return;
1280 }
1281 }
1282 }
1283
1284 if (remote_ship_exploding) {
1285 remote_explosion_timer -= dt;
1286 if (remote_explosion_timer <= 0.0f) {
1287 remote_ship_exploding = false;
1288 }
1289 }
1290
1291 if (!multiplayer.is_host() || !state.has_value()) {
1292 return;
1293 }
1294
1295 constexpr float SHIP_TO_SHIP_COLLISION_RADIUS = 2.4f;
1296 if (!ship.exploding && !remote_ship_exploding && remote_ship.visible &&
1297 glm::length(ship.position - remote_ship.position) < SHIP_TO_SHIP_COLLISION_RADIUS) {
1298 const glm::vec3 remote_collision_position = remote_ship.position;
1299 start_multiplayer_local_explosion();
1300 remote_ship_exploding = true;
1301 remote_ship.visible = false;
1302 remote_explosion_timer = 1.5f;
1303 ++client_death_serial;
1304 spawn_ship_explosion(remote_collision_position);
1305 log_game("Ship collision: both pilots destroyed. No kill awarded.", SDL_Color{255, 170, 80, 255});
1306 return;
1307 }
1308
1309 if (!remote_ship_exploding) {
1310 for (const Asteroid &asteroid : asteroids) {
1311 if (asteroid.active && glm::length(asteroid.position - remote_ship.position) < asteroid.radius + 1.8f) {
1312 remote_ship_exploding = true;
1313 remote_explosion_timer = 1.5f;
1314 ++client_death_serial;
1315 spawn_ship_explosion(remote_ship.position);
1316 log_game("Enemy collided with an asteroid.", SDL_Color{255, 170, 80, 255});
1317 break;
1318 }
1319 }
1320 }
1321
1322 for (const NetworkProjectile &projectile : remote_projectiles) {
1323 if (projectile.active == 0U || consumed_remote_projectiles.contains(projectile.id)) {
1324 continue;
1325 }
1326 const glm::vec3 projectile_position{projectile.position[0], projectile.position[1], projectile.position[2]};
1327 for (Asteroid &asteroid : asteroids) {
1328 if (asteroid.active && glm::length(projectile_position - asteroid.position) < asteroid.radius * ASTEROID_PROJECTILE_COLLISION_SCALE) {
1329 consumed_remote_projectiles.insert(projectile.id);
1330 consumed_client_projectile_id = projectile.id;
1331 split_asteroid(asteroid);
1332 break;
1333 }
1334 }
1335 }
1336
1337 if (!remote_ship_exploding) {
1338 for (Projectile &projectile : projectiles) {
1339 if (projectile.active && projectile_distance_to_ship(projectile, remote_ship.position) < 1.8f) {
1340 projectile.active = false;
1341 remote_ship_exploding = true;
1342 remote_explosion_timer = 1.5f;
1343 ++host_kills;
1344 ++client_death_serial;
1345 spawn_ship_explosion(remote_ship.position);
1346 log_game(std::format("Enemy destroyed. Score {}-{}.", host_kills, client_kills));
1347 if (host_kills >= MULTIPLAYER_KILLS_TO_WIN) {
1348 finish_multiplayer_match(1U);
1349 }
1350 break;
1351 }
1352 }
1353 }
1354
1355 if (!ship.exploding) {
1356 for (const NetworkProjectile &projectile : remote_projectiles) {
1357 if (projectile.active == 0U || consumed_remote_projectiles.contains(projectile.id)) {
1358 continue;
1359 }
1360 const glm::vec3 position{projectile.position[0], projectile.position[1], projectile.position[2]};
1361 if (glm::length(position - ship.position) < 1.8f) {
1362 consumed_remote_projectiles.insert(projectile.id);
1363 consumed_client_projectile_id = projectile.id;
1364 start_multiplayer_local_explosion();
1365 ++client_kills;
1366 log_game(std::format("You were destroyed. Score {}-{}.", host_kills, client_kills));
1367 if (client_kills >= MULTIPLAYER_KILLS_TO_WIN) {
1368 finish_multiplayer_match(2U);
1369 }
1370 break;
1371 }
1372 }
1373 }
1374 }
1375
1376#endif
1377
1378 void update_multiplayer(float dt) {
1379 multiplayer_collision_grace = std::max(0.0f, multiplayer_collision_grace - dt);
1380 const NetworkExchange exchange = multiplayer.exchange(make_network_state(true), dt);
1381 for (const NetworkPlayerUpdate &update : exchange.players) {
1382 apply_remote_state(update.player_id, update.state);
1383 if (!multiplayer.is_host() && update.player_id == 0) {
1384 multiplayer_kills = update.state.kills;
1385 multiplayer_death_serials = update.state.death_serials;
1386 consumed_projectile_ids = update.state.consumed_projectile_ids;
1387 multiplayer_winner = update.state.winner;
1388 }
1389 }
1390
1391 if (multiplayer.player_count() <= 1U) {
1392 finish_abandoned_multiplayer_match();
1393 return;
1394 }
1395
1396 const std::uint8_t local_id = multiplayer.local_player_id();
1397 if (local_id >= NETWORK_PLAYER_COUNT)
1398 return;
1399 if (!multiplayer.is_host()) {
1400 if (consumed_projectile_ids[local_id] != 0U) {
1401 for (std::size_t index = 0; index < projectile_ids.size(); ++index) {
1402 if (projectile_ids[index] == consumed_projectile_ids[local_id])
1403 projectiles[index].active = false;
1404 }
1405 }
1406 if (multiplayer_death_serials[local_id] != received_death_serials[local_id]) {
1407 received_death_serials[local_id] = multiplayer_death_serials[local_id];
1408 start_multiplayer_local_explosion();
1409 }
1410 if (multiplayer_winner != 0U)
1411 mode = GameMode::MatchOver;
1412 }
1413
1414 for (std::uint8_t player = 0; player < NETWORK_PLAYER_COUNT; ++player) {
1415 if (player == local_id)
1416 continue;
1417 if (multiplayer_death_serials[player] != received_death_serials[player]) {
1418 received_death_serials[player] = multiplayer_death_serials[player];
1419 remote_ship_exploding[player] = true;
1420 remote_explosion_timers[player] = 1.5f;
1421 spawn_ship_explosion(remote_ships[player].position);
1422 }
1423 if (remote_ship_exploding[player]) {
1424 remote_explosion_timers[player] -= dt;
1425 if (remote_explosion_timers[player] <= 0.0f)
1426 remote_ship_exploding[player] = false;
1427 }
1428 }
1429 if (!multiplayer.is_host())
1430 return;
1431
1432 auto destroy_player = [this](std::uint8_t target) {
1433 if (target == 0) {
1434 start_multiplayer_local_explosion();
1435 } else {
1436 remote_ship_exploding[target] = true;
1437 ++multiplayer_death_serials[target];
1438 }
1439 spawn_ship_explosion(target == 0 ? ship.position : remote_ships[target].position);
1440 };
1441 auto player_position = [this](std::uint8_t player) {
1442 return player == 0 ? ship.position : remote_ships[player].position;
1443 };
1444 auto player_exploding = [this](std::uint8_t player) {
1445 return player == 0 ? ship.exploding : remote_ship_exploding[player];
1446 };
1447
1448 if (multiplayer_collision_grace <= 0.0f) {
1449 constexpr float SHIP_COLLISION_RADIUS = 2.4f;
1450 for (std::uint8_t first = 0; first < NETWORK_PLAYER_COUNT; ++first) {
1451 if (!multiplayer.player_connected()[first] || player_exploding(first))
1452 continue;
1453 for (std::uint8_t second = first + 1; second < NETWORK_PLAYER_COUNT; ++second) {
1454 if (!multiplayer.player_connected()[second] || player_exploding(second))
1455 continue;
1456 if (glm::length(player_position(first) - player_position(second)) < SHIP_COLLISION_RADIUS) {
1457 destroy_player(first);
1458 destroy_player(second);
1459 }
1460 }
1461 }
1462 }
1463
1464 for (std::uint8_t player = 1; player < NETWORK_PLAYER_COUNT; ++player) {
1465 if (!multiplayer.player_connected()[player] || player_exploding(player))
1466 continue;
1467 for (const Asteroid &asteroid : asteroids) {
1468 if (asteroid.active && glm::length(asteroid.position - player_position(player)) < asteroid.radius + 1.8f) {
1469 destroy_player(player);
1470 break;
1471 }
1472 }
1473 }
1474
1475 for (Projectile &projectile : projectiles) {
1476 if (!projectile.active)
1477 continue;
1478 for (std::uint8_t target = 1; target < NETWORK_PLAYER_COUNT; ++target) {
1479 if (multiplayer.player_connected()[target] && !player_exploding(target) &&
1480 projectile_distance_to_ship(projectile, player_position(target)) < 1.8f) {
1481 projectile.active = false;
1482 ++multiplayer_kills[0];
1483 destroy_player(target);
1484 if (multiplayer_kills[0] >= MULTIPLAYER_KILLS_TO_WIN)
1485 finish_multiplayer_match(1U);
1486 break;
1487 }
1488 }
1489 }
1490
1491 for (std::uint8_t shooter = 1; shooter < NETWORK_PLAYER_COUNT; ++shooter) {
1492 if (!multiplayer.player_connected()[shooter])
1493 continue;
1494 for (const NetworkProjectile &projectile : remote_projectiles[shooter]) {
1495 if (projectile.active == 0U || consumed_remote_projectiles[shooter].contains(projectile.id))
1496 continue;
1497 const glm::vec3 position{projectile.position[0], projectile.position[1], projectile.position[2]};
1498 bool consumed = false;
1499 for (std::uint8_t target = 0; target < NETWORK_PLAYER_COUNT; ++target) {
1500 if (target == shooter || !multiplayer.player_connected()[target] || player_exploding(target))
1501 continue;
1502 if (glm::length(position - player_position(target)) < 1.8f) {
1503 consumed_remote_projectiles[shooter].insert(projectile.id);
1504 consumed_projectile_ids[shooter] = projectile.id;
1505 ++multiplayer_kills[shooter];
1506 destroy_player(target);
1507 if (multiplayer_kills[shooter] >= MULTIPLAYER_KILLS_TO_WIN)
1508 finish_multiplayer_match(shooter + 1U);
1509 consumed = true;
1510 break;
1511 }
1512 }
1513 if (consumed)
1514 continue;
1515 for (Asteroid &asteroid : asteroids) {
1516 if (asteroid.active && glm::length(position - asteroid.position) < asteroid.radius * ASTEROID_PROJECTILE_COLLISION_SCALE) {
1517 consumed_remote_projectiles[shooter].insert(projectile.id);
1518 consumed_projectile_ids[shooter] = projectile.id;
1519 split_asteroid(asteroid);
1520 break;
1521 }
1522 }
1523 }
1524 }
1525 }
1526
1527 void draw_lobby(VkCommandBuffer cmd, uint32_t image_index, const VkExtent2D &extent, float aspect) {
1528 update_lobby_network();
1529 lobby_camera_distance += last_delta_time * 4.5f;
1530 camera_position = {
1531 std::sin(elapsed_seconds * 0.18f) * 1.8f,
1532 std::cos(elapsed_seconds * 0.13f) * 0.8f,
1533 4.0f - lobby_camera_distance,
1534 };
1535 const glm::vec3 lobby_camera_target = camera_position + glm::vec3(
1536 std::sin(elapsed_seconds * 0.11f) * 0.12f,
1537 std::cos(elapsed_seconds * 0.09f) * 0.08f,
1538 -1.0f);
1539 view_matrix = glm::lookAt(camera_position, lobby_camera_target, glm::vec3(0.0f, 1.0f, 0.0f));
1540 projection_matrix = glm::perspective(glm::radians(55.0f), aspect, 0.1f, 500.0f);
1541 projection_matrix[1][1] *= -1.0f;
1542 star_field.update(last_delta_time * 2.0f, camera_position, elapsed_seconds);
1543 star_field.setSprite(star_sprite);
1544 star_sprite->updateCamera(image_index, view_matrix, projection_matrix);
1545 star_field.draw();
1546 star_sprite->render(cmd, image_index);
1547 star_sprite->clearQueue();
1548
1549 const int panel_width = 760;
1550 const int panel_x = static_cast<int>(extent.width) / 2 - panel_width / 2;
1551 draw_ui_rect(panel_x, 65, panel_width, 600, {0.015f, 0.025f, 0.08f, 0.90f});
1552 draw_ui_rect(panel_x, 65, panel_width, 3, {0.20f, 0.72f, 1.0f, 1.0f});
1553 draw_ui_rect(panel_x, 662, panel_width, 3, {0.20f, 0.72f, 1.0f, 1.0f});
1554
1555 const float lobby_scale = std::min(static_cast<float>(extent.width) / 1280.0f,
1556 static_cast<float>(extent.height) / 720.0f);
1557 const int lobby_font_size = std::clamp(static_cast<int>(std::lround(22.0f * lobby_scale)), 14, 22);
1558 const int desired_status_font_size = std::clamp(static_cast<int>(std::lround(16.0f * lobby_scale)), 12, 16);
1559 const int desired_roster_font_size = std::clamp(static_cast<int>(std::lround(16.0f * lobby_scale)), 12, 16);
1560 set_ui_font_size(lobby_font_size);
1561 if (desired_status_font_size != lobby_status_font_size) {
1562 lobby_status_font_size = desired_status_font_size;
1563 lobby_status_font.reset(asset_root + "/data/font.ttf", lobby_status_font_size);
1564 }
1565 if (desired_roster_font_size != lobby_roster_font_size) {
1566 lobby_roster_font_size = desired_roster_font_size;
1567 lobby_roster_font.reset(asset_root + "/data/font.ttf", lobby_roster_font_size);
1568 }
1569 printText("ASTEROIDS NET", panel_x + 284, 100, {120, 220, 255, 255});
1570 printText("MULTIPLAYER - UP TO 4", panel_x + 235, 145, {255, 255, 255, 255});
1571
1572 std::vector<std::string> labels;
1573 if (lobby_page == LobbyPage::Main) {
1574 labels = {"HOST MULTIPLAYER MATCH", "JOIN MULTIPLAYER MATCH", "QUIT"};
1575 printText("One pilot hosts. The other pilot connects.", panel_x + 165, 195, {170, 190, 220, 255});
1576 } else if (lobby_page == LobbyPage::Host) {
1577 labels = {"PILOT NAME: " + lobby_player_name, "LISTEN PORT: " + lobby_port,
1578 multiplayer.active() ? "START MATCH" : "START HOSTING", "BACK"};
1579 printText("HOST SETUP", panel_x + 310, 195, {255, 210, 90, 255});
1580 if (multiplayer.active()) {
1581 printText("CODE: " + multiplayer.join_code(), panel_x + 540, 195, {120, 255, 170, 255});
1582 }
1583 } else {
1584 labels = {"PILOT NAME: " + lobby_player_name, "HOST: " + lobby_host_address, "PORT: " + lobby_port,
1585 "JOIN CODE: " + lobby_join_code,
1586 multiplayer.active() ? "WAITING FOR HOST" : "CONNECT", "BACK"};
1587 printText("JOIN SETUP", panel_x + 315, 195, {100, 255, 170, 255});
1588 }
1589
1590 const int first_y = 250;
1591 for (int index = 0; index < static_cast<int>(labels.size()); ++index) {
1592 const int y = first_y + index * 50;
1593 const bool selected = index == lobby_selection;
1594 draw_ui_rect(panel_x + 20, y, panel_width - 40, 42,
1595 selected ? glm::vec4(0.08f, 0.30f, 0.48f, 0.95f) : glm::vec4(0.03f, 0.07f, 0.15f, 0.88f));
1596 if (selected) {
1597 draw_ui_rect(panel_x + 20, y, 5, 42, {0.25f, 0.85f, 1.0f, 1.0f});
1598 }
1599 std::string text = labels[index];
1600 if (lobby_edit_field == index) {
1601 text += "_";
1602 }
1603 printText(text, panel_x + 40, y + 9, selected ? SDL_Color{255, 255, 255, 255} : SDL_Color{170, 195, 220, 255});
1604 }
1605
1606 if (multiplayer.active()) {
1607 printText("CONNECTED PLAYERS", panel_x + 30, 550, {120, 220, 255, 255}, lobby_roster_font);
1608 int roster_x = panel_x + 255;
1609 for (std::uint8_t player = 0; player < NETWORK_PLAYER_COUNT; ++player) {
1610 const std::string name = multiplayer.player_connected()[player] ? multiplayer.player_names()[player] : "Waiting...";
1611 printText(std::format("P{}: {}", player + 1, name), roster_x, 550 + static_cast<int>(player % 2U) * 20,
1612 multiplayer.player_connected()[player] ? SDL_Color{120, 255, 170, 255} : SDL_Color{120, 130, 150, 255},
1613 lobby_roster_font);
1614 if (player == 1)
1615 roster_x = panel_x + 500;
1616 }
1617 }
1618 std::vector<std::string> status_lines;
1619 std::istringstream status_words(lobby_status);
1620 std::string line;
1621 std::string word;
1622 while (status_words >> word) {
1623 const std::string candidate = line.empty() ? word : line + " " + word;
1624 int text_width = 0;
1625 int text_height = 0;
1626 if (!line.empty() && getTextDimensions(candidate, text_width, text_height, lobby_status_font) &&
1627 text_width > panel_width - 60) {
1628 status_lines.push_back(line);
1629 line = word;
1630 } else {
1631 line = candidate;
1632 }
1633 }
1634 if (!line.empty())
1635 status_lines.push_back(line);
1636 for (std::size_t index = 0; index < std::min<std::size_t>(status_lines.size(), 2U); ++index) {
1637 printText(status_lines[index], panel_x + 30, 595 + static_cast<int>(index) * 20, {255, 210, 100, 255}, lobby_status_font);
1638 }
1639 printText("Arrow keys / W,S: select Enter: confirm Esc: back", panel_x + 30, 637, {135, 155, 185, 255},
1640 lobby_status_font);
1641 }
1642
1643 void draw_intro(const VkExtent2D &extent) {
1644 if (intro_sprite == nullptr) {
1645 mode = GameMode::Loading;
1646 return;
1647 }
1648
1649 const Uint32 current_ms = SDL_GetTicks();
1650 if ((current_ms - intro_last_update_ms) > 35U) {
1651 intro_last_update_ms = current_ms;
1652 intro_fade -= 0.01f;
1653 }
1654
1655 if (intro_fade <= 0.0f) {
1656 intro_fade = 0.0f;
1657 if (restart_after_intro) {
1658 restart_after_intro = false;
1659 restart_game();
1660 mode = GameMode::Playing;
1661 intro_last_update_ms = SDL_GetTicks();
1662 log_game("Intro finished. Restarting game.");
1663 } else {
1664 mode = GameMode::Loading;
1665 loading_step_index.store(0, std::memory_order_relaxed);
1666 game_resources_loaded.store(false, std::memory_order_relaxed);
1667 loading_failed.store(false, std::memory_order_relaxed);
1668 model_preload_done.store(false, std::memory_order_relaxed);
1669 model_preload_failed.store(false, std::memory_order_relaxed);
1670 loading_black_frame_pending = false;
1671 loading_black_frame_shown = false;
1672 start_loading_async();
1673 log_game("Intro finished. Loading game resources.");
1674 draw_loading(extent);
1675 }
1676 return;
1677 }
1678
1679 intro_sprite->setShaderParams(static_cast<float>(current_ms) / 1000.0f, 0.0f, 0.0f, intro_fade);
1680 intro_sprite->drawSpriteRect(0, 0, static_cast<int>(extent.width), static_cast<int>(extent.height));
1681 if (intro_rain != nullptr) {
1682 intro_rain->update_and_render(*this, static_cast<int>(extent.width), static_cast<int>(extent.height));
1683 }
1684 }
1685
1686 void draw_loading(const VkExtent2D &extent) {
1687 if (loading_failed.load(std::memory_order_relaxed) || model_preload_failed.load(std::memory_order_relaxed)) {
1688 if (intro_rain != nullptr) {
1689 intro_rain->set_opacity(0.0f);
1690 }
1691 if (loading_thread.joinable()) {
1692 loading_thread.join();
1693 }
1694 printText("Loading failed", 25, 25, {255, 100, 100, 255});
1695 return;
1696 }
1697
1698 if (!game_resources_loaded.load(std::memory_order_relaxed)) {
1699 const int loading_progress_percent = std::clamp((loading_step_index.load(std::memory_order_relaxed) * 100) / loading_step_count, 0, 100);
1700 if (intro_rain != nullptr) {
1701 const float target_rain_opacity = 1.0f - (static_cast<float>(loading_progress_percent) / 100.0f);
1702 loading_rain_opacity = std::lerp(loading_rain_opacity, target_rain_opacity, 0.25f);
1703 intro_rain->set_opacity(loading_rain_opacity);
1704 intro_rain->update_and_render(*this, static_cast<int>(extent.width), static_cast<int>(extent.height));
1705 }
1706 set_ui_font_size(40);
1707 printText("Loading " + std::to_string(loading_progress_percent) + "%", 25, 25, {255, 255, 255, 255});
1708 load_next_game_resource_step();
1709 return;
1710 }
1711
1712 if (loading_black_frame_pending) {
1713 if (!loading_black_frame_shown) {
1714 loading_black_frame_shown = true;
1715 if (intro_rain != nullptr) {
1716 intro_rain->set_opacity(0.0f);
1717 }
1718 return;
1719 }
1720
1721 loading_black_frame_pending = false;
1722 loading_black_frame_shown = false;
1723 }
1724
1725 if (intro_rain != nullptr) {
1726 intro_rain->set_opacity(0.0f);
1727 }
1728 if (loading_thread.joinable()) {
1729 loading_thread.join();
1730 }
1731 intro_last_update_ms = SDL_GetTicks();
1732 mode = GameMode::Lobby;
1733 lobby_page = LobbyPage::Main;
1734 lobby_selection = 0;
1735 lobby_status = "Choose how you want to play.";
1736 log_game("Loading complete. Multiplayer lobby is ready.");
1737 }
1738
1739 bool consume_prepared_ship_model(const std::string &model_vert, const std::string &model_frag) {
1740 std::optional<mxvk::MXModel> model_cpu;
1741 {
1742 std::lock_guard<std::mutex> lock(prepared_model_mutex);
1743 if (!prepared_ship_model.has_value()) {
1744 return false;
1745 }
1746 model_cpu = std::move(prepared_ship_model);
1747 prepared_ship_model.reset();
1748 }
1749
1750 ship_model.load(this, std::move(*model_cpu), "", asset_root + "/data", 1.0f);
1751 ship_model.setShaders(this, model_vert, model_frag);
1752 ship_model.setBackfaceCulling(false);
1753 return true;
1754 }
1755
1756 bool consume_prepared_asteroid_model(std::size_t slot_index, const std::string &model_vert, const std::string &model_frag) {
1757 std::optional<mxvk::MXModel> model_cpu;
1758 std::string texture_path;
1759 {
1760 std::lock_guard<std::mutex> lock(prepared_model_mutex);
1761 if (slot_index >= prepared_asteroid_models.size() || !prepared_asteroid_models[slot_index].has_value()) {
1762 return false;
1763 }
1764 model_cpu = std::move(prepared_asteroid_models[slot_index]);
1765 prepared_asteroid_models[slot_index].reset();
1766 texture_path = prepared_asteroid_texture_paths[slot_index];
1767 prepared_asteroid_texture_paths[slot_index].clear();
1768 }
1769
1770 asteroids[slot_index].model_index = static_cast<int>(slot_index % 3U);
1771 asteroid_models[slot_index].load(this, std::move(*model_cpu), texture_path, asset_root + "/data", 1.0f);
1772 asteroid_models[slot_index].setShaders(this, model_vert, model_frag);
1773 asteroid_models[slot_index].setBackfaceCulling(false);
1774 return true;
1775 }
1776
1777 void load_next_game_resource_step() {
1778 const std::string model_vert = shader_root + "/model.vert.spv";
1779 const std::string model_frag = shader_root + "/model.frag.spv";
1780
1781 const int current_step = loading_step_index.load(std::memory_order_relaxed);
1782 if (current_step == 0) {
1783 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> ui_surface(SDL_CreateSurface(1, 1, SDL_PIXELFORMAT_RGBA32), SDL_DestroySurface);
1784 if (ui_surface == nullptr) {
1785 throw mxvk::Exception("Failed to create asteroids3d UI pixel surface");
1786 }
1787 const SDL_PixelFormatDetails *format_details = SDL_GetPixelFormatDetails(ui_surface->format);
1788 if (format_details == nullptr || !SDL_FillSurfaceRect(ui_surface.get(), nullptr, SDL_MapRGBA(format_details, nullptr, 255, 255, 255, 255))) {
1789 throw mxvk::Exception("Failed to initialize asteroids3d UI pixel surface");
1790 }
1791 ui_pixel = createSprite(ui_surface.get(), asset_root + "/data/sprite.vert.spv", shader_root + "/fade_overlay.frag.spv");
1792 if (ui_pixel == nullptr) {
1793 throw mxvk::Exception("Failed to create asteroids3d UI pixel sprite");
1794 }
1795 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> star_surface(load_color_keyed_png(asset_root + "/data/particle_star.png", 12), SDL_DestroySurface);
1796 star_sprite = createSprite3D(star_surface.get());
1797 if (star_sprite == nullptr) {
1798 throw mxvk::Exception("Failed to create star sprite batch");
1799 }
1800 star_sprite->setDepthTestEnabled(false);
1801 star_sprite->setDepthWriteEnabled(false);
1802 star_sprite->setAlphaDiscardThreshold(0.01f);
1803 } else if (current_step == 1) {
1804 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> fire_surface(load_color_keyed_png(asset_root + "/data/particle_explosion.png", 12), SDL_DestroySurface);
1805 projectile_sprite = createSprite3D(fire_surface.get());
1806 if (projectile_sprite == nullptr) {
1807 throw mxvk::Exception("Failed to create projectile sprite batch");
1808 }
1809 projectile_sprite->setDepthTestEnabled(true);
1810 projectile_sprite->setDepthWriteEnabled(false);
1811 projectile_sprite->setAlphaDiscardThreshold(0.05f);
1812 } else if (current_step == 2) {
1813 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> explosion_surface(load_color_keyed_png(asset_root + "/data/particle_explosion.png", 12), SDL_DestroySurface);
1814 effect_sprite = createSprite3D(explosion_surface.get());
1815 if (effect_sprite == nullptr) {
1816 throw mxvk::Exception("Failed to create effect sprite batch");
1817 }
1818 effect_sprite->setDepthTestEnabled(true);
1819 effect_sprite->setDepthWriteEnabled(false);
1820 effect_sprite->setAlphaDiscardThreshold(0.05f);
1821 } else if (current_step == 3) {
1822 if (!consume_prepared_ship_model(model_vert, model_frag)) {
1823 return;
1824 }
1825 } else if (current_step == 4) {
1826 if (!ship_model.isLoaded()) {
1827 return;
1828 }
1829 for (auto &model : remote_ship_models) {
1830 model.load(this, asset_root + "/data/starship.obj", "", asset_root + "/data", 1.0f);
1831 model.setShaders(this, model_vert, model_frag);
1832 model.setBackfaceCulling(false);
1833 }
1834 } else if (current_step == 5) {
1835 create_flame_resources();
1836 } else if (current_step >= 6 && current_step < 6 + MAX_ASTEROIDS) {
1837 if (!consume_prepared_asteroid_model(static_cast<std::size_t>(current_step - 6), model_vert, model_frag)) {
1838 return;
1839 }
1840 } else if (current_step == 6 + MAX_ASTEROIDS) {
1841 star_field.init(GAME_STARS, 4.0f, 30.0f);
1842 } else if (current_step == 7 + MAX_ASTEROIDS) {
1843 restart_game();
1844 } else {
1845 game_resources_loaded.store(true, std::memory_order_release);
1846 intro_last_update_ms = SDL_GetTicks();
1847 loading_rain_opacity = 0.0f;
1848 loading_black_frame_pending = true;
1849 loading_black_frame_shown = false;
1850 if (intro_rain != nullptr) {
1851 intro_rain->set_opacity(0.0f);
1852 }
1853 return;
1854 }
1855
1856 loading_step_index.store(current_step + 1, std::memory_order_release);
1857 if (loading_step_index.load(std::memory_order_relaxed) >= loading_step_count) {
1858 game_resources_loaded.store(true, std::memory_order_release);
1859 intro_last_update_ms = SDL_GetTicks();
1860 loading_rain_opacity = 0.0f;
1861 loading_black_frame_pending = true;
1862 loading_black_frame_shown = false;
1863 if (intro_rain != nullptr) {
1864 intro_rain->set_opacity(0.0f);
1865 }
1866 return;
1867 }
1868 }
1869
1870 void load_asteroid_model_slot(std::size_t slot_index, const std::string &model_vert, const std::string &model_frag) {
1871 static constexpr std::array<const char *, 3> asteroid_paths = {
1872 "data/asteroid.obj",
1873 "data/asteroid2.obj",
1874 "data/asteroid3.obj",
1875 };
1876
1877 const std::size_t model_variant = slot_index % asteroid_paths.size();
1878 std::string texture_path;
1879 if (model_variant == 0) {
1880 texture_path = asset_root + "/data/rock.tex";
1881 } else if (model_variant == 1) {
1882 texture_path = asset_root + "/data/rock2.tex";
1883 } else {
1884 texture_path = (random_int(0, 1) == 0) ? asset_root + "/data/rock.tex" : asset_root + "/data/rock2.tex";
1885 }
1886
1887 asteroids[slot_index].model_index = static_cast<int>(model_variant);
1888 asteroid_models[slot_index].load(
1889 this,
1890 asset_root + "/" + asteroid_paths[model_variant],
1891 texture_path,
1892 asset_root + "/data",
1893 1.0f);
1894 asteroid_models[slot_index].setShaders(this, model_vert, model_frag);
1895 asteroid_models[slot_index].setBackfaceCulling(false);
1896 }
1897
1898 void restart_game() {
1899 clear_round_state();
1900 spawn_initial_asteroids();
1901 round_time_remaining = ROUND_TIME_LIMIT_SECONDS;
1902 restart_after_intro = false;
1903 log_game("Game state reset: score=0 lives=5.");
1904 }
1905
1906 void clear_round_state() {
1907 ship.position = glm::vec3(0.0f);
1908 ship.prev_position = ship.position;
1909 ship.velocity = glm::vec3(0.0f);
1910 ship.rotation = glm::vec3(0.0f);
1911 ship.current_speed = 1.0f;
1912 ship.visible = true;
1913 ship.exploding = false;
1914 ship.explosion_timer = 0;
1915 ship.lives = 5;
1916 ship.score = 0;
1917 ship.fire_cooldown = 0;
1918 ship.burst_count = 0;
1919 ship.continuous_fire_timer = 0;
1920 ship.overheated = false;
1921 ship.overheat_cooldown = 0;
1922 for (auto &projectile : projectiles) {
1923 projectile.active = false;
1924 }
1925 for (auto &particle : particles) {
1926 particle.active = false;
1927 }
1928 for (auto &asteroid : asteroids) {
1929 asteroid.active = false;
1930 }
1931 keyboard_yaw = 0.0f;
1932 keyboard_pitch = 0.0f;
1933 keyboard_roll = 0.0f;
1934 smooth_yaw = 0.0f;
1935 smooth_pitch = 0.0f;
1936 smooth_roll = 0.0f;
1937 ship_returning_to_field = false;
1938 return_message_cooldown = 0.0f;
1939 set_mouse_look_controls(false);
1940 first_person_camera = false;
1941 camera_transition_active = false;
1942 camera_transition_elapsed = 0.0f;
1943 camera_position = glm::vec3(0.0f, ship.camera_height, ship.camera_distance);
1944 camera_target_position = glm::vec3(0.0f, 0.0f, -6.0f);
1945 camera_up_vector = glm::vec3(0.0f, 1.0f, 0.0f);
1946 round_time_remaining = ROUND_TIME_LIMIT_SECONDS;
1947 }
1948
1949 void spawn_initial_asteroids() {
1950 for (int i = 0; i < 7; ++i) {
1951 glm::vec3 position{0.0f};
1952 do {
1953 position = glm::vec3(
1954 random_float(-90.0f, 90.0f),
1955 random_float(-50.0f, 50.0f),
1956 random_float(-90.0f, 90.0f));
1957 } while (glm::length(position - ship.position) < 28.0f);
1958
1959 spawn_asteroid(position,
1960 glm::vec3(random_float(-0.8f, 0.8f), random_float(-0.8f, 0.8f), random_float(-0.8f, 0.8f)),
1961 random_float(2.8f, 7.0f),
1962 0,
1963 random_int(0, 2));
1964 }
1965 log_game("Initial asteroid field spawned.");
1966 }
1967
1968 void spawn_asteroid(const glm::vec3 &position, const glm::vec3 &velocity, float radius, int generation, int preferred_model_index = -1) {
1969 Asteroid *free_asteroid = find_free_asteroid(preferred_model_index);
1970 if (free_asteroid == nullptr && preferred_model_index >= 0) {
1971 free_asteroid = find_free_asteroid();
1972 }
1973
1974 if (free_asteroid == nullptr) {
1975 log_game("Asteroid spawn skipped: no free asteroid slots.", SDL_Color{255, 190, 90, 255});
1976 return;
1977 }
1978
1979 const int slot_model_index = free_asteroid->model_index;
1980 free_asteroid->position = position;
1981 free_asteroid->velocity = velocity;
1982 free_asteroid->radius = radius;
1983 free_asteroid->generation = generation;
1984 free_asteroid->rotation = glm::vec3(random_float(0.0f, 360.0f), random_float(0.0f, 360.0f), random_float(0.0f, 360.0f));
1985 free_asteroid->rotation_speed = glm::vec3(random_float(-45.0f, 45.0f), random_float(-45.0f, 45.0f), random_float(-45.0f, 45.0f));
1986 free_asteroid->model_index = slot_model_index;
1987 free_asteroid->active = true;
1988 if (generation == 0) {
1989 log_game(std::format("Asteroid spawned at ({:.1f}, {:.1f}, {:.1f}) radius {:.1f}.", position.x, position.y, position.z, radius));
1990 }
1991 }
1992
1993 void handle_input(float dt) {
1994 const bool *keys = SDL_GetKeyboardState(nullptr);
1995 if (keys == nullptr) {
1996 return;
1997 }
1998
1999 if (ship.lives <= 0) {
2000 return;
2001 }
2002
2003 if (controller.getButton(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER) || controller.getButton(SDL_GAMEPAD_BUTTON_DPAD_UP)) {
2004 increase_speed(dt);
2005 } else if (controller.getButton(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER) || controller.getButton(SDL_GAMEPAD_BUTTON_DPAD_DOWN)) {
2006 decrease_speed(dt);
2007 }
2008
2009 auto ramp_axis = [dt](float &value, float target, float rise_rate, float fall_rate) {
2010 const float rate = (std::fabs(target) > std::fabs(value)) ? rise_rate : fall_rate;
2011 const float step = rate * dt;
2012 if (value < target) {
2013 value = std::min(value + step, target);
2014 } else if (value > target) {
2015 value = std::max(value - step, target);
2016 }
2017 };
2018
2019 float yaw_amount = 0.0f;
2020 float pitch_amount = 0.0f;
2021 float roll_amount = 0.0f;
2022 bool manual_roll_input = false;
2023
2024 const float left_x = controller_axis(SDL_GAMEPAD_AXIS_LEFTX);
2025 if (std::fabs(left_x) > 0.001f) {
2026 yaw_amount = -left_x;
2027 }
2028
2029 float keyboard_yaw_target = 0.0f;
2030 if (keys[SDL_SCANCODE_LEFT]) {
2031 keyboard_yaw_target = 1.0f;
2032 } else if (keys[SDL_SCANCODE_RIGHT]) {
2033 keyboard_yaw_target = -1.0f;
2034 }
2035 ramp_axis(keyboard_yaw, keyboard_yaw_target, 3.2f, 8.0f);
2036 if (std::fabs(keyboard_yaw) > 0.001f) {
2037 yaw_amount = keyboard_yaw;
2038 }
2039
2040 float keyboard_pitch_target = 0.0f;
2041 if (!mouse_look_controls) {
2042 if (inverted_controls) {
2043 if (keys[SDL_SCANCODE_W]) {
2044 keyboard_pitch_target = -1.0f;
2045 }
2046 if (keys[SDL_SCANCODE_S]) {
2047 keyboard_pitch_target = 1.0f;
2048 }
2049 } else {
2050 if (keys[SDL_SCANCODE_W]) {
2051 keyboard_pitch_target = 1.0f;
2052 }
2053 if (keys[SDL_SCANCODE_S]) {
2054 keyboard_pitch_target = -1.0f;
2055 }
2056 }
2057 }
2058 ramp_axis(keyboard_pitch, keyboard_pitch_target, 2.8f, 8.0f);
2059 if (std::fabs(keyboard_pitch) > 0.001f) {
2060 pitch_amount = keyboard_pitch;
2061 }
2062
2063 float keyboard_roll_target = 0.0f;
2064 if (keys[SDL_SCANCODE_A]) {
2065 keyboard_roll_target = -1.0f;
2066 } else if (keys[SDL_SCANCODE_D]) {
2067 keyboard_roll_target = 1.0f;
2068 }
2069 ramp_axis(keyboard_roll, keyboard_roll_target, 3.0f, 8.0f);
2070 if (std::fabs(keyboard_roll) > 0.001f) {
2071 roll_amount = keyboard_roll;
2072 manual_roll_input = true;
2073 }
2074
2075 if (controller.getButton(SDL_GAMEPAD_BUTTON_DPAD_LEFT)) {
2076 yaw_amount = 1.0f;
2077 } else if (controller.getButton(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)) {
2078 yaw_amount = -1.0f;
2079 }
2080
2081 const float right_x = controller_axis(SDL_GAMEPAD_AXIS_RIGHTX);
2082 if (std::fabs(right_x) > 0.001f) {
2083 roll_amount = right_x;
2084 manual_roll_input = true;
2085 }
2086
2087 const float right_y = controller_axis(SDL_GAMEPAD_AXIS_RIGHTY);
2088 if (std::fabs(right_y) > 0.001f) {
2089 pitch_amount = inverted_controls ? right_y : -right_y;
2090 }
2091
2092 const float smoothing = std::clamp(dt * 8.0f, 0.0f, 1.0f);
2093 smooth_yaw = glm::mix(smooth_yaw, yaw_amount, smoothing);
2094 smooth_pitch = glm::mix(smooth_pitch, pitch_amount, smoothing);
2095 smooth_roll = glm::mix(smooth_roll, roll_amount, smoothing);
2096
2097 if (std::fabs(smooth_yaw) > 0.01f) {
2098 ship.rotation.y += smooth_yaw * ship.turn_speed * dt;
2099 }
2100 if (std::fabs(smooth_pitch) > 0.01f) {
2101 ship.rotation.x += smooth_pitch * ship.turn_speed * ship.pitch_speed_multiplier * dt;
2102 }
2103 if (manual_roll_input && std::fabs(smooth_roll) > 0.01f) {
2104 ship.rotation.z += smooth_roll * ship.turn_speed * dt;
2105 }
2106 if (!manual_roll_input && std::fabs(smooth_yaw) > 0.01f) {
2107 const float target_roll = -smooth_yaw * 35.0f;
2108 const float roll_diff = target_roll - ship.rotation.z;
2109 ship.rotation.z += roll_diff * 5.0f * dt;
2110 }
2111 if (!manual_roll_input && std::fabs(smooth_yaw) < 0.01f) {
2112 while (ship.rotation.z > 180.0f) {
2113 ship.rotation.z -= 360.0f;
2114 }
2115 while (ship.rotation.z < -180.0f) {
2116 ship.rotation.z += 360.0f;
2117 }
2118 ship.rotation.z = glm::mix(ship.rotation.z, 0.0f, 3.0f * dt);
2119 }
2120
2121 const bool speed_up_key = mouse_look_controls ? keys[SDL_SCANCODE_W] : keys[SDL_SCANCODE_UP];
2122 const bool slow_down_key = mouse_look_controls ? keys[SDL_SCANCODE_S] : keys[SDL_SCANCODE_DOWN];
2123 if (speed_up_key) {
2124 increase_speed(dt);
2125 } else if (slow_down_key) {
2126 decrease_speed(dt);
2127 } else {
2128 if (ship.current_speed > 5.0f) {
2129 decrease_speed(dt * 0.5f);
2130 } else if (ship.current_speed < 5.0f) {
2131 increase_speed(dt * 0.5f);
2132 }
2133 }
2134
2135 const bool firing = keys[SDL_SCANCODE_SPACE] ||
2136 controller.getButton(SDL_GAMEPAD_BUTTON_SOUTH) ||
2137 controller.getAxis(SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > CONTROLLER_DEAD_ZONE;
2138 if (firing) {
2139 if (can_fire()) {
2140 fire_projectile();
2141 }
2142 } else {
2143 update_fire_timer(false);
2144 }
2145 }
2146
2147 void increase_speed(float dt) {
2148 ship.current_speed += ship.turn_speed * dt * 0.2f;
2149 ship.current_speed = std::min(ship.current_speed, ship.max_speed);
2150 }
2151
2152 void decrease_speed(float dt) {
2153 ship.current_speed -= ship.turn_speed * dt * 0.2f;
2154 ship.current_speed = std::max(ship.current_speed, ship.min_speed);
2155 }
2156
2157 bool can_fire() {
2158 if (ship.overheated) {
2159 return false;
2160 }
2161 if (ship.fire_cooldown <= 0) {
2162 if (ship.burst_count < SHOTS_PER_BURST) {
2163 ship.fire_cooldown = FIRE_DELAY;
2164 ship.burst_count++;
2165 return true;
2166 }
2167 ship.fire_cooldown = FIRE_COOLDOWN;
2168 ship.burst_count = 0;
2169 return false;
2170 }
2171 return false;
2172 }
2173
2174 void update_fire_timer(bool firing) {
2175 if (firing && !ship.overheated) {
2176 ship.continuous_fire_timer++;
2177 ship.overheat_cooldown = 0;
2178 if (ship.continuous_fire_timer >= 180) {
2179 ship.overheated = true;
2180 ship.overheat_cooldown = 0;
2181 ship.continuous_fire_timer = 0;
2182 ship.burst_count = 0;
2183 log_game("Weapons overheated.", SDL_Color{255, 150, 80, 255});
2184 }
2185 } else if (firing && ship.overheated) {
2186 ship.overheat_cooldown = 0;
2187 } else {
2188 if (ship.overheated) {
2189 ship.overheat_cooldown++;
2190 if (ship.overheat_cooldown >= 180) {
2191 ship.overheated = false;
2192 ship.overheat_cooldown = 0;
2193 ship.continuous_fire_timer = 0;
2194 log_game("Weapons cooled down.");
2195 }
2196 } else if (ship.continuous_fire_timer > 0) {
2197 ship.continuous_fire_timer--;
2198 }
2199 }
2200 }
2201
2202 static float normalize_degrees(float degrees) {
2203 while (degrees > 180.0f) {
2204 degrees -= 360.0f;
2205 }
2206 while (degrees < -180.0f) {
2207 degrees += 360.0f;
2208 }
2209 return degrees;
2210 }
2211
2212 static float ease_angle_degrees(float current, float target, float blend) {
2213 return current + normalize_degrees(target - current) * std::clamp(blend, 0.0f, 1.0f);
2214 }
2215
2216 glm::vec3 asteroid_field_center() const {
2217 glm::vec3 sum{0.0f};
2218 int count = 0;
2219 for (const auto &asteroid : asteroids) {
2220 if (!asteroid.active) {
2221 continue;
2222 }
2223 sum += asteroid.position;
2224 ++count;
2225 }
2226 if (count == 0) {
2227 return glm::vec3(0.0f);
2228 }
2229 return sum / static_cast<float>(count);
2230 }
2231
2232 bool ship_is_outside_return_volume() const {
2233 constexpr float RETURN_PADDING = 18.0f;
2234 return ship.position.x < BOUNDARY_X_MIN - RETURN_PADDING ||
2235 ship.position.x > BOUNDARY_X_MAX + RETURN_PADDING ||
2236 ship.position.y < BOUNDARY_Y_MIN - RETURN_PADDING ||
2237 ship.position.y > BOUNDARY_Y_MAX + RETURN_PADDING ||
2238 ship.position.z < BOUNDARY_Z_MIN - RETURN_PADDING ||
2239 ship.position.z > BOUNDARY_Z_MAX + RETURN_PADDING;
2240 }
2241
2242 void update_ship_return_to_field(float dt) {
2243 if (active_asteroids() == 0) {
2244 ship_returning_to_field = false;
2245 return;
2246 }
2247
2248 constexpr float RETURN_START_DISTANCE = 145.0f;
2249 constexpr float RETURN_STOP_DISTANCE = 92.0f;
2250 const float nearest_distance = nearest_asteroid_distance();
2251 const bool outside_return_volume = ship_is_outside_return_volume();
2252 if (!ship_returning_to_field && (outside_return_volume || nearest_distance > RETURN_START_DISTANCE)) {
2253 ship_returning_to_field = true;
2254 if (return_message_cooldown <= 0.0f) {
2255 log_game("Return assist engaged: steering back toward the asteroid field.", SDL_Color{120, 220, 255, 255});
2256 return_message_cooldown = 3.0f;
2257 }
2258 } else if (ship_returning_to_field && !outside_return_volume && nearest_distance < RETURN_STOP_DISTANCE) {
2259 ship_returning_to_field = false;
2260 log_game("Return assist disengaged.");
2261 }
2262
2263 if (!ship_returning_to_field) {
2264 return;
2265 }
2266
2267 const glm::vec3 to_field = normalize_or_zero(asteroid_field_center() - ship.position);
2268 const float target_yaw = glm::degrees(std::atan2(-to_field.x, -to_field.z));
2269 const float target_pitch = glm::degrees(std::asin(std::clamp(to_field.y, -1.0f, 1.0f)));
2270 const float blend = 1.0f - std::exp(-dt * 1.8f);
2271 ship.rotation.y = ease_angle_degrees(ship.rotation.y, target_yaw, blend);
2272 ship.rotation.x = ease_angle_degrees(ship.rotation.x, target_pitch, blend);
2273 ship.rotation.z = ease_angle_degrees(ship.rotation.z, 0.0f, blend * 0.8f);
2274 ship.current_speed = std::max(ship.current_speed, 8.0f);
2275 }
2276
2277 void fire_projectile() {
2278 const glm::vec3 forward = ship.forward();
2279 const float muzzle_offset = 0.08f;
2280 const glm::vec3 muzzle = ship.position + forward * muzzle_offset;
2281 for (std::size_t projectile_index = 0; projectile_index < projectiles.size(); ++projectile_index) {
2282 Projectile &projectile = projectiles[projectile_index];
2283 if (projectile.active) {
2284 continue;
2285 }
2286 projectile.position = muzzle;
2287 projectile.prev_position = muzzle;
2288 projectile.velocity = forward * PROJECTILE_SPEED;
2289 projectile.color = PROJECTILE_COLOR;
2290 projectile.lifetime = 0.0f;
2291 projectile.active = true;
2292 projectile_ids[projectile_index] = next_projectile_id++;
2293 if (next_projectile_id == 0U) {
2294 next_projectile_id = 1U;
2295 }
2296#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
2297 play_sound(cannon_sound);
2298#endif
2299 log_game(std::format("Projectile fired from ({:.1f}, {:.1f}, {:.1f}).", muzzle.x, muzzle.y, muzzle.z));
2300 return;
2301 }
2302 log_game("Projectile fire skipped: projectile pool full.", SDL_Color{255, 190, 90, 255});
2303 }
2304
2305 void update_ship(float dt) {
2306 if (ship.exploding) {
2307 ship.explosion_timer--;
2308 if (ship.explosion_timer <= 0) {
2309 ship.exploding = false;
2310 ship.visible = true;
2311 if (multiplayer_match) {
2312 const std::uint8_t player = std::min<std::uint8_t>(multiplayer.local_player_id(), 3U);
2313 ship.position = MULTIPLAYER_SPAWNS[player];
2314 ship.rotation = {0.0f, MULTIPLAYER_SPAWN_YAWS[player], 0.0f};
2315 } else {
2316 ship.position = glm::vec3(0.0f);
2317 ship.rotation = glm::vec3(0.0f);
2318 }
2319 ship.prev_position = ship.position;
2320 ship.velocity = glm::vec3(0.0f);
2321 ship.current_speed = 1.0f;
2322 ship_returning_to_field = false;
2323 clear_particles();
2324 log_game("Ship respawned at origin.");
2325 }
2326 return;
2327 }
2328
2329 if (dt <= 0.0f) {
2330 ship.prev_position = ship.position;
2331 ship.velocity = glm::vec3(0.0f);
2332 return;
2333 }
2334
2335 if (return_message_cooldown > 0.0f) {
2336 return_message_cooldown = std::max(0.0f, return_message_cooldown - dt);
2337 }
2338 update_ship_return_to_field(dt);
2339 const glm::vec3 forward = ship.forward();
2340 ship.prev_position = ship.position;
2341 ship.velocity = forward * ship.current_speed;
2342 ship.position += ship.velocity * dt;
2343 ship.rotation.x = std::clamp(ship.rotation.x, -75.0f, 75.0f);
2344 if (ship.rotation.z > 180.0f) {
2345 ship.rotation.z -= 360.0f;
2346 } else if (ship.rotation.z < -180.0f) {
2347 ship.rotation.z += 360.0f;
2348 }
2349 if (ship.fire_cooldown > 0) {
2350 ship.fire_cooldown--;
2351 }
2352 }
2353
2354 void update_projectiles(float dt) {
2355 for (auto &projectile : projectiles) {
2356 if (!projectile.active) {
2357 continue;
2358 }
2359 projectile.prev_position = projectile.position;
2360 projectile.position += projectile.velocity * dt;
2361 projectile.lifetime += dt;
2362 if (projectile.lifetime >= PROJECTILE_LIFETIME) {
2363 projectile.active = false;
2364 }
2365 }
2366
2367 if (multiplayer_match && !multiplayer.is_host()) {
2368 return;
2369 }
2370
2371 for (auto &asteroid : asteroids) {
2372 if (!asteroid.active) {
2373 continue;
2374 }
2375 for (auto &projectile : projectiles) {
2376 if (!projectile.active) {
2377 continue;
2378 }
2379
2380 const glm::vec3 segment = projectile.position - projectile.prev_position;
2381 const float segment_length_sq = glm::dot(segment, segment);
2382
2383 glm::vec3 closest_point = projectile.prev_position;
2384
2385 if (segment_length_sq > 1e-6f) {
2386 const glm::vec3 to_asteroid = asteroid.position - projectile.prev_position;
2387 const float t = std::clamp(glm::dot(to_asteroid, segment) / segment_length_sq, 0.0f, 1.0f);
2388 closest_point = projectile.prev_position + segment * t;
2389 }
2390
2391 const float dist = glm::length(closest_point - asteroid.position);
2392 const float projectile_hit_radius = asteroid.radius * ASTEROID_PROJECTILE_COLLISION_SCALE;
2393
2394 if (dist < projectile_hit_radius) {
2395 projectile.active = false;
2396 log_game(std::format("Projectile hit asteroid at ({:.1f}, {:.1f}, {:.1f}).", asteroid.position.x, asteroid.position.y, asteroid.position.z));
2397 split_asteroid(asteroid);
2398 break;
2399 }
2400 }
2401 }
2402 }
2403
2404 void split_asteroid(Asteroid &asteroid) {
2405 const glm::vec3 hit_position = asteroid.position;
2406 const int generation = asteroid.generation;
2407 const float radius = asteroid.radius * 0.5f;
2408
2409 spawn_asteroid_explosion(hit_position);
2410#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
2411 play_sound(asteroid_explosion_sound);
2412#endif
2413
2414 if (generation >= MAX_GENERATIONS) {
2415 ship.score += SMALL_ASTEROID_POINTS;
2416 asteroid.active = false;
2417 log_game(std::format("Small asteroid destroyed. Score={}.", ship.score));
2418 return;
2419 }
2420
2421 const int child_count = CHILDREN_PER_SPAWN;
2422 const float child_radius = asteroid.radius * 0.18f;
2423 const glm::vec3 view_forward = normalize_or_zero(hit_position - camera_position);
2424 glm::vec3 split_axis = glm::cross(view_forward, glm::vec3(0.0f, 1.0f, 0.0f));
2425 if (glm::length(split_axis) <= 1e-4f) {
2426 split_axis = glm::cross(view_forward, glm::vec3(1.0f, 0.0f, 0.0f));
2427 }
2428 if (glm::length(split_axis) <= 1e-4f) {
2429 split_axis = glm::vec3(1.0f, 0.0f, 0.0f);
2430 }
2431 split_axis = normalize_or_zero(split_axis);
2432 const glm::vec3 toward_camera = normalize_or_zero(camera_position - hit_position);
2433 const float child_separation = std::max(asteroid.radius * 0.45f, child_radius * 2.0f);
2434 const glm::vec3 depth_bias = toward_camera * (child_separation * 0.12f);
2435 std::array<glm::vec3, CHILDREN_PER_SPAWN> child_positions = {
2436 hit_position - split_axis * child_separation + depth_bias,
2437 hit_position + split_axis * child_separation + depth_bias,
2438 };
2439
2440 for (int i = 0; i < child_count; ++i) {
2441 Asteroid *child = find_free_asteroid(asteroid.model_index);
2442 if (child == nullptr) {
2443 log_game("Asteroid split skipped: no free slot for parent asteroid type.", SDL_Color{255, 190, 90, 255});
2444 break;
2445 }
2446
2447 const glm::vec3 child_offset = child_positions[static_cast<std::size_t>(i)] - hit_position;
2448 const glm::vec3 child_velocity = normalize_or_zero(child_offset) * random_float(5.0f, 9.0f);
2449 child->active = true;
2450 child->position = child_positions[static_cast<std::size_t>(i)];
2451 child->radius = child_radius;
2452 child->generation = generation + 1;
2453 child->rotation = glm::vec3(random_float(0.0f, 360.0f), random_float(0.0f, 360.0f), random_float(0.0f, 360.0f));
2454 child->rotation_speed = asteroid.rotation_speed * random_float(0.8f, 1.5f);
2455 child->model_index = asteroid.model_index;
2456 child->velocity = child_velocity;
2457 log_game(std::format(
2458 "Asteroid child {} spawned at ({:.1f}, {:.1f}, {:.1f}) radius {:.1f}.",
2459 i + 1,
2460 child->position.x,
2461 child->position.y,
2462 child->position.z,
2463 child->radius));
2464 }
2465
2466 if (radius >= 25.0f) {
2467 ship.score += LARGE_ASTEROID_POINTS;
2468 log_game(std::format("Large asteroid split into {} pieces. Score={}.", child_count, ship.score));
2469 } else {
2470 ship.score += MEDIUM_ASTEROID_POINTS;
2471 log_game(std::format("Medium asteroid split into {} pieces. Score={}.", child_count, ship.score));
2472 }
2473
2474 asteroid.active = false;
2475 }
2476
2477 Asteroid *find_free_asteroid(int preferred_model_index = -1) {
2478 for (auto &asteroid : asteroids) {
2479 if (!asteroid.active && (preferred_model_index < 0 || asteroid.model_index == preferred_model_index)) {
2480 return &asteroid;
2481 }
2482 }
2483 return nullptr;
2484 }
2485
2486 void update_asteroids(float dt) {
2487 for (auto &asteroid : asteroids) {
2488 if (!asteroid.active) {
2489 continue;
2490 }
2491 asteroid.position += asteroid.velocity * dt;
2492 asteroid.rotation += asteroid.rotation_speed * dt;
2493 bool bounced = false;
2494 if (asteroid.position.x < BOUNDARY_X_MIN) {
2495 asteroid.position.x = BOUNDARY_X_MIN;
2496 asteroid.velocity.x = -asteroid.velocity.x * BOUNDARY_BOUNCE_FACTOR;
2497 bounced = true;
2498 } else if (asteroid.position.x > BOUNDARY_X_MAX) {
2499 asteroid.position.x = BOUNDARY_X_MAX;
2500 asteroid.velocity.x = -asteroid.velocity.x * BOUNDARY_BOUNCE_FACTOR;
2501 bounced = true;
2502 }
2503 if (asteroid.position.y < BOUNDARY_Y_MIN) {
2504 asteroid.position.y = BOUNDARY_Y_MIN;
2505 asteroid.velocity.y = -asteroid.velocity.y * BOUNDARY_BOUNCE_FACTOR;
2506 bounced = true;
2507 } else if (asteroid.position.y > BOUNDARY_Y_MAX) {
2508 asteroid.position.y = BOUNDARY_Y_MAX;
2509 asteroid.velocity.y = -asteroid.velocity.y * BOUNDARY_BOUNCE_FACTOR;
2510 bounced = true;
2511 }
2512 if (asteroid.position.z < BOUNDARY_Z_MIN) {
2513 asteroid.position.z = BOUNDARY_Z_MIN;
2514 asteroid.velocity.z = -asteroid.velocity.z * BOUNDARY_BOUNCE_FACTOR;
2515 bounced = true;
2516 } else if (asteroid.position.z > BOUNDARY_Z_MAX) {
2517 asteroid.position.z = BOUNDARY_Z_MAX;
2518 asteroid.velocity.z = -asteroid.velocity.z * BOUNDARY_BOUNCE_FACTOR;
2519 bounced = true;
2520 }
2521 if (bounced) {
2522 asteroid.velocity += glm::vec3(random_float(-0.5f, 0.5f), random_float(-0.5f, 0.5f), random_float(-0.5f, 0.5f));
2523 }
2524 if (glm::length(asteroid.velocity) > 0.01f) {
2525 asteroid.velocity *= 0.995f;
2526 }
2527 if (asteroid.rotation.x > 360.0f)
2528 asteroid.rotation.x -= 360.0f;
2529 if (asteroid.rotation.y > 360.0f)
2530 asteroid.rotation.y -= 360.0f;
2531 if (asteroid.rotation.z > 360.0f)
2532 asteroid.rotation.z -= 360.0f;
2533 }
2534
2535 for (auto &asteroid : asteroids) {
2536 if (!asteroid.active) {
2537 continue;
2538 }
2539 const float ship_distance = ship_asteroid_collision_distance(asteroid);
2540 if (ship_distance <= 0.0f) {
2541 log_game(std::format("Ship collision with asteroid overlap {:.2f}.", -ship_distance), SDL_Color{255, 130, 90, 255});
2542 start_ship_explosion();
2543 break;
2544 }
2545 }
2546 }
2547
2548 float ship_asteroid_collision_distance(const Asteroid &asteroid) const {
2549 static constexpr std::array<ShipCollisionSample, 5> ship_samples = {
2550 ShipCollisionSample{{0.0f, 0.0f, -0.55f}, 0.055f},
2551 ShipCollisionSample{{0.0f, 0.06f, -0.16f}, 0.160f},
2552 ShipCollisionSample{{0.0f, 0.08f, 0.34f}, 0.125f},
2553 ShipCollisionSample{{-0.42f, 0.03f, -0.02f}, 0.085f},
2554 ShipCollisionSample{{0.42f, 0.03f, -0.02f}, 0.085f},
2555 };
2556
2557 const float asteroid_collision_radius = asteroid.radius * ASTEROID_SHIP_COLLISION_SCALE;
2558 float nearest_surface_distance = std::numeric_limits<float>::max();
2559
2560 for (const ShipCollisionSample &sample : ship_samples) {
2561 const float ship_scale = rendered_ship_scale();
2562 const glm::vec3 offset = transform_ship_collision_offset(sample.local_position);
2563 const glm::vec3 previous_position = ship.prev_position + offset;
2564 const glm::vec3 current_position = ship.position + offset;
2565 const float center_distance = swept_point_distance_to_asteroid(previous_position, current_position, asteroid.position);
2566 nearest_surface_distance = std::min(nearest_surface_distance, center_distance - asteroid_collision_radius - (sample.radius * ship_scale));
2567 }
2568
2569 return nearest_surface_distance;
2570 }
2571
2572 glm::vec3 transform_ship_collision_offset(const glm::vec3 &local_position) const {
2573 const glm::mat4 model = build_model_matrix(
2574 glm::vec3(0.0f),
2575 ship.rotation,
2576 rendered_ship_scale(),
2577 ship_model.modelCenterOffset());
2578 return glm::vec3(model * glm::vec4(local_position, 1.0f));
2579 }
2580
2581 float rendered_ship_scale() const {
2582 return SHIP_MODEL_SCALE * ship_model.modelRenderScale();
2583 }
2584
2585 float swept_point_distance_to_asteroid(const glm::vec3 &previous_position,
2586 const glm::vec3 &current_position,
2587 const glm::vec3 &asteroid_position) const {
2588 const glm::vec3 segment = current_position - previous_position;
2589 const float segment_length_sq = glm::dot(segment, segment);
2590 glm::vec3 closest_point = current_position;
2591
2592 if (segment_length_sq > 1e-6f) {
2593 const glm::vec3 to_asteroid = asteroid_position - previous_position;
2594 const float t = std::clamp(glm::dot(to_asteroid, segment) / segment_length_sq, 0.0f, 1.0f);
2595 closest_point = previous_position + segment * t;
2596 }
2597
2598 return glm::length(closest_point - asteroid_position);
2599 }
2600
2601 void start_ship_explosion() {
2602 if (ship.exploding) {
2603 return;
2604 }
2605 ship.exploding = true;
2606 ship.visible = false;
2607 ship.explosion_timer = EXPLOSION_DURATION_FRAMES;
2608 if (multiplayer_match && multiplayer.is_host()) {
2609 ++multiplayer_death_serials[0];
2610 }
2611 ship.lives--;
2612 ship.overheated = false;
2613 ship.overheat_cooldown = 0;
2614 ship.continuous_fire_timer = 0;
2615 ship.burst_count = 0;
2616#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
2617 play_sound(crash_sound);
2618#endif
2619 log_game(std::format("Ship destroyed. Lives remaining: {}.", std::max(0, ship.lives)), SDL_Color{255, 120, 80, 255});
2620 if (ship.lives <= 0) {
2621 log_game(std::format("Game over. Final score: {}.", ship.score), SDL_Color{255, 90, 90, 255});
2622 }
2623 spawn_ship_explosion(ship.position);
2624 }
2625
2626 void spawn_asteroid_explosion(const glm::vec3 &position) {
2627 spawn_gl_explosion(position);
2628 }
2629
2630 void spawn_ship_explosion(const glm::vec3 &position) {
2631 spawn_gl_explosion(position);
2632 }
2633
2634 void spawn_gl_explosion(const glm::vec3 &position) {
2635 struct ExplosionWave {
2636 float min_speed;
2637 float max_speed;
2638 float min_size;
2639 float max_size;
2640 float min_lifetime;
2641 float max_lifetime;
2642 glm::vec3 color;
2643 };
2644
2645 constexpr int WAVE_COUNT = 4;
2646 constexpr int MAX_GL_EXPLOSIONS = 5;
2647 constexpr std::array<ExplosionWave, WAVE_COUNT> waves = {
2648 ExplosionWave{40.0f, 60.0f, 0.72f, 1.14f, 1.5f, 2.5f, {1.0f, 1.0f, 1.0f}},
2649 ExplosionWave{30.0f, 45.0f, 0.58f, 0.86f, 2.0f, 3.0f, {1.0f, 1.0f, 1.0f}},
2650 ExplosionWave{20.0f, 35.0f, 0.43f, 0.72f, 2.5f, 3.5f, {1.0f, 1.0f, 1.0f}},
2651 ExplosionWave{10.0f, 25.0f, 0.14f, 0.43f, 3.0f, 4.0f, {1.0f, 1.0f, 1.0f}},
2652 };
2653
2654 const int particles_per_wave = MAX_PARTICLES / (WAVE_COUNT * MAX_GL_EXPLOSIONS);
2655 int spawned = 0;
2656 for (int wave_index = 0; wave_index < WAVE_COUNT; ++wave_index) {
2657 const ExplosionWave &wave = waves[static_cast<std::size_t>(wave_index)];
2658 for (int i = 0; i < particles_per_wave; ++i) {
2659 Particle *particle = find_free_particle();
2660 if (particle == nullptr) {
2661 return;
2662 }
2663
2664 const float theta = random_float(0.0f, 2.0f * PI);
2665 const float phi = random_float(0.0f, PI);
2666 const glm::vec3 dir{
2667 std::sin(phi) * std::cos(theta),
2668 std::sin(phi) * std::sin(theta),
2669 std::cos(phi),
2670 };
2671
2672 const float offset = 0.8f + 0.2f * static_cast<float>(wave_index) / static_cast<float>(WAVE_COUNT);
2673 const float speed = random_float(wave.min_speed, wave.max_speed);
2674 particle->position = position + dir * offset;
2675 particle->velocity = dir * speed + glm::vec3(
2676 random_float(-5.0f, 5.0f),
2677 random_float(-5.0f, 5.0f),
2678 random_float(-5.0f, 5.0f));
2679 particle->color = glm::vec4(
2680 wave.color.r * random_float(0.9f, 1.1f),
2681 wave.color.g * random_float(0.9f, 1.1f),
2682 wave.color.b * random_float(0.9f, 1.1f),
2683 0.1f);
2684 particle->size = random_float(wave.min_size, wave.max_size);
2685 particle->lifetime = 0.0f;
2686 particle->max_lifetime = random_float(wave.min_lifetime, wave.max_lifetime);
2687 particle->active = true;
2688 ++spawned;
2689 }
2690 }
2691 log_game(std::format("Explosion spawned {} particles.", spawned));
2692 }
2693
2694 void spawn_particles(const glm::vec3 &position,
2695 const glm::vec4 &color,
2696 int count,
2697 float min_speed,
2698 float max_speed,
2699 float min_size,
2700 float max_size,
2701 float min_lifetime,
2702 float max_lifetime) {
2703 for (int i = 0; i < count; ++i) {
2704 Particle *particle = find_free_particle();
2705 if (particle == nullptr) {
2706 return;
2707 }
2708 const glm::vec3 dir = normalize_or_zero(glm::vec3(
2709 random_float(-1.0f, 1.0f),
2710 random_float(-1.0f, 1.0f),
2711 random_float(-1.0f, 1.0f)));
2712 particle->position = position;
2713 particle->velocity = dir * random_float(min_speed, max_speed);
2714 particle->color = color;
2715 particle->size = random_float(min_size, max_size);
2716 particle->lifetime = 0.0f;
2717 particle->max_lifetime = random_float(min_lifetime, max_lifetime);
2718 particle->active = true;
2719 }
2720 }
2721
2722 Particle *find_free_particle() {
2723 for (auto &particle : particles) {
2724 if (!particle.active) {
2725 return &particle;
2726 }
2727 }
2728 return nullptr;
2729 }
2730
2731 void update_particles(float dt) {
2732 for (auto &particle : particles) {
2733 if (!particle.active) {
2734 continue;
2735 }
2736 particle.position += particle.velocity * dt;
2737 particle.velocity *= 0.98f;
2738 particle.velocity.y -= 0.5f * dt;
2739 particle.lifetime += dt;
2740
2741 const float life_ratio = particle.lifetime / particle.max_lifetime;
2742 if (life_ratio >= 1.0f) {
2743 particle.active = false;
2744 continue;
2745 }
2746 if (life_ratio < 0.2f) {
2747 particle.color.a = life_ratio / 0.2f;
2748 } else if (life_ratio > 0.8f) {
2749 particle.color.a = (1.0f - life_ratio) / 0.2f;
2750 } else {
2751 particle.color.a = 1.0f;
2752 }
2753 if (life_ratio < 0.3f) {
2754 particle.size *= 1.01f;
2755 } else {
2756 particle.size *= 0.99f;
2757 }
2758 if (particle.color.a < 0.01f) {
2759 particle.active = false;
2760 }
2761 }
2762 }
2763
2764 void clear_particles() {
2765 for (auto &particle : particles) {
2766 particle.active = false;
2767 }
2768 }
2769
2770 void prepare_restart_from_game_over() {
2771 clear_round_state();
2772 restart_after_intro = true;
2773 reset_intro_screen();
2774 }
2775
2776 void reset_intro_screen() {
2777 mode = GameMode::Intro;
2778 intro_fade = 1.0f;
2779 intro_last_update_ms = SDL_GetTicks();
2780 loading_rain_opacity = 1.0f;
2781 if (intro_rain != nullptr) {
2782 intro_rain->set_opacity(1.0f);
2783 intro_rain->reset();
2784 }
2785 }
2786
2787 void update_round_timer(float dt) {
2788 if (mode != GameMode::Playing) {
2789 return;
2790 }
2791
2792 if (round_time_remaining <= 0.0f) {
2793 mode = GameMode::GameOver;
2794 return;
2795 }
2796
2797 round_time_remaining = std::max(0.0f, round_time_remaining - dt);
2798 if (round_time_remaining <= 0.0f) {
2799 mode = GameMode::GameOver;
2800 ship.exploding = false;
2801 ship.visible = false;
2802 ship.fire_cooldown = 0;
2803 ship.burst_count = 0;
2804 ship.continuous_fire_timer = 0;
2805 ship.overheated = false;
2806 ship.overheat_cooldown = 0;
2807 log_game("Time expired. Game over.", SDL_Color{255, 90, 90, 255});
2808 }
2809 }
2810
2811 void set_ui_font_size(int font_size) {
2812 if (font_size == last_font_size) {
2813 return;
2814 }
2815
2816 last_font_size = font_size;
2817 setFont(asset_root + "/data/font.ttf", font_size);
2819 }
2820
2821 std::string format_round_time() const {
2822 const int total_seconds = std::max(0, static_cast<int>(std::ceil(round_time_remaining)));
2823 const int minutes = total_seconds / 60;
2824 const int seconds = total_seconds % 60;
2825 return std::format("{:02d}:{:02d}", minutes, seconds);
2826 }
2827
2828 glm::mat4 ship_rotation_matrix() const {
2829 glm::mat4 rotation(1.0f);
2830 rotation = glm::rotate(rotation, glm::radians(ship.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
2831 rotation = glm::rotate(rotation, glm::radians(ship.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
2832 rotation = glm::rotate(rotation, glm::radians(ship.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
2833 return rotation;
2834 }
2835
2836 struct CameraPose {
2837 glm::vec3 position{0.0f};
2838 glm::vec3 target{0.0f};
2839 glm::vec3 up{0.0f, 1.0f, 0.0f};
2840 };
2841
2842 CameraPose chase_camera_pose(const glm::vec3 &ship_forward) const {
2843 CameraPose pose{};
2844 pose.position = ship.position - ship_forward * ship.camera_distance + glm::vec3(0.0f, ship.camera_height, 0.0f);
2845 pose.target = ship.position + ship_forward * 6.0f;
2846 pose.up = glm::vec3(0.0f, 1.0f, 0.0f);
2847 return pose;
2848 }
2849
2850 CameraPose first_person_camera_pose(const glm::mat4 &ship_rotation_matrix, const glm::vec3 &ship_forward) const {
2851 constexpr glm::vec3 FIRST_PERSON_CAMERA_OFFSET{0.0f, 0.16f, -0.30f};
2852
2853 CameraPose pose{};
2854 const glm::vec3 cockpit_offset = glm::vec3(ship_rotation_matrix * glm::vec4(FIRST_PERSON_CAMERA_OFFSET * rendered_ship_scale(), 0.0f));
2855 pose.position = ship.position + cockpit_offset;
2856 pose.target = pose.position + ship_forward * 8.0f;
2857 pose.up = normalize_or_zero(glm::vec3(ship_rotation_matrix * glm::vec4(0.0f, 1.0f, 0.0f, 0.0f)));
2858 return pose;
2859 }
2860
2861 static float smooth_camera_transition(float value) {
2862 value = std::clamp(value, 0.0f, 1.0f);
2863 return value * value * (3.0f - 2.0f * value);
2864 }
2865
2866 void begin_camera_transition(bool target_first_person_camera) {
2867 first_person_camera = target_first_person_camera;
2868 camera_transition_active = true;
2869 camera_transition_elapsed = 0.0f;
2870 camera_transition_start_position = camera_position;
2871 camera_transition_start_target = camera_target_position;
2872 camera_transition_start_up = camera_up_vector;
2873 }
2874
2875 void update_camera(float dt) {
2876 const glm::mat4 ship_rotation_matrix = this->ship_rotation_matrix();
2877 const glm::vec3 ship_forward = normalize_or_zero(glm::vec3(ship_rotation_matrix * glm::vec4(0.0f, 0.0f, -1.0f, 0.0f)));
2878 const CameraPose target_pose = first_person_camera ? first_person_camera_pose(ship_rotation_matrix, ship_forward) : chase_camera_pose(ship_forward);
2879
2880 if (camera_transition_active) {
2881 camera_transition_elapsed += dt;
2882 const float blend = smooth_camera_transition(camera_transition_elapsed / CAMERA_TRANSITION_SECONDS);
2883 camera_position = glm::mix(camera_transition_start_position, target_pose.position, blend);
2884 camera_target_position = glm::mix(camera_transition_start_target, target_pose.target, blend);
2885 camera_up_vector = normalize_or_zero(glm::mix(camera_transition_start_up, target_pose.up, blend));
2886 if (camera_transition_elapsed >= CAMERA_TRANSITION_SECONDS) {
2887 camera_transition_active = false;
2888 camera_position = target_pose.position;
2889 camera_target_position = target_pose.target;
2890 camera_up_vector = target_pose.up;
2891 }
2892 } else if (first_person_camera) {
2893 camera_position = target_pose.position;
2894 camera_target_position = target_pose.target;
2895 camera_up_vector = target_pose.up;
2896 } else {
2897 camera_position = glm::mix(camera_position, target_pose.position, 1.0f - std::exp(-dt * 10.0f));
2898 camera_target_position = target_pose.target;
2899 camera_up_vector = target_pose.up;
2900 }
2901
2902 view_matrix = glm::lookAt(camera_position, camera_target_position, camera_up_vector);
2903 }
2904
2905 void draw_ship(uint32_t image_index) {
2906 if (!ship.visible) {
2907 return;
2908 }
2909
2910 mxvk::UniformBufferObject ubo{};
2911 ubo.model = build_model_matrix(ship.position, ship.rotation, rendered_ship_scale(), ship_model.modelCenterOffset());
2912 last_ship_model_matrix = ubo.model;
2913 ubo.view = view_matrix;
2914 ubo.proj = projection_matrix;
2915 ubo.fx = glm::vec4(camera_position, elapsed_seconds);
2916 ship_model.updateUBO(image_index, ubo);
2917 ship_model.render(current_command_buffer, image_index, false);
2918 }
2919
2920 void draw_remote_ship(uint32_t image_index, std::uint8_t player) {
2921 Ship &remote_ship = remote_ships[player];
2922 mxvk::VKAbstractModel &remote_ship_model = remote_ship_models[player];
2923 if (!remote_ship.visible || remote_ship_exploding[player] || !remote_ship_model.isLoaded()) {
2924 return;
2925 }
2926 mxvk::UniformBufferObject ubo{};
2927 ubo.model = build_model_matrix(remote_ship.position, remote_ship.rotation, rendered_ship_scale(), remote_ship_model.modelCenterOffset());
2928 last_remote_ship_model_matrices[player] = ubo.model;
2929 ubo.view = view_matrix;
2930 ubo.proj = projection_matrix;
2931 ubo.fx = glm::vec4(camera_position, elapsed_seconds);
2932 remote_ship_model.updateUBO(image_index, ubo);
2933 remote_ship_model.render(current_command_buffer, image_index, false);
2934 }
2935
2936 void draw_asteroids(uint32_t image_index) {
2937 for (std::size_t i = 0; i < asteroids.size(); ++i) {
2938 const Asteroid &asteroid = asteroids[i];
2939 if (!asteroid.active) {
2940 continue;
2941 }
2942 mxvk::UniformBufferObject ubo{};
2943 const float scale = asteroid.radius;
2944 mxvk::VKAbstractModel &asteroid_model = asteroid_models[i];
2945 ubo.model = build_model_matrix(asteroid.position, asteroid.rotation, scale * asteroid_model.modelRenderScale(),
2946 asteroid_model.modelCenterOffset());
2947 ubo.view = view_matrix;
2948 ubo.proj = projection_matrix;
2949 ubo.fx = glm::vec4(camera_position, elapsed_seconds);
2950 asteroid_model.updateUBO(image_index, ubo);
2951 asteroid_model.render(current_command_buffer, image_index, false);
2952 }
2953 }
2954
2955 void draw_projectiles() {
2956 for (const auto &projectile : projectiles) {
2957 if (!projectile.active) {
2958 continue;
2959 }
2960 const float life_factor = 1.0f - (projectile.lifetime / PROJECTILE_LIFETIME);
2961 const float pulse = (0.55f + 0.22f * (1.0f - life_factor)) * (0.9f + 0.1f * std::sin(elapsed_seconds * 12.0f));
2962 const glm::vec4 color = glm::vec4(
2963 std::clamp(projectile.color.r * (0.9f + 0.1f * life_factor), 0.0f, 1.0f),
2964 std::clamp(projectile.color.g * (0.9f + 0.1f * life_factor), 0.0f, 1.0f),
2965 std::clamp(projectile.color.b * (0.9f + 0.1f * life_factor), 0.0f, 1.0f),
2966 std::clamp(projectile.color.a * (0.65f + 0.35f * life_factor), 0.0f, 1.0f));
2967 projectile_sprite->drawSprite(projectile.position, glm::vec2(pulse), color);
2968 }
2969 }
2970
2971 void draw_remote_projectiles() {
2972 for (std::uint8_t player = 0; player < NETWORK_PLAYER_COUNT; ++player) {
2973 if (player == multiplayer.local_player_id())
2974 continue;
2975 for (const NetworkProjectile &projectile : remote_projectiles[player]) {
2976 if (projectile.active == 0U)
2977 continue;
2978 const glm::vec3 position{projectile.position[0], projectile.position[1], projectile.position[2]};
2979 projectile_sprite->drawSprite(position, glm::vec2(0.62f), {0.20f, 0.65f, 1.0f, 1.0f});
2980 }
2981 }
2982 }
2983
2984 void draw_particles() {
2985 for (const auto &particle : particles) {
2986 if (!particle.active) {
2987 continue;
2988 }
2989 effect_sprite->drawSprite(particle.position,
2990 glm::vec2(particle.size),
2991 particle.color);
2992 }
2993 }
2994
2995 void create_flame_resources() {
2996 create_flame_mesh();
2997 create_flame_swapchain_resources();
2998 }
2999
3000 void cleanup_flame_resources() {
3001 cleanup_flame_swapchain_resources();
3002 if (flame_vertex_buffer != VK_NULL_HANDLE) {
3003 vkDestroyBuffer(device, flame_vertex_buffer, nullptr);
3004 flame_vertex_buffer = VK_NULL_HANDLE;
3005 }
3006 if (flame_vertex_buffer_memory != VK_NULL_HANDLE) {
3007 vkFreeMemory(device, flame_vertex_buffer_memory, nullptr);
3008 flame_vertex_buffer_memory = VK_NULL_HANDLE;
3009 }
3010 flame_vertex_count = 0;
3011 }
3012
3013 void cleanup_flame_swapchain_resources() {
3014 if (flame_pipeline != VK_NULL_HANDLE) {
3015 vkDestroyPipeline(device, flame_pipeline, nullptr);
3016 flame_pipeline = VK_NULL_HANDLE;
3017 }
3018 if (flame_pipeline_layout != VK_NULL_HANDLE) {
3019 vkDestroyPipelineLayout(device, flame_pipeline_layout, nullptr);
3020 flame_pipeline_layout = VK_NULL_HANDLE;
3021 }
3022 }
3023
3024 void create_flame_swapchain_resources() {
3025 if (flame_vertex_count == 0 || device == VK_NULL_HANDLE) {
3026 return;
3027 }
3028 create_flame_pipeline();
3029 }
3030
3031 void create_flame_mesh() {
3032 constexpr int segments = 40;
3033 constexpr float base_z = 0.555f;
3034 constexpr float tip_z = 1.02f;
3035 constexpr float base_y = 0.040f;
3036 constexpr float outer_radius = 0.052f;
3037 constexpr float inner_radius = 0.026f;
3038
3039 std::vector<FlameVertex> vertices{};
3040 vertices.reserve(static_cast<std::size_t>(segments) * 6U);
3041
3042 const glm::vec4 outer_base_color{1.0f, 0.42f, 0.08f, 0.50f};
3043 const glm::vec4 outer_tip_color{0.7f, 0.08f, 0.0f, 0.0f};
3044 const glm::vec4 inner_base_color{1.0f, 0.92f, 0.45f, 0.72f};
3045 const glm::vec4 inner_tip_color{1.0f, 0.32f, 0.04f, 0.0f};
3046
3047 auto add_cone = [&](float radius, const glm::vec4 &base_color, const glm::vec4 &tip_color) {
3048 const glm::vec3 tip{0.0f, base_y, tip_z};
3049 for (int i = 0; i < segments; ++i) {
3050 const float a0 = (static_cast<float>(i) / static_cast<float>(segments)) * 2.0f * PI;
3051 const float a1 = (static_cast<float>(i + 1) / static_cast<float>(segments)) * 2.0f * PI;
3052 const glm::vec3 p0{std::cos(a0) * radius, base_y + std::sin(a0) * radius, base_z};
3053 const glm::vec3 p1{std::cos(a1) * radius, base_y + std::sin(a1) * radius, base_z};
3054 vertices.push_back({p0, base_color});
3055 vertices.push_back({p1, base_color});
3056 vertices.push_back({tip, tip_color});
3057 }
3058 };
3059
3060 add_cone(outer_radius, outer_base_color, outer_tip_color);
3061 add_cone(inner_radius, inner_base_color, inner_tip_color);
3062
3063 flame_vertex_count = static_cast<uint32_t>(vertices.size());
3064 const VkDeviceSize buffer_size = sizeof(FlameVertex) * static_cast<VkDeviceSize>(vertices.size());
3065 create_buffer(buffer_size,
3066 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
3067 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
3068 flame_vertex_buffer,
3069 flame_vertex_buffer_memory);
3070
3071 void *data = nullptr;
3072 if (vkMapMemory(device, flame_vertex_buffer_memory, 0, buffer_size, 0, &data) != VK_SUCCESS || data == nullptr) {
3073 throw mxvk::Exception("Failed to map asteroids3d flame vertex buffer");
3074 }
3075 std::memcpy(data, vertices.data(), static_cast<std::size_t>(buffer_size));
3076 vkUnmapMemory(device, flame_vertex_buffer_memory);
3077 }
3078
3079 void create_flame_pipeline() {
3080 cleanup_flame_swapchain_resources();
3081
3082 const std::vector<char> vert_shader_code = loadSpv(shader_root + "/flame.vert.spv");
3083 const std::vector<char> frag_shader_code = loadSpv(shader_root + "/flame.frag.spv");
3084
3085 VkShaderModule vert_shader_module = createShaderModule(device, vert_shader_code);
3086 VkShaderModule frag_shader_module = VK_NULL_HANDLE;
3087
3088 try {
3089 frag_shader_module = createShaderModule(device, frag_shader_code);
3090
3091 VkPipelineShaderStageCreateInfo vert_stage{};
3092 vert_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
3093 vert_stage.stage = VK_SHADER_STAGE_VERTEX_BIT;
3094 vert_stage.module = vert_shader_module;
3095 vert_stage.pName = "main";
3096
3097 VkPipelineShaderStageCreateInfo frag_stage{};
3098 frag_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
3099 frag_stage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
3100 frag_stage.module = frag_shader_module;
3101 frag_stage.pName = "main";
3102
3103 std::array<VkPipelineShaderStageCreateInfo, 2> shader_stages = {vert_stage, frag_stage};
3104
3105 VkVertexInputBindingDescription binding_description{};
3106 binding_description.binding = 0;
3107 binding_description.stride = sizeof(FlameVertex);
3108 binding_description.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
3109
3110 std::array<VkVertexInputAttributeDescription, 2> attributes{};
3111 attributes[0].binding = 0;
3112 attributes[0].location = 0;
3113 attributes[0].format = VK_FORMAT_R32G32B32_SFLOAT;
3114 attributes[0].offset = offsetof(FlameVertex, pos);
3115 attributes[1].binding = 0;
3116 attributes[1].location = 1;
3117 attributes[1].format = VK_FORMAT_R32G32B32A32_SFLOAT;
3118 attributes[1].offset = offsetof(FlameVertex, color);
3119
3120 VkPipelineVertexInputStateCreateInfo vertex_input{};
3121 vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
3122 vertex_input.vertexBindingDescriptionCount = 1;
3123 vertex_input.pVertexBindingDescriptions = &binding_description;
3124 vertex_input.vertexAttributeDescriptionCount = static_cast<uint32_t>(attributes.size());
3125 vertex_input.pVertexAttributeDescriptions = attributes.data();
3126
3127 VkPipelineInputAssemblyStateCreateInfo input_assembly{};
3128 input_assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
3129 input_assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
3130 input_assembly.primitiveRestartEnable = VK_FALSE;
3131
3132 VkPipelineViewportStateCreateInfo viewport_state{};
3133 viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
3134 viewport_state.viewportCount = 1;
3135 viewport_state.scissorCount = 1;
3136
3137 const std::array<VkDynamicState, 2> dynamic_states = {
3138 VK_DYNAMIC_STATE_VIEWPORT,
3139 VK_DYNAMIC_STATE_SCISSOR,
3140 };
3141 VkPipelineDynamicStateCreateInfo dynamic_info{};
3142 dynamic_info.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
3143 dynamic_info.dynamicStateCount = static_cast<uint32_t>(dynamic_states.size());
3144 dynamic_info.pDynamicStates = dynamic_states.data();
3145
3146 VkPipelineRasterizationStateCreateInfo rasterizer{};
3147 rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
3148 rasterizer.depthClampEnable = VK_FALSE;
3149 rasterizer.rasterizerDiscardEnable = VK_FALSE;
3150 rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
3151 rasterizer.lineWidth = 1.0f;
3152 rasterizer.cullMode = VK_CULL_MODE_NONE;
3153 rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
3154 rasterizer.depthBiasEnable = VK_FALSE;
3155
3156 VkPipelineMultisampleStateCreateInfo multisampling{};
3157 multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
3158 multisampling.sampleShadingEnable = VK_FALSE;
3159 multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
3160
3161 VkPipelineDepthStencilStateCreateInfo depth_stencil{};
3162 depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
3163 depth_stencil.depthTestEnable = VK_TRUE;
3164 depth_stencil.depthWriteEnable = VK_FALSE;
3165 depth_stencil.depthCompareOp = VK_COMPARE_OP_LESS;
3166 depth_stencil.depthBoundsTestEnable = VK_FALSE;
3167 depth_stencil.stencilTestEnable = VK_FALSE;
3168
3169 VkPipelineColorBlendAttachmentState color_blend_attachment{};
3170 color_blend_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
3171 VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
3172 color_blend_attachment.blendEnable = VK_TRUE;
3173 color_blend_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
3174 color_blend_attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE;
3175 color_blend_attachment.colorBlendOp = VK_BLEND_OP_ADD;
3176 color_blend_attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
3177 color_blend_attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
3178 color_blend_attachment.alphaBlendOp = VK_BLEND_OP_ADD;
3179
3180 VkPipelineColorBlendStateCreateInfo color_blending{};
3181 color_blending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
3182 color_blending.logicOpEnable = VK_FALSE;
3183 color_blending.attachmentCount = 1;
3184 color_blending.pAttachments = &color_blend_attachment;
3185
3186 VkPushConstantRange push_range{};
3187 push_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
3188 push_range.offset = 0;
3189 push_range.size = sizeof(FlamePushConstants);
3190
3191 VkPipelineLayoutCreateInfo layout_info{};
3192 layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
3193 layout_info.pushConstantRangeCount = 1;
3194 layout_info.pPushConstantRanges = &push_range;
3195
3196 if (vkCreatePipelineLayout(device, &layout_info, nullptr, &flame_pipeline_layout) != VK_SUCCESS) {
3197 throw mxvk::Exception("Failed to create asteroids3d flame pipeline layout");
3198 }
3199
3200 const VkFormat color_format = getSwapchainFormat();
3201 const VkFormat depth_format = getDepthFormat();
3202
3203 VkPipelineRenderingCreateInfo rendering_info{};
3204 rendering_info.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
3205 rendering_info.colorAttachmentCount = 1;
3206 rendering_info.pColorAttachmentFormats = &color_format;
3207 if (depth_format != VK_FORMAT_UNDEFINED) {
3208 rendering_info.depthAttachmentFormat = depth_format;
3209 }
3210
3211 VkGraphicsPipelineCreateInfo pipeline_info{};
3212 pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
3213 pipeline_info.pNext = &rendering_info;
3214 pipeline_info.stageCount = static_cast<uint32_t>(shader_stages.size());
3215 pipeline_info.pStages = shader_stages.data();
3216 pipeline_info.pVertexInputState = &vertex_input;
3217 pipeline_info.pInputAssemblyState = &input_assembly;
3218 pipeline_info.pViewportState = &viewport_state;
3219 pipeline_info.pRasterizationState = &rasterizer;
3220 pipeline_info.pMultisampleState = &multisampling;
3221 pipeline_info.pDepthStencilState = &depth_stencil;
3222 pipeline_info.pColorBlendState = &color_blending;
3223 pipeline_info.pDynamicState = &dynamic_info;
3224 pipeline_info.layout = flame_pipeline_layout;
3225 pipeline_info.renderPass = VK_NULL_HANDLE;
3226 pipeline_info.subpass = 0;
3227
3228 if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &flame_pipeline) != VK_SUCCESS) {
3229 throw mxvk::Exception("Failed to create asteroids3d flame pipeline");
3230 }
3231 } catch (...) {
3232 if (frag_shader_module != VK_NULL_HANDLE) {
3233 vkDestroyShaderModule(device, frag_shader_module, nullptr);
3234 }
3235 vkDestroyShaderModule(device, vert_shader_module, nullptr);
3236 cleanup_flame_swapchain_resources();
3237 throw;
3238 }
3239
3240 vkDestroyShaderModule(device, frag_shader_module, nullptr);
3241 vkDestroyShaderModule(device, vert_shader_module, nullptr);
3242 }
3243
3244 void draw_engine_flame(VkCommandBuffer cmd,
3245 const VkExtent2D &extent,
3246 const glm::mat4 &ship_matrix,
3247 float ship_speed,
3248 bool ship_visible) {
3249 if (!ship_visible) {
3250 return;
3251 }
3252 if (flame_pipeline == VK_NULL_HANDLE || flame_vertex_buffer == VK_NULL_HANDLE || flame_vertex_count == 0) {
3253 return;
3254 }
3255
3256 VkViewport viewport{};
3257 viewport.x = 0.0f;
3258 viewport.y = 0.0f;
3259 viewport.width = static_cast<float>(extent.width);
3260 viewport.height = static_cast<float>(extent.height);
3261 viewport.minDepth = 0.0f;
3262 viewport.maxDepth = 1.0f;
3263 vkCmdSetViewport(cmd, 0, 1, &viewport);
3264
3265 VkRect2D scissor{};
3266 scissor.offset = {0, 0};
3267 scissor.extent = extent;
3268 vkCmdSetScissor(cmd, 0, 1, &scissor);
3269
3270 FlamePushConstants pc{};
3271 pc.mvp = projection_matrix * view_matrix * ship_matrix;
3272 const float flame_power = std::clamp(ship_speed / ship.max_speed, 0.08f, 1.0f);
3273 pc.params = glm::vec4(elapsed_seconds, flame_power, 0.0f, 0.0f);
3274
3275 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, flame_pipeline);
3276 vkCmdPushConstants(cmd,
3277 flame_pipeline_layout,
3278 VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
3279 0,
3280 sizeof(pc),
3281 &pc);
3282
3283 VkBuffer vertex_buffers[] = {flame_vertex_buffer};
3284 VkDeviceSize offsets[] = {0};
3285 vkCmdBindVertexBuffers(cmd, 0, 1, vertex_buffers, offsets);
3286 vkCmdDraw(cmd, flame_vertex_count, 1, 0, 0);
3287 }
3288
3289 void create_buffer(VkDeviceSize size,
3290 VkBufferUsageFlags usage,
3291 VkMemoryPropertyFlags properties,
3292 VkBuffer &buffer,
3293 VkDeviceMemory &buffer_memory) const {
3294 VkBufferCreateInfo buffer_info{};
3295 buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
3296 buffer_info.size = size;
3297 buffer_info.usage = usage;
3298 buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
3299
3300 if (vkCreateBuffer(device, &buffer_info, nullptr, &buffer) != VK_SUCCESS) {
3301 throw mxvk::Exception("Failed to create asteroids3d buffer");
3302 }
3303
3304 VkMemoryRequirements mem_requirements{};
3305 vkGetBufferMemoryRequirements(device, buffer, &mem_requirements);
3306
3307 VkMemoryAllocateInfo alloc_info{};
3308 alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
3309 alloc_info.allocationSize = mem_requirements.size;
3310
3311 try {
3312 alloc_info.memoryTypeIndex = find_memory_type(mem_requirements.memoryTypeBits, properties);
3313 if (vkAllocateMemory(device, &alloc_info, nullptr, &buffer_memory) != VK_SUCCESS) {
3314 throw mxvk::Exception("Failed to allocate asteroids3d buffer memory");
3315 }
3316 if (vkBindBufferMemory(device, buffer, buffer_memory, 0) != VK_SUCCESS) {
3317 throw mxvk::Exception("Failed to bind asteroids3d buffer memory");
3318 }
3319 } catch (...) {
3320 if (buffer_memory != VK_NULL_HANDLE) {
3321 vkFreeMemory(device, buffer_memory, nullptr);
3322 buffer_memory = VK_NULL_HANDLE;
3323 }
3324 if (buffer != VK_NULL_HANDLE) {
3325 vkDestroyBuffer(device, buffer, nullptr);
3326 buffer = VK_NULL_HANDLE;
3327 }
3328 throw;
3329 }
3330 }
3331
3332 [[nodiscard]] uint32_t find_memory_type(uint32_t type_filter, VkMemoryPropertyFlags properties) const {
3333 VkPhysicalDeviceMemoryProperties mem_properties{};
3334 vkGetPhysicalDeviceMemoryProperties(physical_device, &mem_properties);
3335
3336 for (uint32_t i = 0; i < mem_properties.memoryTypeCount; ++i) {
3337 if ((type_filter & (1U << i)) && (mem_properties.memoryTypes[i].propertyFlags & properties) == properties) {
3338 return i;
3339 }
3340 }
3341
3342 throw mxvk::Exception("Failed to find asteroids3d memory type");
3343 }
3344
3345 void draw_ui_rect(int x, int y, int width, int height, const glm::vec4 &color) {
3346 if (ui_pixel == nullptr || width <= 0 || height <= 0) {
3347 return;
3348 }
3349 ui_pixel->setShaderParams(color.r, color.g, color.b, color.a);
3350 ui_pixel->drawSpriteRect(x, y, width, height);
3351 }
3352
3353 std::optional<glm::ivec2> project_world_to_screen(const glm::vec3 &world_position, const VkExtent2D &extent) const {
3354 const glm::vec4 clip = projection_matrix * view_matrix * glm::vec4(world_position, 1.0f);
3355 if (clip.w <= 0.0001f) {
3356 return std::nullopt;
3357 }
3358
3359 const glm::vec3 ndc = glm::vec3(clip) / clip.w;
3360 if (ndc.z < 0.0f || ndc.z > 1.0f) {
3361 return std::nullopt;
3362 }
3363
3364 const int x = static_cast<int>((ndc.x * 0.5f + 0.5f) * static_cast<float>(extent.width));
3365 const int y = static_cast<int>((ndc.y * 0.5f + 0.5f) * static_cast<float>(extent.height));
3366 return glm::ivec2{x, y};
3367 }
3368
3369 bool cannon_has_asteroid_target(const glm::vec3 &muzzle, const glm::vec3 &forward) const {
3370 constexpr float AIM_ASSIST_SCALE = 1.04f;
3371 const float max_range = PROJECTILE_SPEED * PROJECTILE_LIFETIME;
3372 for (const auto &asteroid : asteroids) {
3373 if (!asteroid.active) {
3374 continue;
3375 }
3376
3377 const glm::vec3 to_asteroid = asteroid.position - muzzle;
3378 const float along_ray = glm::dot(to_asteroid, forward);
3379 if (along_ray < 0.0f || along_ray > max_range) {
3380 continue;
3381 }
3382
3383 const glm::vec3 closest_point = muzzle + forward * along_ray;
3384 const float hit_radius = asteroid.radius * ASTEROID_PROJECTILE_COLLISION_SCALE * AIM_ASSIST_SCALE;
3385 if (glm::length(closest_point - asteroid.position) <= hit_radius) {
3386 return true;
3387 }
3388 }
3389 return false;
3390 }
3391
3392 bool cannon_has_opponent_target(const glm::vec3 &muzzle, const glm::vec3 &forward) const {
3393 if (!multiplayer_match)
3394 return false;
3395 constexpr float OPPONENT_TARGET_RADIUS = 1.8f;
3396 const float max_range = PROJECTILE_SPEED * PROJECTILE_LIFETIME;
3397 for (std::uint8_t player = 0; player < NETWORK_PLAYER_COUNT; ++player) {
3398 if (player == multiplayer.local_player_id() || !multiplayer.player_connected()[player] ||
3399 !remote_ships[player].visible || remote_ship_exploding[player])
3400 continue;
3401 const glm::vec3 to_opponent = remote_ships[player].position - muzzle;
3402 const float along_ray = glm::dot(to_opponent, forward);
3403 if (along_ray < 0.0f || along_ray > max_range)
3404 continue;
3405 const glm::vec3 closest_point = muzzle + forward * along_ray;
3406 if (glm::length(closest_point - remote_ships[player].position) <= OPPONENT_TARGET_RADIUS)
3407 return true;
3408 }
3409 return false;
3410 }
3411
3412 void draw_cannon_crosshair(const VkExtent2D &extent) {
3413 if (ui_pixel == nullptr || !ship.visible || ship.exploding || extent.width < 160U || extent.height < 120U) {
3414 return;
3415 }
3416
3417 constexpr float AIM_DISTANCE = 120.0f;
3418 constexpr float MUZZLE_OFFSET = 0.08f;
3419 constexpr int ARM_LENGTH = 18;
3420 constexpr int GAP = 6;
3421 constexpr int THICKNESS = 2;
3422 const glm::vec3 forward = ship.forward();
3423 const glm::vec3 muzzle = ship.position + forward * MUZZLE_OFFSET;
3424 const glm::vec3 aim_position = muzzle + forward * AIM_DISTANCE;
3425 const std::optional<glm::ivec2> screen_position = project_world_to_screen(aim_position, extent);
3426 if (!screen_position.has_value()) {
3427 return;
3428 }
3429
3430 const int x = std::clamp(screen_position->x, ARM_LENGTH + 2, static_cast<int>(extent.width) - ARM_LENGTH - 2);
3431 const int y = std::clamp(screen_position->y, ARM_LENGTH + 2, static_cast<int>(extent.height) - ARM_LENGTH - 2);
3432 const bool opponent_locked = cannon_has_opponent_target(muzzle, forward);
3433 const bool asteroid_locked = cannon_has_asteroid_target(muzzle, forward);
3434 const glm::vec4 shadow = opponent_locked
3435 ? glm::vec4{0.12f, 0.08f, 0.0f, 0.75f}
3436 : (asteroid_locked ? glm::vec4{0.0f, 0.02f, 0.07f, 0.7f} : glm::vec4{0.05f, 0.0f, 0.0f, 0.7f});
3437 const glm::vec4 crosshair_color = opponent_locked
3438 ? glm::vec4{1.0f, 0.86f, 0.08f, 1.0f}
3439 : (asteroid_locked ? glm::vec4{0.12f, 0.58f, 1.0f, 0.98f} : glm::vec4{1.0f, 0.03f, 0.02f, 0.96f});
3440
3441 draw_ui_rect(x - ARM_LENGTH - 1, y - THICKNESS / 2 - 1, ARM_LENGTH - GAP + 2, THICKNESS + 2, shadow);
3442 draw_ui_rect(x + GAP - 1, y - THICKNESS / 2 - 1, ARM_LENGTH - GAP + 2, THICKNESS + 2, shadow);
3443 draw_ui_rect(x - THICKNESS / 2 - 1, y - ARM_LENGTH - 1, THICKNESS + 2, ARM_LENGTH - GAP + 2, shadow);
3444 draw_ui_rect(x - THICKNESS / 2 - 1, y + GAP - 1, THICKNESS + 2, ARM_LENGTH - GAP + 2, shadow);
3445
3446 draw_ui_rect(x - ARM_LENGTH, y - THICKNESS / 2, ARM_LENGTH - GAP, THICKNESS, crosshair_color);
3447 draw_ui_rect(x + GAP, y - THICKNESS / 2, ARM_LENGTH - GAP, THICKNESS, crosshair_color);
3448 draw_ui_rect(x - THICKNESS / 2, y - ARM_LENGTH, THICKNESS, ARM_LENGTH - GAP, crosshair_color);
3449 draw_ui_rect(x - THICKNESS / 2, y + GAP, THICKNESS, ARM_LENGTH - GAP, crosshair_color);
3450 draw_ui_rect(x - 1, y - 1, 3, 3, crosshair_color);
3451 }
3452
3453 void draw_radar(const VkExtent2D &extent) {
3454 if (ui_pixel == nullptr || extent.width < 360U || extent.height < 280U) {
3455 return;
3456 }
3457
3458 constexpr int BORDER = 2;
3459 constexpr float RADAR_RANGE = 180.0f;
3460 const int radar_size = std::clamp(static_cast<int>(std::min(extent.width, extent.height)) / 4, 150, 220);
3461 const int radar_x = 24;
3462 const int radar_y = std::max(230, static_cast<int>(extent.height) - radar_size - 24);
3463 const int inner_x = radar_x + BORDER;
3464 const int inner_y = radar_y + BORDER;
3465 const int inner_size = radar_size - BORDER * 2;
3466 const int center_x = inner_x + inner_size / 2;
3467 const int center_y = inner_y + inner_size / 2;
3468 const float half_size = static_cast<float>(inner_size) * 0.5f;
3469
3470 draw_ui_rect(radar_x, radar_y, radar_size, radar_size, {0.01f, 0.025f, 0.045f, 0.74f});
3471 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};
3472 draw_ui_rect(radar_x, radar_y, radar_size, BORDER, border_color);
3473 draw_ui_rect(radar_x, radar_y + radar_size - BORDER, radar_size, BORDER, border_color);
3474 draw_ui_rect(radar_x, radar_y, BORDER, radar_size, border_color);
3475 draw_ui_rect(radar_x + radar_size - BORDER, radar_y, BORDER, radar_size, border_color);
3476
3477 draw_ui_rect(center_x, inner_y, 1, inner_size, {0.10f, 0.30f, 0.38f, 0.7f});
3478 draw_ui_rect(inner_x, center_y, inner_size, 1, {0.10f, 0.30f, 0.38f, 0.7f});
3479 draw_ui_rect(center_x - inner_size / 4, inner_y, 1, inner_size, {0.08f, 0.22f, 0.28f, 0.45f});
3480 draw_ui_rect(center_x + inner_size / 4, inner_y, 1, inner_size, {0.08f, 0.22f, 0.28f, 0.45f});
3481 draw_ui_rect(inner_x, center_y - inner_size / 4, inner_size, 1, {0.08f, 0.22f, 0.28f, 0.45f});
3482 draw_ui_rect(inner_x, center_y + inner_size / 4, inner_size, 1, {0.08f, 0.22f, 0.28f, 0.45f});
3483
3484 for (const auto &asteroid : asteroids) {
3485 if (!asteroid.active) {
3486 continue;
3487 }
3488 glm::vec2 relative{asteroid.position.x - ship.position.x, asteroid.position.z - ship.position.z};
3489 const float distance = glm::length(relative);
3490 const bool clamped_to_edge = distance > RADAR_RANGE;
3491 if (clamped_to_edge && distance > 1e-4f) {
3492 relative *= RADAR_RANGE / distance;
3493 }
3494 const int dot_x = center_x + static_cast<int>((relative.x / RADAR_RANGE) * half_size);
3495 const int dot_y = center_y + static_cast<int>((relative.y / RADAR_RANGE) * half_size);
3496 const int dot_size = std::clamp(static_cast<int>(asteroid.radius * 0.55f), 3, 8);
3497 const float altitude = std::clamp((asteroid.position.y - BOUNDARY_Y_MIN) / (BOUNDARY_Y_MAX - BOUNDARY_Y_MIN), 0.0f, 1.0f);
3498 const glm::vec4 dot_color = clamped_to_edge
3499 ? glm::vec4{1.0f, 0.38f, 0.16f, 0.9f}
3500 : glm::vec4{1.0f, 0.55f + altitude * 0.28f, 0.18f, 1.0f};
3501 draw_ui_rect(dot_x - dot_size / 2, dot_y - dot_size / 2, dot_size, dot_size, dot_color);
3502 }
3503
3504 if (multiplayer_match) {
3505 for (std::uint8_t player = 0; player < NETWORK_PLAYER_COUNT; ++player) {
3506 if (player == multiplayer.local_player_id() || !multiplayer.player_connected()[player])
3507 continue;
3508 glm::vec2 relative{remote_ships[player].position.x - ship.position.x, remote_ships[player].position.z - ship.position.z};
3509 const float distance = glm::length(relative);
3510 if (distance > RADAR_RANGE && distance > 1e-4f)
3511 relative *= RADAR_RANGE / distance;
3512 const int opponent_x = center_x + static_cast<int>((relative.x / RADAR_RANGE) * half_size);
3513 const int opponent_y = center_y + static_cast<int>((relative.y / RADAR_RANGE) * half_size);
3514 const glm::vec4 color = remote_ship_exploding[player] ? glm::vec4{1.0f, 0.35f, 0.12f, 1.0f} : glm::vec4{1.0f, 0.08f, 0.12f, 1.0f};
3515 draw_ui_rect(opponent_x - 6, opponent_y - 6, 12, 12, color);
3516 draw_ui_rect(opponent_x - 9, opponent_y - 1, 18, 3, color);
3517 draw_ui_rect(opponent_x - 1, opponent_y - 9, 3, 18, color);
3518 }
3519 }
3520
3521 draw_ui_rect(center_x - 5, center_y, 11, 2, {0.95f, 1.0f, 1.0f, 1.0f});
3522 draw_ui_rect(center_x, center_y - 5, 2, 11, {0.95f, 1.0f, 1.0f, 1.0f});
3523
3524 const SDL_Color label_color = ship_returning_to_field ? SDL_Color{255, 180, 80, 255} : SDL_Color{120, 220, 255, 255};
3525 printText(multiplayer_match ? "RADAR - ENEMY RED" : (ship_returning_to_field ? "RADAR RETURN" : "RADAR"), radar_x, std::max(4, radar_y - 22), label_color);
3526 }
3527
3528 void draw_hud([[maybe_unused]] float aspect) {
3529 set_ui_font_size(18);
3530 const SDL_Color white{255, 255, 255, 255};
3531 const SDL_Color red{220, 60, 60, 255};
3532 const SDL_Color yellow{255, 220, 120, 255};
3533 const VkExtent2D extent = getSwapchainExtent();
3534 draw_cannon_crosshair(extent);
3535 draw_radar(extent);
3536 const int right_x = std::max(25, static_cast<int>(extent.width) - 250);
3537 printText("MXVK Asteroids v1.0", right_x, 25, red);
3538 if (multiplayer_match) {
3539 const std::uint8_t local_id = multiplayer.local_player_id();
3540 const unsigned local_kills = local_id < NETWORK_PLAYER_COUNT ? multiplayer_kills[local_id] : 0U;
3541 unsigned enemy_kills = 0;
3542 for (std::uint8_t player = 0; player < NETWORK_PLAYER_COUNT; ++player)
3543 if (player != local_id)
3544 enemy_kills = std::max(enemy_kills, multiplayer_kills[player]);
3545 printText("Kills: " + std::to_string(local_kills) + " / " + std::to_string(MULTIPLAYER_KILLS_TO_WIN), right_x, 50, white);
3546 printText("Leader: " + std::to_string(enemy_kills) + " / " + std::to_string(MULTIPLAYER_KILLS_TO_WIN), right_x, 75, red);
3547 printText("Players: " + std::to_string(multiplayer.player_count()) + " / 4", right_x, 100, white);
3548 printText("UDP host-authoritative", right_x, 125, yellow);
3549 return;
3550 }
3551 printText("Score: " + std::to_string(ship.score), right_x, 50, white);
3552 printText("Lives: " + std::to_string(std::max(0, ship.lives)), right_x, 75, white);
3553 printText("Asteroids: " + std::to_string(active_asteroids()), right_x, 100, white);
3554 printText("Time Left: " + format_round_time(), right_x, 125, round_time_remaining <= 30.0f ? yellow : white);
3555 printText("[F1 for Debug]", right_x, 150, white);
3556 printText(inverted_controls ? "[Inverted] F2/Y" : "[Arcade] F2/Y", right_x, 175, white);
3557 printText("[F3 for Console]", right_x, 200, white);
3558 printText(mouse_look_controls ? "[Mouse Look] F5" : "[Classic Keys] F5", right_x, 225, white);
3559 printText(first_person_camera ? "[First Person] F7" : "[Chase View] F7", right_x, 250, white);
3560
3561 if (!debug_menu) {
3562 return;
3563 }
3564
3565 const float fps = (last_delta_time > 0.0001f) ? (1.0f / last_delta_time) : 0.0f;
3566 printText("Ship X,Y,Z: " + vec3_string(ship.position), 25, 25, white);
3567 printText("Velocity X,Y,Z: " + vec3_string(ship.velocity), 25, 50, white);
3568 printText("FPS: " + std::to_string(fps), 25, 75, white);
3569 printText("Aseroids destroyed: " + std::to_string(MAX_ASTEROIDS - active_asteroids()), 25, 100, white);
3570 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);
3571 printText("Nearest Object: " + std::to_string(nearest_asteroid_distance()), 25, 150, white);
3572 printText("Farthest Object: " + std::to_string(farthest_asteroid_distance()), 25, 175, white);
3573 printText("Speed: " + std::to_string(ship.current_speed) + " / " + std::to_string(ship.max_speed), 25, 200, white);
3574 printText("Controller: " + controller_status(), 25, 225, white);
3575 printText(std::string("Input: ") + (mouse_look_controls ? "Keyboard/mouse" : "Classic keyboard"), 25, 250, white);
3576 printText(std::string("Camera: ") + (first_person_camera ? "First person" : "Chase"), 25, 275, white);
3577 printText("Press ENTER to randomize asteroids", 25, 300, white);
3578 }
3579
3580 int active_asteroids() const {
3581 int count = 0;
3582 for (const auto &asteroid : asteroids) {
3583 if (asteroid.active) {
3584 ++count;
3585 }
3586 }
3587 return count;
3588 }
3589
3590 void draw_multiplayer_end(const VkExtent2D &extent) {
3591 multiplayer.exchange(make_network_state(true), last_delta_time);
3592 set_ui_font_size(22);
3593 const std::uint8_t local_id = multiplayer.local_player_id();
3594 const bool local_won = multiplayer_winner == local_id + 1U;
3595 const std::size_t winner_index = multiplayer_winner > 0U ? multiplayer_winner - 1U : 0U;
3596 const std::string winner_name = winner_index < multiplayer_player_names.size() && !multiplayer_player_names[winner_index].empty()
3597 ? multiplayer_player_names[winner_index]
3598 : std::format("Player {}", multiplayer_winner);
3599 const std::string result_text = winner_name + " WINS";
3600 const std::string score_text = std::format("Final score: {} kills", local_id < NETWORK_PLAYER_COUNT ? multiplayer_kills[local_id] : 0U);
3601 const std::string prompt_text = "Press ENTER to return to the multiplayer lobby";
3602
3603 const auto text_width = [this](const std::string &text) {
3604 int width = 0;
3605 int height = 0;
3606 if (!getTextDimensions(text.c_str(), width, height)) {
3607 return 0;
3608 }
3609 return width;
3610 };
3611 const int widest_text = std::max({text_width(result_text), text_width(score_text), text_width(prompt_text)});
3612 const int panel_width = std::max(620, widest_text + 80);
3613 const int panel_x = static_cast<int>(extent.width) / 2 - panel_width / 2;
3614 const int panel_y = static_cast<int>(extent.height) / 2 - 150;
3615 draw_ui_rect(panel_x, panel_y, panel_width, 300, {0.015f, 0.025f, 0.08f, 0.94f});
3616 draw_ui_rect(panel_x, panel_y, panel_width, 4, local_won ? glm::vec4{0.2f, 1.0f, 0.55f, 1.0f} : glm::vec4{1.0f, 0.2f, 0.2f, 1.0f});
3617 printText(result_text, panel_x + (panel_width - text_width(result_text)) / 2, panel_y + 48,
3618 local_won ? SDL_Color{100, 255, 160, 255} : SDL_Color{255, 90, 90, 255});
3619 printText(score_text, panel_x + (panel_width - text_width(score_text)) / 2, panel_y + 120, {255, 255, 255, 255});
3620 printText(prompt_text, panel_x + (panel_width - text_width(prompt_text)) / 2, panel_y + 205, {255, 220, 120, 255});
3621 }
3622
3623 void draw_end_screen([[maybe_unused]] uint32_t image_index,
3624 [[maybe_unused]] float aspect,
3625 const std::string &title,
3626 const SDL_Color &title_color) {
3627 set_ui_font_size(32);
3628 const SDL_Color white{255, 255, 255, 255};
3629 const SDL_Color yellow{255, 220, 120, 255};
3630 const VkExtent2D extent = getSwapchainExtent();
3631
3632 const std::string score_text = "Final Score: " + std::to_string(ship.score);
3633 const std::string prompt = "Press ENTER to start over";
3634
3635 int title_w = 0;
3636 int title_h = 0;
3637 if (getTextDimensions(title.c_str(), title_w, title_h)) {
3638 printText(title.c_str(),
3639 static_cast<int>(extent.width) / 2 - title_w / 2,
3640 static_cast<int>(extent.height) / 2 - title_h,
3641 title_color);
3642 } else {
3643 printText(title.c_str(), 24, 20, title_color);
3644 }
3645
3646 int score_w = 0;
3647 int score_h = 0;
3648 if (getTextDimensions(score_text.c_str(), score_w, score_h)) {
3649 printText(score_text.c_str(),
3650 static_cast<int>(extent.width) / 2 - score_w / 2,
3651 static_cast<int>(extent.height) / 2 + 10,
3652 white);
3653 } else {
3654 printText(score_text.c_str(), 24, 70, white);
3655 }
3656
3657 int prompt_w = 0;
3658 int prompt_h = 0;
3659 if (getTextDimensions(prompt.c_str(), prompt_w, prompt_h)) {
3660 printText(prompt.c_str(),
3661 static_cast<int>(extent.width) / 2 - prompt_w / 2,
3662 static_cast<int>(extent.height) / 2 + score_h + 28,
3663 yellow);
3664 } else {
3665 printText(prompt.c_str(), 24, 100, yellow);
3666 }
3667 }
3668
3669 void draw_game_over([[maybe_unused]] uint32_t image_index, [[maybe_unused]] float aspect) {
3670 draw_end_screen(image_index, aspect, "Game over", SDL_Color{235, 60, 60, 255});
3671 }
3672
3673 VkCommandBuffer current_command_buffer = VK_NULL_HANDLE;
3674 float last_delta_time = 1.0f / 60.0f;
3675
3676 std::string vec3_string(const glm::vec3 &value) const {
3677 return std::to_string(value.x) + ", " + std::to_string(value.y) + ", " + std::to_string(value.z);
3678 }
3679
3680 float nearest_asteroid_distance() const {
3681 float nearest = 999999.0f;
3682 for (const auto &asteroid : asteroids) {
3683 if (asteroid.active) {
3684 nearest = std::min(nearest, glm::length(ship.position - asteroid.position));
3685 }
3686 }
3687 return nearest;
3688 }
3689
3690 float farthest_asteroid_distance() const {
3691 float farthest = 0.0f;
3692 for (const auto &asteroid : asteroids) {
3693 if (asteroid.active) {
3694 farthest = std::max(farthest, glm::length(ship.position - asteroid.position));
3695 }
3696 }
3697 return farthest;
3698 }
3699 };
3700
3701} // namespace space
3702
3704 Asteroids3DWindow window(args.path, args.width, args.height, args.fullscreen, args.enable_vsync, args.enable_crt, args.disable_sound);
3705 window.loop();
3706}
Lightweight, header-only, template command-line argument parser.
Shared simulation constants, data types, and utility functions.
Entry point for the networked 3D Asteroids application.
Player ship state for the Asteroids simulation.
Animated starfield used by the Asteroids scene.
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.
bool isLoaded() const
True once the model has been uploaded to GPU buffers.
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.
Main Vulkan window wrapper for MXVK.
Definition mxvk.hpp:37
VkExtent2D getSwapchainExtent() const noexcept
Get the current swapchain extent.
Definition mxvk.hpp:186
void loop()
Run the main event/render loop.
Definition mxvk.cpp:600
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:3578
VkDevice device
Definition mxvk.hpp:485
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:3477
VkFormat depth_format
Definition mxvk.hpp:494
static VkShaderModule createShaderModule(VkDevice device, const std::vector< char > &spv_bytes)
Create a shader module from SPIR-V bytecode.
Definition mxvk.cpp:145
bool getTextDimensions(const std::string &text, int &width, int &height)
Measure text dimensions in pixels.
Definition mxvk.cpp:3069
void clearTextQueue()
Clear all queued text draw calls for the current frame.
Definition mxvk.cpp:3063
SDL_Window * getSDLWindow() const noexcept
Get the underlying SDL window handle.
Definition mxvk.hpp:165
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
Definition mxvk.cpp:593
void exit()
Request loop termination.
Definition mxvk.cpp:1126
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:1154
VK_Window()=default
Construct an empty window object.
VkPhysicalDevice physical_device
Definition mxvk.hpp:484
VkFormat getDepthFormat() const noexcept
Get the depth format used for dynamic rendering attachments.
Definition mxvk.hpp:189
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
Definition mxvk.cpp:2991
void setPostProcessingEnabled(bool enabled)
Definition mxvk.hpp:284
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:3018
static std::vector< char > loadSpv(const std::string &path)
Load a SPIR-V file from disk.
Definition mxvk.cpp:141
void setPostProcessingShaderTimeEnabled(bool enabled)
Keep shader param 1 updated with elapsed render time in seconds.
Definition mxvk.cpp:1248
VkFormat getSwapchainFormat() const noexcept
Get the swapchain color format.
Definition mxvk.hpp:183
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, bool disable_sound)
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.
UDP multiplayer state exchange for Asteroids.
#define MXVK_VALIDATION
Definition mxvk.hpp:27
High-level model wrapper integrated with MXVK dynamic rendering.
In-game command console for MXVK / Vulkan windows.
SDL3 joystick and gamepad RAII wrappers.
PNG image loading and saving utilities via SDL3.
SDL3_mixer audio subsystem wrapper.
std::filesystem::path texture_path(const std::string &filename, const std::string &asset_path)
Definition main.cpp:147
RainConfig make_matrix_rain_config(const std::string &asset_root, bool binary_glyph_mode)
Definition rain.cpp:157
Utilities for loading and saving PNG images.
Definition mxvk.hpp:30
SDL_Surface * LoadPNG(const char *file)
Load a PNG file into an SDL_Surface.
Definition mxvk_png.cpp:103
std::string resolve_shader_root(const std::string &asset_root)
constexpr std::array< glm::vec3, NETWORK_PLAYER_COUNT > MULTIPLAYER_SPAWNS
std::string resolve_asset_root(const std::string &path)
constexpr std::array< float, NETWORK_PLAYER_COUNT > MULTIPLAYER_SPAWN_YAWS
GameMode
High-level state of the Asteroids application.
@ Lobby
Multiplayer lobby.
@ Loading
Asset-loading screen.
@ Intro
Introductory screen.
@ GameComplete
Completed game.
@ GameOver
Player has no remaining lives.
@ MatchOver
Completed multiplayer round.
@ 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 std::size_t NETWORK_PLAYER_COUNT
Maximum number of players in a session.
constexpr float ASTEROID_PROJECTILE_COLLISION_SCALE
Projectile collision-radius adjustment.
constexpr float BOUNDARY_Y_MAX
Maximum simulation y-coordinate.
constexpr int MAX_ASTEROIDS
Maximum locally simulated asteroids.
constexpr int MAX_PARTICLES
Maximum particles in the shared particle pool.
constexpr float BOUNDARY_Y_MIN
Minimum simulation y-coordinate.
constexpr float ASTEROID_SHIP_COLLISION_SCALE
Ship collision-radius adjustment.
constexpr glm::vec4 PROJECTILE_COLOR
Default projectile color.
constexpr int SHOTS_PER_BURST
Projectiles fired in one burst.
constexpr int EXPLOSION_DURATION_FRAMES
Ship explosion duration in frames.
constexpr int FIRE_DELAY
Frames between shots within a burst.
constexpr float BOUNDARY_X_MIN
Minimum simulation x-coordinate.
constexpr int LARGE_ASTEROID_POINTS
Score awarded for a large asteroid.
constexpr float BOUNDARY_BOUNCE_FACTOR
Boundary collision response multiplier.
constexpr float PROJECTILE_SPEED
Projectile travel speed in world units per second.
constexpr float BOUNDARY_Z_MIN
Minimum simulation z-coordinate.
void run_asteroids3d(const Arguments &args)
Runs the Asteroids application.
constexpr int MEDIUM_ASTEROID_POINTS
Score awarded for a medium asteroid.
constexpr float PI
Single-precision value of pi.
constexpr float SHIP_MODEL_SCALE
Uniform ship model scale.
constexpr float PROJECTILE_LIFETIME
Projectile lifetime in seconds.
constexpr Sint16 CONTROLLER_DEAD_ZONE
Controller axis dead-zone magnitude.
constexpr float CONTROLLER_AXIS_MAX
Maximum signed controller axis magnitude.
float random_float(float min_value, float max_value)
Generates a uniformly distributed floating-point value.
constexpr int GAME_STARS
Number of stars in the background field.
constexpr float BOUNDARY_Z_MAX
Maximum simulation z-coordinate.
Plain data structure returned by proc_args() with all common libmx2 CLI options.
Definition argz.hpp:730
bool fullscreen
Whether fullscreen mode was requested.
Definition argz.hpp:736
bool enable_vsync
Enable FIFO present mode / v-sync (--enable-vsync).
Definition argz.hpp:750
int height
Viewport height in pixels (default: 720).
Definition argz.hpp:733
std::string path
Asset root; proc_args() defaults it to the executable directory.
Definition argz.hpp:735
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:732
bool enable_crt
Enable CRT post-processing at startup (--enable-crt).
Definition argz.hpp:749
bool disable_sound
Disable application background music (--disable-sound).
Definition argz.hpp:752
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
float y
Definition space.cpp:59
float x
Definition space.cpp:59
bool active
Definition space.cpp:62
float lifetime
Definition space.cpp:61
bool exploding
Definition space.cpp:50
std::string color
Definition rain.hpp:19