8#include <glm/gtc/quaternion.hpp>
12 void DefenderWindow::init_ufos() {
13 for (
auto &ufo : ufos) {
19 void DefenderWindow::init_asteroids() {
20 for (
auto &asteroid : asteroids) {
21 asteroid.active =
false;
26 void DefenderWindow::start_level() {
27 const int ufo_count = std::min(
MAX_UFOS, level + 2);
28 const int asteroid_count = std::min(
MAX_ASTEROIDS, std::max(1, level));
30 for (
auto &ufo : ufos) {
33 for (
auto &asteroid : asteroids) {
34 asteroid.active =
false;
37 for (
int i = 0; i < ufo_count; ++i) {
38 respawn_ufo(ufos[
static_cast<std::size_t
>(i)]);
40 for (
int i = 0; i < asteroid_count; ++i) {
41 respawn_asteroid(asteroids[
static_cast<std::size_t
>(i)]);
45 log_game(std::format(
"Level {} started: {} UFO(s), {} asteroid(s).", level, ufo_count, asteroid_count));
48 void DefenderWindow::update_level_progress() {
49 if (!level_active || ship_respawning || game_over || active_ufo_count() != 0 || active_asteroid_count() != 0) {
55 log_game(std::format(
"Level cleared. Advancing to level {}.", level), SDL_Color{120, 255, 180, 255});
59 void DefenderWindow::respawn_ufo(
Ufo &ufo,
bool initial_spawn) {
72 if (sprite_roll < 0.34f) {
74 }
else if (sprite_roll < 0.67f) {
79 const float collision_radius = ufo_collision_radius(ufo);
80 for (
int attempt = 0; attempt < ENEMY_SPAWN_ATTEMPTS; ++attempt) {
83 if (enemy_spawn_is_clear(candidate, collision_radius, &ufo,
nullptr)) {
84 ufo.position = candidate;
87 if (attempt == ENEMY_SPAWN_ATTEMPTS - 1) {
88 ufo.position = candidate;
91 ufo.respawn_timer = 0.0f;
95 void DefenderWindow::update_ufos(
float dt) {
96 for (
auto &ufo : ufos) {
101 ufo.phase += dt * ufo.bob_speed;
102 ufo.position += ufo.velocity * dt;
103 ufo.position.y += std::sin(ufo.phase) * dt * 1.35f;
104 if (ufo.position.y <
WORLD_BOTTOM + 0.8f || ufo.position.y > playable_world_top - 0.8f) {
105 ufo.velocity.y = -ufo.velocity.y;
106 ufo.position.y = std::clamp(ufo.position.y,
WORLD_BOTTOM + 0.8f, playable_world_top - 0.8f);
113 void DefenderWindow::respawn_asteroid(
Asteroid &asteroid,
bool initial_spawn) {
116 asteroid.collision_radius = asteroid.scale * 1.25f;
118 asteroid.rotation = glm::vec3(
122 asteroid.angular_velocity = glm::vec3(
126 for (
int attempt = 0; attempt < ENEMY_SPAWN_ATTEMPTS; ++attempt) {
129 if (enemy_spawn_is_clear(candidate, asteroid.collision_radius,
nullptr, &asteroid)) {
130 asteroid.position = candidate;
133 if (attempt == ENEMY_SPAWN_ATTEMPTS - 1) {
134 asteroid.position = candidate;
137 asteroid.respawn_timer = 0.0f;
141 void DefenderWindow::update_asteroids(
float dt) {
142 for (
auto &asteroid : asteroids) {
147 asteroid.position += asteroid.velocity * dt;
149 asteroid.rotation += asteroid.angular_velocity * dt;
150 asteroid.rotation.
x = std::fmod(asteroid.rotation.
x, 360.0f);
151 asteroid.rotation.
y = std::fmod(asteroid.rotation.
y, 360.0f);
152 asteroid.rotation.z = std::fmod(asteroid.rotation.z, 360.0f);
154 if (asteroid.position.
y <
WORLD_BOTTOM + asteroid.collision_radius || asteroid.position.
y > playable_world_top - asteroid.collision_radius) {
155 asteroid.velocity.
y = -asteroid.velocity.
y;
156 asteroid.position.
y = std::clamp(asteroid.position.
y,
WORLD_BOTTOM + asteroid.collision_radius, playable_world_top - asteroid.collision_radius);
161 [[nodiscard]]
float DefenderWindow::ufo_collision_radius(
const Ufo &ufo)
const {
163 return ufo.base_size * 0.46f + ENEMY_SEPARATION_PADDING;
165 return ufo.base_size * 0.58f + ENEMY_SEPARATION_PADDING;
168 [[nodiscard]]
bool DefenderWindow::enemy_spawn_is_clear(
const glm::vec3 &position,
float radius,
const Ufo *ignored_ufo,
const Asteroid *ignored_asteroid)
const {
169 for (
const Ufo &ufo : ufos) {
170 if (!ufo.active || &ufo == ignored_ufo) {
173 const glm::vec2 delta{
wrapped_delta_x(ufo.position.x, position.x), ufo.position.y - position.y};
174 const float min_distance = ufo_collision_radius(ufo) + radius;
175 if (glm::dot(delta, delta) < min_distance * min_distance) {
180 for (
const Asteroid &asteroid : asteroids) {
181 if (!asteroid.
active || &asteroid == ignored_asteroid) {
184 const glm::vec2 delta{
wrapped_delta_x(asteroid.position.
x, position.x), asteroid.position.
y - position.y};
185 const float min_distance = asteroid.collision_radius + radius;
186 if (glm::dot(delta, delta) < min_distance * min_distance) {
194 void DefenderWindow::clamp_enemy_to_world_y(glm::vec3 &position, glm::vec3 &velocity,
float radius) {
195 if (position.y <
WORLD_BOTTOM + radius || position.y > playable_world_top - radius) {
196 velocity.y = -velocity.y;
197 position.y = std::clamp(position.y,
WORLD_BOTTOM + radius, playable_world_top - radius);
201 void DefenderWindow::separate_enemies(glm::vec3 &first_position,
202 glm::vec3 &first_velocity,
204 glm::vec3 &second_position,
205 glm::vec3 &second_velocity,
208 const float second_x =
nearest_world_x(second_position.x, first_position.x);
209 const glm::vec2 delta{second_x - first_position.x, second_position.y - first_position.y};
210 const float min_distance = first_radius + second_radius;
211 const float distance_squared = glm::dot(delta, delta);
212 if (distance_squared >= min_distance * min_distance) {
216 const float distance = std::sqrt(std::max(distance_squared, 0.0001f));
217 const glm::vec2 normal = distance > 0.0001f ? delta / distance : glm::vec2{1.0f, 0.0f};
218 const float first_mass = first_radius * first_radius;
219 const float second_mass = second_radius * second_radius;
220 const float first_inverse_mass = 1.0f / first_mass;
221 const float second_inverse_mass = 1.0f / second_mass;
222 const float inverse_mass_sum = first_inverse_mass + second_inverse_mass;
223 const float overlap = min_distance - distance;
225 const glm::vec2 separation = normal * (overlap / inverse_mass_sum);
226 first_position.x -= separation.x * first_inverse_mass;
227 first_position.y -= separation.y * first_inverse_mass;
228 second_position.x =
wrap_world_x(second_x + separation.x * second_inverse_mass);
229 second_position.y += separation.y * second_inverse_mass;
232 const glm::vec2 relative_velocity{second_velocity.x - first_velocity.x, second_velocity.y - first_velocity.y};
233 const float velocity_along_normal = glm::dot(relative_velocity, normal);
234 if (velocity_along_normal >= 0.0f) {
238 const float impulse_magnitude = -(1.0f + restitution) * velocity_along_normal / inverse_mass_sum;
239 const glm::vec2 impulse = impulse_magnitude * normal;
240 first_velocity.x -= impulse.x * first_inverse_mass;
241 first_velocity.y -= impulse.y * first_inverse_mass;
242 second_velocity.x += impulse.x * second_inverse_mass;
243 second_velocity.y += impulse.y * second_inverse_mass;
246 void DefenderWindow::resolve_enemy_overlaps() {
247 constexpr float asteroid_restitution = 0.92f;
248 constexpr float mixed_restitution = 0.72f;
249 constexpr int separation_iterations = 4;
251 for (
int iteration = 0; iteration < separation_iterations; ++iteration) {
252 for (std::size_t i = 0; i < ufos.size(); ++i) {
253 Ufo &first = ufos[i];
258 for (std::size_t j = i + 1; j < ufos.size(); ++j) {
259 Ufo &second = ufos[j];
260 if (!second.active) {
263 separate_enemies(first.position, first.velocity, ufo_collision_radius(first), second.position, second.velocity, ufo_collision_radius(second), mixed_restitution);
267 for (std::size_t i = 0; i < asteroids.size(); ++i) {
268 Asteroid &first = asteroids[i];
273 for (std::size_t j = i + 1; j < asteroids.size(); ++j) {
274 Asteroid &second = asteroids[j];
279 separate_enemies(first.position, first.velocity, first.collision_radius, second.position, second.velocity, second.collision_radius, asteroid_restitution);
283 for (Ufo &ufo : ufos) {
287 for (Asteroid &asteroid : asteroids) {
291 separate_enemies(ufo.position, ufo.velocity, ufo_collision_radius(ufo), asteroid.position, asteroid.velocity, asteroid.collision_radius, mixed_restitution);
295 for (Ufo &ufo : ufos) {
297 clamp_enemy_to_world_y(ufo.position, ufo.velocity, ufo_collision_radius(ufo));
300 for (Asteroid &asteroid : asteroids) {
302 clamp_enemy_to_world_y(asteroid.position, asteroid.velocity, asteroid.collision_radius);
308 [[nodiscard]]
int DefenderWindow::current_ufo_frame(
const Ufo &ufo)
const {
309 return static_cast<int>(std::floor(elapsed_seconds * 12.0f + ufo.phase * 1.7f)) %
UFO_ANIMATION_FRAMES;
312 [[nodiscard]]
float DefenderWindow::current_ufo_pulse(
const Ufo &ufo)
const {
313 return 1.0f + std::sin(elapsed_seconds * 2.2f + ufo.phase) * 0.04f;
316 [[nodiscard]] glm::vec2 DefenderWindow::ufo_draw_size(
const Ufo &ufo,
float pulse)
const {
318 return glm::vec2(ufo.base_size * 0.82f * pulse);
320 return glm::vec2(ufo.base_size * pulse, ufo.base_size * 0.58f * pulse);
323 void DefenderWindow::draw_ufos() {
324 for (
const auto &ufo : ufos) {
328 const float pulse = current_ufo_pulse(ufo);
329 const int frame = current_ufo_frame(ufo);
330 ufo_sprite_sets[
static_cast<std::size_t
>(ufo.sprite_set)][
static_cast<std::size_t
>(frame)]->drawSprite(
nearest_world_position(ufo.position, camera_position.x), ufo_draw_size(ufo, pulse), ufo.tint, 0.0f);
334 void DefenderWindow::draw_terrain() {
335 constexpr float TERRAIN_SEGMENT_WIDTH = 8.0f;
336 constexpr float TERRAIN_LINE_WIDTH = 0.07f;
337 constexpr int VISIBLE_SEGMENTS = 8;
338 const int first_segment =
static_cast<int>(std::floor((camera_position.x -
static_cast<float>(VISIBLE_SEGMENTS) * TERRAIN_SEGMENT_WIDTH) / TERRAIN_SEGMENT_WIDTH));
339 const glm::vec4 terrain_color{0.18f, 0.86f, 0.40f, 0.92f};
341 for (
int segment = first_segment; segment < first_segment + VISIBLE_SEGMENTS * 2 + 1; ++segment) {
342 const float left_x =
static_cast<float>(segment) * TERRAIN_SEGMENT_WIDTH;
343 const float right_x = left_x + TERRAIN_SEGMENT_WIDTH;
346 const glm::vec2 delta{right_x - left_x, right_y - left_y};
347 terrain_sprite->drawSprite({(left_x + right_x) * 0.5f, (left_y + right_y) * 0.5f, 0.0f},
348 {glm::length(delta), TERRAIN_LINE_WIDTH}, terrain_color, std::atan2(delta.y, delta.x));
352 void DefenderWindow::draw_asteroids(VkCommandBuffer cmd, uint32_t image_index,
const glm::mat4 &view,
const glm::mat4 &projection) {
353 for (std::size_t i = 0; i < asteroids.size(); ++i) {
354 const Asteroid &asteroid = asteroids[i];
359 mxvk::UniformBufferObject ubo{};
360 ubo.
model = build_asteroid_model_matrix(asteroid, asteroid_models[i]);
362 ubo.
proj = projection;
363 asteroid_models[i].updateUBO(image_index, ubo);
364 asteroid_models[i].render(cmd, image_index,
false);
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)
constexpr int MAX_ASTEROIDS
float terrain_height(float world_x)
constexpr float WORLD_HALF_WIDTH
constexpr int UFO_ANIMATION_FRAMES
constexpr float PI
Single-precision value of pi.
float random_float(float min_value, float max_value)
Generates a uniformly distributed floating-point value.