gl-asteroids.cpp (14817B)
1 #include <GLFW/glfw3.h> 2 #include <alsa/asoundlib.h> 3 #include <cmath> 4 #include <vector> 5 #include <span> 6 #include <random> 7 #include <array> 8 #include <iostream> 9 #include <algorithm> 10 #include <thread> 11 #include <chrono> 12 #include <optional> 13 #include <functional> 14 15 extern "C" char const* __asan_default_options() { return "detect_leaks=0"; } 16 17 /// SOUNDS 18 19 using sample = std::span<short>; 20 21 std::mutex queued_sounds_mutex; 22 std::vector<sample> queued_sounds; 23 24 void play_sample(sample s) 25 { 26 std::lock_guard lock(queued_sounds_mutex); 27 queued_sounds.push_back(s); 28 } 29 30 void sound_routine(std::stop_token token) 31 { 32 snd_pcm_t* handle; 33 34 snd_pcm_open(&handle, "default", SND_PCM_STREAM_PLAYBACK, 0); 35 snd_pcm_set_params(handle, SND_PCM_FORMAT_S16, SND_PCM_ACCESS_RW_INTERLEAVED, 1, 48000, 1, 10000); 36 37 std::vector<sample> playing_sounds; 38 39 while (not token.stop_requested()) 40 { 41 { 42 std::lock_guard lock(queued_sounds_mutex); 43 std::ranges::move(queued_sounds, back_inserter(playing_sounds)); 44 queued_sounds.clear(); 45 } 46 47 std::array<short, 2048> buf; 48 for (auto& b : buf) 49 { 50 b = 0; 51 std::erase_if(playing_sounds, [](auto s){ return s.empty(); }); 52 for (auto& s : playing_sounds) 53 { 54 b += s[0]; 55 s = sample{s.data()+1, s.size()-1}; 56 } 57 } 58 59 snd_pcm_writei(handle, buf.data(), buf.size()); 60 } 61 snd_pcm_drain(handle); 62 snd_pcm_close(handle); 63 } 64 65 std::jthread sound(sound_routine); 66 67 /// GRAPHICS 68 69 constexpr int window_size = 700; 70 71 struct coord 72 { 73 coord() = default; 74 coord(float s) 75 : x(s) 76 , y(s) 77 {} 78 coord(float _x, float _y) 79 : x(_x) 80 , y(_y) 81 {} 82 float x = 0, y = 0; 83 }; 84 85 struct color 86 { 87 float r, g, b; 88 }; 89 90 struct visual 91 { 92 GLint mode; 93 std::span<coord const> verts; 94 std::span<color const> cols; 95 }; 96 97 coord const shipv[] = {{0, 2}, {1, -1}, {-1, -1}}; 98 color const shipc[] = {{1,1,1}, {.5,.5,.5}}; 99 100 coord const flamev[3] = {{0, -2.5}, {.6, -.25}, {-.6, -.25}}; 101 color const flamec[] = {{1,0,0}, {1,1,0}}; 102 103 coord const rockv1[] = {{0, 0}, {1, 0}, {0, 1}, {-1, 0}, {0, -1}, {1, 0}}; 104 coord const rockv2[] = {{0, 0}, {1, 0}, {.7,.7}, {0, 1}, {-1, 0}, {-.9,-.9}, {0, -1}, {1, 0}}; 105 coord const rockv3[] = {{0, 0}, {1, 0}, {.7,.4}, {0, 1}, {-.8,.6}, {-1, 0}, {-.6,-.5}, {0, -1}, {1, 0}}; 106 coord const rockv4[] = {{0, 0}, {1, 0}, {.6,.4}, {0, .5}, {-.3,.6}, {-1, 0}, {-.6,-.5}, {0, -1}, {1, 0}}; 107 108 color const rockc[] = {{.5,.5,.5}, {.2,.2,.2}}; 109 color const powerupcb[] = {{.8,.4,.2}, {.4,.2,.1}}; 110 color const powerupcf[] = {{.2,.4,.8}, {.1,.2,.4}}; 111 color const powerupca[] = {{.8,.8,.2}, {.4,.4,.1}}; 112 113 coord const bulletv[] = {{0, 1}, {.25, .25}, {-.25, .25}, {.25, -1}, {-.25, -1}}; 114 color const bulletc[] = {{.0,.8,.0}, {.0,.4,.0}, {.0,.4,.0}, {.0,.0,.0}}; 115 116 coord starsv[64]; 117 118 visual ship{GL_TRIANGLES, shipv, shipc}; 119 visual flame{GL_TRIANGLES, flamev, flamec}; 120 visual bullet(GL_TRIANGLE_STRIP, bulletv, bulletc); 121 visual rock1{GL_TRIANGLE_FAN, rockv1, rockc}; 122 visual rock2{GL_TRIANGLE_FAN, rockv2, rockc}; 123 visual rock3{GL_TRIANGLE_FAN, rockv3, rockc}; 124 visual rock4{GL_TRIANGLE_FAN, rockv4, rockc}; 125 visual powerup_firerate{GL_TRIANGLE_FAN, rockv1, powerupcf}; 126 visual powerup_bulletspeed{GL_TRIANGLE_FAN, rockv1, powerupcb}; 127 visual powerup_acceleration{GL_TRIANGLE_FAN, rockv1, powerupca}; 128 std::array<visual const*, 4> rock_types = {&rock1, &rock2, &rock3, &rock4}; 129 130 float wrap(float n, float min, float max) 131 { 132 if (n >= max) 133 return n - max + min; 134 else if (n < min) 135 return n + max - min; 136 else 137 return n; 138 } 139 140 void glCoord(coord c) 141 { 142 glVertex2f(c.x, c.y); 143 } 144 void glColor(color c) 145 { 146 glColor3f(c.r, c.g, c.b); 147 } 148 149 coord operator +(coord lhs, coord rhs) 150 { 151 return {lhs.x + rhs.x, lhs.y + rhs.y}; 152 } 153 154 coord& operator +=(coord& lhs, coord rhs) 155 { 156 return lhs = lhs + rhs; 157 } 158 159 coord operator -(coord rhs) 160 { 161 return {-rhs.x, -rhs.y}; 162 } 163 164 coord operator - (coord lhs, coord rhs) 165 { 166 return lhs + -rhs; 167 } 168 169 coord& operator -=(coord& lhs, coord rhs) 170 { 171 return lhs = lhs - rhs; 172 } 173 174 coord operator *(coord lhs, coord rhs) 175 { 176 return {lhs.x * rhs.x, lhs.y * rhs.y}; 177 } 178 179 coord& operator *=(coord& lhs, coord rhs) 180 { 181 lhs = lhs * rhs; 182 return lhs; 183 } 184 185 /// PHYSICS 186 187 coord rotate(coord o, float a) 188 { 189 float s = sin(a), c = cos(a); 190 return {o.x*c + o.y*s, o.y*c - o.x*s}; 191 } 192 193 coord coord_from_rotation(float a) 194 { 195 return {(float)sin(a), (float)cos(a)}; 196 } 197 198 struct physics 199 { 200 coord pos, vel; 201 float rot, ang_mom, scale; 202 void move(float dt) 203 { 204 pos += vel * dt; 205 rot += ang_mom * dt; 206 } 207 static void wrap_pos(float& pos, float vel) 208 { 209 if (pos > 1 && vel > 0) 210 pos -= 2; 211 else if (pos < -1 && vel < 0) 212 pos += 2; 213 } 214 void wrap_pos(void) 215 { 216 wrap_pos(pos.x, vel.x); 217 wrap_pos(pos.y, vel.y); 218 } 219 static bool left_axis(float pos, float vel, float scale) 220 { 221 if (vel < 0) 222 pos = -pos; 223 224 return pos > 1 + scale; 225 } 226 bool left_arena() const 227 { 228 return left_axis(pos.x, vel.x, scale) or left_axis(pos.y, vel.y, scale); 229 } 230 }; 231 232 bool is_colliding(physics const& a, physics const& b) 233 { 234 float len = a.scale + b.scale; 235 auto d = a.pos - b.pos; 236 237 return d.x * d.x + d.y * d.y < len * len; 238 } 239 240 void draw(visual const& v, physics const& p) 241 { 242 glBegin(v.mode); 243 244 for (unsigned i = 0; i < v.verts.size(); ++i) 245 { 246 if (i < v.cols.size()) 247 glColor(v.cols[i]); 248 glCoord(rotate(v.verts[i] * p.scale, p.rot) + p.pos); 249 } 250 251 glEnd(); 252 } 253 254 void draw_wrapped(visual const& v, physics p) 255 { 256 static coord const offsets[] = {{0,0}, {2,0}, {-2,2}, {-2,-2}, {2,-2}}; 257 258 for (auto o : offsets) 259 { 260 p.pos += o; 261 draw(v, p); 262 } 263 } 264 265 struct entity 266 { 267 visual const* vis; 268 physics phys; 269 270 entity() = delete; 271 entity(visual const& _v, physics _p) 272 : vis(&_v) 273 , phys(_p) 274 {} 275 276 void draw() 277 { 278 ::draw(*vis, phys); 279 } 280 }; 281 282 static bool left_arena(entity const& e) 283 { 284 return e.phys.left_arena(); 285 } 286 287 std::random_device rd; 288 std::default_random_engine gen(rd()); 289 290 struct particle : entity 291 { 292 float ttl; 293 294 static particle& make_particle(entity e, float ttl) 295 { 296 static std::uniform_real_distribution<float> d(.5, 1); 297 return particles.emplace_back(e, ttl * d(gen)); 298 } 299 static void move_all(float dt) 300 { 301 for (auto& p : particles) 302 { 303 p.ttl -= dt; 304 p.phys.move(dt); 305 } 306 std::erase_if(particles, [](auto& p) { return p.ttl <= 0; }); 307 std::erase_if(particles, left_arena); 308 } 309 static void draw_all() 310 { 311 for (auto& p : particles) 312 p.draw(); 313 } 314 private: 315 static std::vector<particle> particles; 316 }; 317 318 std::vector<particle> particle::particles; 319 320 static GLFWwindow* w; 321 322 bool is_pressed(int key) 323 { 324 return glfwGetKey(w, key) != GLFW_RELEASE; 325 } 326 327 struct powerup : entity 328 { 329 std::function<void(void)> cb; 330 331 powerup(entity e, std::function<void(void)> c) 332 : entity(e) 333 , cb(c) 334 {} 335 }; 336 337 std::vector<entity> rocks; 338 std::vector<entity> bullets; 339 std::optional<powerup> powerups; 340 341 physics ship_phys{{}, {}, 0.f, 0.f, .06f}; 342 bool up = false; 343 344 int lives = 3; 345 unsigned ammo = 10; 346 347 void draw_scene(float t) 348 { 349 glClearColor(0,0,0,1); 350 glClear(GL_COLOR_BUFFER_BIT); 351 352 glColor({1,1,1}); 353 glBegin(GL_POINTS); 354 for (auto v : starsv) 355 glCoord(v); 356 glEnd(); 357 358 particle::draw_all(); 359 for (auto& r : rocks) 360 r.draw(); 361 for (auto& b : bullets) 362 b.draw(); 363 if (powerups) 364 powerups->draw(); 365 366 if (up) 367 { 368 auto f = ship_phys; 369 f.scale *= 1 + 0.1 * sin(t * 49) * sin(t * 9); 370 draw_wrapped(flame, f); 371 } 372 draw_wrapped(ship, ship_phys); 373 374 glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); 375 376 for (int i = 0; i < lives-1; ++i) 377 draw(ship, {{.9f, -.9f + i * .15f}, {}, 0.f, 0.f, .04f}); 378 379 glBegin(GL_LINES); 380 glColor({.0,.4,.0}); 381 glCoord({-.95f, -.9f}); 382 glCoord({-.95f, ammo * .09f - .9f}); 383 glEnd(); 384 385 glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); 386 387 glfwSwapBuffers(w); 388 glfwPollEvents(); 389 } 390 391 visual const& random_rock_visual(void) 392 { 393 static unsigned rtype; 394 395 ++rtype; 396 rtype %= rock_types.size(); 397 398 return *rock_types[rtype]; 399 } 400 401 void joy_cb(int jid, int event) 402 { 403 switch (event) 404 { 405 case GLFW_CONNECTED: 406 std::cout << 'J' << jid << ": CONNECTED\n"; 407 return; 408 case GLFW_DISCONNECTED: 409 std::cout << 'J' << jid << ": DISCONNECTED\n"; 410 return; 411 default: 412 std::cout << 'J' << jid << ": " << event << '\n'; 413 } 414 } 415 416 int main(int, char* argv[]) 417 { 418 signed score = 0; 419 float firerate = 2, bullet_speed = 1, acceleration = 0.8; 420 421 std::uniform_real_distribution<float> dis_p(-1,1); 422 std::uniform_real_distribution<float> dis_s(0.05,0.25); 423 std::uniform_real_distribution<float> dis_v(0.02,0.05); 424 std::uniform_int_distribution<int> dis_d(0,7); 425 426 std::array<short, 2 * 512> tone; 427 for (unsigned i = 0; i < tone.size(); ++i) 428 tone[i] = sin(i * 6.283 * 440 / 48000) * 50 * 128; 429 430 std::array<short, 2 * 512> square; 431 for (unsigned i = 0; i < square.size(); ++i) 432 square[i] = ((i & 0x80) - 0x40) * 50; 433 434 std::array<short, 3 * 512> saw; 435 for (unsigned i = 0; i < saw.size(); ++i) 436 saw[i] = (i % 0x80 - 0x40) * 30; 437 438 std::array<short, 8 * 512> blip; 439 for (unsigned i = 0; i < blip.size(); ++i) 440 blip[i] = sin(i * (i + 440) * 6.283 / 10 / 48000) * 50 * 128; 441 442 std::array<short, 16 * 1024> bang; 443 for (unsigned i = 0; i < bang.size(); ++i) 444 bang[i] = 2048 * dis_p(gen) / exp(i * .00005) * (2 + cos(i * 6.283 * 15 / 48000)); 445 446 glfwInit(); 447 448 w = glfwCreateWindow(window_size, window_size, argv[0], NULL, NULL); 449 glfwSetJoystickCallback(joy_cb); 450 int wx, wy; 451 glfwGetFramebufferSize(w, &wx, &wy); 452 std::cout << "x: " << wx << " y: " << wy << '\n'; 453 454 glfwMakeContextCurrent(w); 455 456 float last_t = glfwGetTime(); 457 458 float rock_time = 1, bullet_time = 0, ammo_refill_time = .3, powerup_time = 10; 459 460 for (auto& c : starsv) 461 c.x = dis_p(gen), c.y = dis_p(gen); 462 463 while (!glfwWindowShouldClose(w) && not is_pressed(GLFW_KEY_ESCAPE)) 464 { 465 float t = glfwGetTime(); 466 float dt = t - last_t; 467 468 if (rock_time < t) 469 { 470 rock_time += (rocks.size() < 3 ? 2 : 6) / log(2*t+1); 471 472 int dir = dis_d(gen); 473 474 auto scale = dis_s(gen); 475 coord p = {dis_p(gen) / 2 - .5f, -1 - scale}; 476 coord v(dis_v(gen), dis_v(gen)); 477 478 v *= 1+log(t+1); 479 480 if (dir & 1) 481 v.x = -v.x, p.x = -p.x; 482 if (dir & 2) 483 p.y = -p.y, v.y = -v.y; 484 if (dir & 4) 485 { 486 std::swap(p.x, p.y); 487 std::swap(v.x, v.y); 488 } 489 490 static std::uniform_real_distribution<float> ang(-3,3); 491 rocks.emplace_back(random_rock_visual(), physics{p, v, 0, ang(gen), scale}); 492 } 493 494 if (powerup_time < t) 495 { 496 powerup_time += 15; 497 int dir = dis_d(gen); 498 auto scale = .03f; 499 static std::uniform_real_distribution<float> pos(-.9,.9); 500 coord p = {pos(gen) / 2 - .5f, -1 - scale}; 501 coord v = {0, .3f}; 502 if (dir & 1) 503 v.x = -v.x, p.x = -p.x; 504 if (dir & 2) 505 p.y = -p.y, v.y = -v.y; 506 if (dir & 4) 507 { 508 std::swap(p.x, p.y); 509 std::swap(v.x, v.y); 510 } 511 512 visual const* vis; 513 std::function<void(void)> cb; 514 515 static int type; 516 switch (type++ % 3) 517 { 518 case 0: 519 vis = &powerup_bulletspeed; 520 cb = [&firerate](){ firerate += .5f; }; 521 break; 522 case 1: 523 vis = &powerup_firerate; 524 cb = [&bullet_speed](){ bullet_speed += .2f; }; 525 break; 526 case 2: 527 vis = &powerup_acceleration; 528 cb = [&acceleration](){ acceleration += .2f; }; 529 break; 530 } 531 532 powerups.emplace(entity{*vis, physics{p, v, 0, 0, scale}}, cb); 533 } 534 535 if (ammo < 20 && ammo_refill_time < t) 536 { 537 ++ammo; 538 ammo_refill_time += .4; 539 } 540 541 if (is_pressed(GLFW_KEY_SPACE) && bullet_time < t && ammo) 542 { 543 play_sample(tone); 544 --ammo; 545 bullet_time = t + 1/firerate; 546 547 bullets.emplace_back(bullet, ship_phys); 548 auto& b = bullets.back().phys; 549 550 b.ang_mom = 0; 551 b.scale /= 1.25; 552 auto vel = coord_from_rotation(b.rot) * bullet_speed; 553 b.vel += vel; 554 ship_phys.vel -= vel * .05f; 555 } 556 557 last_t = t; 558 559 int kl = is_pressed(GLFW_KEY_LEFT); 560 int kr = is_pressed(GLFW_KEY_RIGHT); 561 562 if (kl ^ kr) 563 ship_phys.ang_mom = (kr ? 1 : -1) * (up ? 3 : 2); 564 else 565 ship_phys.ang_mom = 0; 566 567 up = is_pressed(GLFW_KEY_UP); 568 569 if (up) 570 ship_phys.vel += coord_from_rotation(ship_phys.rot) * dt * acceleration; 571 572 // resolve motion 573 574 for (auto& r : rocks) 575 r.phys.move(dt); 576 for (auto& b : bullets) 577 b.phys.move(dt); 578 if (powerups) 579 powerups->phys.move(dt); 580 581 ship_phys.move(dt); 582 ship_phys.wrap_pos(); 583 particle::move_all(dt); 584 585 // handle collisions 586 587 for (unsigned i = 0; i < rocks.size();) 588 { 589 auto r = rocks.begin() + i; 590 for (auto b = bullets.begin(); b < bullets.end();) 591 { 592 if (is_colliding(r->phys, b->phys)) 593 { 594 play_sample(square); 595 for (unsigned j = 0; j < 7; ++j) 596 { 597 static std::uniform_real_distribution<float> scale(0.005,0.025); 598 entity debris = *r; 599 debris.vis = &random_rock_visual(); 600 debris.phys.vel.x += .3 * dis_p(gen); 601 debris.phys.vel.y += .3 * dis_p(gen); 602 debris.phys.scale = scale(gen); 603 particle::make_particle(debris, 1); 604 } 605 606 if (r->phys.scale > .13f) 607 { 608 coord split{dis_v(gen), dis_v(gen)}; 609 610 auto nr = *r; 611 612 static std::uniform_real_distribution<float> dis_z(.3,.6); 613 float sf = dis_z(gen); 614 615 r->phys.scale *= sf; 616 nr.phys.scale *= .9 - sf; 617 618 r->vis = &random_rock_visual(); 619 nr.vis = &random_rock_visual(); 620 621 r->phys.vel += 2*split; 622 nr.phys.vel -= split; 623 624 nr.phys.ang_mom *= -1.5f; 625 626 rocks.push_back(nr); 627 ++i; 628 } 629 else 630 rocks.erase(r); 631 632 b = bullets.erase(b); 633 score += 3; 634 goto bang; 635 } 636 ++b; 637 } 638 ++i; 639 bang: continue; 640 } 641 642 if (powerups) 643 { 644 for (auto b = bullets.begin(); b < bullets.end(); ++b) 645 { 646 if (not is_colliding(b->phys, powerups->phys)) 647 continue; 648 649 play_sample(saw); 650 651 for (unsigned j = 0; j < 4; ++j) 652 { 653 entity debris = *powerups; 654 debris.phys.vel.x += .3 * dis_p(gen); 655 debris.phys.vel.y += .3 * dis_p(gen); 656 debris.phys.scale *= 1.5 * dis_s(gen); 657 particle::make_particle(debris, 2); 658 } 659 powerups.reset(); 660 score -= 10; 661 bullets.erase(b); 662 break; 663 } 664 if (is_colliding(ship_phys, powerups->phys)) 665 { 666 play_sample(blip); 667 powerups->cb(); 668 powerups.reset(); 669 score += 10; 670 } 671 } 672 673 for (auto r = rocks.begin(); r != rocks.end(); /**/) 674 { 675 if (!is_colliding(ship_phys, r->phys)) 676 { 677 ++r; 678 continue; 679 } 680 681 play_sample(bang); 682 lives--; 683 684 for (unsigned j = 0; j < 7; ++j) 685 { 686 static std::uniform_real_distribution<float> scale(0.005,0.025); 687 entity debris = *r; 688 debris.vis = &random_rock_visual(); 689 debris.phys.vel.x += .3 * dis_p(gen); 690 debris.phys.vel.y += .3 * dis_p(gen); 691 debris.phys.scale = scale(gen); 692 particle::make_particle(debris, 1); 693 } 694 r = rocks.erase(r); 695 } 696 697 if (!lives) 698 { 699 using namespace std::chrono_literals; 700 auto timeout = std::chrono::steady_clock::now() + 1s; 701 score += t; 702 std::cout << "Score: " << score << '\n'; 703 while (std::chrono::steady_clock::now() < timeout) 704 draw_scene(t); 705 break; 706 } 707 708 score -= std::erase_if(bullets, left_arena); 709 score -= std::erase_if(rocks, left_arena); 710 711 if (powerups && left_arena(*powerups)) 712 { 713 powerups.reset(); 714 --score; 715 } 716 717 draw_scene(t); 718 } 719 720 glfwDestroyWindow(w); 721 glfwTerminate(); 722 723 return 0; 724 }