Components and supplies
1
Grove - Buzzer - Piezo
3
Push Button
1
32x8 LED matrix display
1
NodeMCU ESP8266
3
10k Resistor
Tools and machines
1
Soldering kit
Apps and platforms
1
Arduino IDE
Project description
Code
code
cpp
...
1/*ESP8266 Horizontal Shooter Game on 8x32 Matrix WS2812b 2by mircemk, May 2025 3*/ 4 5 6#include <FastLED.h> 7 8// Matrix dimensions 9#define MATRIX_WIDTH 32 10#define MATRIX_HEIGHT 8 11#define NUM_LEDS (MATRIX_WIDTH * MATRIX_HEIGHT) 12 13// Pin definitions 14#define LED_PIN D6 15#define BTN_UP D2 16#define BTN_DOWN D3 17#define BTN_FIRE D4 18#define BUZZER_PIN D8 19 20// Game parameters 21#define PLAYER_COLOR CRGB::Green 22#define MISSILE_COLOR CRGB::Yellow 23#define ENEMY_COLOR CRGB::Red 24#define ENEMY_WEAPON_COLOR CRGB::Magenta 25#define ENEMY_MISSILE_COLOR CRGB::Magenta 26 27#define PLAYER_SPEED 1 28#define MISSILE_SPEED 2 29#define ENEMY_SPEED 0.5f 30#define ENEMY_FIRE_RATE 0.02 31 32#define SCROLL_SPEED 80 33#define START_TEXT "PRESS FIRE TO START" 34#define GAMEOVER_TEXT "GAME OVER" 35#define SCORE_TEXT "SCORE - " 36 37// Sound definitions 38#define SOUND_SHOOT_FREQ 2000 39#define SOUND_SHOOT_DURATION 40 40 41#define SOUND_ENEMY_HIT_FREQ1 800 42#define SOUND_ENEMY_HIT_FREQ2 1200 43#define SOUND_ENEMY_HIT_DURATION 20 44 45#define SOUND_GAMEOVER_FREQ1 400 46#define SOUND_GAMEOVER_FREQ2 300 47#define SOUND_GAMEOVER_DURATION 100 48 49#define SOUND_START_FREQ 1000 50#define SOUND_START_DURATION 150 51 52#define SOUND_SCORE_FREQ 1500 53#define SOUND_SCORE_DURATION 50 54 55#define SQUARE_WAVE_DUTY_CYCLE 50 // Percentage of time the pin is HIGH 56 57// Sound management 58unsigned long soundEndTime = 0; 59bool soundActive = false; 60 61void playTone(int frequency, int duration) { 62 unsigned long period = 1000000L / frequency; // Period in microseconds 63 unsigned long halfPeriod = period / 2; 64 unsigned long startTime = micros(); 65 66 while (micros() - startTime < duration * 1000L) { 67 digitalWrite(BUZZER_PIN, HIGH); 68 delayMicroseconds(halfPeriod); 69 digitalWrite(BUZZER_PIN, LOW); 70 delayMicroseconds(halfPeriod); 71 } 72 noTone(BUZZER_PIN); // Ensure the buzzer is off after the tone 73} 74 75void updateSound() { 76 if (soundActive && millis() > soundEndTime) { 77 noTone(BUZZER_PIN); 78 soundActive = false; 79 } 80} 81 82void playEnemyDestroyedSound() { 83 playTone(SOUND_ENEMY_HIT_FREQ1, SOUND_ENEMY_HIT_DURATION); 84 delay(60); 85 playTone(SOUND_ENEMY_HIT_FREQ2, SOUND_ENEMY_HIT_DURATION); 86} 87 88void playGameOverSound() { 89 playTone(SOUND_GAMEOVER_FREQ1, SOUND_GAMEOVER_DURATION); 90 delay(350); 91 playTone(SOUND_GAMEOVER_FREQ2, SOUND_GAMEOVER_DURATION); 92} 93 94// Game state variables 95bool upButtonPressed = false; 96bool downButtonPressed = false; 97unsigned long lastMoveTime = 0; 98#define MOVE_COOLDOWN 100 99 100CRGB leds[NUM_LEDS]; 101 102enum GameState { 103 TITLE_SCREEN, 104 PLAYING, 105 GAME_OVER, 106 SCORE_DISPLAY 107}; 108 109GameState gameState = TITLE_SCREEN; 110 111unsigned long gameOverStartTime = 0; 112bool waitingForFireButton = false; 113 114// Game objects 115struct Player { 116 int x = 0; 117 int y = MATRIX_HEIGHT / 2; 118 bool moveRequested = false; 119} player; 120 121struct Missile { 122 float xPos; 123 int x; 124 int y; 125 bool active = false; 126}; 127 128#define MAX_MISSILES 3 129Missile playerMissiles[MAX_MISSILES]; 130Missile enemyMissiles[5]; 131 132struct Enemy { 133 float xPos; 134 int x; 135 int y; 136 bool active = false; 137 bool hasWeapon = true; 138}; 139 140#define MAX_ENEMIES 3 141Enemy enemies[MAX_ENEMIES]; 142 143int score = 0; 144int lives = 3; 145unsigned long lastEnemySpawn = 0; 146#define ENEMY_SPAWN_RATE 1500 147 148bool fireButtonPressed = false; 149unsigned long lastFireTime = 0; 150#define FIRE_COOLDOWN 300 151 152// Font 5x4 153const uint8_t font5x4[44][4] = { 154 {31,20,20,31}, {31,21,21,10}, {14,17,17,10}, {31,17,17,14}, 155 {31,21,21,17}, {31,20,20,16}, {14,17,21,14}, {31,4,4,31}, 156 {17,31,17,17}, {2,1,1,30}, {31,4,10,17}, {31,1,1,1}, 157 {31,12,12,31}, {31,12,3,31}, {14,17,17,14}, {31,20,20,8}, 158 {14,17,19,14}, {31,20,22,9}, {8,21,21,2}, {16,16,31,16}, 159 {30,1,1,30}, {28,3,3,28}, {31,3,12,31}, {27,4,4,27}, 160 {24,4,3,28}, {19,21,25,17}, {0,0,0,0}, 161 {14,17,17,14}, {0,17,31,1}, {19,21,21,9}, 162 {17,21,21,14}, {28,4,4,31}, {30,21,21,18}, 163 {14,21,21,2}, {16,19,20,24}, {10,21,21,10}, 164 {8,21,21,14}, {0,4,0,0}, {0,0,0,0} 165}; 166 167int getCharIndex(char c) { 168 if (c >= 'A' && c <= 'Z') return c - 'A'; 169 if (c >= '0' && c <= '9') return c - '0' + 26; // Correct mapping for digits 170 if (c == '-') return 36; 171 if (c == ' ') return 37; 172 return 37; 173} 174 175int XY(int x, int y) { 176 int flippedX = (MATRIX_WIDTH - 1) - x; 177 int flippedY = (MATRIX_HEIGHT - 1) - y; 178 if (flippedX % 2 == 0) { 179 return (flippedX * MATRIX_HEIGHT) + flippedY; 180 } else { 181 return (flippedX * MATRIX_HEIGHT) + (MATRIX_HEIGHT - 1 - flippedY); 182 } 183} 184 185int XY_text(int x, int y) { 186 x = (MATRIX_WIDTH - 1) - x; 187 y = (MATRIX_HEIGHT - 1) - y; 188 if (x % 2 == 0) { 189 return (x * MATRIX_HEIGHT) + y; 190 } else { 191 return (x * MATRIX_HEIGHT) + (MATRIX_HEIGHT - 1 - y); 192 } 193} 194 195void enemyDestructionAnimation(int enemyX, int enemyY) { 196 // Light up the top and bottom positions around the enemy's red LED 197 // Adjust the positions if necessary to match your LED layout 198 leds[XY(enemyX, enemyY - 1)] = CRGB::Orange; 199 leds[XY(enemyX, enemyY + 1)] = CRGB::Orange; 200 FastLED.show(); 201 202 // Keep the animation for 100 milliseconds before clearing 203 delay(100); 204 205 // Clear the animated LEDs (set to black) 206 leds[XY(enemyX, enemyY - 1)] = CRGB::Black; 207 leds[XY(enemyX, enemyY + 1)] = CRGB::Black; 208 FastLED.show(); 209} 210 211void drawChar(int x, int y, char c, CRGB color) { 212 int charIndex; 213 if (c >= '0' && c <= '9') { 214 charIndex = getCharIndex(c + 1); // Try adding 1 to the character value 215 } else { 216 charIndex = getCharIndex(c); 217 } 218 219 for (int col = 0; col < 4; col++) { 220 if (x + col >= 0 && x + col < MATRIX_WIDTH) { 221 uint8_t pattern = font5x4[charIndex][col]; 222 for (int row = 0; row < 5; row++) { 223 if (pattern & (1 << (4 - row))) { 224 if (y + row >= 0 && y + row < MATRIX_HEIGHT) { 225 leds[XY_text(x + col, y + row)] = color; 226 } 227 } 228 } 229 } 230 } 231} 232 233void scrollText(const char* text, CRGB color) { 234 static int scrollX = MATRIX_WIDTH; 235 static unsigned long lastScroll = 0; 236 237 if (millis() - lastScroll > SCROLL_SPEED) { 238 FastLED.clear(); 239 int textLen = strlen(text); 240 for (int i = 0; i < textLen; i++) { 241 drawChar(scrollX + (i * 5), 1, text[i], color); 242 } 243 FastLED.show(); 244 scrollX--; 245 if (scrollX < -(textLen * 5)) { 246 scrollX = MATRIX_WIDTH; 247 } 248 lastScroll = millis(); 249 } 250} 251 252void firePlayerMissile() { 253 for (int i = 0; i < MAX_MISSILES; i++) { 254 if (!playerMissiles[i].active) { 255 playerMissiles[i].xPos = player.x + 1; 256 playerMissiles[i].x = (int)playerMissiles[i].xPos; 257 playerMissiles[i].y = player.y; 258 playerMissiles[i].active = true; 259 playTone(SOUND_SHOOT_FREQ, SOUND_SHOOT_DURATION); 260 break; 261 } 262 } 263} 264 265void handleInput() { 266 if (digitalRead(BTN_UP) == LOW) { 267 if (!upButtonPressed && millis() - lastMoveTime > MOVE_COOLDOWN) { 268 player.y = max(0, player.y - 1); 269 upButtonPressed = true; 270 lastMoveTime = millis(); 271 } 272 } else { 273 upButtonPressed = false; 274 } 275 276 if (digitalRead(BTN_DOWN) == LOW) { 277 if (!downButtonPressed && millis() - lastMoveTime > MOVE_COOLDOWN) { 278 player.y = min(MATRIX_HEIGHT-1, player.y + 1); 279 downButtonPressed = true; 280 lastMoveTime = millis(); 281 } 282 } else { 283 downButtonPressed = false; 284 } 285 286 if (digitalRead(BTN_FIRE) == LOW) { 287 if (!fireButtonPressed && millis() - lastFireTime > FIRE_COOLDOWN) { 288 firePlayerMissile(); 289 fireButtonPressed = true; 290 lastFireTime = millis(); 291 } 292 } else { 293 fireButtonPressed = false; 294 } 295} 296 297void updateGame() { 298 // Update player missiles 299 for (int i = 0; i < MAX_MISSILES; i++) { 300 if (playerMissiles[i].active) { 301 playerMissiles[i].xPos += MISSILE_SPEED; 302 playerMissiles[i].x = (int)playerMissiles[i].xPos; 303 if (playerMissiles[i].x >= MATRIX_WIDTH) { 304 playerMissiles[i].active = false; 305 } 306 } 307 } 308 309 // Spawn enemies 310 if (millis() - lastEnemySpawn > ENEMY_SPAWN_RATE) { 311 lastEnemySpawn = millis(); 312 for (int i = 0; i < MAX_ENEMIES; i++) { 313 if (!enemies[i].active) { 314 enemies[i].xPos = MATRIX_WIDTH - 1; 315 enemies[i].x = (int)enemies[i].xPos; 316 enemies[i].y = random(0, MATRIX_HEIGHT - 1); 317 enemies[i].active = true; 318 enemies[i].hasWeapon = true; 319 break; 320 } 321 } 322 } 323 324 // Update enemies 325 for (int i = 0; i < MAX_ENEMIES; i++) { 326 if (enemies[i].active) { 327 enemies[i].xPos -= ENEMY_SPEED; 328 enemies[i].x = (int)enemies[i].xPos; 329 330 // Enemy firing 331 if (enemies[i].hasWeapon && random(100) < (ENEMY_FIRE_RATE * 100)) { 332 for (int j = 0; j < 5; j++) { 333 if (!enemyMissiles[j].active) { 334 enemyMissiles[j].xPos = enemies[i].xPos - 1; 335 enemyMissiles[j].x = (int)enemyMissiles[j].xPos; 336 enemyMissiles[j].y = enemies[i].y; 337 enemyMissiles[j].active = true; 338 break; 339 } 340 } 341 } 342 343 if (enemies[i].xPos < 0) enemies[i].active = false; 344 } 345 } 346 347 // Update enemy missiles 348 for (int i = 0; i < 5; i++) { 349 if (enemyMissiles[i].active) { 350 enemyMissiles[i].xPos -= ENEMY_SPEED; 351 enemyMissiles[i].x = (int)enemyMissiles[i].xPos; 352 if (enemyMissiles[i].xPos < 0) enemyMissiles[i].active = false; 353 } 354 } 355 356 checkCollisions(); 357} 358 359void checkCollisions() { 360 // Player missiles vs enemies 361 for (int m = 0; m < MAX_MISSILES; m++) { 362 if (playerMissiles[m].active) { 363 for (int e = 0; e < MAX_ENEMIES; e++) { 364 if (enemies[e].active) { 365 if ((playerMissiles[m].x == enemies[e].x && playerMissiles[m].y == enemies[e].y) || 366 (playerMissiles[m].x == enemies[e].x - 1 && playerMissiles[m].y == enemies[e].y)) { 367 playerMissiles[m].active = false; 368 // Save enemy position before deactivating (for animation) 369 int enemyX = enemies[e].x; 370 int enemyY = enemies[e].y; 371 enemies[e].active = false; 372 score += 10; 373 playEnemyDestroyedSound(); 374 // Trigger the enemy destruction animation 375 enemyDestructionAnimation(enemyX, enemyY); 376 } 377 } 378 } 379 } 380 } 381 382 // Enemy missiles vs player 383 for (int i = 0; i < 5; i++) { 384 if (enemyMissiles[i].active && enemyMissiles[i].x == player.x && enemyMissiles[i].y == player.y) { 385 enemyMissiles[i].active = false; 386 if (--lives <= 0) gameOver(); 387 } 388 } 389 390 // Enemies vs player 391 for (int i = 0; i < MAX_ENEMIES; i++) { 392 if (enemies[i].active && enemies[i].x == player.x && enemies[i].y == player.y) { 393 enemies[i].active = false; 394 if (--lives <= 0) gameOver(); 395 } 396 } 397} 398 399void render() { 400 FastLED.clear(); 401 402 // Draw player 403 leds[XY(player.x, player.y)] = PLAYER_COLOR; 404 405 // Draw player missiles 406 for (int i = 0; i < MAX_MISSILES; i++) { 407 if (playerMissiles[i].active) { 408 leds[XY(playerMissiles[i].x, playerMissiles[i].y)] = MISSILE_COLOR; 409 } 410 } 411 412 // Draw enemies 413 for (int i = 0; i < MAX_ENEMIES; i++) { 414 if (enemies[i].active) { 415 leds[XY(enemies[i].x, enemies[i].y)] = ENEMY_COLOR; 416 if (enemies[i].hasWeapon && enemies[i].x > 0) { 417 leds[XY(enemies[i].x - 1, enemies[i].y)] = ENEMY_WEAPON_COLOR; 418 } 419 } 420 } 421 422 // Draw enemy missiles 423 for (int i = 0; i < 5; i++) { 424 if (enemyMissiles[i].active) { 425 leds[XY(enemyMissiles[i].x, enemyMissiles[i].y)] = ENEMY_MISSILE_COLOR; 426 } 427 } 428 429 // Draw score LEDs 430 int scoreLeds = min(score / 10, MATRIX_WIDTH); 431 for (int i = 0; i < scoreLeds; i++) { 432 leds[XY(MATRIX_WIDTH - 1 - i, MATRIX_HEIGHT - 1)] = CRGB::Green; 433 } 434} 435 436 437void gameOver() { 438 // Flash red 3 times 439 for (int i = 0; i < 3; i++) { 440 fill_solid(leds, NUM_LEDS, CRGB::Red); 441 FastLED.show(); 442 delay(300); 443 FastLED.clear(); 444 FastLED.show(); 445 delay(300); 446 } 447 playGameOverSound(); // play sound after flashes 448 gameState = GAME_OVER; 449} 450 451void loop() { 452 static unsigned long lastFrame = millis(); 453 if (millis() - lastFrame < 33) { 454 delay(1); 455 return; 456 } 457 lastFrame = millis(); 458 459 updateSound(); // keep sound system updated! 460 461 switch (gameState) { 462 case TITLE_SCREEN: 463 scrollText(START_TEXT, CRGB::Green); 464 if (digitalRead(BTN_FIRE) == LOW) { 465 playTone(SOUND_START_FREQ, SOUND_START_DURATION); 466 gameState = PLAYING; 467 resetGame(); 468 delay(200); 469 } 470 break; 471 472 case PLAYING: 473 handleInput(); 474 updateGame(); 475 render(); 476 FastLED.show(); 477 break; 478 479 case GAME_OVER: 480 if (gameOverStartTime == 0) { 481 gameOverStartTime = millis(); 482 waitingForFireButton = false; 483 } 484 scrollText(GAMEOVER_TEXT, CRGB::Red); 485 if (!waitingForFireButton && millis() - gameOverStartTime >= 3000) { 486 waitingForFireButton = true; 487 } 488 if (waitingForFireButton && digitalRead(BTN_FIRE) == LOW) { 489 gameState = SCORE_DISPLAY; 490 gameOverStartTime = 0; 491 waitingForFireButton = false; 492 delay(200); 493 } 494 break; 495 496case SCORE_DISPLAY: 497 static bool scoreInitialized = false; 498 static unsigned long lastScrollUpdate = 0; // Control scroll update rate 499 500 if (!scoreInitialized) { 501 FastLED.clear(); 502 scoreInitialized = true; 503 } 504 505 if (millis() - lastScrollUpdate > 75) { // Update every 75 milliseconds 506 char scoreText[30] = "SCORE "; 507 char scoreValue[6]; // Enough space for a 5-digit score + null terminator 508 itoa(score, scoreValue, 10); // Convert score to string (base 10) 509 strcat(scoreText, scoreValue); // Append score value to "SCORE " 510 FastLED.clear(); 511 scrollText(scoreText, CRGB::Blue); 512 leds[XY(MATRIX_WIDTH-1, MATRIX_HEIGHT-1)] = CRGB::Black; 513 FastLED.show(); 514 lastScrollUpdate = millis(); 515 } 516 517 if (digitalRead(BTN_FIRE) == LOW) { 518 scoreInitialized = false; 519 gameState = TITLE_SCREEN; 520 delay(200); 521 } 522 break; 523 } 524 525 ESP.wdtFeed(); 526} 527 528void setup() { 529 Serial.begin(115200); 530 FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); 531 FastLED.setBrightness(30); 532 533 pinMode(BTN_UP, INPUT_PULLUP); 534 pinMode(BTN_DOWN, INPUT_PULLUP); 535 pinMode(BTN_FIRE, INPUT_PULLUP); 536 pinMode(BUZZER_PIN, OUTPUT); 537 538 resetGame(); 539} 540 541 542void resetGame() { 543 FastLED.clear(); 544 for (int i = 0; i < MAX_MISSILES; i++) playerMissiles[i].active = false; 545 for (int i = 0; i < 5; i++) enemyMissiles[i].active = false; 546 for (int i = 0; i < MAX_ENEMIES; i++) enemies[i].active = false; 547 player.x = 0; 548 player.y = MATRIX_HEIGHT / 2; 549 score = 0; 550 lives = 3; 551 lastEnemySpawn = millis(); 552 player.moveRequested = false; 553 gameOverStartTime = 0; 554 waitingForFireButton = false; 555}
Documentation
Schematic
...
Schematic.jpg

Comments
Only logged in users can leave comments