MXVK Vulkan Framework 0.35.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
space.cpp
Go to the documentation of this file.
1
2#include "mxvk/argz.hpp"
3#include "mxvk/mxvk.hpp"
5
6#include <SDL3/SDL.h>
7
8#include <algorithm>
9#include <cmath>
10#include <cstdint>
11#include <cstdlib>
12#include <cstring>
13#include <ctime>
14#include <format>
15#include <iostream>
16
17#ifndef M_PI
18#define M_PI 3.14159265358979323846
19#endif
20
21#ifndef asteroids_ASSET_DIR
22#define asteroids_ASSET_DIR "."
23#endif
24
25constexpr int GAME_W = 640;
26constexpr int GAME_H = 360;
27constexpr int STAR_COUNT = 800;
28constexpr int MAX_PROJECTILES = 50;
29constexpr int MAX_ASTEROIDS = 30;
30constexpr int MAX_PARTICLES = 40;
31constexpr int ASTEROID_VERTICES = 8;
32constexpr float PROJECTILE_SPEED = 5.0f;
33constexpr int PROJECTILE_LIFETIME = 60;
34constexpr int FIRE_COOLDOWN = 5;
35constexpr int SHOTS_PER_BURST = 5;
36constexpr int FIRE_DELAY = 3;
37constexpr int EXPLOSION_DURATION = 90;
38constexpr float MIN_ASTEROID_RADIUS = 8.0f;
39constexpr int LARGE_ASTEROID_POINTS = 20;
40constexpr int MEDIUM_ASTEROID_POINTS = 50;
41constexpr int SMALL_ASTEROID_POINTS = 100;
42
57
58struct Projectile {
59 float x, y;
60 float vx, vy;
61 float lifetime;
62 bool active;
63};
64
65struct Asteroid {
66 float x, y;
67 float vx, vy;
68 float radius;
69 bool active;
73};
74
75struct Particle {
76 float x, y;
77 float vx, vy;
79 bool active;
80};
81
82struct Star {
83 float x, y, speed;
84 float size;
86 int layer;
87 float r, g, b;
90};
91
93
94template <typename Func> void bresenhamLine(int x0, int y0, int x1, int y1, Func plot) {
95 int dx = abs(x1 - x0);
96 int dy = abs(y1 - y0);
97 int sx = (x0 < x1) ? 1 : -1;
98 int sy = (y0 < y1) ? 1 : -1;
99 int err = dx - dy;
100 while (true) {
101 plot(x0, y0);
102 if (x0 == x1 && y0 == y1)
103 break;
104 int e2 = 2 * err;
105 if (e2 > -dy) {
106 err -= dy;
107 x0 += sx;
108 }
109 if (e2 < dx) {
110 err += dx;
111 y0 += sy;
112 }
113 }
114}
115
117 public:
118 SpaceRoxWindow(const std::string &path, int wx, int wy, bool full, bool enable_vsync) : mxvk::VK_Window("-[ SpaceRox - MXVK ]-", wx, wy, full, MXVK_VALIDATION, enable_vsync), current_path((path.empty() || path == ".") ? std::string(asteroids_ASSET_DIR) : path), w(wx), h(wy) { srand(static_cast<unsigned>(time(nullptr))); }
119 virtual ~SpaceRoxWindow() {
120 if (gamepad != nullptr) {
121 SDL_CloseGamepad(gamepad);
122 gamepad = nullptr;
123 }
124 }
125
126 void proc() override {
127 initializeResources();
128 if (!resources_initialized || pixel == nullptr || starSprite == nullptr) {
129 return;
130 }
131
132 refreshWindowSize();
133
134 Uint32 currentTime = SDL_GetTicks();
135 Uint32 frameTime = currentTime - lastFrameTime;
136 if (frameTime < targetFrameTime) {
137 SDL_Delay(targetFrameTime - frameTime);
138 currentTime = SDL_GetTicks();
139 frameTime = currentTime - lastFrameTime;
140 }
141
142 if (lastFrameTime > 0) {
143 float deltaTime = frameTime / 1000.0f;
144 globalTime += deltaTime;
145 }
146 lastFrameTime = currentTime;
147
148 updateFontSize();
149
151
152 const bool *keys = SDL_GetKeyboardState(nullptr);
153
154 switch (state) {
155 case STATE_COUNTDOWN:
156 updateCountdown();
157 drawCountdown();
158 break;
159 case STATE_LAUNCH:
160 updateLaunch();
161 drawLaunch();
162 break;
163 case STATE_PLAYING:
164
165 if ((keys[SDL_SCANCODE_SPACE] || joyFire) && canFire()) {
166 fireProjectile(ship.x, ship.y, ship.angle);
167 }
168 updateShip();
169 updateFireTimer(keys);
170 updateProjectiles();
171 updateAsteroids();
172 updateExplosion();
173 checkAsteroidCollisions();
174 checkShipAsteroidCollision();
175 checkProjectileAsteroidCollisions();
176 checkAndSpawnAsteroids();
177 handleDeathSequence();
178 drawGame();
179 break;
180 case STATE_GAMEOVER:
181 drawGameOver();
182 break;
183 }
184 }
185
186 void event(SDL_Event &e) override {
187 switch (e.type) {
188 case SDL_EVENT_QUIT:
189 exit();
190 break;
191 case SDL_EVENT_KEY_DOWN:
192 if (e.key.key == SDLK_ESCAPE) {
193 exit();
194 }
195 if (e.key.key == SDLK_SPACE && state == STATE_GAMEOVER) {
196 restartGame();
197 }
198 if (state == STATE_PLAYING) {
199 if (e.key.key == SDLK_LEFT)
200 keyLeft = true;
201 if (e.key.key == SDLK_RIGHT)
202 keyRight = true;
203 if (e.key.key == SDLK_UP)
204 keyThrust = true;
205 }
206 break;
207 case SDL_EVENT_KEY_UP:
208 if (state == STATE_PLAYING) {
209 if (e.key.key == SDLK_LEFT)
210 keyLeft = false;
211 if (e.key.key == SDLK_RIGHT)
212 keyRight = false;
213 if (e.key.key == SDLK_UP)
214 keyThrust = false;
215 }
216 break;
217 case SDL_EVENT_GAMEPAD_ADDED:
218 openGamepad(e.gdevice.which);
219 joyRestLX = 0;
220 joyRestLY = 0;
221 break;
222 case SDL_EVENT_GAMEPAD_REMOVED:
223 if (gamepad != nullptr && e.gdevice.which == gamepadId) {
224 SDL_CloseGamepad(gamepad);
225 gamepad = nullptr;
226 gamepadId = 0;
227 joyLeft = joyRight = joyThrust = joyFire = false;
228 joyRestLX = 0;
229 joyRestLY = 0;
230 joyCenterStable = 0;
231 }
232 break;
233 case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
234 handleControllerButton(e.gbutton.button);
235 break;
236 case SDL_EVENT_GAMEPAD_BUTTON_UP:
237 handleControllerButtonUp(e.gbutton.button);
238 break;
239 }
240 }
241
242 void handleControllerButton(Uint8 button) {
243 switch (state) {
244 case STATE_GAMEOVER:
245 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
246 restartGame();
247 }
248 break;
249 case STATE_PLAYING:
250 if (button == SDL_GAMEPAD_BUTTON_SOUTH) {
251 joyFire = true;
252 } else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
253
254 } else if (button == SDL_GAMEPAD_BUTTON_START) {
255 }
256 break;
257 default:
258 break;
259 }
260 }
261
262 void handleControllerButtonUp(Uint8 button) {
263 if (state == STATE_PLAYING) {
264 if (button == SDL_GAMEPAD_BUTTON_SOUTH) {
265 joyFire = false;
266 }
267 }
268 }
269
271 if (gamepad == nullptr) {
272 joyLeft = joyRight = joyThrust = joyFire = false;
273 joyCenterStable = 0;
274 return;
275 }
276
277 if (SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_START)) {
278 exit();
279 return;
280 }
281
282 if (state != STATE_PLAYING || ship.exploding) {
283 joyLeft = joyRight = joyThrust = joyFire = false;
284 return;
285 }
286
287 Sint16 rawLX = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
288 Sint16 rawLY = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
289
290 if (abs(rawLX) < JOY_RECENTER_ZONE && abs(rawLY) < JOY_RECENTER_ZONE) {
291 if (joyCenterStable < JOY_RECENTER_FRAMES) {
292 joyCenterStable++;
293 }
294 if (joyCenterStable >= JOY_RECENTER_FRAMES) {
295 joyRestLX = rawLX;
296 joyRestLY = rawLY;
297 }
298 } else {
299 joyCenterStable = 0;
300 }
301
302 int lx = static_cast<int>(rawLX) - static_cast<int>(joyRestLX);
303 int ly = static_cast<int>(rawLY) - static_cast<int>(joyRestLY);
304 joyLeft = (lx < -JOY_ROTATE_ZONE);
305 joyRight = (lx > JOY_ROTATE_ZONE);
306 joyThrust = (ly < -JOY_THRUST_ZONE);
307 Sint16 rt = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER);
308 joyFire = (rt > JOY_THRUST_ZONE) || SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_SOUTH);
309 }
310
311 private:
312 std::string current_path;
313 bool resources_initialized = false;
314 int w = GAME_W;
315 int h = GAME_H;
316
317 mxvk::VK_Sprite *pixel = nullptr;
318 mxvk::VK_Sprite *starSprite = nullptr;
319
320 Uint32 lastFrameTime = 0;
321 static constexpr Uint32 targetFrameTime = 1000 / 60;
322 float globalTime = 0.0f;
323
324 Ship ship{};
325 Projectile projectiles[MAX_PROJECTILES]{};
326 Asteroid asteroids[MAX_ASTEROIDS]{};
327 Particle particles[MAX_PARTICLES]{};
328 Star stars[STAR_COUNT * 3]{};
329 bool starsInit = false;
330
332 bool keyLeft = false, keyRight = false, keyThrust = false;
333 bool keyFire = false;
334 bool joyLeft = false, joyRight = false, joyThrust = false;
335 bool joyFire = false;
336 bool wasExploding = false;
337
338 SDL_Gamepad *gamepad = nullptr;
339 SDL_JoystickID gamepadId = 0;
340 static constexpr Sint16 JOY_ROTATE_ZONE = 16000;
341 static constexpr Sint16 JOY_THRUST_ZONE = 12000;
342 static constexpr Sint16 JOY_RECENTER_ZONE = 8000;
343 static constexpr int JOY_RECENTER_FRAMES = 12;
344 Sint16 joyRestLX = 0, joyRestLY = 0;
345 int joyCenterStable = 0;
346
347 Uint32 countdownTimer = 0;
348 Uint32 countdownDuration = 1000;
349 Uint32 countdownStartMs = 0;
350 int countdownNumber = 3;
351 Uint32 launchTimer = 0;
352 Uint32 launchDuration = 1000;
353 Uint32 launchStartMs = 0;
354 float shipLaunchY = 0;
355
356 int lastFontSize = 0;
357
358 float sx() const { return static_cast<float>(w) / GAME_W; }
359 float sy() const { return static_cast<float>(h) / GAME_H; }
360
361 int toScreenX(float gx) const { return static_cast<int>(gx * sx()); }
362 int toScreenY(float gy) const { return static_cast<int>(gy * sy()); }
363 int toScreenW(float gw) const { return std::max(1, static_cast<int>(gw * sx())); }
364 int toScreenH(float gh) const { return std::max(1, static_cast<int>(gh * sy())); }
365
366 void refreshWindowSize() {
367 if (window != nullptr) {
368 SDL_GetWindowSizeInPixels(window.get(), &w, &h);
369 }
370 }
371
372 bool openGamepad(SDL_JoystickID id) {
373 if (gamepad != nullptr && gamepadId == id) {
374 return true;
375 }
376 if (gamepad != nullptr) {
377 SDL_CloseGamepad(gamepad);
378 gamepad = nullptr;
379 gamepadId = 0;
380 }
381 gamepad = SDL_OpenGamepad(id);
382 if (gamepad == nullptr) {
383 return false;
384 }
385 gamepadId = id;
386 return true;
387 }
388
389 void tryOpenFirstGamepad() {
390 if (gamepad != nullptr) {
391 return;
392 }
393 int count = 0;
394 SDL_JoystickID *ids = SDL_GetGamepads(&count);
395 if (ids == nullptr || count <= 0) {
396 if (ids != nullptr) {
397 SDL_free(ids);
398 }
399 return;
400 }
401 openGamepad(ids[0]);
402 SDL_free(ids);
403 }
404
405 void initializeResources() {
406 if (resources_initialized) {
407 return;
408 }
409
410 tryOpenFirstGamepad();
411
412 std::array<std::uint8_t, 4 * 4 * 4> starPixels{};
413 for (int y = 0; y < 4; y++) {
414 for (int x = 0; x < 4; x++) {
415 float dx = (x - 1.5f) / 1.5f;
416 float dy = (y - 1.5f) / 1.5f;
417 float dist = sqrtf(dx * dx + dy * dy);
418 std::uint8_t alpha = (dist < 1.0f) ? static_cast<std::uint8_t>(255.0f * (1.0f - dist)) : 0;
419 const int idx = (y * 4 + x) * 4;
420 starPixels[idx + 0] = 255;
421 starPixels[idx + 1] = 255;
422 starPixels[idx + 2] = 255;
423 starPixels[idx + 3] = alpha;
424 }
425 }
426
427 starSprite = createSprite(4, 4, current_path + "/data/sprite_vert.spv", current_path + "/data/sprite_frag.spv");
428 starSprite->updateTexture(starPixels.data(), 4, 4, 4 * 4);
429
430 const std::array<std::uint8_t, 2 * 2 * 4> whitePixels{
431 255,
432 255,
433 255,
434 255,
435 255,
436 255,
437 255,
438 255,
439 255,
440 255,
441 255,
442 255,
443 255,
444 255,
445 255,
446 255,
447 };
448
449 pixel = createSprite(2, 2, current_path + "/data/sprite_vert.spv", current_path + "/data/solid_frag.spv");
450 pixel->updateTexture(whitePixels.data(), 2, 2, 2 * 4);
451
452 initGame();
453 resources_initialized = true;
454 }
455
456 void setColor(float r, float g, float b, float a = 1.0f) { pixel->setShaderParams(r, g, b, a); }
457
458 void drawPixel(float gx, float gy) {
459 int psz = std::max(1, static_cast<int>(2.0f * std::min(sx(), sy())));
460 pixel->drawSpriteRect(toScreenX(gx), toScreenY(gy), psz, psz);
461 }
462
463 void drawRect(float gx, float gy, float gw, float gh) { pixel->drawSpriteRect(toScreenX(gx), toScreenY(gy), toScreenW(gw), toScreenH(gh)); }
464
465 void drawLine(float x0f, float y0f, float x1f, float y1f) {
466 int x0 = toScreenX(x0f), y0 = toScreenY(y0f);
467 int x1 = toScreenX(x1f), y1 = toScreenY(y1f);
468 int psz = std::max(1, static_cast<int>(1.5f * std::min(sx(), sy())));
469 bresenhamLine(x0, y0, x1, y1, [&](int px, int py) { pixel->drawSpriteRect(px, py, psz, psz); });
470 }
471
472 void drawPoint(int screenX, int screenY, int size = 2) { pixel->drawSpriteRect(screenX, screenY, size, size); }
473
474 void updateFontSize() {
475 int fontSize = static_cast<int>(20.0f * (static_cast<float>(h) / 480.0f));
476 fontSize = std::max(14, std::min(128, fontSize));
477 if (fontSize != lastFontSize) {
478 lastFontSize = fontSize;
479 setFont(current_path + "/data/font.ttf", fontSize);
481 }
482 }
483
484 void initGame() {
485 initShip();
486 initProjectiles();
487 initAsteroids();
488 initStars();
489 state = STATE_COUNTDOWN;
490 countdownTimer = 0;
491 countdownNumber = 3;
492 countdownStartMs = SDL_GetTicks();
493 launchStartMs = 0;
494 wasExploding = false;
495 }
496
497 void restartGame() { initGame(); }
498
499 void initStars() {
500 int starIndex = 0;
501
502 for (int i = 0; i < STAR_COUNT; ++i, ++starIndex) {
503 stars[starIndex].x = static_cast<float>(rand() % GAME_W);
504 stars[starIndex].y = static_cast<float>(rand() % GAME_H);
505 stars[starIndex].speed = 0.15f + (rand() % 40) / 100.0f;
506 stars[starIndex].size = 0.8f + (rand() % 12) / 20.0f;
507 stars[starIndex].brightness = 0.25f + (rand() % 45) / 100.0f;
508 stars[starIndex].layer = 0;
509 assignStarColor(stars[starIndex]);
510 stars[starIndex].twinklePhase = (rand() % 628) / 100.0f;
511 stars[starIndex].twinkleSpeed = 0.4f + (rand() % 150) / 100.0f;
512 }
513
514 for (int i = 0; i < STAR_COUNT; ++i, ++starIndex) {
515 stars[starIndex].x = static_cast<float>(rand() % GAME_W);
516 stars[starIndex].y = static_cast<float>(rand() % GAME_H);
517 stars[starIndex].speed = 0.4f + (rand() % 80) / 100.0f;
518 stars[starIndex].size = 1.2f + (rand() % 18) / 20.0f;
519 stars[starIndex].brightness = 0.45f + (rand() % 55) / 100.0f;
520 stars[starIndex].layer = 1;
521 assignStarColor(stars[starIndex]);
522 stars[starIndex].twinklePhase = (rand() % 628) / 100.0f;
523 stars[starIndex].twinkleSpeed = 0.8f + (rand() % 250) / 100.0f;
524 }
525
526 for (int i = 0; i < STAR_COUNT; ++i, ++starIndex) {
527 stars[starIndex].x = static_cast<float>(rand() % GAME_W);
528 stars[starIndex].y = static_cast<float>(rand() % GAME_H);
529 stars[starIndex].speed = 0.8f + (rand() % 180) / 100.0f;
530 stars[starIndex].size = 1.8f + (rand() % 25) / 20.0f;
531 stars[starIndex].brightness = 0.65f + (rand() % 35) / 100.0f;
532 stars[starIndex].layer = 2;
533 assignStarColor(stars[starIndex]);
534 stars[starIndex].twinklePhase = (rand() % 628) / 100.0f;
535 stars[starIndex].twinkleSpeed = 1.2f + (rand() % 350) / 100.0f;
536 }
537 starsInit = true;
538 }
539
540 void assignStarColor(Star &s) {
541 float roll = (rand() % 1000) / 1000.0f;
542 if (roll < 0.35f) {
543
544 s.r = 0.85f + (rand() % 15) / 100.0f;
545 s.g = 0.90f + (rand() % 10) / 100.0f;
546 s.b = 1.0f;
547 } else if (roll < 0.55f) {
548
549 s.r = 0.5f + (rand() % 20) / 100.0f;
550 s.g = 0.75f + (rand() % 15) / 100.0f;
551 s.b = 1.0f;
552 s.size *= 1.15f;
553 s.brightness *= 1.1f;
554 } else if (roll < 0.70f) {
555
556 s.r = 1.0f;
557 s.g = 0.95f + (rand() % 5) / 100.0f;
558 s.b = 0.6f + (rand() % 30) / 100.0f;
559 s.size *= 1.05f;
560 } else if (roll < 0.82f) {
561
562 s.r = 1.0f;
563 s.g = 0.6f + (rand() % 20) / 100.0f;
564 s.b = 0.2f + (rand() % 20) / 100.0f;
565 s.size *= 1.1f;
566 } else if (roll < 0.90f) {
567
568 s.r = 1.0f;
569 s.g = 0.3f + (rand() % 20) / 100.0f;
570 s.b = 0.2f + (rand() % 15) / 100.0f;
571 s.size *= 1.08f;
572 } else if (roll < 0.96f) {
573
574 s.r = 1.0f;
575 s.g = 1.0f;
576 s.b = 1.0f;
577 s.size *= 1.4f;
578 s.brightness *= 1.4f;
579 } else {
580
581 s.r = 0.9f;
582 s.g = 0.95f;
583 s.b = 1.0f;
584 s.size *= 1.6f;
585 s.brightness *= 1.5f;
586 }
587 }
588
589 void initShip() {
590 ship.x = GAME_W / 2.0f;
591 ship.y = GAME_H / 2.0f;
592 ship.vx = 0;
593 ship.vy = 0;
594 ship.angle = 0;
595 ship.lives = 3;
596 ship.fire_cooldown = 0;
597 ship.burst_count = 0;
598 ship.exploding = false;
599 ship.explosion_timer = 0;
600 ship.score = 0;
601 ship.continuous_fire_timer = 0;
602 ship.overheated = false;
603 ship.overheat_cooldown = 0;
604 for (auto &p : particles)
605 p.active = false;
606 }
607
608 void initProjectiles() {
609 for (auto &p : projectiles)
610 p.active = false;
611 }
612
613 void initAsteroids() {
614 for (auto &a : asteroids)
615 a.active = false;
616 for (int i = 0; i < 4; ++i) {
617 float x, y;
618 do {
619 x = static_cast<float>(rand() % GAME_W);
620 y = static_cast<float>(rand() % GAME_H);
621 } while (hypotf(x - ship.x, y - ship.y) < 100);
622 float vx = ((rand() % 100) - 50) / 50.0f;
623 float vy = ((rand() % 100) - 50) / 50.0f;
624 float radius = 20.0f + (rand() % 20);
625 spawnAsteroid(x, y, vx, vy, radius);
626 }
627 }
628
629 void updateShip() {
630 if (ship.exploding)
631 return;
632 if (keyLeft || joyLeft)
633 ship.angle -= 0.15f;
634 if (keyRight || joyRight)
635 ship.angle += 0.15f;
636 if (keyThrust || joyThrust) {
637 float thrust = 0.2f;
638 ship.vx += sinf(ship.angle) * thrust;
639 ship.vy += -cosf(ship.angle) * thrust;
640 float spd = sqrtf(ship.vx * ship.vx + ship.vy * ship.vy);
641 float maxSpd = 4.0f;
642 if (spd > maxSpd) {
643 ship.vx = (ship.vx / spd) * maxSpd;
644 ship.vy = (ship.vy / spd) * maxSpd;
645 }
646 }
647 ship.vx *= 0.98f;
648 ship.vy *= 0.98f;
649 ship.x += ship.vx;
650 ship.y += ship.vy;
651
652 if (ship.x < 0)
653 ship.x += GAME_W;
654 if (ship.x >= GAME_W)
655 ship.x -= GAME_W;
656 if (ship.y < 0)
657 ship.y += GAME_H;
658 if (ship.y >= GAME_H)
659 ship.y -= GAME_H;
660 if (ship.fire_cooldown > 0)
661 ship.fire_cooldown--;
662 }
663
664 void updateFireTimer(const bool *keys) {
665 bool firing = keys[SDL_SCANCODE_SPACE] || joyFire;
666 if (firing && !ship.overheated) {
667 ship.continuous_fire_timer++;
668 ship.overheat_cooldown = 0;
669 if (ship.continuous_fire_timer >= 180) {
670 ship.overheated = true;
671 ship.overheat_cooldown = 0;
672 ship.continuous_fire_timer = 0;
673 ship.burst_count = 0;
674 }
675 } else if (firing && ship.overheated) {
676 ship.overheat_cooldown = 0;
677 } else {
678 if (ship.overheated) {
679 ship.overheat_cooldown++;
680 if (ship.overheat_cooldown >= 180) {
681 ship.overheated = false;
682 ship.overheat_cooldown = 0;
683 ship.continuous_fire_timer = 0;
684 }
685 } else {
686 if (ship.continuous_fire_timer > 0)
687 ship.continuous_fire_timer--;
688 }
689 }
690 }
691
692 bool canFire() {
693 if (ship.overheated)
694 return false;
695 if (ship.fire_cooldown <= 0) {
696 if (ship.burst_count < SHOTS_PER_BURST) {
697 ship.fire_cooldown = FIRE_DELAY;
698 ship.burst_count++;
699 return true;
700 } else {
701 ship.fire_cooldown = FIRE_COOLDOWN;
702 ship.burst_count = 0;
703 return false;
704 }
705 }
706 return false;
707 }
708
709 void fireProjectile(float x, float y, float angle) {
710 for (auto &p : projectiles) {
711 if (!p.active) {
712 float c = cosf(angle), s = sinf(angle);
713 float lx = 0, ly = -12;
714 p.x = x + (lx * c - ly * s);
715 p.y = y + (lx * s + ly * c);
716 p.vx = sinf(angle) * PROJECTILE_SPEED;
717 p.vy = -cosf(angle) * PROJECTILE_SPEED;
718 p.lifetime = PROJECTILE_LIFETIME;
719 p.active = true;
720 break;
721 }
722 }
723 }
724
725 void updateProjectiles() {
726 for (auto &p : projectiles) {
727 if (!p.active)
728 continue;
729 p.x += p.vx;
730 p.y += p.vy;
731 if (p.x < 0 || p.x >= GAME_W || p.y < 0 || p.y >= GAME_H) {
732 p.active = false;
733 continue;
734 }
735 p.lifetime--;
736 if (p.lifetime <= 0)
737 p.active = false;
738 }
739 }
740
741 void spawnAsteroid(float x, float y, float vx, float vy, float radius) {
742 for (auto &a : asteroids) {
743 if (!a.active) {
744 a.x = x;
745 a.y = y;
746 a.vx = vx;
747 a.vy = vy;
748 a.radius = radius;
749 a.active = true;
750 a.rotation_angle = 0;
751 a.rotation_speed = ((rand() % 100) - 50) / 1000.0f;
752 for (int j = 0; j < ASTEROID_VERTICES; ++j) {
753 float ang = static_cast<float>(j) / ASTEROID_VERTICES * 2 * M_PI;
754 float vr = radius * (0.7f + (rand() % 30) / 100.0f);
755 a.vertices[j][0] = cosf(ang) * vr;
756 a.vertices[j][1] = sinf(ang) * vr;
757 }
758 break;
759 }
760 }
761 }
762
763 void updateAsteroids() {
764 for (auto &a : asteroids) {
765 if (!a.active)
766 continue;
767 a.x += a.vx;
768 a.y += a.vy;
769 a.rotation_angle += a.rotation_speed;
770 if (a.x < 0)
771 a.x += GAME_W;
772 if (a.x >= GAME_W)
773 a.x -= GAME_W;
774 if (a.y < 0)
775 a.y += GAME_H;
776 if (a.y >= GAME_H)
777 a.y -= GAME_H;
778 }
779 }
780
781 void splitAsteroid(int idx) {
782 float x = asteroids[idx].x;
783 float y = asteroids[idx].y;
784 float r = asteroids[idx].radius / 2.0f;
785 if (r < MIN_ASTEROID_RADIUS) {
786 createAsteroidExplosion(x, y);
787 ship.score += SMALL_ASTEROID_POINTS;
788 asteroids[idx].active = false;
789 return;
790 }
791 int splits = 2 + (rand() % 2);
792 for (int i = 0; i < splits; ++i) {
793 float ang = (rand() % 628) / 100.0f;
794 float spd = 0.5f + (rand() % 15) / 10.0f;
795 float nvx = cosf(ang) * spd + asteroids[idx].vx * 0.3f;
796 float nvy = sinf(ang) * spd + asteroids[idx].vy * 0.3f;
797 float off = r * 0.5f;
798 spawnAsteroid(x + cosf(ang) * off, y + sinf(ang) * off, nvx, nvy, r);
799 }
800 if (r >= 25.0f)
801 ship.score += LARGE_ASTEROID_POINTS;
802 else
803 ship.score += MEDIUM_ASTEROID_POINTS;
804 createAsteroidExplosion(x, y);
805 asteroids[idx].active = false;
806 }
807
808 void checkAndSpawnAsteroids() {
809 int active = 0;
810 for (auto &a : asteroids)
811 if (a.active)
812 active++;
813 int minAsteroids = 3;
814 if (active < minAsteroids) {
815 for (int s = 0; s < minAsteroids - active; ++s) {
816 float x, y;
817 int attempts = 0;
818 do {
819 if (rand() % 2) {
820 x = (rand() % 2) ? -30.0f : GAME_W + 30.0f;
821 y = static_cast<float>(rand() % GAME_H);
822 } else {
823 x = static_cast<float>(rand() % GAME_W);
824 y = (rand() % 2) ? -30.0f : GAME_H + 30.0f;
825 }
826 attempts++;
827 } while (hypotf(x - ship.x, y - ship.y) < 100.0f && attempts < 10);
828 float vx = ((rand() % 100) - 50) / 50.0f;
829 float vy = ((rand() % 100) - 50) / 50.0f;
830 float radius;
831 int roll = rand() % 100;
832 if (roll < 20)
833 radius = 30.0f + (rand() % 15);
834 else if (roll < 60)
835 radius = 20.0f + (rand() % 10);
836 else
837 radius = 12.0f + (rand() % 8);
838 spawnAsteroid(x, y, vx, vy, radius);
839 }
840 }
841 }
842
843 void checkAsteroidCollisions() {
844 for (int i = 0; i < MAX_ASTEROIDS; ++i) {
845 if (!asteroids[i].active)
846 continue;
847 for (int j = i + 1; j < MAX_ASTEROIDS; ++j) {
848 if (!asteroids[j].active)
849 continue;
850 float dx = asteroids[i].x - asteroids[j].x;
851 float dy = asteroids[i].y - asteroids[j].y;
852 float dist = sqrtf(dx * dx + dy * dy);
853 float minDist = asteroids[i].radius + asteroids[j].radius;
854 if (dist < minDist && dist > 0) {
855 float nx = dx / dist, ny = dy / dist;
856 float overlap = (minDist - dist) * 0.5f;
857 asteroids[i].x += nx * overlap;
858 asteroids[i].y += ny * overlap;
859 asteroids[j].x -= nx * overlap;
860 asteroids[j].y -= ny * overlap;
861 float rvx = asteroids[i].vx - asteroids[j].vx;
862 float rvy = asteroids[i].vy - asteroids[j].vy;
863 float van = rvx * nx + rvy * ny;
864 if (van > 0)
865 continue;
866 float restitution = 0.8f;
867 float impulse = -(1 + restitution) * van * 0.5f;
868 asteroids[i].vx += impulse * nx;
869 asteroids[i].vy += impulse * ny;
870 asteroids[j].vx -= impulse * nx;
871 asteroids[j].vy -= impulse * ny;
872 }
873 }
874 }
875 }
876
877 void checkShipAsteroidCollision() {
878 if (ship.exploding)
879 return;
880 for (int i = 0; i < MAX_ASTEROIDS; ++i) {
881 if (!asteroids[i].active)
882 continue;
883 float dx = ship.x - asteroids[i].x;
884 float dy = ship.y - asteroids[i].y;
885 float dist = sqrtf(dx * dx + dy * dy);
886 float shipRadius = 8.0f;
887 if (dist < asteroids[i].radius + shipRadius) {
888 startShipExplosion();
889 createAsteroidExplosion(ship.x + dx * 0.5f, ship.y + dy * 0.5f);
890 if (dist > 0) {
891 float nx = -dx / dist, ny = -dy / dist;
892 asteroids[i].vx += nx * 2.0f;
893 asteroids[i].vy += ny * 2.0f;
894 }
895 break;
896 }
897 }
898 }
899
900 void checkProjectileAsteroidCollisions() {
901 for (auto &p : projectiles) {
902 if (!p.active)
903 continue;
904 for (int j = 0; j < MAX_ASTEROIDS; ++j) {
905 if (!asteroids[j].active)
906 continue;
907 float dx = p.x - asteroids[j].x;
908 float dy = p.y - asteroids[j].y;
909 if (sqrtf(dx * dx + dy * dy) < asteroids[j].radius) {
910 p.active = false;
911 if (asteroids[j].radius >= 20.0f)
912 ship.score += LARGE_ASTEROID_POINTS;
913 else if (asteroids[j].radius >= MIN_ASTEROID_RADIUS)
914 ship.score += MEDIUM_ASTEROID_POINTS;
915 else
916 ship.score += SMALL_ASTEROID_POINTS;
917 splitAsteroid(j);
918 break;
919 }
920 }
921 }
922 }
923
924 void startShipExplosion() {
925 if (ship.exploding)
926 return;
927 ship.exploding = true;
928 ship.explosion_timer = EXPLOSION_DURATION;
929 ship.lives--;
930 ship.overheated = false;
931 ship.overheat_cooldown = 0;
932 ship.continuous_fire_timer = 0;
933 ship.burst_count = 0;
934 for (auto &p : particles) {
935 p.x = ship.x + ((rand() % 20) - 10);
936 p.y = ship.y + ((rand() % 20) - 10);
937 float ang = (rand() % 628) / 100.0f;
938 float spd = 0.5f + (rand() % 20) / 10.0f;
939 p.vx = cosf(ang) * spd;
940 p.vy = sinf(ang) * spd;
941 p.lifetime = 30 + (rand() % 60);
942 p.active = true;
943 }
944 }
945
946 void createAsteroidExplosion(float x, float y) {
947 int toCreate = 15 + (rand() % 10);
948 int created = 0;
949 for (auto &p : particles) {
950 if (created >= toCreate)
951 break;
952 if (!p.active) {
953 p.x = x + ((rand() % 16) - 8);
954 p.y = y + ((rand() % 16) - 8);
955 float ang = (rand() % 628) / 100.0f;
956 float spd = 1.0f + (rand() % 20) / 10.0f;
957 p.vx = cosf(ang) * spd;
958 p.vy = sinf(ang) * spd;
959 p.lifetime = 15 + (rand() % 25);
960 p.active = true;
961 created++;
962 }
963 }
964 }
965
966 void updateExplosion() {
967 for (auto &p : particles) {
968 if (!p.active)
969 continue;
970 p.x += p.vx;
971 p.y += p.vy;
972 p.vx *= 0.97f;
973 p.vy *= 0.97f;
974 p.lifetime--;
975 if (p.lifetime <= 0) {
976 p.active = false;
977 continue;
978 }
979 if (p.x < 0)
980 p.x += GAME_W;
981 if (p.x >= GAME_W)
982 p.x -= GAME_W;
983 if (p.y < 0)
984 p.y += GAME_H;
985 if (p.y >= GAME_H)
986 p.y -= GAME_H;
987 }
988 if (ship.exploding) {
989 ship.explosion_timer--;
990 if (ship.explosion_timer <= 0)
991 ship.exploding = false;
992 }
993 }
994
995 void handleDeathSequence() {
996 if (wasExploding && !ship.exploding) {
997 if (ship.lives > 0) {
998 triggerRespawn();
999 } else {
1000 state = STATE_GAMEOVER;
1001 }
1002 wasExploding = false;
1003 } else if (ship.exploding) {
1004 wasExploding = true;
1005 }
1006 }
1007
1008 void triggerRespawn() {
1009 ship.x = GAME_W / 2.0f;
1010 ship.y = GAME_H / 2.0f;
1011 ship.vx = 0;
1012 ship.vy = 0;
1013 ship.angle = 0;
1014 ship.exploding = false;
1015 ship.explosion_timer = 0;
1016 keyLeft = keyRight = keyThrust = keyFire = false;
1017 joyLeft = joyRight = joyThrust = joyFire = false;
1018 state = STATE_COUNTDOWN;
1019 countdownTimer = 0;
1020 countdownNumber = 3;
1021 countdownStartMs = SDL_GetTicks();
1022 launchStartMs = 0;
1023 }
1024
1025 void updateCountdown() {
1026 const Uint32 now = SDL_GetTicks();
1027 const Uint32 elapsed = now - countdownStartMs;
1028
1029 countdownTimer = elapsed % countdownDuration;
1030 countdownNumber = 3 - static_cast<int>(elapsed / countdownDuration);
1031
1032 if (elapsed >= (countdownDuration * 4U)) {
1033 state = STATE_LAUNCH;
1034 launchTimer = 0;
1035 launchStartMs = now;
1036 shipLaunchY = static_cast<float>(GAME_H);
1037 }
1038 }
1039
1040 void updateLaunch() {
1041 const Uint32 now = SDL_GetTicks();
1042 launchTimer = std::min<Uint32>(now - launchStartMs, launchDuration);
1043 if (launchTimer < launchDuration / 2U) {
1044 shipLaunchY = static_cast<float>(GAME_H) - (static_cast<float>(launchTimer) * (static_cast<float>(GAME_H) - ship.y) / static_cast<float>(launchDuration / 2U));
1045 } else {
1046 shipLaunchY = ship.y;
1047 }
1048 if (launchTimer >= launchDuration) {
1049 state = STATE_PLAYING;
1050 if (gamepad != nullptr) {
1051 joyRestLX = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
1052 joyRestLY = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
1053 }
1054 }
1055 }
1056
1057 void drawStars(float speedMul = 1.0f) {
1058
1059 for (auto &s : stars) {
1060 s.y += s.speed * speedMul;
1061 if (s.y >= GAME_H) {
1062 s.x = static_cast<float>(rand() % GAME_W);
1063 s.y = 0;
1064
1065 if (s.layer == 0) {
1066 s.speed = 0.15f + (rand() % 40) / 100.0f;
1067 s.size = 0.8f + (rand() % 12) / 20.0f;
1068 s.brightness = 0.25f + (rand() % 45) / 100.0f;
1069 } else if (s.layer == 1) {
1070 s.speed = 0.4f + (rand() % 80) / 100.0f;
1071 s.size = 1.2f + (rand() % 18) / 20.0f;
1072 s.brightness = 0.45f + (rand() % 55) / 100.0f;
1073 } else {
1074 s.speed = 0.8f + (rand() % 180) / 100.0f;
1075 s.size = 1.8f + (rand() % 25) / 20.0f;
1076 s.brightness = 0.65f + (rand() % 35) / 100.0f;
1077 }
1078 assignStarColor(s);
1079 s.twinklePhase = (rand() % 628) / 100.0f;
1080 }
1081
1082 s.twinklePhase += s.twinkleSpeed * 0.016f;
1083 float twinkle = 0.7f + 0.3f * sinf(s.twinklePhase);
1084 float finalBrightness = s.brightness * twinkle;
1085
1086 starSprite->setShaderParams(s.r * finalBrightness, s.g * finalBrightness, s.b * finalBrightness, finalBrightness);
1087
1088 int screenSize = static_cast<int>(s.size * std::min(sx(), sy()));
1089 screenSize = std::max(2, screenSize);
1090 starSprite->drawSpriteRect(toScreenX(s.x), toScreenY(s.y), screenSize, screenSize);
1091 }
1092 }
1093
1094 void drawShipAt(float px, float py) {
1095 if (ship.exploding) {
1096 drawExplosion();
1097 return;
1098 }
1099 float c = cosf(ship.angle), s = sinf(ship.angle);
1100 auto rot = [&](float lx, float ly, float &ox, float &oy) {
1101 ox = px + (lx * c - ly * s);
1102 oy = py + (lx * s + ly * c);
1103 };
1104
1105 float nose_x, nose_y, lw_x, lw_y, rw_x, rw_y;
1106 float le_x, le_y, re_x, re_y, ce_x, ce_y;
1107 float ld_x, ld_y, rd_x, rd_y;
1108 float cl_x, cl_y, cr_x, cr_y, cc_x, cc_y;
1109
1110 rot(0, -15, nose_x, nose_y);
1111 rot(-12, 8, lw_x, lw_y);
1112 rot(12, 8, rw_x, rw_y);
1113 rot(-8, 12, le_x, le_y);
1114 rot(8, 12, re_x, re_y);
1115 rot(0, 10, ce_x, ce_y);
1116 rot(-6, 2, ld_x, ld_y);
1117 rot(6, 2, rd_x, rd_y);
1118 rot(-3, -8, cl_x, cl_y);
1119 rot(3, -8, cr_x, cr_y);
1120 rot(0, -5, cc_x, cc_y);
1121
1122 if (ship.overheated) {
1123 setColor(1.0f, 0.0f, 0.0f, 1.0f);
1124 } else if (ship.continuous_fire_timer > 120) {
1125 int heat = ship.continuous_fire_timer - 120;
1126 setColor(1.0f, std::max(0.0f, 1.0f - heat / 64.0f), 0.0f, 1.0f);
1127 } else {
1128 setColor(1.0f, 1.0f, 1.0f, 1.0f);
1129 }
1130
1131 drawLine(nose_x, nose_y, lw_x, lw_y);
1132 drawLine(nose_x, nose_y, rw_x, rw_y);
1133 drawLine(lw_x, lw_y, rw_x, rw_y);
1134
1135 drawLine(lw_x, lw_y, le_x, le_y);
1136 drawLine(rw_x, rw_y, re_x, re_y);
1137 drawLine(le_x, le_y, ce_x, ce_y);
1138 drawLine(re_x, re_y, ce_x, ce_y);
1139
1140 if (ship.overheated)
1141 setColor(0.78f, 0.0f, 0.0f, 1.0f);
1142 else if (ship.continuous_fire_timer > 120)
1143 setColor(0.78f, std::max(0.0f, 0.78f - (ship.continuous_fire_timer - 120) / 80.0f), 0.0f, 1.0f);
1144 else
1145 setColor(0.78f, 0.78f, 0.78f, 1.0f);
1146 drawLine(nose_x, nose_y, ld_x, ld_y);
1147 drawLine(nose_x, nose_y, rd_x, rd_y);
1148 drawLine(ld_x, ld_y, rd_x, rd_y);
1149 drawLine(ld_x, ld_y, lw_x, lw_y);
1150 drawLine(rd_x, rd_y, rw_x, rw_y);
1151
1152 if (ship.overheated)
1153 setColor(0.0f, 0.5f, 0.5f, 1.0f);
1154 else
1155 setColor(0.0f, 1.0f, 1.0f, 1.0f);
1156 drawLine(cl_x, cl_y, cr_x, cr_y);
1157 drawLine(cl_x, cl_y, cc_x, cc_y);
1158 drawLine(cr_x, cr_y, cc_x, cc_y);
1159
1160 setColor(0.59f, 0.59f, 0.59f, 1.0f);
1161 drawLine(cc_x, cc_y, ce_x, ce_y);
1162
1163 if (keyThrust || joyThrust) {
1164 float f1x, f1y, f2x, f2y, f3x, f3y;
1165 rot(-6, 18, f1x, f1y);
1166 rot(0, 22, f2x, f2y);
1167 rot(6, 18, f3x, f3y);
1168 setColor(1.0f, 0.39f, 0.0f, 1.0f);
1169 drawLine(le_x, le_y, f1x, f1y);
1170 drawLine(ce_x, ce_y, f2x, f2y);
1171 drawLine(re_x, re_y, f3x, f3y);
1172 float ifx, ify;
1173 rot(0, 20, ifx, ify);
1174 setColor(1.0f, 1.0f, 0.0f, 1.0f);
1175 drawLine(ce_x, ce_y, ifx, ify);
1176 }
1177 }
1178
1179 void drawProjectiles() {
1180 setColor(1.0f, 0.0f, 0.0f, 1.0f);
1181 for (auto &p : projectiles) {
1182 if (!p.active)
1183 continue;
1184 float endX = p.x - p.vx * 0.5f;
1185 float endY = p.y - p.vy * 0.5f;
1186 drawLine(p.x, p.y, endX, endY);
1187 }
1188 }
1189
1190 void drawAsteroids() {
1191 for (int i = 0; i < MAX_ASTEROIDS; ++i) {
1192 if (!asteroids[i].active)
1193 continue;
1194 auto &a = asteroids[i];
1195 float cosR = cosf(a.rotation_angle);
1196 float sinR = sinf(a.rotation_angle);
1197
1198 float rv[ASTEROID_VERTICES][2];
1199 for (int j = 0; j < ASTEROID_VERTICES; ++j) {
1200 rv[j][0] = a.vertices[j][0] * cosR - a.vertices[j][1] * sinR;
1201 rv[j][1] = a.vertices[j][0] * sinR + a.vertices[j][1] * cosR;
1202 }
1203
1204 setColor(1.0f, 1.0f, 1.0f, 1.0f);
1205 for (int j = 0; j < ASTEROID_VERTICES; ++j) {
1206 int next = (j + 1) % ASTEROID_VERTICES;
1207 drawLine(a.x + rv[j][0], a.y + rv[j][1], a.x + rv[next][0], a.y + rv[next][1]);
1208 }
1209
1210 float iv[ASTEROID_VERTICES][2];
1211 for (int j = 0; j < ASTEROID_VERTICES; ++j) {
1212 iv[j][0] = rv[j][0] * 0.6f;
1213 iv[j][1] = rv[j][1] * 0.6f;
1214 }
1215 setColor(0.59f, 0.59f, 0.59f, 1.0f);
1216 for (int j = 0; j < ASTEROID_VERTICES; ++j) {
1217 int next = (j + 1) % ASTEROID_VERTICES;
1218 drawLine(a.x + iv[j][0], a.y + iv[j][1], a.x + iv[next][0], a.y + iv[next][1]);
1219 }
1220
1221 setColor(0.39f, 0.39f, 0.39f, 1.0f);
1222 for (int j = 0; j < ASTEROID_VERTICES; j += 2) {
1223 drawLine(a.x + rv[j][0], a.y + rv[j][1], a.x + iv[j][0], a.y + iv[j][1]);
1224 }
1225
1226 setColor(0.71f, 0.71f, 0.71f, 1.0f);
1227 for (int j = 0; j < ASTEROID_VERTICES; j += 2) {
1228 int next = (j + 1) % ASTEROID_VERTICES;
1229 drawLine(a.x + rv[j][0], a.y + rv[j][1], a.x + iv[next][0], a.y + iv[next][1]);
1230 }
1231 }
1232 }
1233
1234 void drawExplosion() {
1235 for (auto &p : particles) {
1236 if (!p.active)
1237 continue;
1238 float lp = static_cast<float>(p.lifetime) / 50.0f;
1239 if (lp > 0.7f)
1240 setColor(1.0f, 1.0f, 1.0f, 1.0f);
1241 else if (lp > 0.5f)
1242 setColor(1.0f, 1.0f, 0.0f, 1.0f);
1243 else if (lp > 0.2f)
1244 setColor(1.0f, 0.5f, 0.0f, 1.0f);
1245 else
1246 setColor(1.0f, 0.0f, 0.0f, 1.0f);
1247 drawRect(p.x, p.y, 2, 2);
1248 }
1249 }
1250
1251 void drawHUD() {
1252 std::string buf = std::format("Score: {}", ship.score);
1253 printText(buf.c_str(), toScreenX(10), toScreenY(10), {255, 255, 255, 255});
1254 buf = std::format("Lives: {}", ship.lives);
1255 printText(buf.c_str(), toScreenX(10), toScreenY(30), {255, 255, 255, 255});
1256 }
1257
1258 void drawCountdown() {
1259 drawStars(0.3f);
1260 if (countdownNumber > 0) {
1261 std::string buf = std::format("{}", countdownNumber);
1262
1263 int textW, textH;
1264 if (getTextDimensions(buf.c_str(), textW, textH)) {
1265
1266 int centerX = w / 2;
1267 int centerY = h / 2;
1268 int textX = centerX - textW / 2;
1269 int textY = centerY - textH / 2;
1270 printText(buf.c_str(), textX, textY, {255, 255, 0, 255});
1271
1272 if (((countdownTimer / 167U) % 2U) != 0U) {
1273 setColor(1.0f, 1.0f, 0.0f, 0.5f);
1274 int padding = std::max(15, static_cast<int>(15 * std::min(sx(), sy())));
1275 int rectX = textX - padding;
1276 int rectY = textY - padding;
1277 int rectW = textW + padding * 2;
1278 int rectH = textH + padding * 2;
1279 int thickness = std::max(3, static_cast<int>(3 * std::min(sx(), sy())));
1280
1281 pixel->drawSpriteRect(rectX, rectY, rectW, thickness);
1282
1283 pixel->drawSpriteRect(rectX, rectY + rectH - thickness, rectW, thickness);
1284
1285 pixel->drawSpriteRect(rectX, rectY, thickness, rectH);
1286
1287 pixel->drawSpriteRect(rectX + rectW - thickness, rectY, thickness, rectH);
1288 }
1289 }
1290 } else {
1291
1292 int textW, textH;
1293 if (getTextDimensions("LAUNCH!", textW, textH)) {
1294 printText("LAUNCH!", w / 2 - textW / 2, h / 2 - textH / 2, {0, 255, 0, 255});
1295 } else {
1296 printText("LAUNCH!", toScreenX(270), toScreenY(160), {0, 255, 0, 255});
1297 }
1298 }
1299
1300 int textW, textH;
1301 if (getTextDimensions("PREPARE FOR MISSION", textW, textH)) {
1302 printText("PREPARE FOR MISSION", w / 2 - textW / 2, h / 2 + textH * 2, {255, 255, 255, 255});
1303 } else {
1304 printText("PREPARE FOR MISSION", toScreenX(220), toScreenY(195), {255, 255, 255, 255});
1305 }
1306
1307 std::string lbuf = std::format("Lives: {}", ship.lives);
1308 std::string sbuf = std::format("Score: {}", ship.score);
1309 if (getTextDimensions(lbuf.c_str(), textW, textH)) {
1310 printText(lbuf.c_str(), w / 2 - textW / 2, h / 2 + textH * 3 + 10, {255, 255, 255, 255});
1311 if (getTextDimensions(sbuf.c_str(), textW, textH)) {
1312 printText(sbuf.c_str(), w / 2 - textW / 2, h / 2 + textH * 4 + 15, {255, 255, 255, 255});
1313 }
1314 } else {
1315 printText(lbuf.c_str(), toScreenX(260), toScreenY(215), {255, 255, 255, 255});
1316 printText(sbuf.c_str(), toScreenX(260), toScreenY(235), {255, 255, 255, 255});
1317 }
1318 }
1319
1320 void drawLaunch() {
1321 drawStars(0.5f);
1322 if (launchTimer > launchDuration / 4)
1323 drawAsteroids();
1324
1325 float tempY = ship.y;
1326 ship.y = shipLaunchY;
1327 drawShipAt(ship.x, shipLaunchY);
1328 ship.y = tempY;
1329
1330 if (launchTimer < launchDuration / 2) {
1331 setColor(1.0f, 0.39f, 0.0f, 1.0f);
1332 for (int i = 0; i < 12; ++i) {
1333 float tx = ship.x + (rand() % 8) - 4;
1334 float ty = shipLaunchY + 15 + i * 2;
1335 if (ty < GAME_H)
1336 drawPixel(tx, ty);
1337 }
1338 }
1339 if (launchTimer >= launchDuration / 2) {
1340 int textW, textH;
1341 if (getTextDimensions("MISSION START!", textW, textH)) {
1342 printText("MISSION START!", w / 2 - textW / 2, h / 2 - textH / 2, {0, 255, 255, 255});
1343 } else {
1344 printText("MISSION START!", toScreenX(240), toScreenY(170), {0, 255, 255, 255});
1345 }
1346 }
1347 }
1348
1349 void drawGame() {
1350 drawStars(1.0f);
1351 drawShipAt(ship.x, ship.y);
1352 drawProjectiles();
1353 drawAsteroids();
1354 drawExplosion();
1355 drawHUD();
1356 }
1357
1358 void drawGameOver() {
1359 drawStars(0.2f);
1360 printText("GAME OVER", toScreenX(255), toScreenY(130), {255, 0, 0, 255});
1361
1362 std::string buf = std::format("Final Score: {}", ship.score);
1363 printText(buf.c_str(), toScreenX(235), toScreenY(165), {255, 255, 255, 255});
1364 printText("Press SPACE to begin", toScreenX(215), toScreenY(195), {255, 255, 0, 255});
1365 }
1366};
1367
1368int main(int argc, char **argv) {
1369 Arguments args = proc_args(argc, argv);
1370 try {
1371 SpaceRoxWindow window(args.path, args.width, args.height, args.fullscreen, args.enable_vsync);
1372 window.loop();
1373 } catch (mxvk::Exception &e) {
1374 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
1375 return EXIT_FAILURE;
1376 } catch (ArgException<std::string> &e) {
1377 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
1378 return EXIT_FAILURE;
1379 }
1380 return EXIT_SUCCESS;
1381}
Lightweight, header-only, template command-line argument parser.
Arguments proc_args(int &argc, char **argv)
Parse standard libmx2 command-line options from main()'s argv.
Definition argz.hpp:854
Exception thrown by Argz::proc() on unrecognised or malformed options.
Definition argz.hpp:169
void event(SDL_Event &e) override
Handle one SDL event.
Definition space.cpp:186
void handleControllerButtonUp(Uint8 button)
Definition space.cpp:262
SpaceRoxWindow(const std::string &path, int wx, int wy, bool full, bool enable_vsync)
Definition space.cpp:118
void handleControllerButton(Uint8 button)
Definition space.cpp:242
void proc() override
Execute one processing/update step.
Definition space.cpp:126
void pollController()
Definition space.cpp:270
virtual ~SpaceRoxWindow()
Definition space.cpp:119
std::string text() const
Main Vulkan window wrapper for MXVK.
Definition mxvk.hpp:39
void loop()
Run the main event/render loop.
Definition mxvk.cpp:651
VK_Sprite * createSprite(const std::string &pngPath, const std::string &vertexShaderPath="", const std::string &fragmentShaderPath="")
Create a sprite from a PNG file and register it with this window.
Definition mxvk.cpp:4455
bool getTextDimensions(const std::string &text, int &width, int &height)
Measure text dimensions in pixels.
Definition mxvk.cpp:4004
void clearTextQueue()
Clear all queued text draw calls for the current frame.
Definition mxvk.cpp:3964
std::unique_ptr< SDL_Window, SDLWindowDeleter > window
Definition mxvk.hpp:601
void exit()
Request loop termination.
Definition mxvk.cpp:1378
VK_Window()=default
Construct an empty window object.
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
Definition mxvk.cpp:3872
void printText(const std::string &text, int x, int y, const SDL_Color &col)
Queue a text string for rendering during the current frame.
Definition mxvk.cpp:3919
#define MXVK_VALIDATION
Definition mxvk.hpp:28
int main()
Definition main.py:165
Utilities for loading and saving PNG images.
Definition mxvk.hpp:31
constexpr int SMALL_ASTEROID_POINTS
Definition space.cpp:41
constexpr int MEDIUM_ASTEROID_POINTS
Definition space.cpp:40
constexpr int GAME_H
Definition space.cpp:26
constexpr int GAME_W
Definition space.cpp:25
constexpr float MIN_ASTEROID_RADIUS
Definition space.cpp:38
constexpr int PROJECTILE_LIFETIME
Definition space.cpp:33
GameState
Definition space.cpp:92
@ STATE_GAMEOVER
Definition space.cpp:92
@ STATE_COUNTDOWN
Definition space.cpp:92
@ STATE_LAUNCH
Definition space.cpp:92
@ STATE_PLAYING
Definition space.cpp:92
constexpr int STAR_COUNT
Definition space.cpp:27
constexpr int FIRE_COOLDOWN
Definition space.cpp:34
constexpr int ASTEROID_VERTICES
Definition space.cpp:31
void bresenhamLine(int x0, int y0, int x1, int y1, Func plot)
Definition space.cpp:94
constexpr int MAX_PROJECTILES
Definition space.cpp:28
constexpr int MAX_PARTICLES
Definition space.cpp:30
#define asteroids_ASSET_DIR
Definition space.cpp:22
constexpr int SHOTS_PER_BURST
Definition space.cpp:35
constexpr float PROJECTILE_SPEED
Definition space.cpp:32
constexpr int MAX_ASTEROIDS
Definition space.cpp:29
constexpr int LARGE_ASTEROID_POINTS
Definition space.cpp:39
#define M_PI
Definition space.cpp:18
constexpr int FIRE_DELAY
Definition space.cpp:36
constexpr int EXPLOSION_DURATION
Definition space.cpp:37
Plain data structure returned by proc_args() with all common libmx2 CLI options.
Definition argz.hpp:718
bool fullscreen
Whether fullscreen mode was requested.
Definition argz.hpp:724
bool enable_vsync
Enable FIFO present mode / v-sync (--enable-vsync).
Definition argz.hpp:738
int height
Viewport height in pixels (default: 720).
Definition argz.hpp:721
std::string path
Asset root; proc_args() defaults it to the executable directory.
Definition argz.hpp:723
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:720
bool active
Definition space.cpp:69
float vertices[ASTEROID_VERTICES][2]
Definition space.cpp:70
float x
Definition space.cpp:66
float y
Definition space.cpp:66
float rotation_speed
Definition space.cpp:72
float vx
Definition space.cpp:67
float vy
Definition space.cpp:67
float radius
Definition space.cpp:68
float rotation_angle
Definition space.cpp:71
float y
Definition space.cpp:76
int lifetime
Definition space.cpp:78
bool active
Definition space.cpp:79
float vx
Definition space.cpp:77
float vy
Definition space.cpp:77
float x
Definition space.cpp:76
float y
Definition space.cpp:59
float x
Definition space.cpp:59
float vy
Definition space.cpp:60
float vx
Definition space.cpp:60
bool active
Definition space.cpp:62
float lifetime
Definition space.cpp:61
Definition space.cpp:43
float angle
Definition space.cpp:46
float y
Definition space.cpp:44
int continuous_fire_timer
Definition space.cpp:53
int overheat_cooldown
Definition space.cpp:55
int burst_count
Definition space.cpp:49
int score
Definition space.cpp:52
int explosion_timer
Definition space.cpp:51
int fire_cooldown
Definition space.cpp:48
bool overheated
Definition space.cpp:54
float vx
Definition space.cpp:45
float x
Definition space.cpp:44
bool exploding
Definition space.cpp:50
float vy
Definition space.cpp:45
int lives
Definition space.cpp:47
Definition space.cpp:82
float speed
Definition space.cpp:83
float brightness
Definition space.cpp:85
float x
Definition space.cpp:83
float y
Definition space.cpp:83
float twinkleSpeed
Definition space.cpp:89
float r
Definition space.cpp:87
float twinklePhase
Definition space.cpp:88
float g
Definition space.cpp:87
float size
Definition space.cpp:84
int layer
Definition space.cpp:86
float b
Definition space.cpp:87