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