MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
defender_enemies.cpp
Go to the documentation of this file.
1#include "defender_window.hpp"
2
3#include <algorithm>
4#include <cmath>
5#include <cstddef>
6#include <format>
7
8#include <glm/gtc/quaternion.hpp>
9
10namespace defender {
11
12 void DefenderWindow::init_ufos() {
13 for (auto &ufo : ufos) {
14 ufo.active = false;
15 ufo.respawn_timer = space::random_float(0.2f, 2.0f);
16 }
17 }
18
19 void DefenderWindow::init_asteroids() {
20 for (auto &asteroid : asteroids) {
21 asteroid.active = false;
22 asteroid.respawn_timer = space::random_float(0.6f, 4.5f);
23 }
24 }
25
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));
29
30 for (auto &ufo : ufos) {
31 ufo.active = false;
32 }
33 for (auto &asteroid : asteroids) {
34 asteroid.active = false;
35 }
36
37 for (int i = 0; i < ufo_count; ++i) {
38 respawn_ufo(ufos[static_cast<std::size_t>(i)]);
39 }
40 for (int i = 0; i < asteroid_count; ++i) {
41 respawn_asteroid(asteroids[static_cast<std::size_t>(i)]);
42 }
43
44 level_active = true;
45 log_game(std::format("Level {} started: {} UFO(s), {} asteroid(s).", level, ufo_count, asteroid_count));
46 }
47
48 void DefenderWindow::update_level_progress() {
49 if (!level_active || ship_respawning || game_over || active_ufo_count() != 0 || active_asteroid_count() != 0) {
50 return;
51 }
52
53 ++level;
54 level_active = false;
55 log_game(std::format("Level cleared. Advancing to level {}.", level), SDL_Color{120, 255, 180, 255});
56 start_level();
57 }
58
59 void DefenderWindow::respawn_ufo(Ufo &ufo, bool initial_spawn) {
60 const float side = space::random_float(0.0f, 1.0f) < 0.5f ? -1.0f : 1.0f;
61 ufo.velocity = glm::vec3(-side * space::random_float(3.5f, 8.5f), space::random_float(-0.8f, 0.8f), 0.0f);
62 ufo.tint = glm::vec4(
63 space::random_float(0.82f, 1.0f),
64 space::random_float(0.88f, 1.0f),
65 space::random_float(0.90f, 1.0f),
66 1.0f);
67 ufo.base_size = space::random_float(3.6f, 4.4f);
68 ufo.phase = space::random_float(0.0f, 2.0f * space::PI);
69 ufo.bob_speed = space::random_float(1.1f, 2.8f);
70 ufo.spin_speed = space::random_float(2.8f, 6.2f) * (space::random_float(0.0f, 1.0f) < 0.5f ? -1.0f : 1.0f);
71 const float sprite_roll = space::random_float(0.0f, 1.0f);
72 if (sprite_roll < 0.34f) {
73 ufo.sprite_set = UfoSpriteSet::Classic;
74 } else if (sprite_roll < 0.67f) {
75 ufo.sprite_set = UfoSpriteSet::Ufox;
76 } else {
77 ufo.sprite_set = UfoSpriteSet::Alien;
78 }
79 const float collision_radius = ufo_collision_radius(ufo);
80 for (int attempt = 0; attempt < ENEMY_SPAWN_ATTEMPTS; ++attempt) {
81 const float distance = initial_spawn ? space::random_float(-WORLD_HALF_WIDTH, WORLD_HALF_WIDTH) : side * space::random_float(34.0f, 56.0f);
82 const glm::vec3 candidate{wrap_world_x(camera_center_x + distance), space::random_float(WORLD_BOTTOM + collision_radius, playable_world_top - collision_radius), space::random_float(-1.2f, 1.2f)};
83 if (enemy_spawn_is_clear(candidate, collision_radius, &ufo, nullptr)) {
84 ufo.position = candidate;
85 break;
86 }
87 if (attempt == ENEMY_SPAWN_ATTEMPTS - 1) {
88 ufo.position = candidate;
89 }
90 }
91 ufo.respawn_timer = 0.0f;
92 ufo.active = true;
93 }
94
95 void DefenderWindow::update_ufos(float dt) {
96 for (auto &ufo : ufos) {
97 if (!ufo.active) {
98 continue;
99 }
100
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);
107 }
108
109 ufo.position.x = wrap_world_x(ufo.position.x);
110 }
111 }
112
113 void DefenderWindow::respawn_asteroid(Asteroid &asteroid, bool initial_spawn) {
114 const float side = space::random_float(0.0f, 1.0f) < 0.5f ? -1.0f : 1.0f;
115 asteroid.scale = space::random_float(0.72f, 2.15f);
116 asteroid.collision_radius = asteroid.scale * 1.25f;
117 asteroid.velocity = glm::vec3(-side * space::random_float(1.8f, 5.6f), space::random_float(-0.55f, 0.55f), 0.0f);
118 asteroid.rotation = glm::vec3(
119 space::random_float(0.0f, 360.0f),
120 space::random_float(0.0f, 360.0f),
121 space::random_float(0.0f, 360.0f));
122 asteroid.angular_velocity = glm::vec3(
123 space::random_float(-74.0f, 74.0f),
124 space::random_float(-105.0f, 105.0f),
125 space::random_float(-62.0f, 62.0f));
126 for (int attempt = 0; attempt < ENEMY_SPAWN_ATTEMPTS; ++attempt) {
127 const float distance = initial_spawn ? space::random_float(-WORLD_HALF_WIDTH, WORLD_HALF_WIDTH) : side * space::random_float(36.0f, 68.0f);
128 const glm::vec3 candidate{wrap_world_x(camera_center_x + distance), space::random_float(WORLD_BOTTOM + asteroid.collision_radius, playable_world_top - asteroid.collision_radius), space::random_float(-1.7f, 1.3f)};
129 if (enemy_spawn_is_clear(candidate, asteroid.collision_radius, nullptr, &asteroid)) {
130 asteroid.position = candidate;
131 break;
132 }
133 if (attempt == ENEMY_SPAWN_ATTEMPTS - 1) {
134 asteroid.position = candidate;
135 }
136 }
137 asteroid.respawn_timer = 0.0f;
138 asteroid.active = true;
139 }
140
141 void DefenderWindow::update_asteroids(float dt) {
142 for (auto &asteroid : asteroids) {
143 if (!asteroid.active) {
144 continue;
145 }
146
147 asteroid.position += asteroid.velocity * dt;
148 asteroid.position.x = wrap_world_x(asteroid.position.x);
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);
153
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);
157 }
158 }
159 }
160
161 [[nodiscard]] float DefenderWindow::ufo_collision_radius(const Ufo &ufo) const {
162 if (ufo.sprite_set == UfoSpriteSet::Alien) {
163 return ufo.base_size * 0.46f + ENEMY_SEPARATION_PADDING;
164 }
165 return ufo.base_size * 0.58f + ENEMY_SEPARATION_PADDING;
166 }
167
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) {
171 continue;
172 }
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) {
176 return false;
177 }
178 }
179
180 for (const Asteroid &asteroid : asteroids) {
181 if (!asteroid.active || &asteroid == ignored_asteroid) {
182 continue;
183 }
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) {
187 return false;
188 }
189 }
190
191 return true;
192 }
193
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);
198 }
199 }
200
201 void DefenderWindow::separate_enemies(glm::vec3 &first_position,
202 glm::vec3 &first_velocity,
203 float first_radius,
204 glm::vec3 &second_position,
205 glm::vec3 &second_velocity,
206 float second_radius,
207 float restitution) {
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) {
213 return;
214 }
215
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;
224
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;
230 first_position.x = wrap_world_x(first_position.x);
231
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) {
235 return;
236 }
237
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;
244 }
245
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;
250
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];
254 if (!first.active) {
255 continue;
256 }
257
258 for (std::size_t j = i + 1; j < ufos.size(); ++j) {
259 Ufo &second = ufos[j];
260 if (!second.active) {
261 continue;
262 }
263 separate_enemies(first.position, first.velocity, ufo_collision_radius(first), second.position, second.velocity, ufo_collision_radius(second), mixed_restitution);
264 }
265 }
266
267 for (std::size_t i = 0; i < asteroids.size(); ++i) {
268 Asteroid &first = asteroids[i];
269 if (!first.active) {
270 continue;
271 }
272
273 for (std::size_t j = i + 1; j < asteroids.size(); ++j) {
274 Asteroid &second = asteroids[j];
275 if (!second.active) {
276 continue;
277 }
278
279 separate_enemies(first.position, first.velocity, first.collision_radius, second.position, second.velocity, second.collision_radius, asteroid_restitution);
280 }
281 }
282
283 for (Ufo &ufo : ufos) {
284 if (!ufo.active) {
285 continue;
286 }
287 for (Asteroid &asteroid : asteroids) {
288 if (!asteroid.active) {
289 continue;
290 }
291 separate_enemies(ufo.position, ufo.velocity, ufo_collision_radius(ufo), asteroid.position, asteroid.velocity, asteroid.collision_radius, mixed_restitution);
292 }
293 }
294
295 for (Ufo &ufo : ufos) {
296 if (ufo.active) {
297 clamp_enemy_to_world_y(ufo.position, ufo.velocity, ufo_collision_radius(ufo));
298 }
299 }
300 for (Asteroid &asteroid : asteroids) {
301 if (asteroid.active) {
302 clamp_enemy_to_world_y(asteroid.position, asteroid.velocity, asteroid.collision_radius);
303 }
304 }
305 }
306 }
307
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;
310 }
311
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;
314 }
315
316 [[nodiscard]] glm::vec2 DefenderWindow::ufo_draw_size(const Ufo &ufo, float pulse) const {
317 if (ufo.sprite_set == UfoSpriteSet::Alien) {
318 return glm::vec2(ufo.base_size * 0.82f * pulse);
319 }
320 return glm::vec2(ufo.base_size * pulse, ufo.base_size * 0.58f * pulse);
321 }
322
323 void DefenderWindow::draw_ufos() {
324 for (const auto &ufo : ufos) {
325 if (!ufo.active) {
326 continue;
327 }
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);
331 }
332 }
333
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};
340
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;
344 const float left_y = terrain_height(left_x);
345 const float right_y = terrain_height(right_x);
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));
349 }
350 }
351
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];
355 if (!asteroid.active) {
356 continue;
357 }
358
359 mxvk::UniformBufferObject ubo{};
360 ubo.model = build_asteroid_model_matrix(asteroid, asteroid_models[i]);
361 ubo.view = view;
362 ubo.proj = projection;
363 asteroid_models[i].updateUBO(image_index, ubo);
364 asteroid_models[i].render(cmd, image_index, false);
365 }
366 }
367
368} // namespace defender
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_UFOS
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.
bool active
Definition space.cpp:69
float x
Definition space.cpp:66
float y
Definition space.cpp:66