22 asset_root((path.empty() || path ==
".") ? std::string(DEFENDER_ASSET_DIR) : path) {
24 setFont(asset_root +
"/data/font.ttf", DEFAULT_FONT_SIZE);
25 hud_font.reset(asset_root +
"/data/font.ttf", HUD_FONT_SIZE);
26 countdown_font.reset(asset_root +
"/data/font.ttf", COUNTDOWN_FONT_SIZE);
28 const std::string model_vert = asset_root +
"/data/model.vert.spv";
29 const std::string model_frag = asset_root +
"/data/model.frag.spv";
30 ship_model.load(
this, asset_root +
"/data/starship.obj", asset_root +
"/data/starship.mtl", asset_root +
"/data", 1.0f);
31 ship_model.setShaders(
this, model_vert, model_frag);
32 for (
auto &asteroid_model : asteroid_models) {
33 asteroid_model.load(
this, asset_root +
"/data/asteroid.obj", asset_root +
"/data/asteroid.mtl", asset_root +
"/data", 1.0f);
34 asteroid_model.setShaders(
this, model_vert, model_frag);
38 asset_root +
"/data/intro.png",
39 asset_root +
"/data/sprite.vert.spv",
40 asset_root +
"/data/intro.frag.spv");
44 asset_root +
"/data/sprite.vert.spv",
45 asset_root +
"/data/fade_overlay.frag.spv");
46 const uint32_t black_pixel = 0xFF000000u;
47 fade_overlay_sprite->updateTexture(&black_pixel, 1, 1);
51 intro_rain_config.
color =
"#ff0000";
54 intro_rain = std::make_unique<matrix::Rain>(*
this, std::move(intro_rain_config));
58 star_sprite->setDepthTestEnabled(
false);
59 star_sprite->setDepthWriteEnabled(
false);
60 star_sprite->setAlphaDiscardThreshold(0.02f);
62 star_field.setSprite(star_sprite);
64 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> terrain_surface(SDL_CreateSurface(1, 1, SDL_PIXELFORMAT_RGBA32), SDL_DestroySurface);
65 if (terrain_surface ==
nullptr) {
68 const SDL_PixelFormatDetails *terrain_format = SDL_GetPixelFormatDetails(terrain_surface->format);
69 if (terrain_format ==
nullptr) {
70 throw mxvk::Exception(
"Failed to query defender terrain surface format");
72 SDL_FillSurfaceRect(terrain_surface.get(),
nullptr, SDL_MapRGBA(terrain_format,
nullptr, 255, 255, 255, 255));
74 terrain_sprite->setDepthTestEnabled(
false);
75 terrain_sprite->setDepthWriteEnabled(
false);
76 terrain_sprite->setAlphaDiscardThreshold(0.01f);
78 projectile_sprite =
createSprite3D(asset_root +
"/data/particle_projectile.png");
79 projectile_sprite->setDepthTestEnabled(
true);
80 projectile_sprite->setDepthWriteEnabled(
false);
81 projectile_sprite->setAlphaDiscardThreshold(0.05f);
84 const std::string frame_path = asset_root + std::format(
"/data/ufo_lights_{:02d}.png", i);
85 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> saucer_surface(
space::load_color_keyed_png(frame_path, 8, 24), SDL_DestroySurface);
88 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Classic)][
static_cast<std::size_t
>(i)]->setDepthTestEnabled(
true);
89 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Classic)][
static_cast<std::size_t
>(i)]->setDepthWriteEnabled(
false);
90 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Classic)][
static_cast<std::size_t
>(i)]->setAlphaDiscardThreshold(0.02f);
92 const std::string ufox_frame_path = asset_root + std::format(
"/data/ufox{}.png", i + 1);
93 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> ufox_surface(
load_light_background_png(ufox_frame_path), SDL_DestroySurface);
96 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Ufox)][
static_cast<std::size_t
>(i)]->setDepthTestEnabled(
true);
97 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Ufox)][
static_cast<std::size_t
>(i)]->setDepthWriteEnabled(
false);
98 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Ufox)][
static_cast<std::size_t
>(i)]->setAlphaDiscardThreshold(0.02f);
100 const std::string alien_frame_path = asset_root + std::format(
"/data/alien{}.png", i + 1);
101 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> alien_surface(
load_light_background_png(alien_frame_path), SDL_DestroySurface);
104 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Alien)][
static_cast<std::size_t
>(i)]->setDepthTestEnabled(
true);
105 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Alien)][
static_cast<std::size_t
>(i)]->setDepthWriteEnabled(
false);
106 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Alien)][
static_cast<std::size_t
>(i)]->setAlphaDiscardThreshold(0.02f);
109 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> explosion_surface(
space::load_color_keyed_png(asset_root +
"/data/particle_explosion.png", 12), SDL_DestroySurface);
111 effect_sprite->setDepthTestEnabled(
true);
112 effect_sprite->setDepthWriteEnabled(
false);
113 effect_sprite->setAlphaDiscardThreshold(0.05f);
117 create_flame_resources();
119 ship.position = glm::vec3(0.0f, 0.0f, 0.0f);
120 ship.velocity = glm::vec3(0.0f, 0.0f, 0.0f);
121 ship.current_speed = 0.0f;
122 ship.min_speed = 0.0f;
123 ship.max_speed = 28.0f;
124 ship.rotation = glm::vec3(0.0f, -90.0f, 0.0f);
126#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
127 background_music = std::make_unique<mxvk::VK_Mixer>();
128 background_music_track = background_music->loadMusic(asset_root +
"/data/background.ogg");
129 crash_sound = background_music->loadWav(asset_root +
"/data/crash.wav");
130 shoot_sound = background_music->loadWav(asset_root +
"/data/shoot.wav");
131 takeoff_sound = background_music->loadWav(asset_root +
"/data/takeoff.wav");
132 asteroid_explosion_sound = background_music->loadWav(asset_root +
"/data/asteroid.wav");
133 ufo_explosion_sound = background_music->loadWav(asset_root +
"/data/ufo.wav");
134 ensure_background_music_playing();
177 if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
178 if (!controller.active()) {
179 controller.connectEvent(e);
181 sync_controller_connection();
184 if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
185 controller.connectEvent(e);
186 sync_controller_connection();
190 const bool was_console_visible =
console.isVisible();
191 const bool is_console_toggle = e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_F3;
193 if (is_console_toggle) {
195 log_game(
console.isVisible() ?
"Console opened." :
"Console closed.");
198 if (was_console_visible) {
203 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
204 log_game(
"Exit requested.");
208 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_F4 && !e.key.repeat) {
209 show_fps_counter = !show_fps_counter;
210 log_game(std::string(
"FPS counter ") + (show_fps_counter ?
"enabled." :
"disabled."));
213 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_F8 && !e.key.repeat) {
214 crt_enabled = !crt_enabled;
216 log_game(std::string(
"CRT effect ") + (crt_enabled ?
"enabled." :
"disabled."));
219 if (mode ==
GameMode::Intro && e.type == SDL_EVENT_KEY_DOWN && (e.key.key == SDLK_SPACE || e.key.key == SDLK_RETURN || e.key.key == SDLK_KP_ENTER)) {
221 log_game(
"Intro skipped from keyboard.");
224 if (game_over && e.type == SDL_EVENT_KEY_DOWN && (e.key.key == SDLK_RETURN || e.key.key == SDLK_KP_ENTER)) {
226 log_game(
"Game restarted from keyboard.");
230 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
231 if (e.gbutton.button == SDL_GAMEPAD_BUTTON_BACK) {
232 log_game(
"Exit requested from controller.");
236 if (mode ==
GameMode::Intro && (e.gbutton.button == SDL_GAMEPAD_BUTTON_SOUTH || e.gbutton.button == SDL_GAMEPAD_BUTTON_START)) {
238 log_game(
"Intro skipped from controller.");
241 if (game_over && (e.gbutton.button == SDL_GAMEPAD_BUTTON_SOUTH || e.gbutton.button == SDL_GAMEPAD_BUTTON_START)) {
243 log_game(
"Game restarted from controller.");
247 if (e.gbutton.button == SDL_GAMEPAD_BUTTON_SOUTH) {
249 }
else if (e.gbutton.button == SDL_GAMEPAD_BUTTON_WEST) {
250 reverse_pressed =
true;
255 if (e.type == SDL_EVENT_KEY_DOWN || e.type == SDL_EVENT_KEY_UP) {
256 const bool pressed = e.type == SDL_EVENT_KEY_DOWN;
257 const bool accepts_ship_thrust = mode ==
GameMode::Playing && !game_over && !ship_respawning;
258 if (e.key.key == SDLK_A) {
259 roll_left_pressed = pressed;
260 }
else if (e.key.key == SDLK_S) {
261 roll_right_pressed = pressed;
262 }
else if (e.key.key == SDLK_X) {
263 if (accepts_ship_thrust && pressed && !e.key.repeat) {
264 reverse_pressed =
true;
265 }
else if (!accepts_ship_thrust) {
266 reverse_pressed =
false;
268 }
else if (e.key.key == SDLK_Z) {
269 propulsion_pressed = accepts_ship_thrust && pressed;
270 }
else if (e.key.key == SDLK_D) {
271 boost_pressed = accepts_ship_thrust && pressed;
272 }
else if (e.key.key == SDLK_UP || e.key.key == SDLK_W) {
273 up_pressed = pressed;
274 }
else if (e.key.key == SDLK_DOWN) {
275 down_pressed = pressed;
276 }
else if (e.key.key == SDLK_SPACE && pressed && !e.key.repeat) {
328#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
329 ensure_background_music_playing();
331 const auto now = std::chrono::steady_clock::now();
332 const float delta_seconds = std::chrono::duration<float>(now - last_frame_time).count();
333 last_frame_time = now;
334 const float dt = std::min(delta_seconds, 0.1f);
335 elapsed_seconds += dt;
336 update_fps_counter(delta_seconds);
338 const bool console_visible =
console.isVisible();
339 if (!console_visible) {
340 sync_controller_connection();
341 update_controller_input();
342 update_barrel_roll(dt);
343 if (ship_respawning) {
348 update_asteroids(dt);
349 resolve_enemy_overlaps();
350 update_projectiles(dt);
351 check_projectile_ufo_hits();
352 check_projectile_asteroid_hits();
353 check_ship_ufo_collisions();
354 check_ship_asteroid_collisions();
355 update_level_progress();
358 update_particles(dt);
361 update_playable_world_top(extent);
362 const float aspect = (extent.height > 0U) ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height) : 1.0f;
371 update_camera_scroll(aspect);
372 update_camera_position(dt);
375 const glm::mat4 view = glm::lookAt(camera_position, camera_target, glm::vec3(0.0f, 1.0f, 0.0f));
376 glm::mat4 projection = glm::perspective(glm::radians(50.0f), aspect, 0.1f, 400.0f);
377 projection[1][1] *= -1.0f;
379 draw_countdown(cmd, image_index, extent, view, projection);
381 draw_intro_fade_in(extent);
387 update_camera_scroll(aspect);
389 update_camera_position(dt);
392 const glm::mat4 view = glm::lookAt(camera_position, camera_target, glm::vec3(0.0f, 1.0f, 0.0f));
393 glm::mat4 projection = glm::perspective(glm::radians(50.0f), aspect, 0.1f, 400.0f);
394 projection[1][1] *= -1.0f;
396 VkRect2D gameplay_scissor{};
397 gameplay_scissor.offset = {0, std::min(
GAME_VIEWPORT_TOP,
static_cast<int>(extent.height))};
398 gameplay_scissor.extent = {extent.width, extent.height -
static_cast<uint32_t
>(gameplay_scissor.offset.y)};
399 vkCmdSetScissor(cmd, 0, 1, &gameplay_scissor);
401 star_field.update(dt, camera_position, elapsed_seconds);
402 star_sprite->updateCamera(image_index, view, projection);
404 star_sprite->render(cmd, image_index);
405 star_sprite->clearQueue();
407 terrain_sprite->updateCamera(image_index, view, projection);
409 terrain_sprite->render(cmd, image_index);
410 terrain_sprite->clearQueue();
412 for (
auto &sprite_set : ufo_sprite_sets) {
414 sprite->updateCamera(image_index, view, projection);
418 for (
auto &sprite_set : ufo_sprite_sets) {
420 sprite->render(cmd, image_index);
421 sprite->clearQueue();
425 draw_asteroids(cmd, image_index, view, projection);
427 projectile_sprite->updateCamera(image_index, view, projection);
429 projectile_sprite->render(cmd, image_index);
430 projectile_sprite->clearQueue();
433 ubo.
model = build_ship_model_matrix();
434 last_ship_model_matrix = ubo.
model;
436 ubo.
proj = projection;
437 if (ship.visible && !ship_respawning) {
438 ship_model.updateUBO(image_index, ubo);
439 ship_model.render(cmd, image_index,
false);
440 draw_engine_flame(cmd, extent, view, projection);
443 effect_sprite->updateCamera(image_index, view, projection);
445 effect_sprite->render(cmd, image_index);
446 effect_sprite->clearQueue();
448 VkRect2D full_scissor{};
449 full_scissor.extent = extent;
450 vkCmdSetScissor(cmd, 0, 1, &full_scissor);
452 if (!console_visible) {