/*ESP8266 Horizontal Shooter Game on 8x32 Matrix WS2812b
by mircemk, May 2025
*/
#include <FastLED.h>
// Matrix dimensions
#define MATRIX_WIDTH 32
#define MATRIX_HEIGHT 8
#define NUM_LEDS (MATRIX_WIDTH * MATRIX_HEIGHT)
// Pin definitions
#define LED_PIN D6
#define BTN_UP D2
#define BTN_DOWN D3
#define BTN_FIRE D4
#define BUZZER_PIN D8
// Game parameters
#define PLAYER_COLOR CRGB::Green
#define MISSILE_COLOR CRGB::Yellow
#define ENEMY_COLOR CRGB::Red
#define ENEMY_WEAPON_COLOR CRGB::Magenta
#define ENEMY_MISSILE_COLOR CRGB::Magenta
#define PLAYER_SPEED 1
#define MISSILE_SPEED 2
#define ENEMY_SPEED 0.5f
#define ENEMY_FIRE_RATE 0.02
#define SCROLL_SPEED 80
#define START_TEXT "PRESS FIRE TO START"
#define GAMEOVER_TEXT "GAME OVER"
#define SCORE_TEXT "SCORE - "
// Sound definitions
#define SOUND_SHOOT_FREQ 2000
#define SOUND_SHOOT_DURATION 40
#define SOUND_ENEMY_HIT_FREQ1 800
#define SOUND_ENEMY_HIT_FREQ2 1200
#define SOUND_ENEMY_HIT_DURATION 20
#define SOUND_GAMEOVER_FREQ1 400
#define SOUND_GAMEOVER_FREQ2 300
#define SOUND_GAMEOVER_DURATION 100
#define SOUND_START_FREQ 1000
#define SOUND_START_DURATION 150
#define SOUND_SCORE_FREQ 1500
#define SOUND_SCORE_DURATION 50
#define SQUARE_WAVE_DUTY_CYCLE 50 // Percentage of time the pin is HIGH
// Sound management
unsigned long soundEndTime = 0;
bool soundActive = false;
void playTone(int frequency, int duration) {
unsigned long period = 1000000L / frequency; // Period in microseconds
unsigned long halfPeriod = period / 2;
unsigned long startTime = micros();
while (micros() - startTime < duration * 1000L) {
digitalWrite(BUZZER_PIN, HIGH);
delayMicroseconds(halfPeriod);
digitalWrite(BUZZER_PIN, LOW);
delayMicroseconds(halfPeriod);
}
noTone(BUZZER_PIN); // Ensure the buzzer is off after the tone
}
void updateSound() {
if (soundActive && millis() > soundEndTime) {
noTone(BUZZER_PIN);
soundActive = false;
}
}
void playEnemyDestroyedSound() {
playTone(SOUND_ENEMY_HIT_FREQ1, SOUND_ENEMY_HIT_DURATION);
delay(60);
playTone(SOUND_ENEMY_HIT_FREQ2, SOUND_ENEMY_HIT_DURATION);
}
void playGameOverSound() {
playTone(SOUND_GAMEOVER_FREQ1, SOUND_GAMEOVER_DURATION);
delay(350);
playTone(SOUND_GAMEOVER_FREQ2, SOUND_GAMEOVER_DURATION);
}
// Game state variables
bool upButtonPressed = false;
bool downButtonPressed = false;
unsigned long lastMoveTime = 0;
#define MOVE_COOLDOWN 100
CRGB leds[NUM_LEDS];
enum GameState {
TITLE_SCREEN,
PLAYING,
GAME_OVER,
SCORE_DISPLAY
};
GameState gameState = TITLE_SCREEN;
unsigned long gameOverStartTime = 0;
bool waitingForFireButton = false;
// Game objects
struct Player {
int x = 0;
int y = MATRIX_HEIGHT / 2;
bool moveRequested = false;
} player;
struct Missile {
float xPos;
int x;
int y;
bool active = false;
};
#define MAX_MISSILES 3
Missile playerMissiles[MAX_MISSILES];
Missile enemyMissiles[5];
struct Enemy {
float xPos;
int x;
int y;
bool active = false;
bool hasWeapon = true;
};
#define MAX_ENEMIES 3
Enemy enemies[MAX_ENEMIES];
int score = 0;
int lives = 3;
unsigned long lastEnemySpawn = 0;
#define ENEMY_SPAWN_RATE 1500
bool fireButtonPressed = false;
unsigned long lastFireTime = 0;
#define FIRE_COOLDOWN 300
// Font 5x4
const uint8_t font5x4[44][4] = {
{31,20,20,31}, {31,21,21,10}, {14,17,17,10}, {31,17,17,14},
{31,21,21,17}, {31,20,20,16}, {14,17,21,14}, {31,4,4,31},
{17,31,17,17}, {2,1,1,30}, {31,4,10,17}, {31,1,1,1},
{31,12,12,31}, {31,12,3,31}, {14,17,17,14}, {31,20,20,8},
{14,17,19,14}, {31,20,22,9}, {8,21,21,2}, {16,16,31,16},
{30,1,1,30}, {28,3,3,28}, {31,3,12,31}, {27,4,4,27},
{24,4,3,28}, {19,21,25,17}, {0,0,0,0},
{14,17,17,14}, {0,17,31,1}, {19,21,21,9},
{17,21,21,14}, {28,4,4,31}, {30,21,21,18},
{14,21,21,2}, {16,19,20,24}, {10,21,21,10},
{8,21,21,14}, {0,4,0,0}, {0,0,0,0}
};
int getCharIndex(char c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= '0' && c <= '9') return c - '0' + 26; // Correct mapping for digits
if (c == '-') return 36;
if (c == ' ') return 37;
return 37;
}
int XY(int x, int y) {
int flippedX = (MATRIX_WIDTH - 1) - x;
int flippedY = (MATRIX_HEIGHT - 1) - y;
if (flippedX % 2 == 0) {
return (flippedX * MATRIX_HEIGHT) + flippedY;
} else {
return (flippedX * MATRIX_HEIGHT) + (MATRIX_HEIGHT - 1 - flippedY);
}
}
int XY_text(int x, int y) {
x = (MATRIX_WIDTH - 1) - x;
y = (MATRIX_HEIGHT - 1) - y;
if (x % 2 == 0) {
return (x * MATRIX_HEIGHT) + y;
} else {
return (x * MATRIX_HEIGHT) + (MATRIX_HEIGHT - 1 - y);
}
}
void enemyDestructionAnimation(int enemyX, int enemyY) {
// Light up the top and bottom positions around the enemy's red LED
// Adjust the positions if necessary to match your LED layout
leds[XY(enemyX, enemyY - 1)] = CRGB::Orange;
leds[XY(enemyX, enemyY + 1)] = CRGB::Orange;
FastLED.show();
// Keep the animation for 100 milliseconds before clearing
delay(100);
// Clear the animated LEDs (set to black)
leds[XY(enemyX, enemyY - 1)] = CRGB::Black;
leds[XY(enemyX, enemyY + 1)] = CRGB::Black;
FastLED.show();
}
void drawChar(int x, int y, char c, CRGB color) {
int charIndex;
if (c >= '0' && c <= '9') {
charIndex = getCharIndex(c + 1); // Try adding 1 to the character value
} else {
charIndex = getCharIndex(c);
}
for (int col = 0; col < 4; col++) {
if (x + col >= 0 && x + col < MATRIX_WIDTH) {
uint8_t pattern = font5x4[charIndex][col];
for (int row = 0; row < 5; row++) {
if (pattern & (1 << (4 - row))) {
if (y + row >= 0 && y + row < MATRIX_HEIGHT) {
leds[XY_text(x + col, y + row)] = color;
}
}
}
}
}
}
void scrollText(const char* text, CRGB color) {
static int scrollX = MATRIX_WIDTH;
static unsigned long lastScroll = 0;
if (millis() - lastScroll > SCROLL_SPEED) {
FastLED.clear();
int textLen = strlen(text);
for (int i = 0; i < textLen; i++) {
drawChar(scrollX + (i * 5), 1, text[i], color);
}
FastLED.show();
scrollX--;
if (scrollX < -(textLen * 5)) {
scrollX = MATRIX_WIDTH;
}
lastScroll = millis();
}
}
void firePlayerMissile() {
for (int i = 0; i < MAX_MISSILES; i++) {
if (!playerMissiles[i].active) {
playerMissiles[i].xPos = player.x + 1;
playerMissiles[i].x = (int)playerMissiles[i].xPos;
playerMissiles[i].y = player.y;
playerMissiles[i].active = true;
playTone(SOUND_SHOOT_FREQ, SOUND_SHOOT_DURATION);
break;
}
}
}
void handleInput() {
if (digitalRead(BTN_UP) == LOW) {
if (!upButtonPressed && millis() - lastMoveTime > MOVE_COOLDOWN) {
player.y = max(0, player.y - 1);
upButtonPressed = true;
lastMoveTime = millis();
}
} else {
upButtonPressed = false;
}
if (digitalRead(BTN_DOWN) == LOW) {
if (!downButtonPressed && millis() - lastMoveTime > MOVE_COOLDOWN) {
player.y = min(MATRIX_HEIGHT-1, player.y + 1);
downButtonPressed = true;
lastMoveTime = millis();
}
} else {
downButtonPressed = false;
}
if (digitalRead(BTN_FIRE) == LOW) {
if (!fireButtonPressed && millis() - lastFireTime > FIRE_COOLDOWN) {
firePlayerMissile();
fireButtonPressed = true;
lastFireTime = millis();
}
} else {
fireButtonPressed = false;
}
}
void updateGame() {
// Update player missiles
for (int i = 0; i < MAX_MISSILES; i++) {
if (playerMissiles[i].active) {
playerMissiles[i].xPos += MISSILE_SPEED;
playerMissiles[i].x = (int)playerMissiles[i].xPos;
if (playerMissiles[i].x >= MATRIX_WIDTH) {
playerMissiles[i].active = false;
}
}
}
// Spawn enemies
if (millis() - lastEnemySpawn > ENEMY_SPAWN_RATE) {
lastEnemySpawn = millis();
for (int i = 0; i < MAX_ENEMIES; i++) {
if (!enemies[i].active) {
enemies[i].xPos = MATRIX_WIDTH - 1;
enemies[i].x = (int)enemies[i].xPos;
enemies[i].y = random(0, MATRIX_HEIGHT - 1);
enemies[i].active = true;
enemies[i].hasWeapon = true;
break;
}
}
}
// Update enemies
for (int i = 0; i < MAX_ENEMIES; i++) {
if (enemies[i].active) {
enemies[i].xPos -= ENEMY_SPEED;
enemies[i].x = (int)enemies[i].xPos;
// Enemy firing
if (enemies[i].hasWeapon && random(100) < (ENEMY_FIRE_RATE * 100)) {
for (int j = 0; j < 5; j++) {
if (!enemyMissiles[j].active) {
enemyMissiles[j].xPos = enemies[i].xPos - 1;
enemyMissiles[j].x = (int)enemyMissiles[j].xPos;
enemyMissiles[j].y = enemies[i].y;
enemyMissiles[j].active = true;
break;
}
}
}
if (enemies[i].xPos < 0) enemies[i].active = false;
}
}
// Update enemy missiles
for (int i = 0; i < 5; i++) {
if (enemyMissiles[i].active) {
enemyMissiles[i].xPos -= ENEMY_SPEED;
enemyMissiles[i].x = (int)enemyMissiles[i].xPos;
if (enemyMissiles[i].xPos < 0) enemyMissiles[i].active = false;
}
}
checkCollisions();
}
void checkCollisions() {
// Player missiles vs enemies
for (int m = 0; m < MAX_MISSILES; m++) {
if (playerMissiles[m].active) {
for (int e = 0; e < MAX_ENEMIES; e++) {
if (enemies[e].active) {
if ((playerMissiles[m].x == enemies[e].x && playerMissiles[m].y == enemies[e].y) ||
(playerMissiles[m].x == enemies[e].x - 1 && playerMissiles[m].y == enemies[e].y)) {
playerMissiles[m].active = false;
// Save enemy position before deactivating (for animation)
int enemyX = enemies[e].x;
int enemyY = enemies[e].y;
enemies[e].active = false;
score += 10;
playEnemyDestroyedSound();
// Trigger the enemy destruction animation
enemyDestructionAnimation(enemyX, enemyY);
}
}
}
}
}
// Enemy missiles vs player
for (int i = 0; i < 5; i++) {
if (enemyMissiles[i].active && enemyMissiles[i].x == player.x && enemyMissiles[i].y == player.y) {
enemyMissiles[i].active = false;
if (--lives <= 0) gameOver();
}
}
// Enemies vs player
for (int i = 0; i < MAX_ENEMIES; i++) {
if (enemies[i].active && enemies[i].x == player.x && enemies[i].y == player.y) {
enemies[i].active = false;
if (--lives <= 0) gameOver();
}
}
}
void render() {
FastLED.clear();
// Draw player
leds[XY(player.x, player.y)] = PLAYER_COLOR;
// Draw player missiles
for (int i = 0; i < MAX_MISSILES; i++) {
if (playerMissiles[i].active) {
leds[XY(playerMissiles[i].x, playerMissiles[i].y)] = MISSILE_COLOR;
}
}
// Draw enemies
for (int i = 0; i < MAX_ENEMIES; i++) {
if (enemies[i].active) {
leds[XY(enemies[i].x, enemies[i].y)] = ENEMY_COLOR;
if (enemies[i].hasWeapon && enemies[i].x > 0) {
leds[XY(enemies[i].x - 1, enemies[i].y)] = ENEMY_WEAPON_COLOR;
}
}
}
// Draw enemy missiles
for (int i = 0; i < 5; i++) {
if (enemyMissiles[i].active) {
leds[XY(enemyMissiles[i].x, enemyMissiles[i].y)] = ENEMY_MISSILE_COLOR;
}
}
// Draw score LEDs
int scoreLeds = min(score / 10, MATRIX_WIDTH);
for (int i = 0; i < scoreLeds; i++) {
leds[XY(MATRIX_WIDTH - 1 - i, MATRIX_HEIGHT - 1)] = CRGB::Green;
}
}
void gameOver() {
// Flash red 3 times
for (int i = 0; i < 3; i++) {
fill_solid(leds, NUM_LEDS, CRGB::Red);
FastLED.show();
delay(300);
FastLED.clear();
FastLED.show();
delay(300);
}
playGameOverSound(); // play sound after flashes
gameState = GAME_OVER;
}
void loop() {
static unsigned long lastFrame = millis();
if (millis() - lastFrame < 33) {
delay(1);
return;
}
lastFrame = millis();
updateSound(); // keep sound system updated!
switch (gameState) {
case TITLE_SCREEN:
scrollText(START_TEXT, CRGB::Green);
if (digitalRead(BTN_FIRE) == LOW) {
playTone(SOUND_START_FREQ, SOUND_START_DURATION);
gameState = PLAYING;
resetGame();
delay(200);
}
break;
case PLAYING:
handleInput();
updateGame();
render();
FastLED.show();
break;
case GAME_OVER:
if (gameOverStartTime == 0) {
gameOverStartTime = millis();
waitingForFireButton = false;
}
scrollText(GAMEOVER_TEXT, CRGB::Red);
if (!waitingForFireButton && millis() - gameOverStartTime >= 3000) {
waitingForFireButton = true;
}
if (waitingForFireButton && digitalRead(BTN_FIRE) == LOW) {
gameState = SCORE_DISPLAY;
gameOverStartTime = 0;
waitingForFireButton = false;
delay(200);
}
break;
case SCORE_DISPLAY:
static bool scoreInitialized = false;
static unsigned long lastScrollUpdate = 0; // Control scroll update rate
if (!scoreInitialized) {
FastLED.clear();
scoreInitialized = true;
}
if (millis() - lastScrollUpdate > 75) { // Update every 75 milliseconds
char scoreText[30] = "SCORE ";
char scoreValue[6]; // Enough space for a 5-digit score + null terminator
itoa(score, scoreValue, 10); // Convert score to string (base 10)
strcat(scoreText, scoreValue); // Append score value to "SCORE "
FastLED.clear();
scrollText(scoreText, CRGB::Blue);
leds[XY(MATRIX_WIDTH-1, MATRIX_HEIGHT-1)] = CRGB::Black;
FastLED.show();
lastScrollUpdate = millis();
}
if (digitalRead(BTN_FIRE) == LOW) {
scoreInitialized = false;
gameState = TITLE_SCREEN;
delay(200);
}
break;
}
ESP.wdtFeed();
}
void setup() {
Serial.begin(115200);
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(30);
pinMode(BTN_UP, INPUT_PULLUP);
pinMode(BTN_DOWN, INPUT_PULLUP);
pinMode(BTN_FIRE, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
resetGame();
}
void resetGame() {
FastLED.clear();
for (int i = 0; i < MAX_MISSILES; i++) playerMissiles[i].active = false;
for (int i = 0; i < 5; i++) enemyMissiles[i].active = false;
for (int i = 0; i < MAX_ENEMIES; i++) enemies[i].active = false;
player.x = 0;
player.y = MATRIX_HEIGHT / 2;
score = 0;
lives = 3;
lastEnemySpawn = millis();
player.moveRequested = false;
gameOverStartTime = 0;
waitingForFireButton = false;
}
Comments