95 Asteroids3DWindow(
const std::string &path,
int width,
int height,
bool fullscreen,
bool enable_vsync,
bool enable_crt,
bool disable_sound)
97 asset_root(resolve_asset_root(path)),
98 shader_root(resolve_shader_root(asset_root)),
99 crt_enabled(enable_crt),
100 background_music_disabled(disable_sound) {
101 const char *video_driver = SDL_GetCurrentVideoDriver();
102 const bool uses_wayland = video_driver !=
nullptr && std::strcmp(video_driver,
"wayland") == 0;
104 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> window_icon(
105 mxvk::LoadPNG((asset_root +
"/data/asteroids_icon.png").c_str()), SDL_DestroySurface);
106 if (window_icon !=
nullptr && !SDL_SetWindowIcon(
getSDLWindow(), window_icon.get())) {
107 std::cerr <<
"asteroids-net: could not set SDL window icon: " << SDL_GetError() <<
'\n';
115 load_loading_screen_resources();
118#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
119 load_sound_effects();
120 ensure_background_music_playing();
125 if (mouse_capture_active) {
127 mouse_capture_active =
false;
129 if (
device != VK_NULL_HANDLE) {
132 if (loading_thread.joinable()) {
133 loading_thread.join();
135#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
137 sound_effects->stopMusic();
140 cleanup_flame_resources();
141 ship_model.cleanup(
this);
142 for (
auto &model : asteroid_models) {
145 for (
auto &model : remote_ship_models) {
148 if (star_sprite !=
nullptr) {
149 star_sprite->cleanup();
151 if (projectile_sprite !=
nullptr) {
152 projectile_sprite->cleanup();
154 if (effect_sprite !=
nullptr) {
155 effect_sprite->cleanup();
161 if (e.type == SDL_EVENT_GAMEPAD_ADDED ||
162 e.type == SDL_EVENT_GAMEPAD_REMOVED ||
163 e.type == SDL_EVENT_JOYSTICK_ADDED ||
164 e.type == SDL_EVENT_JOYSTICK_REMOVED) {
165 if (e.type == SDL_EVENT_GAMEPAD_ADDED || e.type == SDL_EVENT_GAMEPAD_REMOVED) {
166 controller.connectEvent(e);
168 sync_controller_connection();
172 const bool was_console_visible =
console.isVisible();
173 const bool is_console_toggle = e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_F3;
175 if (is_console_toggle) {
176 log_game(
console.isVisible() ?
"Console opened." :
"Console closed.");
177 sync_mouse_capture();
180 if (was_console_visible) {
185 handle_lobby_event(e);
189 (e.key.key == SDLK_RETURN || e.key.key == SDLK_KP_ENTER || e.key.key == SDLK_ESCAPE)) {
191 multiplayer_match =
false;
193 lobby_page = LobbyPage::Main;
195 lobby_status =
"Choose how you want to play.";
199 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
201 log_game(
"Exit requested while playing.");
206 log_game(
"Exit requested from intro screen.");
211 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_F8 && !e.key.repeat) {
212 crt_enabled = !crt_enabled;
214 log_game(std::string(
"CRT effect ") + (crt_enabled ?
"enabled." :
"disabled."));
218 e.type == SDL_EVENT_KEY_DOWN &&
219 (e.key.key == SDLK_SPACE || e.key.key == SDLK_RETURN)) {
221 log_game(
"Intro skipped. Starting game.");
225 e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN &&
226 e.gbutton.button == SDL_GAMEPAD_BUTTON_SOUTH) {
228 log_game(
"Intro skipped from controller. Starting game.");
231 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
232 if (e.gbutton.button == SDL_GAMEPAD_BUTTON_BACK) {
233 log_game(
"Exit requested from controller.");
237 if (e.gbutton.button == SDL_GAMEPAD_BUTTON_WEST) {
238 debug_menu = !debug_menu;
239 log_game(std::string(
"Debug HUD ") + (debug_menu ?
"enabled from controller." :
"disabled from controller."));
242 if (e.gbutton.button == SDL_GAMEPAD_BUTTON_NORTH) {
243 inverted_controls = !inverted_controls;
244 log_game(std::string(
"Controls set to ") + (inverted_controls ?
"inverted from controller." :
"arcade from controller."));
249 log_game(
"Game restarted from controller.");
253 (e.gbutton.button == SDL_GAMEPAD_BUTTON_SOUTH || e.gbutton.button == SDL_GAMEPAD_BUTTON_START)) {
254 prepare_restart_from_game_over();
255 log_game(
"End screen acknowledged from controller. Returning to intro.");
260 if (e.key.key == SDLK_RETURN || e.key.key == SDLK_KP_ENTER) {
261 prepare_restart_from_game_over();
262 log_game(
"End screen acknowledged from keyboard. Returning to intro.");
267 if (e.key.key == SDLK_F1) {
268 debug_menu = !debug_menu;
269 log_game(std::string(
"Debug HUD ") + (debug_menu ?
"enabled." :
"disabled."));
272 if (e.key.key == SDLK_F2) {
273 inverted_controls = !inverted_controls;
274 log_game(std::string(
"Controls set to ") + (inverted_controls ?
"inverted." :
"arcade."));
277 if (e.key.key == SDLK_F5 && !e.key.repeat) {
278 set_mouse_look_controls(!mouse_look_controls);
279 log_game(std::string(
"Control scheme set to ") + (mouse_look_controls ?
"keyboard/mouse." :
"classic keyboard."));
282 if (e.key.key == SDLK_F7 && !e.key.repeat) {
283 begin_camera_transition(!first_person_camera);
284 log_game(std::string(
"Camera set to ") + (first_person_camera ?
"first person." :
"chase view."));
288 if (mode ==
GameMode::Playing && mouse_look_controls && e.type == SDL_EVENT_MOUSE_MOTION) {
289 if (ignore_next_mouse_motion) {
290 ignore_next_mouse_motion =
false;
293 apply_mouse_look(e.motion.xrel, e.motion.yrel);
296 if (mode ==
GameMode::Playing && mouse_look_controls && e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
305 if (intro_sprite !=
nullptr) {
306 intro_sprite->rebuildPipeline();
308 if (intro_rain !=
nullptr) {
309 intro_rain->resize(*
this);
311 if (!game_resources_loaded.load(std::memory_order_relaxed)) {
312 cleanup_flame_swapchain_resources();
315 ship_model.resize(
this);
316 for (
auto &model : remote_ship_models) {
319 for (
auto &model : asteroid_models) {
322 if (star_sprite !=
nullptr) {
323 star_sprite->resize(
this);
325 if (projectile_sprite !=
nullptr) {
326 projectile_sprite->resize(
this);
328 if (effect_sprite !=
nullptr) {
329 effect_sprite->resize(
this);
331 cleanup_flame_swapchain_resources();
332 create_flame_swapchain_resources();
336 current_command_buffer = cmd;
337 const auto now = std::chrono::steady_clock::now();
338 const float delta_seconds = std::chrono::duration<float>(now - last_frame_time).count();
339 last_frame_time = now;
340 const float dt = std::min(delta_seconds, 0.1f);
341 last_delta_time = dt;
342 elapsed_seconds += dt;
344 sync_controller_connection();
345#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
346 ensure_background_music_playing();
350 const float aspect = (extent.height > 0U) ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height) : 1.0f;
359 draw_loading(extent);
365 draw_lobby(cmd, image_index, extent, aspect);
371 draw_multiplayer_end(extent);
377 draw_end_screen(image_index, aspect,
"Game over", SDL_Color{235, 60, 60, 255});
383 draw_end_screen(image_index, aspect,
"Mission complete", SDL_Color{255, 220, 120, 255});
388 const bool console_visible =
console.isVisible();
389 sync_mouse_capture();
390 if (!multiplayer_match) {
391 update_round_timer(dt);
394 draw_game_over(image_index, aspect);
398 if (!console_visible) {
401 update_ship(console_visible ? 0.0f : dt);
402 update_projectiles(dt);
403 if (multiplayer_match) {
404 if (multiplayer.is_host()) {
405 update_asteroids(dt);
407 update_multiplayer(dt);
409 update_asteroids(dt);
411 update_particles(dt);
413 if (ship.lives <= 0 && !ship.exploding) {
415 ship.visible =
false;
416 if (star_sprite !=
nullptr) {
417 star_sprite->clearQueue();
419 if (projectile_sprite !=
nullptr) {
420 projectile_sprite->clearQueue();
422 if (effect_sprite !=
nullptr) {
423 effect_sprite->clearQueue();
425 draw_game_over(image_index, aspect);
430 if (!multiplayer_match && mode ==
GameMode::Playing && active_asteroids() == 0) {
432 ship.visible =
false;
433 log_game(
"All asteroids cleared. Mission complete.", SDL_Color{120, 255, 160, 255});
437 if (star_sprite !=
nullptr) {
438 star_sprite->clearQueue();
440 if (projectile_sprite !=
nullptr) {
441 projectile_sprite->clearQueue();
443 if (effect_sprite !=
nullptr) {
444 effect_sprite->clearQueue();
446 draw_end_screen(image_index, aspect,
"Mission complete", SDL_Color{255, 220, 120, 255});
452 projection_matrix = glm::perspective(glm::radians(50.0f), aspect, 0.1f, 500.0f);
453 projection_matrix[1][1] *= -1.0f;
455 star_field.update(dt, camera_position, elapsed_seconds);
456 star_field.setSprite(star_sprite);
458 star_sprite->updateCamera(image_index, view_matrix, projection_matrix);
459 projectile_sprite->updateCamera(image_index, view_matrix, projection_matrix);
460 effect_sprite->updateCamera(image_index, view_matrix, projection_matrix);
463 star_sprite->render(cmd, image_index);
464 star_sprite->clearQueue();
466 draw_asteroids(image_index);
467 if (!first_person_camera && !camera_transition_active) {
468 draw_ship(image_index);
469 draw_engine_flame(cmd, extent, last_ship_model_matrix, ship.current_speed, ship.visible && !ship.exploding);
471 if (multiplayer_match) {
473 if (player == multiplayer.local_player_id() || !multiplayer.player_connected()[player]) {
476 draw_remote_ship(image_index, player);
477 draw_engine_flame(cmd, extent, last_remote_ship_model_matrices[player], std::max(remote_ships[player].current_speed, 1.0f),
478 remote_ships[player].visible && !remote_ship_exploding[player]);
482 if (multiplayer_match) {
483 draw_remote_projectiles();
486 projectile_sprite->render(cmd, image_index);
487 projectile_sprite->clearQueue();
488 effect_sprite->render(cmd, image_index);
489 effect_sprite->clearQueue();
498 std::string asset_root;
499 std::string shader_root;
500 std::chrono::steady_clock::time_point last_frame_time = std::chrono::steady_clock::now();
501 float elapsed_seconds = 0.0f;
503 float intro_fade = 1.0f;
504 Uint32 intro_last_update_ms = 0;
505 float loading_rain_opacity = 1.0f;
506 static constexpr int INTRO_RAIN_TEXTURE_WIDTH = 1280;
507 static constexpr int INTRO_RAIN_TEXTURE_HEIGHT = 720;
508 bool loading_black_frame_pending =
false;
509 bool loading_black_frame_shown =
false;
510 bool restart_after_intro =
false;
511 enum class LobbyPage {
516 LobbyPage lobby_page = LobbyPage::Main;
517 int lobby_selection = 0;
518 int lobby_edit_field = -1;
519 std::string lobby_player_name =
"Pilot 1";
520 std::string lobby_host_address =
"127.0.0.1";
521 std::string lobby_port =
"48120";
522 std::string lobby_join_code{};
523 std::string lobby_status =
"Choose how you want to play.";
524 float lobby_camera_distance = 0.0f;
525 bool debug_menu =
false;
526 bool inverted_controls =
false;
527 bool mouse_look_controls =
false;
528 bool mouse_capture_active =
false;
529 bool ignore_next_mouse_motion =
false;
530 bool first_person_camera =
false;
531 bool camera_transition_active =
false;
532 bool crt_enabled =
false;
533 bool background_music_disabled =
false;
534 bool ship_returning_to_field =
false;
535 float keyboard_yaw = 0.0f;
536 float keyboard_pitch = 0.0f;
537 float keyboard_roll = 0.0f;
538 float smooth_yaw = 0.0f;
539 float smooth_pitch = 0.0f;
540 float smooth_roll = 0.0f;
541 float return_message_cooldown = 0.0f;
542 float camera_transition_elapsed = 0.0f;
543 static constexpr float CAMERA_TRANSITION_SECONDS = 0.75f;
544 static constexpr float MOUSE_LOOK_SENSITIVITY = 0.04f;
547 std::array<Ship, NETWORK_PLAYER_COUNT> remote_ships{};
548 std::array<Projectile, MAX_PROJECTILES> projectiles{};
549 std::array<Asteroid, MAX_ASTEROIDS> asteroids{};
550 std::array<Particle, MAX_PARTICLES> particles{};
551 StarField star_field{};
552 glm::vec3 camera_position{0.0f, 1.6f, 6.0f};
553 glm::vec3 camera_target_position{0.0f, 1.6f, 0.0f};
554 glm::vec3 camera_up_vector{0.0f, 1.0f, 0.0f};
555 glm::vec3 camera_transition_start_position{0.0f, 1.6f, 6.0f};
556 glm::vec3 camera_transition_start_target{0.0f, 1.6f, 0.0f};
557 glm::vec3 camera_transition_start_up{0.0f, 1.0f, 0.0f};
558 glm::mat4 view_matrix{1.0f};
559 glm::mat4 projection_matrix{1.0f};
561 mxvk::VKAbstractModel ship_model{};
562 std::array<mxvk::VKAbstractModel, NETWORK_PLAYER_COUNT> remote_ship_models{};
563 std::array<mxvk::VKAbstractModel, MAX_ASTEROIDS> asteroid_models{};
564 mxvk::VK_Sprite3D *star_sprite =
nullptr;
565 mxvk::VK_Sprite3D *projectile_sprite =
nullptr;
566 mxvk::VK_Sprite3D *effect_sprite =
nullptr;
567 mxvk::VK_Sprite *intro_sprite =
nullptr;
568 mxvk::VK_Sprite *ui_pixel =
nullptr;
569 std::unique_ptr<matrix::Rain> intro_rain{};
570 mxvk::Font lobby_status_font{};
571 mxvk::Font lobby_roster_font{};
572 mxvk::VK_Console console;
573 mxvk::VK_Controller controller;
574#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
575 std::unique_ptr<mxvk::VK_Mixer> sound_effects{};
576 int background_music_track = -1;
577 int crash_sound = -1;
578 int cannon_sound = -1;
579 int asteroid_explosion_sound = -1;
581 bool console_ready =
false;
582 std::atomic<bool> game_resources_loaded{
false};
583 std::atomic<bool> loading_failed{
false};
584 std::atomic<bool> model_preload_done{
false};
585 std::atomic<bool> model_preload_failed{
false};
586 int last_font_size = 0;
587 int lobby_status_font_size = 16;
588 int lobby_roster_font_size = 16;
590 std::atomic<int> loading_step_index{0};
592 glm::mat4 last_ship_model_matrix{1.0f};
593 std::array<glm::mat4, NETWORK_PLAYER_COUNT> last_remote_ship_model_matrices{};
594 VkBuffer flame_vertex_buffer = VK_NULL_HANDLE;
595 VkDeviceMemory flame_vertex_buffer_memory = VK_NULL_HANDLE;
596 VkPipeline flame_pipeline = VK_NULL_HANDLE;
597 VkPipelineLayout flame_pipeline_layout = VK_NULL_HANDLE;
598 std::thread loading_thread{};
599 std::string loading_error{};
600 std::mutex prepared_model_mutex{};
601 std::optional<mxvk::MXModel> prepared_ship_model{};
602 std::array<std::optional<mxvk::MXModel>,
MAX_ASTEROIDS> prepared_asteroid_models{};
603 std::array<std::string, MAX_ASTEROIDS> prepared_asteroid_texture_paths{};
604 uint32_t flame_vertex_count = 0;
605 MultiplayerSession multiplayer{};
606 bool multiplayer_match =
false;
607 float multiplayer_collision_grace = 0.0f;
608 std::array<std::uint32_t, NETWORK_PLAYER_COUNT> multiplayer_kills{};
609 std::array<std::string, NETWORK_PLAYER_COUNT> multiplayer_player_names{};
610 std::array<std::uint32_t, NETWORK_PLAYER_COUNT> multiplayer_death_serials{};
611 std::array<std::uint32_t, NETWORK_PLAYER_COUNT> received_death_serials{};
612 std::uint8_t multiplayer_winner = 0;
613 std::array<bool, NETWORK_PLAYER_COUNT> remote_ship_exploding{};
614 std::array<float, NETWORK_PLAYER_COUNT> remote_explosion_timers{};
615 std::array<std::array<NetworkProjectile, NETWORK_PROJECTILE_COUNT>,
NETWORK_PLAYER_COUNT> remote_projectiles{};
616 std::array<std::uint32_t, MAX_PROJECTILES> projectile_ids{};
617 std::uint32_t next_projectile_id = 1;
618 std::array<std::unordered_set<std::uint32_t>,
NETWORK_PLAYER_COUNT> consumed_remote_projectiles{};
619 std::array<std::uint32_t, NETWORK_PLAYER_COUNT> consumed_projectile_ids{};
620 bool host_requested_start =
false;
622 void log_game(
const std::string &message, SDL_Color color = SDL_Color{180, 220, 255, 255}) {
623 if (!console_ready) {
626 console.printLine(
"[game] " + message, color);
629 void configure_console() {
630 console.attach(*
this, asset_root +
"/data/font.ttf", 20);
631 console.setSpriteYOriginTopLeft(
true);
632 console.setPrompt(
"asteroids> ");
633 console_ready =
true;
634 console.printLine(
"Press F3 to open/close the console.");
635 console.printLine(
"Type 'help' for asteroids3d commands.");
636 log_game(
"Console attached.");
637 log_game(
"asteroids3d initialized.");
638 console.setCommandCallback([
this](mxvk::VK_Window &,
const std::vector<std::string> &args, std::ostream &out) {
643 const std::string &cmd = args.front();
645 out <<
"asteroids3d commands:\n"
646 <<
" clear Clear console output\n"
647 <<
" echo <text> Print text to the console\n"
648 <<
" status Print score, lives, mode, and asteroid count\n"
649 <<
" restart Restart the game\n"
650 <<
" intro Return to the intro screen\n"
651 <<
" play Start or resume play\n"
652 <<
" debug Toggle debug HUD\n"
653 <<
" controls Toggle arcade/inverted pitch controls\n"
654 <<
" input Toggle classic keyboard versus keyboard/mouse controls\n"
655 <<
" about Print program banner\n"
656 <<
" quit / exit Close the window\n";
661 for (std::size_t i = 1; i < args.size(); ++i) {
670 if (cmd ==
"status") {
676 out <<
"Mode: " << mode_name <<
'\n'
677 <<
"Score: " << ship.score <<
'\n'
678 <<
"Lives: " << std::max(0, ship.lives) <<
'\n'
679 <<
"Asteroids: " << active_asteroids() <<
'\n'
680 <<
"Time left: " << format_round_time() <<
'\n'
681 <<
"Speed: " << ship.current_speed <<
" / " << ship.max_speed <<
'\n'
682 <<
"Control scheme: " << (mouse_look_controls ?
"keyboard/mouse" :
"classic keyboard") <<
'\n'
683 <<
"Camera: " << (first_person_camera ?
"first person" :
"chase") <<
'\n'
684 <<
"Controls: " << (inverted_controls ?
"inverted" :
"arcade") <<
'\n'
685 <<
"Controller: " << controller_status() <<
'\n'
686 <<
"Debug HUD: " << (debug_menu ?
"on" :
"off") <<
'\n';
690 if (cmd ==
"restart") {
693 log_game(
"Game restarted from console.");
694 out <<
"Game restarted.";
698 if (cmd ==
"intro") {
699 reset_intro_screen();
700 log_game(
"Returned to intro screen from console.");
701 out <<
"Intro screen active.";
707 log_game(
"Play mode activated from console.");
712 if (cmd ==
"debug") {
713 debug_menu = !debug_menu;
714 log_game(std::string(
"Debug HUD ") + (debug_menu ?
"enabled from console." :
"disabled from console."));
715 out <<
"Debug HUD " << (debug_menu ?
"enabled." :
"disabled.");
719 if (cmd ==
"controls") {
720 inverted_controls = !inverted_controls;
721 log_game(std::string(
"Controls set to ") + (inverted_controls ?
"inverted from console." :
"arcade from console."));
722 out <<
"Controls set to " << (inverted_controls ?
"inverted." :
"arcade.");
726 if (cmd ==
"input") {
727 set_mouse_look_controls(!mouse_look_controls);
728 log_game(std::string(
"Control scheme set to ") + (mouse_look_controls ?
"keyboard/mouse from console." :
"classic keyboard from console."));
729 out <<
"Control scheme set to " << (mouse_look_controls ?
"keyboard/mouse." :
"classic keyboard.");
733 if (cmd ==
"about") {
734 out <<
"asteroids3d: MXVK port of gl_asteroids.\n";
738 if (cmd ==
"quit" || cmd ==
"exit") {
739 log_game(
"Exit requested from console.");
740 out <<
"Closing window...";
749 bool open_controller() {
751 if (controller.open(i)) {
752 log_game(
"Controller connected: " + controller.name());
759 void sync_controller_connection() {
760 if (!controller.active()) {
765 std::string controller_status()
const {
766 return controller.active() ? (
"Connected: " + controller.name()) :
"Disconnected";
769 float controller_axis(SDL_GamepadAxis axis)
const {
770 if (!controller.active()) {
774 const Sint16 raw_value = controller.getAxis(axis);
775 const float magnitude =
static_cast<float>(std::abs(
static_cast<int>(raw_value)));
784 const float curved = normalized * normalized;
785 return raw_value < 0 ? -curved : curved;
788 void set_mouse_look_controls(
bool enabled) {
789 mouse_look_controls = enabled;
791 keyboard_pitch = 0.0f;
792 keyboard_roll = 0.0f;
796 sync_mouse_capture();
799 void sync_mouse_capture() {
800 const bool should_capture = mouse_look_controls && mode ==
GameMode::Playing && !console.isVisible();
801 if (should_capture == mouse_capture_active) {
805 SDL_SetWindowRelativeMouseMode(
getSDLWindow(), should_capture);
806 mouse_capture_active = should_capture;
807 ignore_next_mouse_motion = should_capture;
810 void apply_mouse_look(
float delta_x,
float delta_y) {
811 ship.rotation.y -= delta_x * MOUSE_LOOK_SENSITIVITY;
812 const float pitch_delta = inverted_controls ? delta_y * MOUSE_LOOK_SENSITIVITY : -delta_y * MOUSE_LOOK_SENSITIVITY;
813 ship.rotation.x = std::clamp(ship.rotation.x + pitch_delta, -75.0f, 75.0f);
816#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
817 std::string asteroids3d_asset_path(
const std::string &filename)
const {
818 return asset_root +
"/data/" + filename;
821 std::string sound_effect_path(
const std::string &filename)
const {
822 return asset_root +
"/data/" + filename;
825 void load_sound_effects() {
826 sound_effects = std::make_unique<mxvk::VK_Mixer>();
827 if (!background_music_disabled) {
828 background_music_track = sound_effects->loadMusic(asteroids3d_asset_path(
"music.ogg"));
830 crash_sound = sound_effects->loadWav(sound_effect_path(
"crash.wav"));
831 cannon_sound = sound_effects->loadWav(asteroids3d_asset_path(
"cannon.wav"));
832 asteroid_explosion_sound = sound_effects->loadWav(sound_effect_path(
"asteroid.wav"));
835 void ensure_background_music_playing() {
836 if (background_music_disabled || !sound_effects || background_music_track < 0) {
839 if (!sound_effects->isMusicPlaying(background_music_track)) {
840 if (sound_effects->playMusic(background_music_track, -1) != 0) {
841 throw mxvk::Exception(
"Could not start asteroids3d background music");
846 void play_sound(
int sound_id) {
847 if (!sound_effects || sound_id < 0) {
850 sound_effects->playWav(sound_id);
854 void load_loading_screen_resources() {
855 set_ui_font_size(18);
856 lobby_status_font.reset(asset_root +
"/data/font.ttf", lobby_status_font_size);
857 lobby_roster_font.reset(asset_root +
"/data/font.ttf", lobby_roster_font_size);
860 asset_root +
"/data/intro.png",
861 asset_root +
"/data/sprite.vert.spv",
862 shader_root +
"/intro.frag.spv");
864 intro_rain_config.
color =
"#ff0000";
867 intro_rain = std::make_unique<matrix::Rain>(*
this, std::move(intro_rain_config));
868 reset_intro_screen();
869 loading_step_index.store(0, std::memory_order_relaxed);
870 game_resources_loaded.store(
false, std::memory_order_relaxed);
871 loading_failed.store(
false, std::memory_order_relaxed);
874 void start_loading_async() {
875 if (loading_thread.joinable()) {
876 loading_thread.join();
878 loading_thread = std::thread([
this]() {
881 }
catch (
const std::exception &e) {
882 loading_error = e.what();
883 model_preload_failed.store(
true, std::memory_order_release);
885 loading_error =
"unknown loading error";
886 model_preload_failed.store(
true, std::memory_order_release);
891 void preload_models() {
892 std::optional<mxvk::MXModel> ship_model_cpu;
893 ship_model_cpu.emplace();
894 ship_model_cpu->load(asset_root +
"/data/starship.obj", 1.0f);
896 std::array<std::optional<mxvk::MXModel>,
MAX_ASTEROIDS> asteroid_models_cpu{};
897 std::array<std::string, MAX_ASTEROIDS> asteroid_texture_paths{};
899 std::mt19937
rng(std::random_device{}());
900 std::uniform_int_distribution<int> rock_variant_dist(0, 2);
901 for (std::size_t slot_index = 0; slot_index <
MAX_ASTEROIDS; ++slot_index) {
902 static constexpr std::array<const char *, 3> asteroid_paths = {
904 "data/asteroid2.obj",
905 "data/asteroid3.obj",
908 const std::size_t model_variant = slot_index % asteroid_paths.size();
910 if (model_variant == 0) {
912 }
else if (model_variant == 1) {
915 texture_path = (rock_variant_dist(
rng) == 0) ? asset_root +
"/data/rock.tex" : asset_root +
"/data/rock2.tex";
918 asteroid_models_cpu[slot_index].emplace();
919 asteroid_models_cpu[slot_index]->load(asset_root +
"/" + asteroid_paths[model_variant], 1.0f);
924 std::lock_guard<std::mutex> lock(prepared_model_mutex);
925 prepared_ship_model = std::move(ship_model_cpu);
926 prepared_asteroid_models = std::move(asteroid_models_cpu);
927 prepared_asteroid_texture_paths = std::move(asteroid_texture_paths);
930 model_preload_done.store(
true, std::memory_order_release);
933 int lobby_item_count()
const {
934 if (lobby_page == LobbyPage::Host) {
937 if (lobby_page == LobbyPage::Join) {
943 void stop_lobby_editing() {
944 lobby_edit_field = -1;
948 void activate_lobby_item() {
949 if (lobby_page == LobbyPage::Main) {
950 if (lobby_selection == 0) {
951 lobby_page = LobbyPage::Host;
953 lobby_status =
"Set your pilot name and listen port.";
954 }
else if (lobby_selection == 1) {
955 lobby_page = LobbyPage::Join;
957 lobby_status =
"Enter the host address to join the match.";
964 const bool host_page = lobby_page == LobbyPage::Host;
965 const int action_item = host_page ? 2 : 4;
966 const int back_item = host_page ? 3 : 5;
967 if (lobby_selection < action_item) {
968 lobby_edit_field = lobby_selection;
970 lobby_status =
"Editing field. Press Enter when finished.";
971 }
else if (lobby_selection == action_item) {
972 stop_lobby_editing();
973 if (lobby_player_name.empty() || lobby_port.empty() ||
974 (!host_page && (lobby_host_address.empty() || lobby_join_code.size() != 8U))) {
975 lobby_status =
"Complete every field before continuing.";
980 if (multiplayer.active() && multiplayer.is_host()) {
981 if (multiplayer.player_count() < 2U) {
982 lobby_status =
"At least one other player must join before starting.";
984 host_requested_start =
true;
985 lobby_status =
"Starting match...";
988 multiplayer.host(lobby_port, lobby_player_name);
989 host_requested_start =
false;
990 lobby_status = multiplayer.port_mapping_status();
993 if (multiplayer.active()) {
994 lobby_status =
"Connected. Waiting for the host to start the match.";
996 multiplayer.join(lobby_host_address, lobby_port, lobby_player_name, lobby_join_code);
997 lobby_status =
"Connecting to " + lobby_host_address +
":" + lobby_port +
"...";
1000 }
catch (
const std::exception &error) {
1002 lobby_status = std::string(
"Network error: ") + error.what();
1004 }
else if (lobby_selection == back_item) {
1005 stop_lobby_editing();
1007 host_requested_start =
false;
1008 lobby_page = LobbyPage::Main;
1009 lobby_selection = 0;
1010 lobby_status =
"Choose how you want to play.";
1014 void handle_lobby_event(
const SDL_Event &
event) {
1015 if (
event.type == SDL_EVENT_TEXT_INPUT && lobby_edit_field >= 0) {
1016 std::string *field = &lobby_player_name;
1017 if (lobby_page == LobbyPage::Join && lobby_edit_field == 1) {
1018 field = &lobby_host_address;
1019 }
else if ((lobby_page == LobbyPage::Host && lobby_edit_field == 1) ||
1020 (lobby_page == LobbyPage::Join && lobby_edit_field == 2)) {
1021 field = &lobby_port;
1022 }
else if (lobby_page == LobbyPage::Join && lobby_edit_field == 3) {
1023 field = &lobby_join_code;
1025 if (field == &lobby_join_code) {
1026 for (
const char character : std::string(
event.text.text)) {
1027 if (field->size() < 8U && std::isalnum(
static_cast<unsigned char>(character))) {
1028 field->push_back(
static_cast<char>(std::toupper(
static_cast<unsigned char>(character))));
1031 }
else if (field->size() < 32U) {
1032 *field +=
event.text.text;
1037 if (
event.type == SDL_EVENT_KEY_DOWN) {
1038 if (lobby_edit_field >= 0) {
1039 std::string *field = &lobby_player_name;
1040 if (lobby_page == LobbyPage::Join && lobby_edit_field == 1) {
1041 field = &lobby_host_address;
1042 }
else if ((lobby_page == LobbyPage::Host && lobby_edit_field == 1) ||
1043 (lobby_page == LobbyPage::Join && lobby_edit_field == 2)) {
1044 field = &lobby_port;
1045 }
else if (lobby_page == LobbyPage::Join && lobby_edit_field == 3) {
1046 field = &lobby_join_code;
1048 if (
event.key.key == SDLK_BACKSPACE && !field->empty()) {
1050 }
else if (
event.key.key == SDLK_RETURN ||
event.key.key == SDLK_KP_ENTER ||
event.key.key == SDLK_ESCAPE) {
1051 stop_lobby_editing();
1052 lobby_status =
"Setup updated.";
1056 if (
event.key.key == SDLK_UP ||
event.key.key == SDLK_W) {
1057 lobby_selection = (lobby_selection + lobby_item_count() - 1) % lobby_item_count();
1058 }
else if (
event.key.key == SDLK_DOWN ||
event.key.key == SDLK_S) {
1059 lobby_selection = (lobby_selection + 1) % lobby_item_count();
1060 }
else if (
event.key.key == SDLK_RETURN ||
event.key.key == SDLK_KP_ENTER ||
event.key.key == SDLK_SPACE) {
1061 activate_lobby_item();
1062 }
else if (
event.key.key == SDLK_ESCAPE) {
1063 if (lobby_page == LobbyPage::Main) {
1067 host_requested_start =
false;
1068 lobby_page = LobbyPage::Main;
1069 lobby_selection = 0;
1070 lobby_status =
"Choose how you want to play.";
1076 if (
event.type == SDL_EVENT_MOUSE_MOTION ||
event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
1078 const int panel_width = 760;
1079 const int panel_x =
static_cast<int>(extent.width) / 2 - panel_width / 2;
1080 const int first_y = 250;
1081 const int item_height = 50;
1082 const float mouse_x =
event.type == SDL_EVENT_MOUSE_MOTION ?
event.motion.x :
event.button.x;
1083 const float mouse_y =
event.type == SDL_EVENT_MOUSE_MOTION ?
event.motion.y :
event.button.y;
1084 const int item = (
static_cast<int>(mouse_y) - first_y) / item_height;
1085 if (mouse_x >= panel_x + 20 && mouse_x <= panel_x + panel_width - 20 &&
1086 mouse_y >= first_y && item >= 0 && item < lobby_item_count()) {
1087 lobby_selection = item;
1088 if (
event.type == SDL_EVENT_MOUSE_BUTTON_DOWN &&
event.button.button == SDL_BUTTON_LEFT) {
1089 activate_lobby_item();
1095 NetworkState make_network_state(
bool match_started)
const {
1096 NetworkState state{};
1097 state.position = {ship.position.x, ship.position.y, ship.position.z};
1098 state.rotation = {ship.rotation.x, ship.rotation.y, ship.rotation.z};
1099 state.current_speed = ship.current_speed;
1100 state.exploding = ship.exploding ? 1U : 0U;
1101 state.match_started = match_started ? 1U : 0U;
1102 state.kills = multiplayer_kills;
1103 state.death_serials = multiplayer_death_serials;
1104 state.consumed_projectile_ids = consumed_projectile_ids;
1105 state.winner = multiplayer_winner;
1106 std::size_t output_index = 0;
1107 for (std::size_t projectile_index = 0; projectile_index < projectiles.size(); ++projectile_index) {
1108 const Projectile &projectile = projectiles[projectile_index];
1109 if (!projectile.
active || output_index >= state.projectiles.size()) {
1112 NetworkProjectile &output = state.projectiles[output_index++];
1113 output.id = projectile_ids[projectile_index];
1114 output.position = {projectile.position.
x, projectile.position.
y, projectile.position.z};
1115 output.velocity = {projectile.velocity.
x, projectile.velocity.
y, projectile.velocity.z};
1116 output.lifetime = projectile.
lifetime;
1119 if (multiplayer.is_host()) {
1120 std::size_t asteroid_output_index = 0;
1121 for (std::size_t asteroid_index = 0; asteroid_index < asteroids.size(); ++asteroid_index) {
1122 const Asteroid &asteroid = asteroids[asteroid_index];
1123 if (!asteroid.
active || asteroid_output_index >= state.asteroids.size()) {
1126 NetworkAsteroid &output = state.asteroids[asteroid_output_index++];
1127 output.position = {asteroid.position.
x, asteroid.position.
y, asteroid.position.z};
1128 output.rotation = {asteroid.rotation.
x, asteroid.rotation.
y, asteroid.rotation.z};
1129 output.radius = asteroid.
radius;
1130 output.slot =
static_cast<std::uint8_t
>(asteroid_index);
1137 void apply_remote_state(std::uint8_t player,
const NetworkState &state) {
1141 Ship &remote_ship = remote_ships[player];
1142 remote_ship.prev_position = remote_ship.position;
1143 remote_ship.position = {state.position[0], state.position[1], state.position[2]};
1144 remote_ship.rotation = {state.rotation[0], state.rotation[1], state.rotation[2]};
1145 remote_ship.current_speed = state.current_speed;
1146 remote_ship.visible = state.
exploding == 0U;
1147 remote_projectiles[player] = state.projectiles;
1148 if (!multiplayer.is_host() && player == 0) {
1149 for (Asteroid &asteroid : asteroids) {
1150 asteroid.active =
false;
1152 for (
const NetworkAsteroid &network_asteroid : state.asteroids) {
1153 if (network_asteroid.active == 0U || network_asteroid.slot >= asteroids.size()) {
1156 Asteroid &asteroid = asteroids[network_asteroid.slot];
1157 asteroid.position = {network_asteroid.position[0], network_asteroid.position[1], network_asteroid.position[2]};
1158 asteroid.rotation = {network_asteroid.rotation[0], network_asteroid.rotation[1], network_asteroid.rotation[2]};
1159 asteroid.
radius = network_asteroid.radius;
1165 void begin_multiplayer_match() {
1166 multiplayer_match =
true;
1167 multiplayer_collision_grace = 2.5f;
1168 multiplayer_kills = {};
1169 multiplayer_player_names = multiplayer.player_names();
1170 multiplayer_death_serials = {};
1171 received_death_serials = {};
1172 multiplayer_winner = 0;
1173 remote_ship_exploding = {};
1174 remote_explosion_timers = {};
1175 for (
auto &consumed : consumed_remote_projectiles)
1177 consumed_projectile_ids = {};
1178 clear_round_state();
1180 const std::uint8_t local_id = std::min<std::uint8_t>(multiplayer.local_player_id(), 3U);
1183 ship.current_speed = 6.0f;
1184 ship.prev_position = ship.position;
1188 remote_ships[player].current_speed = 6.0f;
1189 remote_ships[player].visible = multiplayer.player_connected()[player];
1191 if (multiplayer.is_host()) {
1192 spawn_initial_asteroids();
1194 for (Asteroid &asteroid : asteroids) {
1195 asteroid.active =
false;
1199 log_game(std::format(
"UDP match started with {} players.", multiplayer.player_count()));
1202 void update_lobby_network() {
1203 if (!multiplayer.active()) {
1206 const NetworkExchange exchange = multiplayer.exchange(make_network_state(host_requested_start), last_delta_time);
1207 for (
const NetworkPlayerUpdate &update : exchange.players) {
1208 apply_remote_state(update.player_id, update.state);
1209 if (!multiplayer.is_host() && update.player_id == 0 && update.state.match_started != 0U) {
1210 begin_multiplayer_match();
1214 if (multiplayer.is_host() && host_requested_start) {
1215 multiplayer.exchange(make_network_state(
true), 1.0f / 20.0f);
1216 begin_multiplayer_match();
1220 static float projectile_distance_to_ship(
const Projectile &projectile,
const glm::vec3 &target) {
1221 const glm::vec3 segment = projectile.position - projectile.prev_position;
1222 const float length_squared = glm::dot(segment, segment);
1223 if (length_squared <= 1e-6f) {
1224 return glm::length(projectile.position - target);
1226 const float amount = std::clamp(glm::dot(target - projectile.prev_position, segment) / length_squared, 0.0f, 1.0f);
1227 return glm::length(projectile.prev_position + segment * amount - target);
1230 void start_multiplayer_local_explosion() {
1231 if (ship.exploding) {
1234 start_ship_explosion();
1238 void finish_multiplayer_match(std::uint8_t winner) {
1239 multiplayer_winner = winner;
1240 multiplayer.exchange(make_network_state(
true), 1.0f / 20.0f);
1244 void finish_abandoned_multiplayer_match() {
1245 const auto winner = std::max_element(multiplayer_kills.begin(), multiplayer_kills.end());
1246 const std::uint8_t winner_id =
static_cast<std::uint8_t
>(std::distance(multiplayer_kills.begin(), winner) + 1);
1247 log_game(std::format(
"All opponents disconnected. Player {} wins with {} kills.", winner_id, *winner));
1248 finish_multiplayer_match(winner_id);
1252 void update_multiplayer_legacy(
float dt) {
1253 const std::optional<NetworkState> state = multiplayer.exchange(make_network_state(
true), dt);
1254 if (state.has_value()) {
1255 apply_remote_state(*state);
1256 if (!multiplayer.is_host()) {
1257 host_kills = state->host_kills;
1258 client_kills = state->client_kills;
1259 multiplayer_winner = state->winner;
1260 if (state->consumed_client_projectile_id != 0U) {
1261 for (std::size_t projectile_index = 0; projectile_index < projectile_ids.size(); ++projectile_index) {
1262 if (projectile_ids[projectile_index] == state->consumed_client_projectile_id) {
1263 projectiles[projectile_index].active =
false;
1267 if (state->client_death_serial != received_client_death_serial) {
1268 received_client_death_serial = state->client_death_serial;
1269 start_multiplayer_local_explosion();
1271 if (state->host_death_serial != received_host_death_serial) {
1272 received_host_death_serial = state->host_death_serial;
1273 remote_ship_exploding =
true;
1274 remote_explosion_timer = 1.5f;
1275 spawn_ship_explosion(remote_ship.position);
1277 if (multiplayer_winner != 0U) {
1284 if (remote_ship_exploding) {
1285 remote_explosion_timer -= dt;
1286 if (remote_explosion_timer <= 0.0f) {
1287 remote_ship_exploding =
false;
1291 if (!multiplayer.is_host() || !state.has_value()) {
1295 constexpr float SHIP_TO_SHIP_COLLISION_RADIUS = 2.4f;
1296 if (!ship.exploding && !remote_ship_exploding && remote_ship.visible &&
1297 glm::length(ship.position - remote_ship.position) < SHIP_TO_SHIP_COLLISION_RADIUS) {
1298 const glm::vec3 remote_collision_position = remote_ship.position;
1299 start_multiplayer_local_explosion();
1300 remote_ship_exploding =
true;
1301 remote_ship.visible =
false;
1302 remote_explosion_timer = 1.5f;
1303 ++client_death_serial;
1304 spawn_ship_explosion(remote_collision_position);
1305 log_game(
"Ship collision: both pilots destroyed. No kill awarded.", SDL_Color{255, 170, 80, 255});
1309 if (!remote_ship_exploding) {
1310 for (
const Asteroid &asteroid : asteroids) {
1311 if (asteroid.active && glm::length(asteroid.position - remote_ship.position) < asteroid.radius + 1.8f) {
1312 remote_ship_exploding =
true;
1313 remote_explosion_timer = 1.5f;
1314 ++client_death_serial;
1315 spawn_ship_explosion(remote_ship.position);
1316 log_game(
"Enemy collided with an asteroid.", SDL_Color{255, 170, 80, 255});
1322 for (
const NetworkProjectile &projectile : remote_projectiles) {
1323 if (projectile.
active == 0U || consumed_remote_projectiles.contains(projectile.id)) {
1326 const glm::vec3 projectile_position{projectile.position[0], projectile.position[1], projectile.position[2]};
1327 for (Asteroid &asteroid : asteroids) {
1329 consumed_remote_projectiles.insert(projectile.id);
1330 consumed_client_projectile_id = projectile.id;
1331 split_asteroid(asteroid);
1337 if (!remote_ship_exploding) {
1338 for (Projectile &projectile : projectiles) {
1339 if (projectile.
active && projectile_distance_to_ship(projectile, remote_ship.position) < 1.8f) {
1340 projectile.
active =
false;
1341 remote_ship_exploding =
true;
1342 remote_explosion_timer = 1.5f;
1344 ++client_death_serial;
1345 spawn_ship_explosion(remote_ship.position);
1346 log_game(std::format(
"Enemy destroyed. Score {}-{}.", host_kills, client_kills));
1347 if (host_kills >= MULTIPLAYER_KILLS_TO_WIN) {
1348 finish_multiplayer_match(1U);
1355 if (!ship.exploding) {
1356 for (
const NetworkProjectile &projectile : remote_projectiles) {
1357 if (projectile.
active == 0U || consumed_remote_projectiles.contains(projectile.id)) {
1360 const glm::vec3 position{projectile.position[0], projectile.position[1], projectile.position[2]};
1361 if (glm::length(position - ship.position) < 1.8f) {
1362 consumed_remote_projectiles.insert(projectile.id);
1363 consumed_client_projectile_id = projectile.id;
1364 start_multiplayer_local_explosion();
1366 log_game(std::format(
"You were destroyed. Score {}-{}.", host_kills, client_kills));
1367 if (client_kills >= MULTIPLAYER_KILLS_TO_WIN) {
1368 finish_multiplayer_match(2U);
1378 void update_multiplayer(
float dt) {
1379 multiplayer_collision_grace = std::max(0.0f, multiplayer_collision_grace - dt);
1380 const NetworkExchange exchange = multiplayer.exchange(make_network_state(
true), dt);
1381 for (
const NetworkPlayerUpdate &update : exchange.players) {
1382 apply_remote_state(update.player_id, update.state);
1383 if (!multiplayer.is_host() && update.player_id == 0) {
1384 multiplayer_kills = update.state.kills;
1385 multiplayer_death_serials = update.state.death_serials;
1386 consumed_projectile_ids = update.state.consumed_projectile_ids;
1387 multiplayer_winner = update.state.winner;
1391 if (multiplayer.player_count() <= 1U) {
1392 finish_abandoned_multiplayer_match();
1396 const std::uint8_t local_id = multiplayer.local_player_id();
1399 if (!multiplayer.is_host()) {
1400 if (consumed_projectile_ids[local_id] != 0U) {
1401 for (std::size_t index = 0; index < projectile_ids.size(); ++index) {
1402 if (projectile_ids[index] == consumed_projectile_ids[local_id])
1403 projectiles[index].active =
false;
1406 if (multiplayer_death_serials[local_id] != received_death_serials[local_id]) {
1407 received_death_serials[local_id] = multiplayer_death_serials[local_id];
1408 start_multiplayer_local_explosion();
1410 if (multiplayer_winner != 0U)
1415 if (player == local_id)
1417 if (multiplayer_death_serials[player] != received_death_serials[player]) {
1418 received_death_serials[player] = multiplayer_death_serials[player];
1419 remote_ship_exploding[player] =
true;
1420 remote_explosion_timers[player] = 1.5f;
1421 spawn_ship_explosion(remote_ships[player].position);
1423 if (remote_ship_exploding[player]) {
1424 remote_explosion_timers[player] -= dt;
1425 if (remote_explosion_timers[player] <= 0.0f)
1426 remote_ship_exploding[player] =
false;
1429 if (!multiplayer.is_host())
1432 auto destroy_player = [
this](std::uint8_t target) {
1434 start_multiplayer_local_explosion();
1436 remote_ship_exploding[target] =
true;
1437 ++multiplayer_death_serials[target];
1439 spawn_ship_explosion(target == 0 ? ship.position : remote_ships[target].position);
1441 auto player_position = [
this](std::uint8_t player) {
1442 return player == 0 ? ship.position : remote_ships[player].position;
1444 auto player_exploding = [
this](std::uint8_t player) {
1445 return player == 0 ? ship.exploding : remote_ship_exploding[player];
1448 if (multiplayer_collision_grace <= 0.0f) {
1449 constexpr float SHIP_COLLISION_RADIUS = 2.4f;
1451 if (!multiplayer.player_connected()[first] || player_exploding(first))
1454 if (!multiplayer.player_connected()[second] || player_exploding(second))
1456 if (glm::length(player_position(first) - player_position(second)) < SHIP_COLLISION_RADIUS) {
1457 destroy_player(first);
1458 destroy_player(second);
1465 if (!multiplayer.player_connected()[player] || player_exploding(player))
1467 for (
const Asteroid &asteroid : asteroids) {
1468 if (asteroid.active && glm::length(asteroid.position - player_position(player)) < asteroid.radius + 1.8f) {
1469 destroy_player(player);
1475 for (Projectile &projectile : projectiles) {
1479 if (multiplayer.player_connected()[target] && !player_exploding(target) &&
1480 projectile_distance_to_ship(projectile, player_position(target)) < 1.8f) {
1481 projectile.
active =
false;
1482 ++multiplayer_kills[0];
1483 destroy_player(target);
1484 if (multiplayer_kills[0] >= MULTIPLAYER_KILLS_TO_WIN)
1485 finish_multiplayer_match(1U);
1492 if (!multiplayer.player_connected()[shooter])
1494 for (
const NetworkProjectile &projectile : remote_projectiles[shooter]) {
1495 if (projectile.
active == 0U || consumed_remote_projectiles[shooter].contains(projectile.id))
1497 const glm::vec3 position{projectile.position[0], projectile.position[1], projectile.position[2]};
1498 bool consumed =
false;
1500 if (target == shooter || !multiplayer.player_connected()[target] || player_exploding(target))
1502 if (glm::length(position - player_position(target)) < 1.8f) {
1503 consumed_remote_projectiles[shooter].insert(projectile.id);
1504 consumed_projectile_ids[shooter] = projectile.id;
1505 ++multiplayer_kills[shooter];
1506 destroy_player(target);
1507 if (multiplayer_kills[shooter] >= MULTIPLAYER_KILLS_TO_WIN)
1508 finish_multiplayer_match(shooter + 1U);
1515 for (Asteroid &asteroid : asteroids) {
1517 consumed_remote_projectiles[shooter].insert(projectile.id);
1518 consumed_projectile_ids[shooter] = projectile.id;
1519 split_asteroid(asteroid);
1527 void draw_lobby(VkCommandBuffer cmd, uint32_t image_index,
const VkExtent2D &extent,
float aspect) {
1528 update_lobby_network();
1529 lobby_camera_distance += last_delta_time * 4.5f;
1531 std::sin(elapsed_seconds * 0.18f) * 1.8f,
1532 std::cos(elapsed_seconds * 0.13f) * 0.8f,
1533 4.0f - lobby_camera_distance,
1535 const glm::vec3 lobby_camera_target = camera_position + glm::vec3(
1536 std::sin(elapsed_seconds * 0.11f) * 0.12f,
1537 std::cos(elapsed_seconds * 0.09f) * 0.08f,
1539 view_matrix = glm::lookAt(camera_position, lobby_camera_target, glm::vec3(0.0f, 1.0f, 0.0f));
1540 projection_matrix = glm::perspective(glm::radians(55.0f), aspect, 0.1f, 500.0f);
1541 projection_matrix[1][1] *= -1.0f;
1542 star_field.update(last_delta_time * 2.0f, camera_position, elapsed_seconds);
1543 star_field.setSprite(star_sprite);
1544 star_sprite->updateCamera(image_index, view_matrix, projection_matrix);
1546 star_sprite->render(cmd, image_index);
1547 star_sprite->clearQueue();
1549 const int panel_width = 760;
1550 const int panel_x =
static_cast<int>(extent.width) / 2 - panel_width / 2;
1551 draw_ui_rect(panel_x, 65, panel_width, 600, {0.015f, 0.025f, 0.08f, 0.90f});
1552 draw_ui_rect(panel_x, 65, panel_width, 3, {0.20f, 0.72f, 1.0f, 1.0f});
1553 draw_ui_rect(panel_x, 662, panel_width, 3, {0.20f, 0.72f, 1.0f, 1.0f});
1555 const float lobby_scale = std::min(
static_cast<float>(extent.width) / 1280.0f,
1556 static_cast<float>(extent.height) / 720.0f);
1557 const int lobby_font_size = std::clamp(
static_cast<int>(std::lround(22.0f * lobby_scale)), 14, 22);
1558 const int desired_status_font_size = std::clamp(
static_cast<int>(std::lround(16.0f * lobby_scale)), 12, 16);
1559 const int desired_roster_font_size = std::clamp(
static_cast<int>(std::lround(16.0f * lobby_scale)), 12, 16);
1560 set_ui_font_size(lobby_font_size);
1561 if (desired_status_font_size != lobby_status_font_size) {
1562 lobby_status_font_size = desired_status_font_size;
1563 lobby_status_font.reset(asset_root +
"/data/font.ttf", lobby_status_font_size);
1565 if (desired_roster_font_size != lobby_roster_font_size) {
1566 lobby_roster_font_size = desired_roster_font_size;
1567 lobby_roster_font.reset(asset_root +
"/data/font.ttf", lobby_roster_font_size);
1569 printText(
"ASTEROIDS NET", panel_x + 284, 100, {120, 220, 255, 255});
1570 printText(
"MULTIPLAYER - UP TO 4", panel_x + 235, 145, {255, 255, 255, 255});
1572 std::vector<std::string> labels;
1573 if (lobby_page == LobbyPage::Main) {
1574 labels = {
"HOST MULTIPLAYER MATCH",
"JOIN MULTIPLAYER MATCH",
"QUIT"};
1575 printText(
"One pilot hosts. The other pilot connects.", panel_x + 165, 195, {170, 190, 220, 255});
1576 }
else if (lobby_page == LobbyPage::Host) {
1577 labels = {
"PILOT NAME: " + lobby_player_name,
"LISTEN PORT: " + lobby_port,
1578 multiplayer.active() ?
"START MATCH" :
"START HOSTING",
"BACK"};
1579 printText(
"HOST SETUP", panel_x + 310, 195, {255, 210, 90, 255});
1580 if (multiplayer.active()) {
1581 printText(
"CODE: " + multiplayer.join_code(), panel_x + 540, 195, {120, 255, 170, 255});
1584 labels = {
"PILOT NAME: " + lobby_player_name,
"HOST: " + lobby_host_address,
"PORT: " + lobby_port,
1585 "JOIN CODE: " + lobby_join_code,
1586 multiplayer.active() ?
"WAITING FOR HOST" :
"CONNECT",
"BACK"};
1587 printText(
"JOIN SETUP", panel_x + 315, 195, {100, 255, 170, 255});
1590 const int first_y = 250;
1591 for (
int index = 0; index < static_cast<int>(labels.size()); ++index) {
1592 const int y = first_y + index * 50;
1593 const bool selected = index == lobby_selection;
1594 draw_ui_rect(panel_x + 20, y, panel_width - 40, 42,
1595 selected ? glm::vec4(0.08f, 0.30f, 0.48f, 0.95f) : glm::vec4(0.03f, 0.07f, 0.15f, 0.88f));
1597 draw_ui_rect(panel_x + 20, y, 5, 42, {0.25f, 0.85f, 1.0f, 1.0f});
1599 std::string text = labels[index];
1600 if (lobby_edit_field == index) {
1603 printText(text, panel_x + 40, y + 9, selected ? SDL_Color{255, 255, 255, 255} : SDL_Color{170, 195, 220, 255});
1606 if (multiplayer.active()) {
1607 printText(
"CONNECTED PLAYERS", panel_x + 30, 550, {120, 220, 255, 255}, lobby_roster_font);
1608 int roster_x = panel_x + 255;
1610 const std::string name = multiplayer.player_connected()[player] ? multiplayer.player_names()[player] :
"Waiting...";
1611 printText(std::format(
"P{}: {}", player + 1, name), roster_x, 550 +
static_cast<int>(player % 2U) * 20,
1612 multiplayer.player_connected()[player] ? SDL_Color{120, 255, 170, 255} : SDL_Color{120, 130, 150, 255},
1615 roster_x = panel_x + 500;
1618 std::vector<std::string> status_lines;
1619 std::istringstream status_words(lobby_status);
1622 while (status_words >> word) {
1623 const std::string candidate = line.empty() ? word : line +
" " + word;
1625 int text_height = 0;
1626 if (!line.empty() &&
getTextDimensions(candidate, text_width, text_height, lobby_status_font) &&
1627 text_width > panel_width - 60) {
1628 status_lines.push_back(line);
1635 status_lines.push_back(line);
1636 for (std::size_t index = 0; index < std::min<std::size_t>(status_lines.size(), 2U); ++index) {
1637 printText(status_lines[index], panel_x + 30, 595 +
static_cast<int>(index) * 20, {255, 210, 100, 255}, lobby_status_font);
1639 printText(
"Arrow keys / W,S: select Enter: confirm Esc: back", panel_x + 30, 637, {135, 155, 185, 255},
1643 void draw_intro(
const VkExtent2D &extent) {
1644 if (intro_sprite ==
nullptr) {
1649 const Uint32 current_ms = SDL_GetTicks();
1650 if ((current_ms - intro_last_update_ms) > 35U) {
1651 intro_last_update_ms = current_ms;
1652 intro_fade -= 0.01f;
1655 if (intro_fade <= 0.0f) {
1657 if (restart_after_intro) {
1658 restart_after_intro =
false;
1661 intro_last_update_ms = SDL_GetTicks();
1662 log_game(
"Intro finished. Restarting game.");
1665 loading_step_index.store(0, std::memory_order_relaxed);
1666 game_resources_loaded.store(
false, std::memory_order_relaxed);
1667 loading_failed.store(
false, std::memory_order_relaxed);
1668 model_preload_done.store(
false, std::memory_order_relaxed);
1669 model_preload_failed.store(
false, std::memory_order_relaxed);
1670 loading_black_frame_pending =
false;
1671 loading_black_frame_shown =
false;
1672 start_loading_async();
1673 log_game(
"Intro finished. Loading game resources.");
1674 draw_loading(extent);
1679 intro_sprite->setShaderParams(
static_cast<float>(current_ms) / 1000.0f, 0.0f, 0.0f, intro_fade);
1680 intro_sprite->drawSpriteRect(0, 0,
static_cast<int>(extent.width),
static_cast<int>(extent.height));
1681 if (intro_rain !=
nullptr) {
1682 intro_rain->update_and_render(*
this,
static_cast<int>(extent.width),
static_cast<int>(extent.height));
1686 void draw_loading(
const VkExtent2D &extent) {
1687 if (loading_failed.load(std::memory_order_relaxed) || model_preload_failed.load(std::memory_order_relaxed)) {
1688 if (intro_rain !=
nullptr) {
1689 intro_rain->set_opacity(0.0f);
1691 if (loading_thread.joinable()) {
1692 loading_thread.join();
1694 printText(
"Loading failed", 25, 25, {255, 100, 100, 255});
1698 if (!game_resources_loaded.load(std::memory_order_relaxed)) {
1699 const int loading_progress_percent = std::clamp((loading_step_index.load(std::memory_order_relaxed) * 100) / loading_step_count, 0, 100);
1700 if (intro_rain !=
nullptr) {
1701 const float target_rain_opacity = 1.0f - (
static_cast<float>(loading_progress_percent) / 100.0f);
1702 loading_rain_opacity = std::lerp(loading_rain_opacity, target_rain_opacity, 0.25f);
1703 intro_rain->set_opacity(loading_rain_opacity);
1704 intro_rain->update_and_render(*
this,
static_cast<int>(extent.width),
static_cast<int>(extent.height));
1706 set_ui_font_size(40);
1707 printText(
"Loading " + std::to_string(loading_progress_percent) +
"%", 25, 25, {255, 255, 255, 255});
1708 load_next_game_resource_step();
1712 if (loading_black_frame_pending) {
1713 if (!loading_black_frame_shown) {
1714 loading_black_frame_shown =
true;
1715 if (intro_rain !=
nullptr) {
1716 intro_rain->set_opacity(0.0f);
1721 loading_black_frame_pending =
false;
1722 loading_black_frame_shown =
false;
1725 if (intro_rain !=
nullptr) {
1726 intro_rain->set_opacity(0.0f);
1728 if (loading_thread.joinable()) {
1729 loading_thread.join();
1731 intro_last_update_ms = SDL_GetTicks();
1733 lobby_page = LobbyPage::Main;
1734 lobby_selection = 0;
1735 lobby_status =
"Choose how you want to play.";
1736 log_game(
"Loading complete. Multiplayer lobby is ready.");
1739 bool consume_prepared_ship_model(
const std::string &model_vert,
const std::string &model_frag) {
1740 std::optional<mxvk::MXModel> model_cpu;
1742 std::lock_guard<std::mutex> lock(prepared_model_mutex);
1743 if (!prepared_ship_model.has_value()) {
1746 model_cpu = std::move(prepared_ship_model);
1747 prepared_ship_model.reset();
1750 ship_model.load(
this, std::move(*model_cpu),
"", asset_root +
"/data", 1.0f);
1751 ship_model.setShaders(
this, model_vert, model_frag);
1752 ship_model.setBackfaceCulling(
false);
1756 bool consume_prepared_asteroid_model(std::size_t slot_index,
const std::string &model_vert,
const std::string &model_frag) {
1757 std::optional<mxvk::MXModel> model_cpu;
1760 std::lock_guard<std::mutex> lock(prepared_model_mutex);
1761 if (slot_index >= prepared_asteroid_models.size() || !prepared_asteroid_models[slot_index].has_value()) {
1764 model_cpu = std::move(prepared_asteroid_models[slot_index]);
1765 prepared_asteroid_models[slot_index].reset();
1766 texture_path = prepared_asteroid_texture_paths[slot_index];
1767 prepared_asteroid_texture_paths[slot_index].clear();
1770 asteroids[slot_index].model_index =
static_cast<int>(slot_index % 3U);
1771 asteroid_models[slot_index].load(
this, std::move(*model_cpu), texture_path, asset_root +
"/data", 1.0f);
1772 asteroid_models[slot_index].setShaders(
this, model_vert, model_frag);
1773 asteroid_models[slot_index].setBackfaceCulling(
false);
1777 void load_next_game_resource_step() {
1778 const std::string model_vert = shader_root +
"/model.vert.spv";
1779 const std::string model_frag = shader_root +
"/model.frag.spv";
1781 const int current_step = loading_step_index.load(std::memory_order_relaxed);
1782 if (current_step == 0) {
1783 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> ui_surface(SDL_CreateSurface(1, 1, SDL_PIXELFORMAT_RGBA32), SDL_DestroySurface);
1784 if (ui_surface ==
nullptr) {
1785 throw mxvk::Exception(
"Failed to create asteroids3d UI pixel surface");
1787 const SDL_PixelFormatDetails *format_details = SDL_GetPixelFormatDetails(ui_surface->format);
1788 if (format_details ==
nullptr || !SDL_FillSurfaceRect(ui_surface.get(),
nullptr, SDL_MapRGBA(format_details,
nullptr, 255, 255, 255, 255))) {
1789 throw mxvk::Exception(
"Failed to initialize asteroids3d UI pixel surface");
1791 ui_pixel =
createSprite(ui_surface.get(), asset_root +
"/data/sprite.vert.spv", shader_root +
"/fade_overlay.frag.spv");
1792 if (ui_pixel ==
nullptr) {
1793 throw mxvk::Exception(
"Failed to create asteroids3d UI pixel sprite");
1795 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> star_surface(
load_color_keyed_png(asset_root +
"/data/particle_star.png", 12), SDL_DestroySurface);
1797 if (star_sprite ==
nullptr) {
1798 throw mxvk::Exception(
"Failed to create star sprite batch");
1800 star_sprite->setDepthTestEnabled(
false);
1801 star_sprite->setDepthWriteEnabled(
false);
1802 star_sprite->setAlphaDiscardThreshold(0.01f);
1803 }
else if (current_step == 1) {
1804 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> fire_surface(
load_color_keyed_png(asset_root +
"/data/particle_explosion.png", 12), SDL_DestroySurface);
1806 if (projectile_sprite ==
nullptr) {
1807 throw mxvk::Exception(
"Failed to create projectile sprite batch");
1809 projectile_sprite->setDepthTestEnabled(
true);
1810 projectile_sprite->setDepthWriteEnabled(
false);
1811 projectile_sprite->setAlphaDiscardThreshold(0.05f);
1812 }
else if (current_step == 2) {
1813 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> explosion_surface(
load_color_keyed_png(asset_root +
"/data/particle_explosion.png", 12), SDL_DestroySurface);
1815 if (effect_sprite ==
nullptr) {
1816 throw mxvk::Exception(
"Failed to create effect sprite batch");
1818 effect_sprite->setDepthTestEnabled(
true);
1819 effect_sprite->setDepthWriteEnabled(
false);
1820 effect_sprite->setAlphaDiscardThreshold(0.05f);
1821 }
else if (current_step == 3) {
1822 if (!consume_prepared_ship_model(model_vert, model_frag)) {
1825 }
else if (current_step == 4) {
1826 if (!ship_model.isLoaded()) {
1829 for (
auto &model : remote_ship_models) {
1830 model.load(
this, asset_root +
"/data/starship.obj",
"", asset_root +
"/data", 1.0f);
1831 model.setShaders(
this, model_vert, model_frag);
1832 model.setBackfaceCulling(
false);
1834 }
else if (current_step == 5) {
1835 create_flame_resources();
1836 }
else if (current_step >= 6 && current_step < 6 +
MAX_ASTEROIDS) {
1837 if (!consume_prepared_asteroid_model(
static_cast<std::size_t
>(current_step - 6), model_vert, model_frag)) {
1845 game_resources_loaded.store(
true, std::memory_order_release);
1846 intro_last_update_ms = SDL_GetTicks();
1847 loading_rain_opacity = 0.0f;
1848 loading_black_frame_pending =
true;
1849 loading_black_frame_shown =
false;
1850 if (intro_rain !=
nullptr) {
1851 intro_rain->set_opacity(0.0f);
1856 loading_step_index.store(current_step + 1, std::memory_order_release);
1857 if (loading_step_index.load(std::memory_order_relaxed) >= loading_step_count) {
1858 game_resources_loaded.store(
true, std::memory_order_release);
1859 intro_last_update_ms = SDL_GetTicks();
1860 loading_rain_opacity = 0.0f;
1861 loading_black_frame_pending =
true;
1862 loading_black_frame_shown =
false;
1863 if (intro_rain !=
nullptr) {
1864 intro_rain->set_opacity(0.0f);
1870 void load_asteroid_model_slot(std::size_t slot_index,
const std::string &model_vert,
const std::string &model_frag) {
1871 static constexpr std::array<const char *, 3> asteroid_paths = {
1872 "data/asteroid.obj",
1873 "data/asteroid2.obj",
1874 "data/asteroid3.obj",
1877 const std::size_t model_variant = slot_index % asteroid_paths.size();
1879 if (model_variant == 0) {
1881 }
else if (model_variant == 1) {
1887 asteroids[slot_index].model_index =
static_cast<int>(model_variant);
1888 asteroid_models[slot_index].load(
1890 asset_root +
"/" + asteroid_paths[model_variant],
1892 asset_root +
"/data",
1894 asteroid_models[slot_index].setShaders(
this, model_vert, model_frag);
1895 asteroid_models[slot_index].setBackfaceCulling(
false);
1898 void restart_game() {
1899 clear_round_state();
1900 spawn_initial_asteroids();
1902 restart_after_intro =
false;
1903 log_game(
"Game state reset: score=0 lives=5.");
1906 void clear_round_state() {
1907 ship.position = glm::vec3(0.0f);
1908 ship.prev_position = ship.position;
1909 ship.velocity = glm::vec3(0.0f);
1910 ship.rotation = glm::vec3(0.0f);
1911 ship.current_speed = 1.0f;
1912 ship.visible =
true;
1913 ship.exploding =
false;
1914 ship.explosion_timer = 0;
1917 ship.fire_cooldown = 0;
1918 ship.burst_count = 0;
1919 ship.continuous_fire_timer = 0;
1920 ship.overheated =
false;
1921 ship.overheat_cooldown = 0;
1922 for (
auto &projectile : projectiles) {
1923 projectile.
active =
false;
1925 for (
auto &particle : particles) {
1926 particle.active =
false;
1928 for (
auto &asteroid : asteroids) {
1929 asteroid.active =
false;
1931 keyboard_yaw = 0.0f;
1932 keyboard_pitch = 0.0f;
1933 keyboard_roll = 0.0f;
1935 smooth_pitch = 0.0f;
1937 ship_returning_to_field =
false;
1938 return_message_cooldown = 0.0f;
1939 set_mouse_look_controls(
false);
1940 first_person_camera =
false;
1941 camera_transition_active =
false;
1942 camera_transition_elapsed = 0.0f;
1943 camera_position = glm::vec3(0.0f, ship.camera_height, ship.camera_distance);
1944 camera_target_position = glm::vec3(0.0f, 0.0f, -6.0f);
1945 camera_up_vector = glm::vec3(0.0f, 1.0f, 0.0f);
1949 void spawn_initial_asteroids() {
1950 for (
int i = 0; i < 7; ++i) {
1951 glm::vec3 position{0.0f};
1953 position = glm::vec3(
1957 }
while (glm::length(position - ship.position) < 28.0f);
1959 spawn_asteroid(position,
1965 log_game(
"Initial asteroid field spawned.");
1968 void spawn_asteroid(
const glm::vec3 &position,
const glm::vec3 &velocity,
float radius,
int generation,
int preferred_model_index = -1) {
1969 Asteroid *free_asteroid = find_free_asteroid(preferred_model_index);
1970 if (free_asteroid ==
nullptr && preferred_model_index >= 0) {
1971 free_asteroid = find_free_asteroid();
1974 if (free_asteroid ==
nullptr) {
1975 log_game(
"Asteroid spawn skipped: no free asteroid slots.", SDL_Color{255, 190, 90, 255});
1979 const int slot_model_index = free_asteroid->model_index;
1980 free_asteroid->position = position;
1981 free_asteroid->velocity = velocity;
1982 free_asteroid->
radius = radius;
1983 free_asteroid->generation = generation;
1986 free_asteroid->model_index = slot_model_index;
1987 free_asteroid->
active =
true;
1988 if (generation == 0) {
1989 log_game(std::format(
"Asteroid spawned at ({:.1f}, {:.1f}, {:.1f}) radius {:.1f}.", position.x, position.y, position.z, radius));
1993 void handle_input(
float dt) {
1994 const bool *keys = SDL_GetKeyboardState(
nullptr);
1995 if (keys ==
nullptr) {
1999 if (ship.lives <= 0) {
2003 if (controller.getButton(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER) || controller.getButton(SDL_GAMEPAD_BUTTON_DPAD_UP)) {
2005 }
else if (controller.getButton(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER) || controller.getButton(SDL_GAMEPAD_BUTTON_DPAD_DOWN)) {
2009 auto ramp_axis = [dt](
float &value,
float target,
float rise_rate,
float fall_rate) {
2010 const float rate = (std::fabs(target) > std::fabs(value)) ? rise_rate : fall_rate;
2011 const float step = rate * dt;
2012 if (value < target) {
2013 value = std::min(value + step, target);
2014 }
else if (value > target) {
2015 value = std::max(value - step, target);
2019 float yaw_amount = 0.0f;
2020 float pitch_amount = 0.0f;
2021 float roll_amount = 0.0f;
2022 bool manual_roll_input =
false;
2024 const float left_x = controller_axis(SDL_GAMEPAD_AXIS_LEFTX);
2025 if (std::fabs(left_x) > 0.001f) {
2026 yaw_amount = -left_x;
2029 float keyboard_yaw_target = 0.0f;
2030 if (keys[SDL_SCANCODE_LEFT]) {
2031 keyboard_yaw_target = 1.0f;
2032 }
else if (keys[SDL_SCANCODE_RIGHT]) {
2033 keyboard_yaw_target = -1.0f;
2035 ramp_axis(keyboard_yaw, keyboard_yaw_target, 3.2f, 8.0f);
2036 if (std::fabs(keyboard_yaw) > 0.001f) {
2037 yaw_amount = keyboard_yaw;
2040 float keyboard_pitch_target = 0.0f;
2041 if (!mouse_look_controls) {
2042 if (inverted_controls) {
2043 if (keys[SDL_SCANCODE_W]) {
2044 keyboard_pitch_target = -1.0f;
2046 if (keys[SDL_SCANCODE_S]) {
2047 keyboard_pitch_target = 1.0f;
2050 if (keys[SDL_SCANCODE_W]) {
2051 keyboard_pitch_target = 1.0f;
2053 if (keys[SDL_SCANCODE_S]) {
2054 keyboard_pitch_target = -1.0f;
2058 ramp_axis(keyboard_pitch, keyboard_pitch_target, 2.8f, 8.0f);
2059 if (std::fabs(keyboard_pitch) > 0.001f) {
2060 pitch_amount = keyboard_pitch;
2063 float keyboard_roll_target = 0.0f;
2064 if (keys[SDL_SCANCODE_A]) {
2065 keyboard_roll_target = -1.0f;
2066 }
else if (keys[SDL_SCANCODE_D]) {
2067 keyboard_roll_target = 1.0f;
2069 ramp_axis(keyboard_roll, keyboard_roll_target, 3.0f, 8.0f);
2070 if (std::fabs(keyboard_roll) > 0.001f) {
2071 roll_amount = keyboard_roll;
2072 manual_roll_input =
true;
2075 if (controller.getButton(SDL_GAMEPAD_BUTTON_DPAD_LEFT)) {
2077 }
else if (controller.getButton(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)) {
2081 const float right_x = controller_axis(SDL_GAMEPAD_AXIS_RIGHTX);
2082 if (std::fabs(right_x) > 0.001f) {
2083 roll_amount = right_x;
2084 manual_roll_input =
true;
2087 const float right_y = controller_axis(SDL_GAMEPAD_AXIS_RIGHTY);
2088 if (std::fabs(right_y) > 0.001f) {
2089 pitch_amount = inverted_controls ? right_y : -right_y;
2092 const float smoothing = std::clamp(dt * 8.0f, 0.0f, 1.0f);
2093 smooth_yaw = glm::mix(smooth_yaw, yaw_amount, smoothing);
2094 smooth_pitch = glm::mix(smooth_pitch, pitch_amount, smoothing);
2095 smooth_roll = glm::mix(smooth_roll, roll_amount, smoothing);
2097 if (std::fabs(smooth_yaw) > 0.01f) {
2098 ship.rotation.y += smooth_yaw * ship.turn_speed * dt;
2100 if (std::fabs(smooth_pitch) > 0.01f) {
2101 ship.rotation.x += smooth_pitch * ship.turn_speed * ship.pitch_speed_multiplier * dt;
2103 if (manual_roll_input && std::fabs(smooth_roll) > 0.01f) {
2104 ship.rotation.z += smooth_roll * ship.turn_speed * dt;
2106 if (!manual_roll_input && std::fabs(smooth_yaw) > 0.01f) {
2107 const float target_roll = -smooth_yaw * 35.0f;
2108 const float roll_diff = target_roll - ship.rotation.z;
2109 ship.rotation.z += roll_diff * 5.0f * dt;
2111 if (!manual_roll_input && std::fabs(smooth_yaw) < 0.01f) {
2112 while (ship.rotation.z > 180.0f) {
2113 ship.rotation.z -= 360.0f;
2115 while (ship.rotation.z < -180.0f) {
2116 ship.rotation.z += 360.0f;
2118 ship.rotation.z = glm::mix(ship.rotation.z, 0.0f, 3.0f * dt);
2121 const bool speed_up_key = mouse_look_controls ? keys[SDL_SCANCODE_W] : keys[SDL_SCANCODE_UP];
2122 const bool slow_down_key = mouse_look_controls ? keys[SDL_SCANCODE_S] : keys[SDL_SCANCODE_DOWN];
2125 }
else if (slow_down_key) {
2128 if (ship.current_speed > 5.0f) {
2129 decrease_speed(dt * 0.5f);
2130 }
else if (ship.current_speed < 5.0f) {
2131 increase_speed(dt * 0.5f);
2135 const bool firing = keys[SDL_SCANCODE_SPACE] ||
2136 controller.getButton(SDL_GAMEPAD_BUTTON_SOUTH) ||
2143 update_fire_timer(
false);
2147 void increase_speed(
float dt) {
2148 ship.current_speed += ship.turn_speed * dt * 0.2f;
2149 ship.current_speed = std::min(ship.current_speed, ship.max_speed);
2152 void decrease_speed(
float dt) {
2153 ship.current_speed -= ship.turn_speed * dt * 0.2f;
2154 ship.current_speed = std::max(ship.current_speed, ship.min_speed);
2158 if (ship.overheated) {
2161 if (ship.fire_cooldown <= 0) {
2168 ship.burst_count = 0;
2174 void update_fire_timer(
bool firing) {
2175 if (firing && !ship.overheated) {
2176 ship.continuous_fire_timer++;
2177 ship.overheat_cooldown = 0;
2178 if (ship.continuous_fire_timer >= 180) {
2179 ship.overheated =
true;
2180 ship.overheat_cooldown = 0;
2181 ship.continuous_fire_timer = 0;
2182 ship.burst_count = 0;
2183 log_game(
"Weapons overheated.", SDL_Color{255, 150, 80, 255});
2185 }
else if (firing && ship.overheated) {
2186 ship.overheat_cooldown = 0;
2188 if (ship.overheated) {
2189 ship.overheat_cooldown++;
2190 if (ship.overheat_cooldown >= 180) {
2191 ship.overheated =
false;
2192 ship.overheat_cooldown = 0;
2193 ship.continuous_fire_timer = 0;
2194 log_game(
"Weapons cooled down.");
2196 }
else if (ship.continuous_fire_timer > 0) {
2197 ship.continuous_fire_timer--;
2202 static float normalize_degrees(
float degrees) {
2203 while (degrees > 180.0f) {
2206 while (degrees < -180.0f) {
2212 static float ease_angle_degrees(
float current,
float target,
float blend) {
2213 return current + normalize_degrees(target - current) * std::clamp(blend, 0.0f, 1.0f);
2216 glm::vec3 asteroid_field_center()
const {
2217 glm::vec3 sum{0.0f};
2219 for (
const auto &asteroid : asteroids) {
2220 if (!asteroid.active) {
2223 sum += asteroid.position;
2227 return glm::vec3(0.0f);
2229 return sum /
static_cast<float>(count);
2232 bool ship_is_outside_return_volume()
const {
2233 constexpr float RETURN_PADDING = 18.0f;
2242 void update_ship_return_to_field(
float dt) {
2243 if (active_asteroids() == 0) {
2244 ship_returning_to_field =
false;
2248 constexpr float RETURN_START_DISTANCE = 145.0f;
2249 constexpr float RETURN_STOP_DISTANCE = 92.0f;
2250 const float nearest_distance = nearest_asteroid_distance();
2251 const bool outside_return_volume = ship_is_outside_return_volume();
2252 if (!ship_returning_to_field && (outside_return_volume || nearest_distance > RETURN_START_DISTANCE)) {
2253 ship_returning_to_field =
true;
2254 if (return_message_cooldown <= 0.0f) {
2255 log_game(
"Return assist engaged: steering back toward the asteroid field.", SDL_Color{120, 220, 255, 255});
2256 return_message_cooldown = 3.0f;
2258 }
else if (ship_returning_to_field && !outside_return_volume && nearest_distance < RETURN_STOP_DISTANCE) {
2259 ship_returning_to_field =
false;
2260 log_game(
"Return assist disengaged.");
2263 if (!ship_returning_to_field) {
2267 const glm::vec3 to_field =
normalize_or_zero(asteroid_field_center() - ship.position);
2268 const float target_yaw = glm::degrees(std::atan2(-to_field.x, -to_field.z));
2269 const float target_pitch = glm::degrees(std::asin(std::clamp(to_field.y, -1.0f, 1.0f)));
2270 const float blend = 1.0f - std::exp(-dt * 1.8f);
2271 ship.rotation.y = ease_angle_degrees(ship.rotation.y, target_yaw, blend);
2272 ship.rotation.x = ease_angle_degrees(ship.rotation.x, target_pitch, blend);
2273 ship.rotation.z = ease_angle_degrees(ship.rotation.z, 0.0f, blend * 0.8f);
2274 ship.current_speed = std::max(ship.current_speed, 8.0f);
2277 void fire_projectile() {
2278 const glm::vec3 forward = ship.forward();
2279 const float muzzle_offset = 0.08f;
2280 const glm::vec3 muzzle = ship.position + forward * muzzle_offset;
2281 for (std::size_t projectile_index = 0; projectile_index < projectiles.size(); ++projectile_index) {
2282 Projectile &projectile = projectiles[projectile_index];
2286 projectile.position = muzzle;
2287 projectile.prev_position = muzzle;
2291 projectile.
active =
true;
2292 projectile_ids[projectile_index] = next_projectile_id++;
2293 if (next_projectile_id == 0U) {
2294 next_projectile_id = 1U;
2296#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
2297 play_sound(cannon_sound);
2299 log_game(std::format(
"Projectile fired from ({:.1f}, {:.1f}, {:.1f}).", muzzle.x, muzzle.y, muzzle.z));
2302 log_game(
"Projectile fire skipped: projectile pool full.", SDL_Color{255, 190, 90, 255});
2305 void update_ship(
float dt) {
2306 if (ship.exploding) {
2307 ship.explosion_timer--;
2308 if (ship.explosion_timer <= 0) {
2309 ship.exploding =
false;
2310 ship.visible =
true;
2311 if (multiplayer_match) {
2312 const std::uint8_t player = std::min<std::uint8_t>(multiplayer.local_player_id(), 3U);
2316 ship.position = glm::vec3(0.0f);
2317 ship.rotation = glm::vec3(0.0f);
2319 ship.prev_position = ship.position;
2320 ship.velocity = glm::vec3(0.0f);
2321 ship.current_speed = 1.0f;
2322 ship_returning_to_field =
false;
2324 log_game(
"Ship respawned at origin.");
2330 ship.prev_position = ship.position;
2331 ship.velocity = glm::vec3(0.0f);
2335 if (return_message_cooldown > 0.0f) {
2336 return_message_cooldown = std::max(0.0f, return_message_cooldown - dt);
2338 update_ship_return_to_field(dt);
2339 const glm::vec3 forward = ship.forward();
2340 ship.prev_position = ship.position;
2341 ship.velocity = forward * ship.current_speed;
2342 ship.position += ship.velocity * dt;
2343 ship.rotation.x = std::clamp(ship.rotation.x, -75.0f, 75.0f);
2344 if (ship.rotation.z > 180.0f) {
2345 ship.rotation.z -= 360.0f;
2346 }
else if (ship.rotation.z < -180.0f) {
2347 ship.rotation.z += 360.0f;
2349 if (ship.fire_cooldown > 0) {
2350 ship.fire_cooldown--;
2354 void update_projectiles(
float dt) {
2355 for (
auto &projectile : projectiles) {
2356 if (!projectile.
active) {
2359 projectile.prev_position = projectile.position;
2360 projectile.position += projectile.velocity * dt;
2363 projectile.
active =
false;
2367 if (multiplayer_match && !multiplayer.is_host()) {
2371 for (
auto &asteroid : asteroids) {
2372 if (!asteroid.active) {
2375 for (
auto &projectile : projectiles) {
2376 if (!projectile.
active) {
2380 const glm::vec3 segment = projectile.position - projectile.prev_position;
2381 const float segment_length_sq = glm::dot(segment, segment);
2383 glm::vec3 closest_point = projectile.prev_position;
2385 if (segment_length_sq > 1e-6f) {
2386 const glm::vec3 to_asteroid = asteroid.position - projectile.prev_position;
2387 const float t = std::clamp(glm::dot(to_asteroid, segment) / segment_length_sq, 0.0f, 1.0f);
2388 closest_point = projectile.prev_position + segment * t;
2391 const float dist = glm::length(closest_point - asteroid.position);
2394 if (dist < projectile_hit_radius) {
2395 projectile.
active =
false;
2396 log_game(std::format(
"Projectile hit asteroid at ({:.1f}, {:.1f}, {:.1f}).", asteroid.position.x, asteroid.position.y, asteroid.position.z));
2397 split_asteroid(asteroid);
2404 void split_asteroid(Asteroid &asteroid) {
2405 const glm::vec3 hit_position = asteroid.position;
2406 const int generation = asteroid.generation;
2407 const float radius = asteroid.
radius * 0.5f;
2409 spawn_asteroid_explosion(hit_position);
2410#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
2411 play_sound(asteroid_explosion_sound);
2417 log_game(std::format(
"Small asteroid destroyed. Score={}.", ship.score));
2422 const float child_radius = asteroid.
radius * 0.18f;
2423 const glm::vec3 view_forward =
normalize_or_zero(hit_position - camera_position);
2424 glm::vec3 split_axis = glm::cross(view_forward, glm::vec3(0.0f, 1.0f, 0.0f));
2425 if (glm::length(split_axis) <= 1e-4f) {
2426 split_axis = glm::cross(view_forward, glm::vec3(1.0f, 0.0f, 0.0f));
2428 if (glm::length(split_axis) <= 1e-4f) {
2429 split_axis = glm::vec3(1.0f, 0.0f, 0.0f);
2432 const glm::vec3 toward_camera =
normalize_or_zero(camera_position - hit_position);
2433 const float child_separation = std::max(asteroid.
radius * 0.45f, child_radius * 2.0f);
2434 const glm::vec3 depth_bias = toward_camera * (child_separation * 0.12f);
2435 std::array<glm::vec3, CHILDREN_PER_SPAWN> child_positions = {
2436 hit_position - split_axis * child_separation + depth_bias,
2437 hit_position + split_axis * child_separation + depth_bias,
2440 for (
int i = 0; i < child_count; ++i) {
2441 Asteroid *child = find_free_asteroid(asteroid.model_index);
2442 if (child ==
nullptr) {
2443 log_game(
"Asteroid split skipped: no free slot for parent asteroid type.", SDL_Color{255, 190, 90, 255});
2447 const glm::vec3 child_offset = child_positions[
static_cast<std::size_t
>(i)] - hit_position;
2450 child->position = child_positions[
static_cast<std::size_t
>(i)];
2451 child->
radius = child_radius;
2452 child->generation = generation + 1;
2455 child->model_index = asteroid.model_index;
2456 child->velocity = child_velocity;
2457 log_game(std::format(
2458 "Asteroid child {} spawned at ({:.1f}, {:.1f}, {:.1f}) radius {:.1f}.",
2466 if (radius >= 25.0f) {
2468 log_game(std::format(
"Large asteroid split into {} pieces. Score={}.", child_count, ship.score));
2471 log_game(std::format(
"Medium asteroid split into {} pieces. Score={}.", child_count, ship.score));
2477 Asteroid *find_free_asteroid(
int preferred_model_index = -1) {
2478 for (
auto &asteroid : asteroids) {
2479 if (!asteroid.
active && (preferred_model_index < 0 || asteroid.model_index == preferred_model_index)) {
2486 void update_asteroids(
float dt) {
2487 for (
auto &asteroid : asteroids) {
2491 asteroid.position += asteroid.velocity * dt;
2493 bool bounced =
false;
2524 if (glm::length(asteroid.velocity) > 0.01f) {
2525 asteroid.velocity *= 0.995f;
2527 if (asteroid.rotation.
x > 360.0f)
2528 asteroid.rotation.
x -= 360.0f;
2529 if (asteroid.rotation.
y > 360.0f)
2530 asteroid.rotation.
y -= 360.0f;
2531 if (asteroid.rotation.z > 360.0f)
2532 asteroid.rotation.z -= 360.0f;
2535 for (
auto &asteroid : asteroids) {
2539 const float ship_distance = ship_asteroid_collision_distance(asteroid);
2540 if (ship_distance <= 0.0f) {
2541 log_game(std::format(
"Ship collision with asteroid overlap {:.2f}.", -ship_distance), SDL_Color{255, 130, 90, 255});
2542 start_ship_explosion();
2548 float ship_asteroid_collision_distance(
const Asteroid &asteroid)
const {
2549 static constexpr std::array<ShipCollisionSample, 5> ship_samples = {
2550 ShipCollisionSample{{0.0f, 0.0f, -0.55f}, 0.055f},
2551 ShipCollisionSample{{0.0f, 0.06f, -0.16f}, 0.160f},
2552 ShipCollisionSample{{0.0f, 0.08f, 0.34f}, 0.125f},
2553 ShipCollisionSample{{-0.42f, 0.03f, -0.02f}, 0.085f},
2554 ShipCollisionSample{{0.42f, 0.03f, -0.02f}, 0.085f},
2558 float nearest_surface_distance = std::numeric_limits<float>::max();
2560 for (
const ShipCollisionSample &sample : ship_samples) {
2561 const float ship_scale = rendered_ship_scale();
2562 const glm::vec3 offset = transform_ship_collision_offset(sample.local_position);
2563 const glm::vec3 previous_position = ship.prev_position + offset;
2564 const glm::vec3 current_position = ship.position + offset;
2565 const float center_distance = swept_point_distance_to_asteroid(previous_position, current_position, asteroid.position);
2566 nearest_surface_distance = std::min(nearest_surface_distance, center_distance - asteroid_collision_radius - (sample.radius * ship_scale));
2569 return nearest_surface_distance;
2572 glm::vec3 transform_ship_collision_offset(
const glm::vec3 &local_position)
const {
2576 rendered_ship_scale(),
2577 ship_model.modelCenterOffset());
2578 return glm::vec3(model * glm::vec4(local_position, 1.0f));
2581 float rendered_ship_scale()
const {
2585 float swept_point_distance_to_asteroid(
const glm::vec3 &previous_position,
2586 const glm::vec3 ¤t_position,
2587 const glm::vec3 &asteroid_position)
const {
2588 const glm::vec3 segment = current_position - previous_position;
2589 const float segment_length_sq = glm::dot(segment, segment);
2590 glm::vec3 closest_point = current_position;
2592 if (segment_length_sq > 1e-6f) {
2593 const glm::vec3 to_asteroid = asteroid_position - previous_position;
2594 const float t = std::clamp(glm::dot(to_asteroid, segment) / segment_length_sq, 0.0f, 1.0f);
2595 closest_point = previous_position + segment * t;
2598 return glm::length(closest_point - asteroid_position);
2601 void start_ship_explosion() {
2602 if (ship.exploding) {
2605 ship.exploding =
true;
2606 ship.visible =
false;
2608 if (multiplayer_match && multiplayer.is_host()) {
2609 ++multiplayer_death_serials[0];
2612 ship.overheated =
false;
2613 ship.overheat_cooldown = 0;
2614 ship.continuous_fire_timer = 0;
2615 ship.burst_count = 0;
2616#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
2617 play_sound(crash_sound);
2619 log_game(std::format(
"Ship destroyed. Lives remaining: {}.", std::max(0, ship.lives)), SDL_Color{255, 120, 80, 255});
2620 if (ship.lives <= 0) {
2621 log_game(std::format(
"Game over. Final score: {}.", ship.score), SDL_Color{255, 90, 90, 255});
2623 spawn_ship_explosion(ship.position);
2626 void spawn_asteroid_explosion(
const glm::vec3 &position) {
2627 spawn_gl_explosion(position);
2630 void spawn_ship_explosion(
const glm::vec3 &position) {
2631 spawn_gl_explosion(position);
2634 void spawn_gl_explosion(
const glm::vec3 &position) {
2635 struct ExplosionWave {
2645 constexpr int WAVE_COUNT = 4;
2646 constexpr int MAX_GL_EXPLOSIONS = 5;
2647 constexpr std::array<ExplosionWave, WAVE_COUNT> waves = {
2648 ExplosionWave{40.0f, 60.0f, 0.72f, 1.14f, 1.5f, 2.5f, {1.0f, 1.0f, 1.0f}},
2649 ExplosionWave{30.0f, 45.0f, 0.58f, 0.86f, 2.0f, 3.0f, {1.0f, 1.0f, 1.0f}},
2650 ExplosionWave{20.0f, 35.0f, 0.43f, 0.72f, 2.5f, 3.5f, {1.0f, 1.0f, 1.0f}},
2651 ExplosionWave{10.0f, 25.0f, 0.14f, 0.43f, 3.0f, 4.0f, {1.0f, 1.0f, 1.0f}},
2654 const int particles_per_wave =
MAX_PARTICLES / (WAVE_COUNT * MAX_GL_EXPLOSIONS);
2656 for (
int wave_index = 0; wave_index < WAVE_COUNT; ++wave_index) {
2657 const ExplosionWave &wave = waves[
static_cast<std::size_t
>(wave_index)];
2658 for (
int i = 0; i < particles_per_wave; ++i) {
2659 Particle *particle = find_free_particle();
2660 if (particle ==
nullptr) {
2666 const glm::vec3 dir{
2667 std::sin(phi) * std::cos(theta),
2668 std::sin(phi) * std::sin(theta),
2672 const float offset = 0.8f + 0.2f *
static_cast<float>(wave_index) /
static_cast<float>(WAVE_COUNT);
2673 const float speed =
random_float(wave.min_speed, wave.max_speed);
2674 particle->position = position + dir * offset;
2675 particle->velocity = dir * speed + glm::vec3(
2679 particle->color = glm::vec4(
2684 particle->size =
random_float(wave.min_size, wave.max_size);
2686 particle->max_lifetime =
random_float(wave.min_lifetime, wave.max_lifetime);
2691 log_game(std::format(
"Explosion spawned {} particles.", spawned));
2694 void spawn_particles(
const glm::vec3 &position,
2695 const glm::vec4 &color,
2702 float max_lifetime) {
2703 for (
int i = 0; i < count; ++i) {
2704 Particle *particle = find_free_particle();
2705 if (particle ==
nullptr) {
2712 particle->position = position;
2713 particle->velocity = dir *
random_float(min_speed, max_speed);
2714 particle->color = color;
2717 particle->max_lifetime =
random_float(min_lifetime, max_lifetime);
2722 Particle *find_free_particle() {
2723 for (
auto &particle : particles) {
2731 void update_particles(
float dt) {
2732 for (
auto &particle : particles) {
2736 particle.position += particle.velocity * dt;
2737 particle.velocity *= 0.98f;
2738 particle.velocity.
y -= 0.5f * dt;
2741 const float life_ratio = particle.
lifetime / particle.max_lifetime;
2742 if (life_ratio >= 1.0f) {
2746 if (life_ratio < 0.2f) {
2747 particle.color.a = life_ratio / 0.2f;
2748 }
else if (life_ratio > 0.8f) {
2749 particle.color.a = (1.0f - life_ratio) / 0.2f;
2751 particle.color.a = 1.0f;
2753 if (life_ratio < 0.3f) {
2754 particle.size *= 1.01f;
2756 particle.size *= 0.99f;
2758 if (particle.color.a < 0.01f) {
2764 void clear_particles() {
2765 for (
auto &particle : particles) {
2770 void prepare_restart_from_game_over() {
2771 clear_round_state();
2772 restart_after_intro =
true;
2773 reset_intro_screen();
2776 void reset_intro_screen() {
2779 intro_last_update_ms = SDL_GetTicks();
2780 loading_rain_opacity = 1.0f;
2781 if (intro_rain !=
nullptr) {
2782 intro_rain->set_opacity(1.0f);
2783 intro_rain->reset();
2787 void update_round_timer(
float dt) {
2792 if (round_time_remaining <= 0.0f) {
2797 round_time_remaining = std::max(0.0f, round_time_remaining - dt);
2798 if (round_time_remaining <= 0.0f) {
2800 ship.exploding =
false;
2801 ship.visible =
false;
2802 ship.fire_cooldown = 0;
2803 ship.burst_count = 0;
2804 ship.continuous_fire_timer = 0;
2805 ship.overheated =
false;
2806 ship.overheat_cooldown = 0;
2807 log_game(
"Time expired. Game over.", SDL_Color{255, 90, 90, 255});
2821 std::string format_round_time()
const {
2822 const int total_seconds = std::max(0,
static_cast<int>(std::ceil(round_time_remaining)));
2823 const int minutes = total_seconds / 60;
2824 const int seconds = total_seconds % 60;
2825 return std::format(
"{:02d}:{:02d}", minutes, seconds);
2828 glm::mat4 ship_rotation_matrix()
const {
2829 glm::mat4 rotation(1.0f);
2830 rotation = glm::rotate(rotation, glm::radians(ship.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
2831 rotation = glm::rotate(rotation, glm::radians(ship.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
2832 rotation = glm::rotate(rotation, glm::radians(ship.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
2837 glm::vec3 position{0.0f};
2838 glm::vec3 target{0.0f};
2839 glm::vec3 up{0.0f, 1.0f, 0.0f};
2842 CameraPose chase_camera_pose(
const glm::vec3 &ship_forward)
const {
2844 pose.position = ship.position - ship_forward * ship.camera_distance + glm::vec3(0.0f, ship.camera_height, 0.0f);
2845 pose.target = ship.position + ship_forward * 6.0f;
2846 pose.up = glm::vec3(0.0f, 1.0f, 0.0f);
2850 CameraPose first_person_camera_pose(
const glm::mat4 &ship_rotation_matrix,
const glm::vec3 &ship_forward)
const {
2851 constexpr glm::vec3 FIRST_PERSON_CAMERA_OFFSET{0.0f, 0.16f, -0.30f};
2854 const glm::vec3 cockpit_offset = glm::vec3(ship_rotation_matrix * glm::vec4(FIRST_PERSON_CAMERA_OFFSET * rendered_ship_scale(), 0.0f));
2855 pose.position = ship.position + cockpit_offset;
2856 pose.target = pose.position + ship_forward * 8.0f;
2857 pose.up =
normalize_or_zero(glm::vec3(ship_rotation_matrix * glm::vec4(0.0f, 1.0f, 0.0f, 0.0f)));
2861 static float smooth_camera_transition(
float value) {
2862 value = std::clamp(value, 0.0f, 1.0f);
2863 return value * value * (3.0f - 2.0f * value);
2866 void begin_camera_transition(
bool target_first_person_camera) {
2867 first_person_camera = target_first_person_camera;
2868 camera_transition_active =
true;
2869 camera_transition_elapsed = 0.0f;
2870 camera_transition_start_position = camera_position;
2871 camera_transition_start_target = camera_target_position;
2872 camera_transition_start_up = camera_up_vector;
2875 void update_camera(
float dt) {
2876 const glm::mat4 ship_rotation_matrix = this->ship_rotation_matrix();
2877 const glm::vec3 ship_forward =
normalize_or_zero(glm::vec3(ship_rotation_matrix * glm::vec4(0.0f, 0.0f, -1.0f, 0.0f)));
2878 const CameraPose target_pose = first_person_camera ? first_person_camera_pose(ship_rotation_matrix, ship_forward) : chase_camera_pose(ship_forward);
2880 if (camera_transition_active) {
2881 camera_transition_elapsed += dt;
2882 const float blend = smooth_camera_transition(camera_transition_elapsed / CAMERA_TRANSITION_SECONDS);
2883 camera_position = glm::mix(camera_transition_start_position, target_pose.position, blend);
2884 camera_target_position = glm::mix(camera_transition_start_target, target_pose.target, blend);
2885 camera_up_vector =
normalize_or_zero(glm::mix(camera_transition_start_up, target_pose.up, blend));
2886 if (camera_transition_elapsed >= CAMERA_TRANSITION_SECONDS) {
2887 camera_transition_active =
false;
2888 camera_position = target_pose.position;
2889 camera_target_position = target_pose.target;
2890 camera_up_vector = target_pose.up;
2892 }
else if (first_person_camera) {
2893 camera_position = target_pose.position;
2894 camera_target_position = target_pose.target;
2895 camera_up_vector = target_pose.up;
2897 camera_position = glm::mix(camera_position, target_pose.position, 1.0f - std::exp(-dt * 10.0f));
2898 camera_target_position = target_pose.target;
2899 camera_up_vector = target_pose.up;
2902 view_matrix = glm::lookAt(camera_position, camera_target_position, camera_up_vector);
2905 void draw_ship(uint32_t image_index) {
2906 if (!ship.visible) {
2910 mxvk::UniformBufferObject ubo{};
2911 ubo.
model =
build_model_matrix(ship.position, ship.rotation, rendered_ship_scale(), ship_model.modelCenterOffset());
2912 last_ship_model_matrix = ubo.
model;
2913 ubo.
view = view_matrix;
2914 ubo.
proj = projection_matrix;
2915 ubo.
fx = glm::vec4(camera_position, elapsed_seconds);
2916 ship_model.updateUBO(image_index, ubo);
2917 ship_model.render(current_command_buffer, image_index,
false);
2920 void draw_remote_ship(uint32_t image_index, std::uint8_t player) {
2921 Ship &remote_ship = remote_ships[player];
2922 mxvk::VKAbstractModel &remote_ship_model = remote_ship_models[player];
2923 if (!remote_ship.visible || remote_ship_exploding[player] || !remote_ship_model.
isLoaded()) {
2926 mxvk::UniformBufferObject ubo{};
2928 last_remote_ship_model_matrices[player] = ubo.
model;
2929 ubo.
view = view_matrix;
2930 ubo.
proj = projection_matrix;
2931 ubo.
fx = glm::vec4(camera_position, elapsed_seconds);
2932 remote_ship_model.
updateUBO(image_index, ubo);
2933 remote_ship_model.
render(current_command_buffer, image_index,
false);
2936 void draw_asteroids(uint32_t image_index) {
2937 for (std::size_t i = 0; i < asteroids.size(); ++i) {
2938 const Asteroid &asteroid = asteroids[i];
2942 mxvk::UniformBufferObject ubo{};
2943 const float scale = asteroid.
radius;
2944 mxvk::VKAbstractModel &asteroid_model = asteroid_models[i];
2947 ubo.
view = view_matrix;
2948 ubo.
proj = projection_matrix;
2949 ubo.
fx = glm::vec4(camera_position, elapsed_seconds);
2950 asteroid_model.
updateUBO(image_index, ubo);
2951 asteroid_model.
render(current_command_buffer, image_index,
false);
2955 void draw_projectiles() {
2956 for (
const auto &projectile : projectiles) {
2957 if (!projectile.
active) {
2961 const float pulse = (0.55f + 0.22f * (1.0f - life_factor)) * (0.9f + 0.1f * std::sin(elapsed_seconds * 12.0f));
2962 const glm::vec4 color = glm::vec4(
2963 std::clamp(projectile.color.r * (0.9f + 0.1f * life_factor), 0.0f, 1.0f),
2964 std::clamp(projectile.color.g * (0.9f + 0.1f * life_factor), 0.0f, 1.0f),
2965 std::clamp(projectile.color.b * (0.9f + 0.1f * life_factor), 0.0f, 1.0f),
2966 std::clamp(projectile.color.a * (0.65f + 0.35f * life_factor), 0.0f, 1.0f));
2967 projectile_sprite->drawSprite(projectile.position, glm::vec2(pulse), color);
2971 void draw_remote_projectiles() {
2973 if (player == multiplayer.local_player_id())
2975 for (
const NetworkProjectile &projectile : remote_projectiles[player]) {
2976 if (projectile.
active == 0U)
2978 const glm::vec3 position{projectile.position[0], projectile.position[1], projectile.position[2]};
2979 projectile_sprite->drawSprite(position, glm::vec2(0.62f), {0.20f, 0.65f, 1.0f, 1.0f});
2984 void draw_particles() {
2985 for (
const auto &particle : particles) {
2989 effect_sprite->drawSprite(particle.position,
2990 glm::vec2(particle.size),
2995 void create_flame_resources() {
2996 create_flame_mesh();
2997 create_flame_swapchain_resources();
3000 void cleanup_flame_resources() {
3001 cleanup_flame_swapchain_resources();
3002 if (flame_vertex_buffer != VK_NULL_HANDLE) {
3003 vkDestroyBuffer(
device, flame_vertex_buffer,
nullptr);
3004 flame_vertex_buffer = VK_NULL_HANDLE;
3006 if (flame_vertex_buffer_memory != VK_NULL_HANDLE) {
3007 vkFreeMemory(
device, flame_vertex_buffer_memory,
nullptr);
3008 flame_vertex_buffer_memory = VK_NULL_HANDLE;
3010 flame_vertex_count = 0;
3013 void cleanup_flame_swapchain_resources() {
3014 if (flame_pipeline != VK_NULL_HANDLE) {
3015 vkDestroyPipeline(
device, flame_pipeline,
nullptr);
3016 flame_pipeline = VK_NULL_HANDLE;
3018 if (flame_pipeline_layout != VK_NULL_HANDLE) {
3019 vkDestroyPipelineLayout(
device, flame_pipeline_layout,
nullptr);
3020 flame_pipeline_layout = VK_NULL_HANDLE;
3024 void create_flame_swapchain_resources() {
3025 if (flame_vertex_count == 0 ||
device == VK_NULL_HANDLE) {
3028 create_flame_pipeline();
3031 void create_flame_mesh() {
3032 constexpr int segments = 40;
3033 constexpr float base_z = 0.555f;
3034 constexpr float tip_z = 1.02f;
3035 constexpr float base_y = 0.040f;
3036 constexpr float outer_radius = 0.052f;
3037 constexpr float inner_radius = 0.026f;
3039 std::vector<FlameVertex> vertices{};
3040 vertices.reserve(
static_cast<std::size_t
>(segments) * 6U);
3042 const glm::vec4 outer_base_color{1.0f, 0.42f, 0.08f, 0.50f};
3043 const glm::vec4 outer_tip_color{0.7f, 0.08f, 0.0f, 0.0f};
3044 const glm::vec4 inner_base_color{1.0f, 0.92f, 0.45f, 0.72f};
3045 const glm::vec4 inner_tip_color{1.0f, 0.32f, 0.04f, 0.0f};
3047 auto add_cone = [&](
float radius,
const glm::vec4 &base_color,
const glm::vec4 &tip_color) {
3048 const glm::vec3 tip{0.0f, base_y, tip_z};
3049 for (
int i = 0; i < segments; ++i) {
3050 const float a0 = (
static_cast<float>(i) /
static_cast<float>(segments)) * 2.0f *
PI;
3051 const float a1 = (
static_cast<float>(i + 1) /
static_cast<float>(segments)) * 2.0f *
PI;
3052 const glm::vec3 p0{std::cos(a0) * radius, base_y + std::sin(a0) * radius, base_z};
3053 const glm::vec3 p1{std::cos(a1) * radius, base_y + std::sin(a1) * radius, base_z};
3054 vertices.push_back({p0, base_color});
3055 vertices.push_back({p1, base_color});
3056 vertices.push_back({tip, tip_color});
3060 add_cone(outer_radius, outer_base_color, outer_tip_color);
3061 add_cone(inner_radius, inner_base_color, inner_tip_color);
3063 flame_vertex_count =
static_cast<uint32_t
>(vertices.size());
3064 const VkDeviceSize
buffer_size =
sizeof(FlameVertex) *
static_cast<VkDeviceSize
>(vertices.size());
3065 create_buffer(buffer_size,
3066 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
3067 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
3068 flame_vertex_buffer,
3069 flame_vertex_buffer_memory);
3071 void *data =
nullptr;
3072 if (vkMapMemory(
device, flame_vertex_buffer_memory, 0, buffer_size, 0, &data) != VK_SUCCESS || data ==
nullptr) {
3073 throw mxvk::Exception(
"Failed to map asteroids3d flame vertex buffer");
3075 std::memcpy(data, vertices.data(),
static_cast<std::size_t
>(buffer_size));
3076 vkUnmapMemory(
device, flame_vertex_buffer_memory);
3079 void create_flame_pipeline() {
3080 cleanup_flame_swapchain_resources();
3082 const std::vector<char> vert_shader_code =
loadSpv(shader_root +
"/flame.vert.spv");
3083 const std::vector<char> frag_shader_code =
loadSpv(shader_root +
"/flame.frag.spv");
3086 VkShaderModule frag_shader_module = VK_NULL_HANDLE;
3091 VkPipelineShaderStageCreateInfo vert_stage{};
3092 vert_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
3093 vert_stage.stage = VK_SHADER_STAGE_VERTEX_BIT;
3094 vert_stage.module = vert_shader_module;
3095 vert_stage.pName =
"main";
3097 VkPipelineShaderStageCreateInfo frag_stage{};
3098 frag_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
3099 frag_stage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
3100 frag_stage.module = frag_shader_module;
3101 frag_stage.pName =
"main";
3103 std::array<VkPipelineShaderStageCreateInfo, 2> shader_stages = {vert_stage, frag_stage};
3105 VkVertexInputBindingDescription binding_description{};
3106 binding_description.binding = 0;
3107 binding_description.stride =
sizeof(FlameVertex);
3108 binding_description.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
3110 std::array<VkVertexInputAttributeDescription, 2> attributes{};
3111 attributes[0].binding = 0;
3112 attributes[0].location = 0;
3113 attributes[0].format = VK_FORMAT_R32G32B32_SFLOAT;
3114 attributes[0].offset = offsetof(FlameVertex, pos);
3115 attributes[1].binding = 0;
3116 attributes[1].location = 1;
3117 attributes[1].format = VK_FORMAT_R32G32B32A32_SFLOAT;
3118 attributes[1].offset = offsetof(FlameVertex, color);
3120 VkPipelineVertexInputStateCreateInfo vertex_input{};
3121 vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
3122 vertex_input.vertexBindingDescriptionCount = 1;
3123 vertex_input.pVertexBindingDescriptions = &binding_description;
3124 vertex_input.vertexAttributeDescriptionCount =
static_cast<uint32_t
>(attributes.size());
3125 vertex_input.pVertexAttributeDescriptions = attributes.data();
3127 VkPipelineInputAssemblyStateCreateInfo input_assembly{};
3128 input_assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
3129 input_assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
3130 input_assembly.primitiveRestartEnable = VK_FALSE;
3132 VkPipelineViewportStateCreateInfo viewport_state{};
3133 viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
3134 viewport_state.viewportCount = 1;
3135 viewport_state.scissorCount = 1;
3137 const std::array<VkDynamicState, 2> dynamic_states = {
3138 VK_DYNAMIC_STATE_VIEWPORT,
3139 VK_DYNAMIC_STATE_SCISSOR,
3141 VkPipelineDynamicStateCreateInfo dynamic_info{};
3142 dynamic_info.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
3143 dynamic_info.dynamicStateCount =
static_cast<uint32_t
>(dynamic_states.size());
3144 dynamic_info.pDynamicStates = dynamic_states.data();
3146 VkPipelineRasterizationStateCreateInfo rasterizer{};
3147 rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
3148 rasterizer.depthClampEnable = VK_FALSE;
3149 rasterizer.rasterizerDiscardEnable = VK_FALSE;
3150 rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
3151 rasterizer.lineWidth = 1.0f;
3152 rasterizer.cullMode = VK_CULL_MODE_NONE;
3153 rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
3154 rasterizer.depthBiasEnable = VK_FALSE;
3156 VkPipelineMultisampleStateCreateInfo multisampling{};
3157 multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
3158 multisampling.sampleShadingEnable = VK_FALSE;
3159 multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
3161 VkPipelineDepthStencilStateCreateInfo depth_stencil{};
3162 depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
3163 depth_stencil.depthTestEnable = VK_TRUE;
3164 depth_stencil.depthWriteEnable = VK_FALSE;
3165 depth_stencil.depthCompareOp = VK_COMPARE_OP_LESS;
3166 depth_stencil.depthBoundsTestEnable = VK_FALSE;
3167 depth_stencil.stencilTestEnable = VK_FALSE;
3169 VkPipelineColorBlendAttachmentState color_blend_attachment{};
3170 color_blend_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
3171 VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
3172 color_blend_attachment.blendEnable = VK_TRUE;
3173 color_blend_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
3174 color_blend_attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE;
3175 color_blend_attachment.colorBlendOp = VK_BLEND_OP_ADD;
3176 color_blend_attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
3177 color_blend_attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
3178 color_blend_attachment.alphaBlendOp = VK_BLEND_OP_ADD;
3180 VkPipelineColorBlendStateCreateInfo color_blending{};
3181 color_blending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
3182 color_blending.logicOpEnable = VK_FALSE;
3183 color_blending.attachmentCount = 1;
3184 color_blending.pAttachments = &color_blend_attachment;
3186 VkPushConstantRange push_range{};
3187 push_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
3188 push_range.offset = 0;
3189 push_range.size =
sizeof(FlamePushConstants);
3191 VkPipelineLayoutCreateInfo layout_info{};
3192 layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
3193 layout_info.pushConstantRangeCount = 1;
3194 layout_info.pPushConstantRanges = &push_range;
3196 if (vkCreatePipelineLayout(
device, &layout_info,
nullptr, &flame_pipeline_layout) != VK_SUCCESS) {
3197 throw mxvk::Exception(
"Failed to create asteroids3d flame pipeline layout");
3203 VkPipelineRenderingCreateInfo rendering_info{};
3204 rendering_info.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
3205 rendering_info.colorAttachmentCount = 1;
3206 rendering_info.pColorAttachmentFormats = &color_format;
3211 VkGraphicsPipelineCreateInfo pipeline_info{};
3212 pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
3213 pipeline_info.pNext = &rendering_info;
3214 pipeline_info.stageCount =
static_cast<uint32_t
>(shader_stages.size());
3215 pipeline_info.pStages = shader_stages.data();
3216 pipeline_info.pVertexInputState = &vertex_input;
3217 pipeline_info.pInputAssemblyState = &input_assembly;
3218 pipeline_info.pViewportState = &viewport_state;
3219 pipeline_info.pRasterizationState = &rasterizer;
3220 pipeline_info.pMultisampleState = &multisampling;
3221 pipeline_info.pDepthStencilState = &depth_stencil;
3222 pipeline_info.pColorBlendState = &color_blending;
3223 pipeline_info.pDynamicState = &dynamic_info;
3224 pipeline_info.layout = flame_pipeline_layout;
3225 pipeline_info.renderPass = VK_NULL_HANDLE;
3226 pipeline_info.subpass = 0;
3228 if (vkCreateGraphicsPipelines(
device, VK_NULL_HANDLE, 1, &pipeline_info,
nullptr, &flame_pipeline) != VK_SUCCESS) {
3229 throw mxvk::Exception(
"Failed to create asteroids3d flame pipeline");
3232 if (frag_shader_module != VK_NULL_HANDLE) {
3233 vkDestroyShaderModule(
device, frag_shader_module,
nullptr);
3235 vkDestroyShaderModule(
device, vert_shader_module,
nullptr);
3236 cleanup_flame_swapchain_resources();
3240 vkDestroyShaderModule(
device, frag_shader_module,
nullptr);
3241 vkDestroyShaderModule(
device, vert_shader_module,
nullptr);
3244 void draw_engine_flame(VkCommandBuffer cmd,
3245 const VkExtent2D &extent,
3246 const glm::mat4 &ship_matrix,
3248 bool ship_visible) {
3249 if (!ship_visible) {
3252 if (flame_pipeline == VK_NULL_HANDLE || flame_vertex_buffer == VK_NULL_HANDLE || flame_vertex_count == 0) {
3256 VkViewport viewport{};
3259 viewport.width =
static_cast<float>(extent.width);
3260 viewport.height =
static_cast<float>(extent.height);
3261 viewport.minDepth = 0.0f;
3262 viewport.maxDepth = 1.0f;
3263 vkCmdSetViewport(cmd, 0, 1, &viewport);
3266 scissor.offset = {0, 0};
3267 scissor.extent = extent;
3268 vkCmdSetScissor(cmd, 0, 1, &scissor);
3270 FlamePushConstants pc{};
3271 pc.mvp = projection_matrix * view_matrix * ship_matrix;
3272 const float flame_power = std::clamp(ship_speed / ship.max_speed, 0.08f, 1.0f);
3273 pc.params = glm::vec4(elapsed_seconds, flame_power, 0.0f, 0.0f);
3275 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, flame_pipeline);
3276 vkCmdPushConstants(cmd,
3277 flame_pipeline_layout,
3278 VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
3283 VkBuffer vertex_buffers[] = {flame_vertex_buffer};
3284 VkDeviceSize offsets[] = {0};
3285 vkCmdBindVertexBuffers(cmd, 0, 1, vertex_buffers, offsets);
3286 vkCmdDraw(cmd, flame_vertex_count, 1, 0, 0);
3289 void create_buffer(VkDeviceSize size,
3290 VkBufferUsageFlags usage,
3291 VkMemoryPropertyFlags properties,
3293 VkDeviceMemory &buffer_memory)
const {
3294 VkBufferCreateInfo buffer_info{};
3295 buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
3296 buffer_info.size = size;
3297 buffer_info.usage = usage;
3298 buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
3300 if (vkCreateBuffer(
device, &buffer_info,
nullptr, &buffer) != VK_SUCCESS) {
3301 throw mxvk::Exception(
"Failed to create asteroids3d buffer");
3304 VkMemoryRequirements mem_requirements{};
3305 vkGetBufferMemoryRequirements(
device, buffer, &mem_requirements);
3307 VkMemoryAllocateInfo alloc_info{};
3308 alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
3309 alloc_info.allocationSize = mem_requirements.size;
3312 alloc_info.memoryTypeIndex = find_memory_type(mem_requirements.memoryTypeBits, properties);
3313 if (vkAllocateMemory(
device, &alloc_info,
nullptr, &buffer_memory) != VK_SUCCESS) {
3314 throw mxvk::Exception(
"Failed to allocate asteroids3d buffer memory");
3316 if (vkBindBufferMemory(
device, buffer, buffer_memory, 0) != VK_SUCCESS) {
3317 throw mxvk::Exception(
"Failed to bind asteroids3d buffer memory");
3320 if (buffer_memory != VK_NULL_HANDLE) {
3321 vkFreeMemory(
device, buffer_memory,
nullptr);
3322 buffer_memory = VK_NULL_HANDLE;
3324 if (buffer != VK_NULL_HANDLE) {
3325 vkDestroyBuffer(
device, buffer,
nullptr);
3326 buffer = VK_NULL_HANDLE;
3332 [[nodiscard]] uint32_t find_memory_type(uint32_t type_filter, VkMemoryPropertyFlags properties)
const {
3333 VkPhysicalDeviceMemoryProperties mem_properties{};
3334 vkGetPhysicalDeviceMemoryProperties(
physical_device, &mem_properties);
3336 for (uint32_t i = 0; i < mem_properties.memoryTypeCount; ++i) {
3337 if ((type_filter & (1U << i)) && (mem_properties.memoryTypes[i].propertyFlags & properties) == properties) {
3342 throw mxvk::Exception(
"Failed to find asteroids3d memory type");
3345 void draw_ui_rect(
int x,
int y,
int width,
int height,
const glm::vec4 &color) {
3346 if (ui_pixel ==
nullptr || width <= 0 || height <= 0) {
3349 ui_pixel->setShaderParams(color.r, color.g, color.b, color.a);
3350 ui_pixel->drawSpriteRect(x, y, width, height);
3353 std::optional<glm::ivec2> project_world_to_screen(
const glm::vec3 &world_position,
const VkExtent2D &extent)
const {
3354 const glm::vec4 clip = projection_matrix * view_matrix * glm::vec4(world_position, 1.0f);
3355 if (clip.w <= 0.0001f) {
3356 return std::nullopt;
3359 const glm::vec3 ndc = glm::vec3(clip) / clip.w;
3360 if (ndc.z < 0.0f || ndc.z > 1.0f) {
3361 return std::nullopt;
3364 const int x =
static_cast<int>((ndc.x * 0.5f + 0.5f) *
static_cast<float>(extent.width));
3365 const int y =
static_cast<int>((ndc.y * 0.5f + 0.5f) *
static_cast<float>(extent.height));
3366 return glm::ivec2{x, y};
3369 bool cannon_has_asteroid_target(
const glm::vec3 &muzzle,
const glm::vec3 &forward)
const {
3370 constexpr float AIM_ASSIST_SCALE = 1.04f;
3372 for (
const auto &asteroid : asteroids) {
3377 const glm::vec3 to_asteroid = asteroid.position - muzzle;
3378 const float along_ray = glm::dot(to_asteroid, forward);
3379 if (along_ray < 0.0f || along_ray > max_range) {
3383 const glm::vec3 closest_point = muzzle + forward * along_ray;
3385 if (glm::length(closest_point - asteroid.position) <= hit_radius) {
3392 bool cannon_has_opponent_target(
const glm::vec3 &muzzle,
const glm::vec3 &forward)
const {
3393 if (!multiplayer_match)
3395 constexpr float OPPONENT_TARGET_RADIUS = 1.8f;
3398 if (player == multiplayer.local_player_id() || !multiplayer.player_connected()[player] ||
3399 !remote_ships[player].visible || remote_ship_exploding[player])
3401 const glm::vec3 to_opponent = remote_ships[player].position - muzzle;
3402 const float along_ray = glm::dot(to_opponent, forward);
3403 if (along_ray < 0.0f || along_ray > max_range)
3405 const glm::vec3 closest_point = muzzle + forward * along_ray;
3406 if (glm::length(closest_point - remote_ships[player].position) <= OPPONENT_TARGET_RADIUS)
3412 void draw_cannon_crosshair(
const VkExtent2D &extent) {
3413 if (ui_pixel ==
nullptr || !ship.visible || ship.exploding || extent.width < 160U || extent.height < 120U) {
3417 constexpr float AIM_DISTANCE = 120.0f;
3418 constexpr float MUZZLE_OFFSET = 0.08f;
3419 constexpr int ARM_LENGTH = 18;
3420 constexpr int GAP = 6;
3421 constexpr int THICKNESS = 2;
3422 const glm::vec3 forward = ship.forward();
3423 const glm::vec3 muzzle = ship.position + forward * MUZZLE_OFFSET;
3424 const glm::vec3 aim_position = muzzle + forward * AIM_DISTANCE;
3425 const std::optional<glm::ivec2> screen_position = project_world_to_screen(aim_position, extent);
3426 if (!screen_position.has_value()) {
3430 const int x = std::clamp(screen_position->x, ARM_LENGTH + 2,
static_cast<int>(extent.width) - ARM_LENGTH - 2);
3431 const int y = std::clamp(screen_position->y, ARM_LENGTH + 2,
static_cast<int>(extent.height) - ARM_LENGTH - 2);
3432 const bool opponent_locked = cannon_has_opponent_target(muzzle, forward);
3433 const bool asteroid_locked = cannon_has_asteroid_target(muzzle, forward);
3434 const glm::vec4 shadow = opponent_locked
3435 ? glm::vec4{0.12f, 0.08f, 0.0f, 0.75f}
3436 : (asteroid_locked ? glm::vec4{0.0f, 0.02f, 0.07f, 0.7f} : glm::vec4{0.05f, 0.0f, 0.0f, 0.7f});
3437 const glm::vec4 crosshair_color = opponent_locked
3438 ? glm::vec4{1.0f, 0.86f, 0.08f, 1.0f}
3439 : (asteroid_locked ? glm::vec4{0.12f, 0.58f, 1.0f, 0.98f} : glm::vec4{1.0f, 0.03f, 0.02f, 0.96f});
3441 draw_ui_rect(x - ARM_LENGTH - 1, y - THICKNESS / 2 - 1, ARM_LENGTH - GAP + 2, THICKNESS + 2, shadow);
3442 draw_ui_rect(x + GAP - 1, y - THICKNESS / 2 - 1, ARM_LENGTH - GAP + 2, THICKNESS + 2, shadow);
3443 draw_ui_rect(x - THICKNESS / 2 - 1, y - ARM_LENGTH - 1, THICKNESS + 2, ARM_LENGTH - GAP + 2, shadow);
3444 draw_ui_rect(x - THICKNESS / 2 - 1, y + GAP - 1, THICKNESS + 2, ARM_LENGTH - GAP + 2, shadow);
3446 draw_ui_rect(x - ARM_LENGTH, y - THICKNESS / 2, ARM_LENGTH - GAP, THICKNESS, crosshair_color);
3447 draw_ui_rect(x + GAP, y - THICKNESS / 2, ARM_LENGTH - GAP, THICKNESS, crosshair_color);
3448 draw_ui_rect(x - THICKNESS / 2, y - ARM_LENGTH, THICKNESS, ARM_LENGTH - GAP, crosshair_color);
3449 draw_ui_rect(x - THICKNESS / 2, y + GAP, THICKNESS, ARM_LENGTH - GAP, crosshair_color);
3450 draw_ui_rect(x - 1, y - 1, 3, 3, crosshair_color);
3453 void draw_radar(
const VkExtent2D &extent) {
3454 if (ui_pixel ==
nullptr || extent.width < 360U || extent.height < 280U) {
3458 constexpr int BORDER = 2;
3459 constexpr float RADAR_RANGE = 180.0f;
3460 const int radar_size = std::clamp(
static_cast<int>(std::min(extent.width, extent.height)) / 4, 150, 220);
3461 const int radar_x = 24;
3462 const int radar_y = std::max(230,
static_cast<int>(extent.height) - radar_size - 24);
3463 const int inner_x = radar_x + BORDER;
3464 const int inner_y = radar_y + BORDER;
3465 const int inner_size = radar_size - BORDER * 2;
3466 const int center_x = inner_x + inner_size / 2;
3467 const int center_y = inner_y + inner_size / 2;
3468 const float half_size =
static_cast<float>(inner_size) * 0.5f;
3470 draw_ui_rect(radar_x, radar_y, radar_size, radar_size, {0.01f, 0.025f, 0.045f, 0.74f});
3471 const glm::vec4 border_color = ship_returning_to_field ? glm::vec4{1.0f, 0.64f, 0.18f, 0.94f} : glm::vec4{0.16f, 0.66f, 0.82f, 0.9f};
3472 draw_ui_rect(radar_x, radar_y, radar_size, BORDER, border_color);
3473 draw_ui_rect(radar_x, radar_y + radar_size - BORDER, radar_size, BORDER, border_color);
3474 draw_ui_rect(radar_x, radar_y, BORDER, radar_size, border_color);
3475 draw_ui_rect(radar_x + radar_size - BORDER, radar_y, BORDER, radar_size, border_color);
3477 draw_ui_rect(center_x, inner_y, 1, inner_size, {0.10f, 0.30f, 0.38f, 0.7f});
3478 draw_ui_rect(inner_x, center_y, inner_size, 1, {0.10f, 0.30f, 0.38f, 0.7f});
3479 draw_ui_rect(center_x - inner_size / 4, inner_y, 1, inner_size, {0.08f, 0.22f, 0.28f, 0.45f});
3480 draw_ui_rect(center_x + inner_size / 4, inner_y, 1, inner_size, {0.08f, 0.22f, 0.28f, 0.45f});
3481 draw_ui_rect(inner_x, center_y - inner_size / 4, inner_size, 1, {0.08f, 0.22f, 0.28f, 0.45f});
3482 draw_ui_rect(inner_x, center_y + inner_size / 4, inner_size, 1, {0.08f, 0.22f, 0.28f, 0.45f});
3484 for (
const auto &asteroid : asteroids) {
3488 glm::vec2 relative{asteroid.position.
x - ship.position.x, asteroid.position.z - ship.position.z};
3489 const float distance = glm::length(relative);
3490 const bool clamped_to_edge = distance > RADAR_RANGE;
3491 if (clamped_to_edge && distance > 1e-4f) {
3492 relative *= RADAR_RANGE / distance;
3494 const int dot_x = center_x +
static_cast<int>((relative.x / RADAR_RANGE) * half_size);
3495 const int dot_y = center_y +
static_cast<int>((relative.y / RADAR_RANGE) * half_size);
3496 const int dot_size = std::clamp(
static_cast<int>(asteroid.
radius * 0.55f), 3, 8);
3498 const glm::vec4 dot_color = clamped_to_edge
3499 ? glm::vec4{1.0f, 0.38f, 0.16f, 0.9f}
3500 : glm::vec4{1.0f, 0.55f + altitude * 0.28f, 0.18f, 1.0f};
3501 draw_ui_rect(dot_x - dot_size / 2, dot_y - dot_size / 2, dot_size, dot_size, dot_color);
3504 if (multiplayer_match) {
3506 if (player == multiplayer.local_player_id() || !multiplayer.player_connected()[player])
3508 glm::vec2 relative{remote_ships[player].position.x - ship.position.x, remote_ships[player].position.z - ship.position.z};
3509 const float distance = glm::length(relative);
3510 if (distance > RADAR_RANGE && distance > 1e-4f)
3511 relative *= RADAR_RANGE / distance;
3512 const int opponent_x = center_x +
static_cast<int>((relative.x / RADAR_RANGE) * half_size);
3513 const int opponent_y = center_y +
static_cast<int>((relative.y / RADAR_RANGE) * half_size);
3514 const glm::vec4 color = remote_ship_exploding[player] ? glm::vec4{1.0f, 0.35f, 0.12f, 1.0f} : glm::vec4{1.0f, 0.08f, 0.12f, 1.0f};
3515 draw_ui_rect(opponent_x - 6, opponent_y - 6, 12, 12, color);
3516 draw_ui_rect(opponent_x - 9, opponent_y - 1, 18, 3, color);
3517 draw_ui_rect(opponent_x - 1, opponent_y - 9, 3, 18, color);
3521 draw_ui_rect(center_x - 5, center_y, 11, 2, {0.95f, 1.0f, 1.0f, 1.0f});
3522 draw_ui_rect(center_x, center_y - 5, 2, 11, {0.95f, 1.0f, 1.0f, 1.0f});
3524 const SDL_Color label_color = ship_returning_to_field ? SDL_Color{255, 180, 80, 255} : SDL_Color{120, 220, 255, 255};
3525 printText(multiplayer_match ?
"RADAR - ENEMY RED" : (ship_returning_to_field ?
"RADAR RETURN" :
"RADAR"), radar_x, std::max(4, radar_y - 22), label_color);
3528 void draw_hud([[maybe_unused]]
float aspect) {
3529 set_ui_font_size(18);
3530 const SDL_Color white{255, 255, 255, 255};
3531 const SDL_Color red{220, 60, 60, 255};
3532 const SDL_Color yellow{255, 220, 120, 255};
3534 draw_cannon_crosshair(extent);
3536 const int right_x = std::max(25,
static_cast<int>(extent.width) - 250);
3537 printText(
"MXVK Asteroids v1.0", right_x, 25, red);
3538 if (multiplayer_match) {
3539 const std::uint8_t local_id = multiplayer.local_player_id();
3540 const unsigned local_kills = local_id <
NETWORK_PLAYER_COUNT ? multiplayer_kills[local_id] : 0U;
3541 unsigned enemy_kills = 0;
3543 if (player != local_id)
3544 enemy_kills = std::max(enemy_kills, multiplayer_kills[player]);
3545 printText(
"Kills: " + std::to_string(local_kills) +
" / " + std::to_string(MULTIPLAYER_KILLS_TO_WIN), right_x, 50, white);
3546 printText(
"Leader: " + std::to_string(enemy_kills) +
" / " + std::to_string(MULTIPLAYER_KILLS_TO_WIN), right_x, 75, red);
3547 printText(
"Players: " + std::to_string(multiplayer.player_count()) +
" / 4", right_x, 100, white);
3548 printText(
"UDP host-authoritative", right_x, 125, yellow);
3551 printText(
"Score: " + std::to_string(ship.score), right_x, 50, white);
3552 printText(
"Lives: " + std::to_string(std::max(0, ship.lives)), right_x, 75, white);
3553 printText(
"Asteroids: " + std::to_string(active_asteroids()), right_x, 100, white);
3554 printText(
"Time Left: " + format_round_time(), right_x, 125, round_time_remaining <= 30.0f ? yellow : white);
3555 printText(
"[F1 for Debug]", right_x, 150, white);
3556 printText(inverted_controls ?
"[Inverted] F2/Y" :
"[Arcade] F2/Y", right_x, 175, white);
3557 printText(
"[F3 for Console]", right_x, 200, white);
3558 printText(mouse_look_controls ?
"[Mouse Look] F5" :
"[Classic Keys] F5", right_x, 225, white);
3559 printText(first_person_camera ?
"[First Person] F7" :
"[Chase View] F7", right_x, 250, white);
3565 const float fps = (last_delta_time > 0.0001f) ? (1.0f / last_delta_time) : 0.0f;
3566 printText(
"Ship X,Y,Z: " + vec3_string(ship.position), 25, 25, white);
3567 printText(
"Velocity X,Y,Z: " + vec3_string(ship.velocity), 25, 50, white);
3568 printText(
"FPS: " + std::to_string(fps), 25, 75, white);
3570 printText(mouse_look_controls ?
"Controls: Mouse look, W/S speed, A/D roll, click/SPACE to shoot" :
"Controls: Arrows to Move, W,S Tilt Up/Down - SPACE to shoot", 25, 125, white);
3571 printText(
"Nearest Object: " + std::to_string(nearest_asteroid_distance()), 25, 150, white);
3572 printText(
"Farthest Object: " + std::to_string(farthest_asteroid_distance()), 25, 175, white);
3573 printText(
"Speed: " + std::to_string(ship.current_speed) +
" / " + std::to_string(ship.max_speed), 25, 200, white);
3574 printText(
"Controller: " + controller_status(), 25, 225, white);
3575 printText(std::string(
"Input: ") + (mouse_look_controls ?
"Keyboard/mouse" :
"Classic keyboard"), 25, 250, white);
3576 printText(std::string(
"Camera: ") + (first_person_camera ?
"First person" :
"Chase"), 25, 275, white);
3577 printText(
"Press ENTER to randomize asteroids", 25, 300, white);
3580 int active_asteroids()
const {
3582 for (
const auto &asteroid : asteroids) {
3590 void draw_multiplayer_end(
const VkExtent2D &extent) {
3591 multiplayer.exchange(make_network_state(
true), last_delta_time);
3592 set_ui_font_size(22);
3593 const std::uint8_t local_id = multiplayer.local_player_id();
3594 const bool local_won = multiplayer_winner == local_id + 1U;
3595 const std::size_t winner_index = multiplayer_winner > 0U ? multiplayer_winner - 1U : 0U;
3596 const std::string winner_name = winner_index < multiplayer_player_names.size() && !multiplayer_player_names[winner_index].empty()
3597 ? multiplayer_player_names[winner_index]
3598 : std::format(
"Player {}", multiplayer_winner);
3599 const std::string result_text = winner_name +
" WINS";
3600 const std::string score_text = std::format(
"Final score: {} kills", local_id <
NETWORK_PLAYER_COUNT ? multiplayer_kills[local_id] : 0U);
3601 const std::string prompt_text =
"Press ENTER to return to the multiplayer lobby";
3603 const auto text_width = [
this](
const std::string &text) {
3611 const int widest_text = std::max({text_width(result_text), text_width(score_text), text_width(prompt_text)});
3612 const int panel_width = std::max(620, widest_text + 80);
3613 const int panel_x =
static_cast<int>(extent.width) / 2 - panel_width / 2;
3614 const int panel_y =
static_cast<int>(extent.height) / 2 - 150;
3615 draw_ui_rect(panel_x, panel_y, panel_width, 300, {0.015f, 0.025f, 0.08f, 0.94f});
3616 draw_ui_rect(panel_x, panel_y, panel_width, 4, local_won ? glm::vec4{0.2f, 1.0f, 0.55f, 1.0f} : glm::vec4{1.0f, 0.2f, 0.2f, 1.0f});
3617 printText(result_text, panel_x + (panel_width - text_width(result_text)) / 2, panel_y + 48,
3618 local_won ? SDL_Color{100, 255, 160, 255} : SDL_Color{255, 90, 90, 255});
3619 printText(score_text, panel_x + (panel_width - text_width(score_text)) / 2, panel_y + 120, {255, 255, 255, 255});
3620 printText(prompt_text, panel_x + (panel_width - text_width(prompt_text)) / 2, panel_y + 205, {255, 220, 120, 255});
3623 void draw_end_screen([[maybe_unused]] uint32_t image_index,
3624 [[maybe_unused]]
float aspect,
3625 const std::string &title,
3626 const SDL_Color &title_color) {
3627 set_ui_font_size(32);
3628 const SDL_Color white{255, 255, 255, 255};
3629 const SDL_Color yellow{255, 220, 120, 255};
3632 const std::string score_text =
"Final Score: " + std::to_string(ship.score);
3633 const std::string prompt =
"Press ENTER to start over";
3639 static_cast<int>(extent.width) / 2 - title_w / 2,
3640 static_cast<int>(extent.height) / 2 - title_h,
3643 printText(title.c_str(), 24, 20, title_color);
3650 static_cast<int>(extent.width) / 2 - score_w / 2,
3651 static_cast<int>(extent.height) / 2 + 10,
3654 printText(score_text.c_str(), 24, 70, white);
3661 static_cast<int>(extent.width) / 2 - prompt_w / 2,
3662 static_cast<int>(extent.height) / 2 + score_h + 28,
3665 printText(prompt.c_str(), 24, 100, yellow);
3669 void draw_game_over([[maybe_unused]] uint32_t image_index, [[maybe_unused]]
float aspect) {
3670 draw_end_screen(image_index, aspect,
"Game over", SDL_Color{235, 60, 60, 255});
3673 VkCommandBuffer current_command_buffer = VK_NULL_HANDLE;
3674 float last_delta_time = 1.0f / 60.0f;
3676 std::string vec3_string(
const glm::vec3 &value)
const {
3677 return std::to_string(value.x) +
", " + std::to_string(value.y) +
", " + std::to_string(value.z);
3680 float nearest_asteroid_distance()
const {
3681 float nearest = 999999.0f;
3682 for (
const auto &asteroid : asteroids) {
3684 nearest = std::min(nearest, glm::length(ship.position - asteroid.position));
3690 float farthest_asteroid_distance()
const {
3691 float farthest = 0.0f;
3692 for (
const auto &asteroid : asteroids) {
3694 farthest = std::max(farthest, glm::length(ship.position - asteroid.position));