#include <Adafruit_GFX.h> // Include the Adafruit Graphics Library
#include <Adafruit_ILI9341.h> // Include the Adafruit ILI9341 Library
#include <Preferences.h> // Include the Preferences library for NVS
#define TFT_CS 15
#define TFT_RST 2
#define TFT_DC 4
#define UP_BUTTON 13
#define DOWN_BUTTON 12
#define LEFT_BUTTON 14
#define RIGHT_BUTTON 27
#define SHOOT_BUTTON 17
#define CROSSHAIR_SPEED 6 // Increase crosshair speed to 6 pixels per movement
// Define difficulty levels
enum Difficulty {
EASY,
NORMAL,
HARD,
NUM_DIFFICULTIES // Add a new enum value for the number of difficulties
};
Difficulty selectedDifficulty = EASY; // Default difficulty
bool difficultySelected = false; // Flag to track if difficulty is selected
bool gameOver = false; // Flag to track if the game is over
// Structure to store spot information
struct Spot {
int x; // X-coordinate
int y; // Y-coordinate
bool active; // Flag to indicate if spot is active
unsigned long spawnTime; // Time when the spot was spawned
};
const int MAX_SPOTS = 1000; // Maximum number of spots
Spot spots[MAX_SPOTS]; // Array to store spots
int crosshairX = 120; // Initial X-coordinate of the crosshair
int crosshairY = 160; // Initial Y-coordinate of the crosshair
int prevCrosshairX = crosshairX; // Previous X-coordinate of the crosshair
int prevCrosshairY = crosshairY; // Previous Y-coordinate of the crosshair
int remainingTime = 0; // Remaining time in seconds
unsigned long lastTime = 0; // Last time the timer was updated in milliseconds
unsigned long lastSpawnTime = 0; // Last time a spot was spawned
int spotSpawnIntervals[NUM_DIFFICULTIES] = {100, 100, 100}; // 調生成間隙
// Score variables
int score = 0;
int highScore = 0; // Variable to store the high score
int scoreIncrements[NUM_DIFFICULTIES] = {5, 10, 15}; // Score increments for each difficulty level
int currentSpotIndex = 0; // Index of the current spot being generated
bool spotCleared = true; // Flag to track if spot is cleared
// Create an instance of the Adafruit ILI9341 TFT LCD display
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
Preferences preferences; // Create an instance of the Preferences library
// Define constants for text and spot dimensions
const int TEXT_HEIGHT = 20; // Height of the text display
const int SPOT_DIAMETER = 20; // Diameter of the spot
void displayMenu() {
tft.fillRect(0, 90, 35, 100, ILI9341_BLACK); // Clear the arrow
tft.setTextSize(2); // Set text size
tft.setTextColor(ILI9341_WHITE); // Set text color
// Display difficulty options
tft.setCursor(20, 50);
tft.println("Select Difficulty:");
tft.setCursor(40, 100);
tft.print("1. Easy");
tft.setCursor(40, 130);
tft.println("2. Normal");
tft.setCursor(40, 160);
tft.println("3. Hard");
// Add arrow here
switch(selectedDifficulty) {
case EASY:
tft.setCursor(15, 100);
break;
case NORMAL:
tft.setCursor(15, 130);
break;
case HARD:
tft.setCursor(15, 160);
break;
}
tft.println("> ");
}
void clearSpot(int index) {
tft.fillCircle(spots[index].x, spots[index].y, 10, ILI9341_BLACK); // Increase radius to 10 pixels
spots[index].active = false; // Mark spot as inactive
}
void setup() {
tft.begin(); // Initialize the display
tft.setRotation(3); // Set display rotation (optional)
pinMode(UP_BUTTON, INPUT_PULLUP); // Set button pins as input with pull-up resistors
pinMode(DOWN_BUTTON, INPUT_PULLUP);
pinMode(LEFT_BUTTON, INPUT_PULLUP);
pinMode(RIGHT_BUTTON, INPUT_PULLUP);
pinMode(SHOOT_BUTTON, INPUT_PULLUP);
preferences.begin("game", false); // Open NVS with "game" namespace
highScore = preferences.getInt("highScore", 0); // Retrieve the high score from NVS
displayMenu(); // Display the difficulty menu
// Set initial remaining time to 60 seconds
remainingTime = 60;
}
void loop() {
unsigned long currentTime = millis(); // Get current time in milliseconds
// If game is over, check for shoot button press to return to the difficulty selection menu
if (gameOver) {
if (digitalRead(SHOOT_BUTTON) == LOW) {
clearScreen(); // Clear the game over screen
gameOver = false;
difficultySelected = false;
selectedDifficulty = EASY;
score = 0;
remainingTime = 60;
displayMenu();
delay(200); // Debounce delay
// Reset game-related variables
crosshairX = 120;
crosshairY = 160;
prevCrosshairX = crosshairX;
prevCrosshairY = crosshairY;
lastTime = currentTime;
lastSpawnTime = currentTime;
currentSpotIndex = 0;
spotCleared = true;
for (int i = 0; i < MAX_SPOTS; i++) {
spots[i].active = false;
}
}
return;
}
// Update remaining time every second if difficulty is selected
if (difficultySelected && currentTime - lastTime >= 1000) {
lastTime = currentTime;
remainingTime--;
// If remaining time is 0, game over
if (remainingTime == 0) {
// Clear the screen
tft.fillScreen(ILI9341_BLACK);
// Display "GAME OVER" in the center of the screen
tft.setTextSize(3);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(60, 100);
tft.println("GAME OVER");
// Display the total score below "GAME OVER"
tft.setTextSize(2);
tft.setCursor(80, 150);
tft.print("Total Score: ");
tft.print(score);
// Update high score if current score is higher
if (score > highScore) {
highScore = score;
preferences.putInt("highScore", highScore); // Save the new high score to NVS
}
// Display the high score
tft.setCursor(80, 180);
tft.print("High Score: ");
tft.print(highScore);
// Set game over flag
gameOver = true;
return;
}
// Update remaining time display
tft.fillRect(200, 0, 120, 20, ILI9341_BLACK);
tft.setTextSize(2);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(200, 0);
tft.print("Time: ");
tft.print(remainingTime);
}
// If difficulty is not selected, handle difficulty selection
if (!difficultySelected) {
if (digitalRead(UP_BUTTON) == LOW) {
selectedDifficulty = static_cast<Difficulty>((selectedDifficulty + 2) % NUM_DIFFICULTIES);
displayMenu();
delay(200); // Debounce delay
} else if (digitalRead(DOWN_BUTTON) == LOW) {
selectedDifficulty = static_cast<Difficulty>((selectedDifficulty + 1) % NUM_DIFFICULTIES);
displayMenu();
delay(200); // Debounce delay
} else if (digitalRead(SHOOT_BUTTON) == LOW) {
difficultySelected = true; // Difficulty selected after shoot button is pressed
tft.fillScreen(ILI9341_BLACK); // Clear the screen
displayScore(); // Display the initial score
lastTime = currentTime; // Reset the timer
lastSpawnTime = currentTime; // Reset the spawn timer
}
} else {
// Check if spot is cleared and generate new spot if needed
if (spotCleared && currentSpotIndex < MAX_SPOTS && currentTime - lastSpawnTime >= spotSpawnIntervals[selectedDifficulty]) {
int spotX, spotY;
do {
// Generate random x-coordinate within display width, excluding edges
spotX = random(20, tft.width() - SPOT_DIAMETER - 20); // Adjusted for excluding edges
// Generate random y-coordinate within display height, excluding edges and text area
spotY = random(TEXT_HEIGHT + 20, tft.height() - SPOT_DIAMETER - 20); // Adjusted for excluding edges and text area
} while (isOverlappingText(spotX, spotY)); // Check if spot overlaps with text
spots[currentSpotIndex].x = spotX;
spots[currentSpotIndex].y = spotY;
spots[currentSpotIndex].active = true; // Mark spot as active
spots[currentSpotIndex].spawnTime = currentTime; // Record the spawn time of the spot
tft.fillCircle(spotX, spotY, 10, ILI9341_RED); // Draw a red spot
currentSpotIndex++; // Move to the next spot index
spotCleared = false; // Reset spot cleared flag
lastSpawnTime = currentTime; // Update last spawn time
}
// Check and clear spots based on difficulty-specific time limits
for (int i = 0; i < currentSpotIndex; i++) {
if (spots[i].active) {
unsigned long spotLifeTime = currentTime - spots[i].spawnTime;
unsigned long spotLifeLimit;
switch (selectedDifficulty) {
case EASY:
spotLifeLimit = 8000; // 8 seconds
break;
case NORMAL:
spotLifeLimit = 6500; // 6.5 seconds
break;
case HARD:
spotLifeLimit = 1000; // 5 seconds
break;
}
if (spotLifeTime >= spotLifeLimit) {
clearSpot(i);
spotCleared = true; // Mark spot as cleared
lastSpawnTime = currentTime; // Reset spawn time for next spot
}
}
}
// Control the crosshair position using directional buttons
if (digitalRead(UP_BUTTON) == LOW && crosshairY > 0) {
prevCrosshairX = crosshairX;
prevCrosshairY = crosshairY;
crosshairY -= CROSSHAIR_SPEED;
} else if (digitalRead(DOWN_BUTTON) == LOW && crosshairY < tft.height() - 1) {
prevCrosshairX = crosshairX;
prevCrosshairY = crosshairY;
crosshairY += CROSSHAIR_SPEED;
} else if (digitalRead(LEFT_BUTTON) == LOW && crosshairX > 0) {
prevCrosshairX = crosshairX;
prevCrosshairY = crosshairY;
crosshairX -= CROSSHAIR_SPEED;
} else if (digitalRead(RIGHT_BUTTON) == LOW && crosshairX < tft.width() - 1) {
prevCrosshairX = crosshairX;
prevCrosshairY = crosshairY;
crosshairX += CROSSHAIR_SPEED;
}
// Check if the shoot button is pressed and crosshair overlaps with a spot
if (digitalRead(SHOOT_BUTTON) == LOW) {
for (int i = 0; i < currentSpotIndex; i++) {
if (spots[i].active && abs(crosshairX - spots[i].x) <= 10 && abs(crosshairY - spots[i].y) <= 10) {
clearSpot(i);
spotCleared = true; // Set spot cleared flag
// Increase the score based on the difficulty
score += scoreIncrements[selectedDifficulty];
displayScore(); // Update the score display
}
}
}
// Redraw the previous position of the crosshair with the background color
tft.drawFastVLine(prevCrosshairX, prevCrosshairY - 10, 21, ILI9341_BLACK); // Vertical line
tft.drawFastHLine(prevCrosshairX - 10, prevCrosshairY, 21, ILI9341_BLACK); // Horizontal line
// Draw the crosshair
tft.drawFastVLine(crosshairX, crosshairY - 10, 21, ILI9341_WHITE); // Vertical line
tft.drawFastHLine(crosshairX - 10, crosshairY, 21, ILI9341_WHITE); // Horizontal line
// Update the previous position of the crosshair
prevCrosshairX = crosshairX;
prevCrosshairY = crosshairY;
}
delay(50); // Introduce delay to prevent rapid spot generation and crosshair movement
}
void clearScreen() {
tft.fillScreen(ILI9341_BLACK); // Clear the screen
}
void displayScore() {
// Clear the previous score display
tft.fillRect(0, 0, 100, 20, ILI9341_BLACK);
// Display the current score on the top left corner of the screen
tft.setTextSize(2);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(0, 0);
tft.print("Score: ");
tft.print(score);
}
// Function to check if spot overlaps with text
bool isOverlappingText(int x, int y) {
// Check if spot overlaps with text in y-axis
if (y - SPOT_DIAMETER < TEXT_HEIGHT) {
return true; // Overlaps with text
}
return false; // Does not overlap with text
}