8#include <glm/ext/matrix_clip_space.hpp>
9#include <glm/ext/matrix_transform.hpp>
10#include <glm/gtc/quaternion.hpp>
14 void DefenderWindow::update_ship(
float dt) {
15 ship.prev_position = ship.position;
17 if (reverse_pressed) {
18 reverse_pressed =
false;
19 ship_forward_direction *= -1.0f;
22 constexpr float horizontal_acceleration = 58.0f;
23 constexpr float horizontal_drag = 2.4f;
24 const bool boost_active = mode ==
GameMode::Playing && (boost_pressed || controller_boost_pressed);
25 const bool propulsion_active = mode ==
GameMode::Playing && (propulsion_pressed || controller_propulsion_pressed || boost_active);
26 const float max_horizontal_speed = ship.max_speed * (boost_active ? 2.0f : 1.0f);
27 if (propulsion_active) {
28 ship.velocity.x += ship_forward_direction * horizontal_acceleration * dt;
30 ship.velocity.x = std::lerp(ship.velocity.x, 0.0f, 1.0f - std::exp(-dt * horizontal_drag));
32 ship.velocity.x = std::clamp(ship.velocity.x, -max_horizontal_speed, max_horizontal_speed);
33 ship.current_speed = std::abs(ship.velocity.x);
35 float vertical_input = 0.0f;
36 if (up_pressed && !down_pressed) {
37 vertical_input = 1.0f;
38 }
else if (down_pressed && !up_pressed) {
39 vertical_input = -1.0f;
41 vertical_input = controller_vertical_input;
44 constexpr float vertical_speed = 19.0f;
45 constexpr float vertical_response = 12.0f;
46 const float target_vertical_velocity = vertical_input * vertical_speed;
47 ship.velocity.y = std::lerp(ship.velocity.y, target_vertical_velocity, 1.0f - std::exp(-dt * vertical_response));
48 ship.velocity.z = 0.0f;
49 ship.position += ship.velocity * dt;
51 ship.position.y = std::clamp(ship.position.y,
WORLD_BOTTOM, playable_world_top);
52 ship.position.z = 0.0f;
54 const float bank = std::clamp(-ship.velocity.y * 2.0f, -22.0f, 22.0f);
55 const float pitch = std::clamp(ship.velocity.y * 0.9f, -12.0f, 12.0f);
56 ship.rotation.x = pitch;
57 ship.rotation.y = ship_forward_direction > 0.0f ? -90.0f : 90.0f;
58 const float thrust_bank = propulsion_active ? ship_forward_direction * -7.0f : 0.0f;
59 ship.rotation.z = bank + thrust_bank;
61 if (ship.fire_cooldown > 0) {
66 if (ship.fire_cooldown <= 0) {
73 void DefenderWindow::update_barrel_roll(
float dt) {
75 if (!ship_visible_mode || game_over || ship_respawning) {
76 barrel_roll_progress = 0.0f;
77 barrel_roll_angle = 0.0f;
81 float roll_input = 0.0f;
82 if ((roll_left_pressed || controller_roll_left_pressed) && !(roll_right_pressed || controller_roll_right_pressed)) {
84 }
else if ((roll_right_pressed || controller_roll_right_pressed) && !(roll_left_pressed || controller_roll_left_pressed)) {
87 if (roll_input != 0.0f) {
88 barrel_roll_direction = roll_input;
91 if (roll_input == 0.0f && barrel_roll_progress <= 0.0f) {
92 barrel_roll_angle = 0.0f;
96 const float roll_speed = 360.0f / BARREL_ROLL_DURATION;
97 barrel_roll_progress += roll_speed * dt;
98 if (barrel_roll_progress >= 360.0f) {
99 barrel_roll_progress = (roll_input != 0.0f) ? std::fmod(barrel_roll_progress, 360.0f) : 0.0f;
101 barrel_roll_angle = barrel_roll_direction * barrel_roll_progress;
104 glm::mat4 DefenderWindow::build_ship_model_matrix() {
105 const glm::quat yaw = glm::angleAxis(glm::radians(ship.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
106 const glm::quat pitch = glm::angleAxis(glm::radians(ship.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
107 const glm::quat bank = glm::angleAxis(glm::radians(ship.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
108 const glm::quat barrel_roll = glm::angleAxis(glm::radians(barrel_roll_angle), glm::vec3(0.0f, 0.0f, 1.0f));
109 const glm::quat orientation = yaw * pitch * bank * barrel_roll;
111 glm::mat4 model(1.0f);
113 model *= glm::mat4_cast(orientation);
114 model = glm::scale(model, glm::vec3(
SHIP_MODEL_SCALE * ship_model.modelRenderScale()));
115 model = glm::translate(model, ship_model.modelCenterOffset());
119 glm::mat4 DefenderWindow::build_asteroid_model_matrix(
const Asteroid &asteroid,
const mxvk::VKAbstractModel &model_resource)
const {
120 const glm::quat yaw = glm::angleAxis(glm::radians(asteroid.rotation.
y), glm::vec3(0.0f, 1.0f, 0.0f));
121 const glm::quat pitch = glm::angleAxis(glm::radians(asteroid.rotation.
x), glm::vec3(1.0f, 0.0f, 0.0f));
122 const glm::quat roll = glm::angleAxis(glm::radians(asteroid.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
124 glm::mat4 model(1.0f);
126 model *= glm::mat4_cast(yaw * pitch * roll);
127 model = glm::scale(model, glm::vec3(asteroid.scale * model_resource.
modelRenderScale()));
132 void DefenderWindow::reset_intro_screen() {
135 intro_last_update_ms = SDL_GetTicks();
136 intro_fade_in_start_ms = 0;
137 if (intro_rain !=
nullptr) {
138 intro_rain->set_opacity(1.0f);
143 void DefenderWindow::start_launch_countdown() {
146 countdown_duration = 1000;
147 countdown_start_ms = SDL_GetTicks();
148 countdown_number = 3;
150 launch_duration = 1000;
154 void DefenderWindow::start_intro_fade_in() {
155 start_launch_countdown();
157 intro_fade_in_start_ms = SDL_GetTicks();
158 log_game(
"Intro faded out. Launch countdown started.");
161 void DefenderWindow::draw_intro(
const VkExtent2D &extent) {
162 if (intro_sprite ==
nullptr) {
163 start_launch_countdown();
167 const Uint32 current_ms = SDL_GetTicks();
168 if ((current_ms - intro_last_update_ms) > 35U) {
169 intro_last_update_ms = current_ms;
173 if (intro_fade <= 0.0f) {
175 if (intro_rain !=
nullptr) {
176 intro_rain->set_opacity(0.0f);
178 start_intro_fade_in();
182 intro_sprite->setShaderParams(
static_cast<float>(current_ms) / 1000.0f, 0.0f, 0.0f, intro_fade);
183 intro_sprite->drawSpriteRect(0, 0,
static_cast<int>(extent.width),
static_cast<int>(extent.height));
184 if (intro_rain !=
nullptr) {
185 intro_rain->set_opacity(intro_fade);
187 printText(
"Press Enter", 24,
static_cast<int>(extent.height) - 52, {255, 230, 80, 255});
190 void DefenderWindow::draw_intro_fade_in(
const VkExtent2D &extent) {
191 const Uint32 elapsed_ms = SDL_GetTicks() - intro_fade_in_start_ms;
192 const float progress = std::clamp(
static_cast<float>(elapsed_ms) /
static_cast<float>(INTRO_FADE_IN_DURATION_MS), 0.0f, 1.0f);
193 const float alpha = 1.0f - progress;
199 draw_fade_overlay(extent, alpha);
202 void DefenderWindow::draw_fade_overlay(
const VkExtent2D &extent,
float alpha) {
203 if (fade_overlay_sprite ==
nullptr) {
207 alpha = std::clamp(alpha, 0.0f, 1.0f);
212 fade_overlay_sprite->setShaderParams(0.0f, 0.0f, 0.0f, alpha);
213 fade_overlay_sprite->drawSpriteRect(0, 0,
static_cast<int>(extent.width),
static_cast<int>(extent.height));
216 void DefenderWindow::update_camera_scroll(
float aspect) {
217 const float visible_half_height = std::tan(glm::radians(25.0f)) *
CAMERA_DISTANCE;
218 const float visible_half_width = visible_half_height * std::max(0.1f, aspect);
221 const float lead_offset = propulsion_active ? ship_forward_direction * visible_half_width * 0.18f : 0.0f;
222 const float desired_camera_x =
wrap_world_x(ship.position.x + lead_offset);
223 const float relative_x =
wrapped_delta_x(ship.position.x, camera_center_x);
225 if (relative_x > scroll_limit) {
226 camera_center_x =
wrap_world_x(ship.position.x - scroll_limit);
227 }
else if (relative_x < -scroll_limit) {
228 camera_center_x =
wrap_world_x(ship.position.x + scroll_limit);
231 if (propulsion_active) {
236 void DefenderWindow::update_camera_position(
float dt) {
237 const float target_x =
nearest_world_x(camera_center_x, camera_position.x);
238 const float blend = 1.0f - std::exp(-dt * 10.0f);
239 camera_position.x =
wrap_world_x(camera_position.x + (target_x - camera_position.x) * blend);
244 void DefenderWindow::update_playable_world_top(
const VkExtent2D &extent) {
245 if (extent.height == 0U) {
248 const float visible_half_height = std::tan(glm::radians(25.0f)) *
CAMERA_DISTANCE;
249 const float top_fraction =
static_cast<float>(
GAME_VIEWPORT_TOP) /
static_cast<float>(extent.height);
250 playable_world_top =
CAMERA_HEIGHT + (0.5f - top_fraction) * 2.0f * visible_half_height;
253 void DefenderWindow::update_countdown() {
254 const Uint32 now = SDL_GetTicks();
255 const Uint32 elapsed = now - countdown_start_ms;
256 countdown_timer = elapsed % countdown_duration;
258 if (elapsed < (countdown_duration * 3U)) {
259 countdown_number = 3 -
static_cast<int>(elapsed / countdown_duration);
265 countdown_number = 0;
266 if (launch_start_ms == 0U) {
267 launch_start_ms = now;
269 launch_timer = std::min<Uint32>(now - launch_start_ms, launch_duration);
270 if (launch_timer >= launch_duration) {
275 reverse_pressed =
false;
276 propulsion_pressed =
false;
277 fire_pressed =
false;
278#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
279 play_sound(takeoff_sound);
281 log_game(
"Mission started.");
285 void DefenderWindow::draw_countdown(VkCommandBuffer cmd, uint32_t image_index,
const VkExtent2D &extent,
const glm::mat4 &view,
const glm::mat4 &projection) {
288 star_field.update(0.0f, camera_position, elapsed_seconds);
289 star_sprite->updateCamera(image_index, view, projection);
291 star_sprite->render(cmd, image_index);
292 star_sprite->clearQueue();
294 if (ship.visible && !ship_respawning) {
295 mxvk::UniformBufferObject ubo{};
296 ubo.
model = build_ship_model_matrix();
298 ubo.
proj = projection;
299 ship_model.updateUBO(image_index, ubo);
300 ship_model.render(cmd, image_index,
false);
303 if (countdown_number > 0) {
304 const std::string number = std::format(
"{}", countdown_number);
311 const bool show_number = ((countdown_timer / 167U) % 2U) == 0U;
314 static_cast<int>(extent.width) / 2 - text_w / 2,
315 static_cast<int>(extent.height) / 2 - text_h / 2,
316 {255, 255, 255, 255},
320 const std::string launch =
"MISSION START!";
328 static_cast<int>(extent.width) / 2 - text_w / 2,
329 static_cast<int>(extent.height) / 2 - text_h / 2,
335 void DefenderWindow::update_fps_counter(
float delta_seconds) {
336 fps_accumulator += delta_seconds;
338 if (fps_accumulator < 0.25f) {
342 const float fps =
static_cast<float>(fps_frame_count) / fps_accumulator;
343 fps_text = std::format(
"FPS: {:.1f}", fps);
344 fps_accumulator = 0.0f;
348 void DefenderWindow::draw_hud(
const VkExtent2D &extent) {
349 const SDL_Color white{255, 255, 255, 255};
350 const SDL_Color yellow{255, 230, 80, 255};
351 const SDL_Color red{255, 70, 60, 255};
352 printText(
"Score: " + std::to_string(score), 24, 22, white, hud_font);
353 printText(
"Level: " + std::to_string(level), 24, 82, yellow, hud_font);
354 printText(
"Lives: " + std::to_string(lives), 24, 142, lives <= 1 ? red : white, hud_font);
355 draw_scanner(extent);
356 if (show_fps_counter) {
362 const int fps_x = std::max(24,
static_cast<int>(extent.width) - fps_w - 24);
370 const std::string title =
"GAME OVER";
371 const std::string final_score =
"Final Score: " + std::to_string(score);
372 const std::string prompt =
"Press Enter to start a new game";
388 const int center_x =
static_cast<int>(extent.width) / 2;
389 const int center_y =
static_cast<int>(extent.height) / 2;
390 printText(title, center_x - title_w / 2, center_y - 52, red);
391 printText(final_score, center_x - score_w / 2, center_y - 12, yellow);
392 printText(prompt, center_x - prompt_w / 2, center_y + 28, white);
395 void DefenderWindow::draw_scanner(
const VkExtent2D &extent) {
396 constexpr int SCANNER_BORDER = 2;
397 const int scanner_width = std::min(760, std::max(420,
static_cast<int>(extent.width) - 320));
398 const int scanner_x = (
static_cast<int>(extent.width) - scanner_width) / 2;
400 const int inner_x = scanner_x + SCANNER_BORDER;
401 const int inner_y = scanner_y + SCANNER_BORDER;
402 const int inner_width = scanner_width - SCANNER_BORDER * 2;
403 const int inner_height =
RADAR_HEIGHT - SCANNER_BORDER * 2;
404 const auto draw_rect = [
this](
int x,
int y,
int width,
int height,
const glm::vec4 &color) {
405 fade_overlay_sprite->setShaderParams(color.r, color.g, color.b, color.a);
406 fade_overlay_sprite->drawSpriteRect(x, y, width, height);
408 const auto x_for = [inner_x, inner_width,
this](
float world_x) {
410 return inner_x + std::clamp(
static_cast<int>(normalized *
static_cast<float>(inner_width - 1)), 0, inner_width - 1);
412 const auto y_for = [
this](
float world_y) {
413 const float normalized = (playable_world_top - std::clamp(world_y,
WORLD_BOTTOM, playable_world_top)) / (playable_world_top -
WORLD_BOTTOM);
414 return inner_y + std::clamp(
static_cast<int>(normalized *
static_cast<float>(inner_height - 1)), 0, inner_height - 1);
417 const std::string title =
"RADAR";
419 int ignored_height = 0;
423 printText(title,
static_cast<int>(extent.width) / 2 - title_width / 2, 6, {120, 220, 255, 255});
425 draw_rect(scanner_x, scanner_y, scanner_width,
RADAR_HEIGHT, {0.02f, 0.05f, 0.09f, 0.94f});
426 draw_rect(scanner_x, scanner_y, scanner_width, SCANNER_BORDER, {0.32f, 0.72f, 0.88f, 0.95f});
427 draw_rect(scanner_x, scanner_y +
RADAR_HEIGHT - SCANNER_BORDER, scanner_width, SCANNER_BORDER, {0.32f, 0.72f, 0.88f, 0.95f});
428 draw_rect(scanner_x, scanner_y, SCANNER_BORDER,
RADAR_HEIGHT, {0.32f, 0.72f, 0.88f, 0.95f});
429 draw_rect(scanner_x + scanner_width - SCANNER_BORDER, scanner_y, SCANNER_BORDER,
RADAR_HEIGHT, {0.32f, 0.72f, 0.88f, 0.95f});
430 for (
int x = inner_x; x < inner_x + inner_width; x += 2) {
431 const float progress =
static_cast<float>(x - inner_x) /
static_cast<float>(inner_width - 1);
433 draw_rect(x, y_for(
terrain_height(world_x)), 2, 2, {0.20f, 0.62f, 0.36f, 0.9f});
436 const float visible_half_height = std::tan(glm::radians(25.0f)) *
CAMERA_DISTANCE;
437 const float visible_half_width = visible_half_height * std::max(0.1f,
static_cast<float>(extent.width) /
static_cast<float>(extent.height));
438 const int bracket_half_width = std::max(1,
static_cast<int>(visible_half_width /
WORLD_WIDTH *
static_cast<float>(inner_width)));
439 const int scanner_center_x = inner_x + inner_width / 2;
440 const int left_bracket_x = scanner_center_x - bracket_half_width;
441 const int right_bracket_x = scanner_center_x + bracket_half_width;
442 constexpr int BRACKET_ARM_LENGTH = 10;
443 draw_rect(left_bracket_x, inner_y, 2, inner_height, {0.25f, 0.85f, 1.0f, 0.9f});
444 draw_rect(left_bracket_x, inner_y, BRACKET_ARM_LENGTH, 2, {0.25f, 0.85f, 1.0f, 0.9f});
445 draw_rect(left_bracket_x, inner_y + inner_height - 2, BRACKET_ARM_LENGTH, 2, {0.25f, 0.85f, 1.0f, 0.9f});
446 draw_rect(right_bracket_x, inner_y, 2, inner_height, {0.25f, 0.85f, 1.0f, 0.9f});
447 draw_rect(right_bracket_x - BRACKET_ARM_LENGTH + 2, inner_y, BRACKET_ARM_LENGTH, 2, {0.25f, 0.85f, 1.0f, 0.9f});
448 draw_rect(right_bracket_x - BRACKET_ARM_LENGTH + 2, inner_y + inner_height - 2, BRACKET_ARM_LENGTH, 2, {0.25f, 0.85f, 1.0f, 0.9f});
450 for (
const Ufo &ufo : ufos) {
455 draw_rect(x_for(ufo.position.x) - 2, y_for(ufo.position.y) - 2, 5, 5, {1.0f, 0.30f, 0.75f, 1.0f});
457 draw_rect(x_for(ufo.position.x) - 2, y_for(ufo.position.y) - 1, 5, 3, {0.25f, 1.0f, 0.46f, 1.0f});
461 for (
const Asteroid &asteroid : asteroids) {
463 draw_rect(x_for(asteroid.position.
x) - 1, y_for(asteroid.position.
y) - 1, 3, 3, {1.0f, 0.58f, 0.18f, 1.0f});
466 for (
const space::Projectile &projectile : projectiles) {
467 if (projectile.active) {
468 draw_rect(x_for(projectile.position.x), y_for(projectile.position.y), 2, 2, {1.0f, 0.95f, 0.38f, 1.0f});
471 if (ship.visible && !ship_respawning &&
static_cast<int>(elapsed_seconds * 8.0f) % 2 == 0) {
472 draw_rect(x_for(ship.position.x) - 3, y_for(ship.position.y) - 3, 7, 7, {1.0f, 1.0f, 1.0f, 1.0f});
float modelRenderScale() const
Access the computed render scale used for normalization.
glm::vec3 modelCenterOffset() const
Access the computed center offset used for normalization.
bool getTextDimensions(const std::string &text, int &width, int &height)
Measure text dimensions in pixels.
void printText(const std::string &text, int x, int y, const SDL_Color &col)
Queue a text string for rendering during the current frame.
glm::vec3 nearest_world_position(glm::vec3 position, float origin_x)
float nearest_world_x(float target, float origin)
float wrapped_delta_x(float target, float origin)
constexpr float WORLD_BOTTOM
float wrap_world_x(float x)
float terrain_height(float world_x)
constexpr float SHIP_MODEL_SCALE
constexpr int FIRE_COOLDOWN
constexpr float CAMERA_SCROLL_EDGE_FRACTION
constexpr float CAMERA_DISTANCE
constexpr int RADAR_HEIGHT
constexpr int GAME_VIEWPORT_TOP
constexpr float WORLD_WIDTH
constexpr float CAMERA_HEIGHT