20 DefenderWindow::DefenderWindow(
const std::string &path,
int width,
int height,
bool fullscreen,
bool enable_vsync) :
mxvk::
VK_Window(
"Defender Starfield Demo", width, height, fullscreen,
MXVK_VALIDATION, enable_vsync), asset_root((path.empty() || path ==
".") ? std::string(DEFENDER_ASSET_DIR) : path) {
22 setFont(asset_root +
"/data/font.ttf", DEFAULT_FONT_SIZE);
23 hud_font.reset(asset_root +
"/data/font.ttf", HUD_FONT_SIZE);
24 countdown_font.reset(asset_root +
"/data/font.ttf", COUNTDOWN_FONT_SIZE);
26 const std::string model_vert = asset_root +
"/data/model.vert.spv";
27 const std::string model_frag = asset_root +
"/data/model.frag.spv";
28 ship_model.load(
this, asset_root +
"/data/starship.obj", asset_root +
"/data/starship.mtl", asset_root +
"/data", 1.0f);
29 ship_model.setShaders(
this, model_vert, model_frag);
30 for (
auto &asteroid_model : asteroid_models) {
31 asteroid_model.load(
this, asset_root +
"/data/asteroid.obj", asset_root +
"/data/asteroid.mtl", asset_root +
"/data", 1.0f);
32 asteroid_model.setShaders(
this, model_vert, model_frag);
35 intro_sprite =
createSprite(asset_root +
"/data/intro.png", asset_root +
"/data/sprite.vert.spv", asset_root +
"/data/intro.frag.spv");
36 fade_overlay_sprite =
createSprite(1, 1, asset_root +
"/data/sprite.vert.spv", asset_root +
"/data/fade_overlay.frag.spv");
37 const uint32_t black_pixel = 0xFF000000u;
38 fade_overlay_sprite->updateTexture(&black_pixel, 1, 1);
42 intro_rain_config.
color =
"#ff0000";
45 intro_rain = std::make_unique<matrix::Rain>(*
this, std::move(intro_rain_config));
49 star_sprite->setDepthTestEnabled(
false);
50 star_sprite->setDepthWriteEnabled(
false);
51 star_sprite->setAlphaDiscardThreshold(0.02f);
53 star_field.setSprite(star_sprite);
55 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> terrain_surface(SDL_CreateSurface(1, 1, SDL_PIXELFORMAT_RGBA32), SDL_DestroySurface);
56 if (terrain_surface ==
nullptr) {
59 const SDL_PixelFormatDetails *terrain_format = SDL_GetPixelFormatDetails(terrain_surface->format);
60 if (terrain_format ==
nullptr) {
61 throw mxvk::Exception(
"Failed to query defender terrain surface format");
63 SDL_FillSurfaceRect(terrain_surface.get(),
nullptr, SDL_MapRGBA(terrain_format,
nullptr, 255, 255, 255, 255));
65 terrain_sprite->setDepthTestEnabled(
false);
66 terrain_sprite->setDepthWriteEnabled(
false);
67 terrain_sprite->setAlphaDiscardThreshold(0.01f);
69 projectile_sprite =
createSprite3D(asset_root +
"/data/particle_projectile.png");
70 projectile_sprite->setDepthTestEnabled(
true);
71 projectile_sprite->setDepthWriteEnabled(
false);
72 projectile_sprite->setAlphaDiscardThreshold(0.05f);
75 const std::string frame_path = asset_root + std::format(
"/data/ufo_lights_{:02d}.png", i);
76 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> saucer_surface(
space::load_color_keyed_png(frame_path, 8, 24), SDL_DestroySurface);
79 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Classic)][
static_cast<std::size_t
>(i)]->setDepthTestEnabled(
true);
80 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Classic)][
static_cast<std::size_t
>(i)]->setDepthWriteEnabled(
false);
81 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Classic)][
static_cast<std::size_t
>(i)]->setAlphaDiscardThreshold(0.02f);
83 const std::string ufox_frame_path = asset_root + std::format(
"/data/ufox{}.png", i + 1);
84 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> ufox_surface(
load_light_background_png(ufox_frame_path), SDL_DestroySurface);
87 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Ufox)][
static_cast<std::size_t
>(i)]->setDepthTestEnabled(
true);
88 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Ufox)][
static_cast<std::size_t
>(i)]->setDepthWriteEnabled(
false);
89 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Ufox)][
static_cast<std::size_t
>(i)]->setAlphaDiscardThreshold(0.02f);
91 const std::string alien_frame_path = asset_root + std::format(
"/data/alien{}.png", i + 1);
92 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> alien_surface(
load_light_background_png(alien_frame_path), SDL_DestroySurface);
95 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Alien)][
static_cast<std::size_t
>(i)]->setDepthTestEnabled(
true);
96 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Alien)][
static_cast<std::size_t
>(i)]->setDepthWriteEnabled(
false);
97 ufo_sprite_sets[
static_cast<std::size_t
>(
UfoSpriteSet::Alien)][
static_cast<std::size_t
>(i)]->setAlphaDiscardThreshold(0.02f);
100 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> explosion_surface(
space::load_color_keyed_png(asset_root +
"/data/particle_explosion.png", 12), SDL_DestroySurface);
102 effect_sprite->setDepthTestEnabled(
true);
103 effect_sprite->setDepthWriteEnabled(
false);
104 effect_sprite->setAlphaDiscardThreshold(0.05f);
108 create_flame_resources();
110 ship.position = glm::vec3(0.0f, 0.0f, 0.0f);
111 ship.velocity = glm::vec3(0.0f, 0.0f, 0.0f);
112 ship.current_speed = 0.0f;
113 ship.min_speed = 0.0f;
114 ship.max_speed = 28.0f;
115 ship.rotation = glm::vec3(0.0f, -90.0f, 0.0f);
117#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
118 background_music = std::make_unique<mxvk::VK_Mixer>();
119 background_music_track = background_music->loadMusic(asset_root +
"/data/background.ogg");
120 crash_sound = background_music->loadWav(asset_root +
"/data/crash.wav");
121 shoot_sound = background_music->loadWav(asset_root +
"/data/shoot.wav");
122 takeoff_sound = background_music->loadWav(asset_root +
"/data/takeoff.wav");
123 asteroid_explosion_sound = background_music->loadWav(asset_root +
"/data/asteroid.wav");
124 ufo_explosion_sound = background_music->loadWav(asset_root +
"/data/ufo.wav");
125 ensure_background_music_playing();
168 if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
169 if (!controller.active()) {
170 controller.connectEvent(e);
172 sync_controller_connection();
175 if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
176 controller.connectEvent(e);
177 sync_controller_connection();
181 const bool was_console_visible =
console.isVisible();
182 const bool is_console_toggle = e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_F3;
184 if (is_console_toggle) {
186 log_game(
console.isVisible() ?
"Console opened." :
"Console closed.");
189 if (was_console_visible) {
194 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
195 log_game(
"Exit requested.");
199 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_F4 && !e.key.repeat) {
200 show_fps_counter = !show_fps_counter;
201 log_game(std::string(
"FPS counter ") + (show_fps_counter ?
"enabled." :
"disabled."));
204 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_F8 && !e.key.repeat) {
205 crt_enabled = !crt_enabled;
207 log_game(std::string(
"CRT effect ") + (crt_enabled ?
"enabled." :
"disabled."));
210 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)) {
212 log_game(
"Intro skipped from keyboard.");
215 if (game_over && e.type == SDL_EVENT_KEY_DOWN && (e.key.key == SDLK_RETURN || e.key.key == SDLK_KP_ENTER)) {
217 log_game(
"Game restarted from keyboard.");
221 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
222 if (e.gbutton.button == SDL_GAMEPAD_BUTTON_BACK) {
223 log_game(
"Exit requested from controller.");
227 if (mode ==
GameMode::Intro && (e.gbutton.button == SDL_GAMEPAD_BUTTON_SOUTH || e.gbutton.button == SDL_GAMEPAD_BUTTON_START)) {
229 log_game(
"Intro skipped from controller.");
232 if (game_over && (e.gbutton.button == SDL_GAMEPAD_BUTTON_SOUTH || e.gbutton.button == SDL_GAMEPAD_BUTTON_START)) {
234 log_game(
"Game restarted from controller.");
238 if (e.gbutton.button == SDL_GAMEPAD_BUTTON_SOUTH) {
240 }
else if (e.gbutton.button == SDL_GAMEPAD_BUTTON_WEST) {
241 reverse_pressed =
true;
246 if (e.type == SDL_EVENT_KEY_DOWN || e.type == SDL_EVENT_KEY_UP) {
247 const bool pressed = e.type == SDL_EVENT_KEY_DOWN;
248 const bool accepts_ship_thrust = mode ==
GameMode::Playing && !game_over && !ship_respawning;
249 if (e.key.key == SDLK_A) {
250 roll_left_pressed = pressed;
251 }
else if (e.key.key == SDLK_S) {
252 roll_right_pressed = pressed;
253 }
else if (e.key.key == SDLK_X) {
254 if (accepts_ship_thrust && pressed && !e.key.repeat) {
255 reverse_pressed =
true;
256 }
else if (!accepts_ship_thrust) {
257 reverse_pressed =
false;
259 }
else if (e.key.key == SDLK_Z) {
260 propulsion_pressed = accepts_ship_thrust && pressed;
261 }
else if (e.key.key == SDLK_D) {
262 boost_pressed = accepts_ship_thrust && pressed;
263 }
else if (e.key.key == SDLK_UP || e.key.key == SDLK_W) {
264 up_pressed = pressed;
265 }
else if (e.key.key == SDLK_DOWN) {
266 down_pressed = pressed;
267 }
else if (e.key.key == SDLK_SPACE && pressed && !e.key.repeat) {
319#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
320 ensure_background_music_playing();
322 const auto now = std::chrono::steady_clock::now();
323 const float delta_seconds = std::chrono::duration<float>(now - last_frame_time).count();
324 last_frame_time = now;
325 const float dt = std::min(delta_seconds, 0.1f);
326 elapsed_seconds += dt;
327 update_fps_counter(delta_seconds);
329 const bool console_visible =
console.isVisible();
330 if (!console_visible) {
331 sync_controller_connection();
332 update_controller_input();
333 update_barrel_roll(dt);
334 if (ship_respawning) {
339 update_asteroids(dt);
340 resolve_enemy_overlaps();
341 update_projectiles(dt);
342 check_projectile_ufo_hits();
343 check_projectile_asteroid_hits();
344 check_ship_ufo_collisions();
345 check_ship_asteroid_collisions();
346 update_level_progress();
349 update_particles(dt);
352 update_playable_world_top(extent);
353 const float aspect = (extent.height > 0U) ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height) : 1.0f;
362 update_camera_scroll(aspect);
363 update_camera_position(dt);
366 const glm::mat4 view = glm::lookAt(camera_position, camera_target, glm::vec3(0.0f, 1.0f, 0.0f));
367 glm::mat4 projection = glm::perspective(glm::radians(50.0f), aspect, 0.1f, 400.0f);
368 projection[1][1] *= -1.0f;
370 draw_countdown(cmd, image_index, extent, view, projection);
372 draw_intro_fade_in(extent);
378 update_camera_scroll(aspect);
380 update_camera_position(dt);
383 const glm::mat4 view = glm::lookAt(camera_position, camera_target, glm::vec3(0.0f, 1.0f, 0.0f));
384 glm::mat4 projection = glm::perspective(glm::radians(50.0f), aspect, 0.1f, 400.0f);
385 projection[1][1] *= -1.0f;
387 VkRect2D gameplay_scissor{};
388 gameplay_scissor.offset = {0, std::min(
GAME_VIEWPORT_TOP,
static_cast<int>(extent.height))};
389 gameplay_scissor.extent = {extent.width, extent.height -
static_cast<uint32_t
>(gameplay_scissor.offset.y)};
390 vkCmdSetScissor(cmd, 0, 1, &gameplay_scissor);
392 star_field.update(dt, camera_position, elapsed_seconds);
393 star_sprite->updateCamera(image_index, view, projection);
395 star_sprite->render(cmd, image_index);
396 star_sprite->clearQueue();
398 terrain_sprite->updateCamera(image_index, view, projection);
400 terrain_sprite->render(cmd, image_index);
401 terrain_sprite->clearQueue();
403 for (
auto &sprite_set : ufo_sprite_sets) {
405 sprite->updateCamera(image_index, view, projection);
409 for (
auto &sprite_set : ufo_sprite_sets) {
411 sprite->render(cmd, image_index);
412 sprite->clearQueue();
416 draw_asteroids(cmd, image_index, view, projection);
418 projectile_sprite->updateCamera(image_index, view, projection);
420 projectile_sprite->render(cmd, image_index);
421 projectile_sprite->clearQueue();
424 ubo.
model = build_ship_model_matrix();
425 last_ship_model_matrix = ubo.
model;
427 ubo.
proj = projection;
428 if (ship.visible && !ship_respawning) {
429 ship_model.updateUBO(image_index, ubo);
430 ship_model.render(cmd, image_index,
false);
431 draw_engine_flame(cmd, extent, view, projection);
434 effect_sprite->updateCamera(image_index, view, projection);
436 effect_sprite->render(cmd, image_index);
437 effect_sprite->clearQueue();
439 VkRect2D full_scissor{};
440 full_scissor.extent = extent;
441 vkCmdSetScissor(cmd, 0, 1, &full_scissor);
443 if (!console_visible) {