【Arduino 动手做】DIY ESP32 TFT 触摸显示屏上的 15 益智游戏
这是一个制作有趣的益智游戏的极其简单的方法,只需要三个组件。“15拼图”是一款经典的滑动拼图游戏,由一个4×4的网格组成,网格中有15个标有数字的方格,中间有一个空白处。游戏的目标是将方格从乱码的初始状态重新排列成数字顺序(从1到15,从左到右,从上到下),然后将它们滑入空白处。
游戏的目标是将牌按升序排列,空白处位于右下角。牌的排列方式有 15! = 1.3 万亿种,但只有一半是可以解决的。
在这个项目中,我将向您展示这个拼图的电子版本,与原始机械版本相比,它也有许多有用的改进和选项。
游戏是在彩色触摸显示屏上进行的,因此不会出现瓷砖粘连或移动困难的情况,这是机械版本中常见的情况。
【Arduino 动手做】DIY ESP32 TFT 触摸显示屏上的 15 益智游戏
硬件部分极其简单,仅由三个组件组成:ESP32 开发板
3.2英寸TFT LCD显示屏ILI9341驱动器带触摸屏
和小蜂鸣器
【Arduino 动手做】DIY ESP32 TFT 触摸显示屏上的 15 益智游戏
需要强调的是,如果您以与给定原理图相同的方式使用显示器与微控制器的连接,则还必须使用专门为该项目适配的TFTesPI库。另外,别忘了告诉您,我使用Arduino IDE版本1.8.16编译了代码,更重要的是,使用ESP32核心版本2.0.4编译了代码。首先,我将介绍如何加入以及游戏中的选项。打开设备后,显示屏上会出现校准触摸屏的指南。
这一点很重要,因为游戏本身是通过触摸屏幕进行的,因此校准屏幕至关重要。我们通过按指定顺序按下显示屏的四个角来进行校准。然后会出现游戏名称,按下按钮即可开始游戏。游戏区域占据了大部分显示屏,右侧有三个控制信息按钮。
顶部按钮显示当前已进行的移动步数。实际上,我们在这里监控游戏的进程和结果。也就是说,游戏的目标是用尽可能少的步数排列数字。
使用中间的按钮,我们可以从棋盘的五种颜色中选择一种。这是一个非常实用的选项,我们可以在游戏的任何时刻使用它,而不会影响结果。
底部按钮也用于开始新游戏。此按钮在游戏开始时以及当前游戏进行到任何阶段(例如,如果我们遇到某些情况)时均可使用。每次按下此按钮,都会随机生成一个新游戏,因此我们可以选择一个至少在视觉上看起来更容易解决的游戏。屏幕上的所有触摸都会伴有相应的声音,使游戏更轻松、更有趣。
最后,当我们完成游戏时,屏幕上会出现一个信息,显示谜题解决了多少步,以及一个开始新游戏的按钮。
关于代码,我只想说几句话。
您可以看到,它的设计方式允许您轻松更改大多数参数,从图块间距和大小到调整游戏中的声音和颜色。
最后,简短总结一下。这是一个制作这款有趣益智游戏的极其简单的方法,只需要三个组件,就能让你享受数小时的乐趣。
【Arduino 动手做】DIY ESP32 TFT 触摸显示屏上的 15 益智游戏
项目代码//-----------------------------------------------------
// 15 Puzzle Game3.2"-Display
// by: mircemk
// License: GNU GPl 3.0
// Created: 2025-03-27 20:52:34
//-----------------------------------------------------
#include <SPI.h>
#include <TFT_eSPI.h>
TFT_eSPI tft = TFT_eSPI();
// Game board configuration
#define GRID_SIZE 4
#define TILE_SIZE 54 // Increased by 1 pixel
#define TILE_SPACING 2
#define GRID_START_X 10
#define GRID_START_Y 9
// Button configuration
#define BUTTON_W 100
#define BUTTON_H 50
#define START_BTN_X 110 // Centered start button for welcome screen
#define START_BTN_Y 145
// Right side menu buttons
#define MENU_BTN_W 75
#define MENU_BTN_H 71
#define MENU_BTN_X 236
#define MENU_BTN_Y1 9 // First button
#define MENU_BTN_Y2 85 // Second button
#define MENU_BTN_Y3 160 // Third button
// Sound configuration
#define SOUND_PIN 2
#define GAME_START_DELAY 1000
// Colors
#define TILE_COLOR TFT_BLUE
#define TILE_TEXT_COLOR TFT_WHITE
#define EMPTY_COLOR TFT_BLACK
#define GRID_COLOR TFT_DARKGREY
// Board colors array
const uint16_t boardColors[] = {
0x001F,// Blue
0xFDA0,// Orange
0xF800,// Red
0x7BE0,//Olivie
0xF81F // Magenta
};
int currentColorIndex = 0;
// Game state variables
uint16_t pixel_x, pixel_y;
byte gameBoard;
int emptyTileX = GRID_SIZE - 1;
int emptyTileY = GRID_SIZE - 1;
bool gameStarted = false;
int moves = 0;
int iEnableButtons = 1;
unsigned long lastSoundTime = 0;
bool soundEnabled = true;
unsigned long lastButtonPress = 0;
#define BUTTON_DEBOUNCE_TIME 250// 250ms debounce
// Function prototypes
void showWelcomeScreen();
void drawFrame(int size, uint16_t color);
void initializeGame();
void drawBoard();
void drawTile(int x, int y);
void drawMenuButtons();
bool isValidMove(int x, int y);
void moveTile(int x, int y);
void silentMoveTile(int x, int y);
void shuffleBoard();
void handleGameTouch();
void checkMenuButtons();
bool checkWin();
void gameWon();
void playSound(int type);
void updateMovesDisplay();
void changeBoardColor();
void setup() {
uint16_t calibrationData;
pinMode(15, OUTPUT);
digitalWrite(15, LOW);
Serial.begin(115200);
tft.init();
tft.setRotation(1);
tft.fillScreen((0xFFFF));
// Calibration screen
tft.setCursor(40, 20, 2);
tft.setTextColor(TFT_RED, TFT_WHITE);
tft.setTextSize(2);
tft.println("Calibration of");
tft.setCursor(40, 60, 2);
tft.println("Display");
tft.setTextColor(TFT_BLACK, TFT_WHITE);
tft.setCursor(40, 100, 2);
tft.println("Touch");
tft.setCursor(40, 140, 2);
tft.println("the indicated corners");
tft.calibrateTouch(calibrationData, TFT_GREEN, TFT_RED, 15);
showWelcomeScreen();
}
void loop() {
static uint16_t color;
if (tft.getTouch(&pixel_x, &pixel_y) && iEnableButtons) {
if (!gameStarted) {
if (pixel_x > START_BTN_X && pixel_x < (START_BTN_X + BUTTON_W) &&
pixel_y > START_BTN_Y && pixel_y < (START_BTN_Y + BUTTON_H)) {
iEnableButtons = 0;
while(tft.getTouch(&pixel_x, &pixel_y)) {
delay(10);
}
playSound(1);
delay(GAME_START_DELAY);
tft.fillScreen(TFT_BLACK);
initializeGame();
shuffleBoard();
gameStarted = true;
while(tft.getTouch(&pixel_x, &pixel_y)) {
delay(10);
}
delay(250);
iEnableButtons = 1;
}
} else {
handleGameTouch();
checkMenuButtons();
}
}
}
void changeBoardColor() {
currentColorIndex = (currentColorIndex + 1) % 5;
for (int y = 0; y < GRID_SIZE; y++) {
for (int x = 0; x < GRID_SIZE; x++) {
if (gameBoard != 0) {
int pixelX = GRID_START_X + x * (TILE_SIZE + TILE_SPACING);
int pixelY = GRID_START_Y + y * (TILE_SIZE + TILE_SPACING);
tft.fillRect(pixelX, pixelY, TILE_SIZE, TILE_SIZE, boardColors);
tft.setTextColor(TILE_TEXT_COLOR);
tft.setTextSize(2);
String number = String(gameBoard);
int textWidth = number.length() * 12;
int textHeight = 14;
int textX = pixelX + (TILE_SIZE - textWidth) / 2;
int textY = pixelY + (TILE_SIZE - textHeight) / 2;
tft.setCursor(textX, textY);
tft.print(number);
}
}
}
}
void playSound(int type) {
if (!soundEnabled) return;
switch(type) {
case 0: // Tile move sound
tone(SOUND_PIN, 200, 50);// Short 200Hz beep
break;
case 1: // Game start sound
soundEnabled = false;
tone(SOUND_PIN, 400, 200);
delay(250);
tone(SOUND_PIN, 600, 200);
delay(250);
tone(SOUND_PIN, 800, 400);
delay(500);
soundEnabled = true;
break;
case 2: // Win sound
soundEnabled = false;
tone(SOUND_PIN, 800, 200);
delay(200);
tone(SOUND_PIN, 1000, 200);
delay(200);
tone(SOUND_PIN, 1200, 400);
delay(500);
soundEnabled = true;
break;
}
}
void updateMovesDisplay() {
// Clear the previous number with MAROON background
tft.fillRect(MENU_BTN_X + 1, MENU_BTN_Y1 + 35, MENU_BTN_W - 2, 20, TFT_MAROON);
tft.setTextColor(TFT_WHITE);
tft.setTextSize(1);
String movesStr = String(moves);
int textWidth = movesStr.length() * 6;
tft.setCursor(MENU_BTN_X + (MENU_BTN_W - textWidth) / 2, MENU_BTN_Y1 + 35);
tft.print(movesStr);
}
void showWelcomeScreen() {
tft.fillScreen(TFT_BLACK);
drawFrame(5, TFT_RED);
tft.setTextColor(TFT_YELLOW);
tft.setTextSize(4);
tft.setCursor(130, 10);
tft.print("15");
tft.setCursor(75, 70);
tft.print("PUZZLE");
tft.fillRect(START_BTN_X, START_BTN_Y, BUTTON_W, BUTTON_H, TFT_RED);
tft.setTextColor(TFT_WHITE);
tft.setTextSize(2);
tft.setCursor(START_BTN_X + 10, START_BTN_Y + 10);
tft.print("START");
}
void drawFrame(int size, uint16_t color) {
for (int i = 0; i < size; i++) {
tft.drawRect(i, i, 320-i*2, 240-i*2, color);
}
}
void initializeGame() {
tft.fillScreen(TFT_BLACK);
drawFrame(5, TFT_RED);
emptyTileX = GRID_SIZE - 1;
emptyTileY = GRID_SIZE - 1;
moves = 0;
currentColorIndex = 0;// Reset color to first color
int value = 1;
for (int y = 0; y < GRID_SIZE; y++) {
for (int x = 0; x < GRID_SIZE; x++) {
if (x == GRID_SIZE-1 && y == GRID_SIZE-1) {
gameBoard = 0;
} else {
gameBoard = value++;
}
}
}
drawBoard();
drawMenuButtons();
}
void drawBoard() {
for (int y = 0; y < GRID_SIZE; y++) {
for (int x = 0; x < GRID_SIZE; x++) {
drawTile(x, y);
}
}
}
void drawTile(int x, int y) {
int pixelX = GRID_START_X + x * (TILE_SIZE + TILE_SPACING);
int pixelY = GRID_START_Y + y * (TILE_SIZE + TILE_SPACING);
if (gameBoard == 0) {
tft.fillRect(pixelX, pixelY, TILE_SIZE, TILE_SIZE, EMPTY_COLOR);
} else {
tft.fillRect(pixelX, pixelY, TILE_SIZE, TILE_SIZE, boardColors);
tft.setTextColor(TILE_TEXT_COLOR);
tft.setTextSize(2);
String number = String(gameBoard);
int textWidth = number.length() * 12;
int textHeight = 14;
int textX = pixelX + (TILE_SIZE - textWidth) / 2;
int textY = pixelY + (TILE_SIZE - textHeight) / 2;
tft.setCursor(textX, textY);
tft.print(number);
}
}
void drawMenuButtons() {
// Draw three menu buttons on the right
for(int i = 0; i < 3; i++) {
int y_pos;
switch(i) {
case 0: y_pos = MENU_BTN_Y1; break;
case 1: y_pos = MENU_BTN_Y2; break;
case 2: y_pos = MENU_BTN_Y3; break;
}
// Set different colors for each button
uint16_t buttonColor;
if (i == 0) buttonColor = TFT_MAROON; // Moves button
else if (i == 1) buttonColor = TFT_BLUE; // Color change button
else buttonColor = TFT_DARKGREEN; // New game button
tft.fillRect(MENU_BTN_X, y_pos, MENU_BTN_W, MENU_BTN_H, buttonColor);
tft.drawRect(MENU_BTN_X, y_pos, MENU_BTN_W, MENU_BTN_H, TFT_WHITE);
if (i == 0) {// Top button - Moves counter
tft.setTextColor(TFT_WHITE);
tft.setTextSize(1);
tft.setCursor(MENU_BTN_X + (MENU_BTN_W - 30) / 2, y_pos + 15);
tft.print("MOVES");
updateMovesDisplay();
}
else if (i == 1) {// Middle button - Color changer
tft.setTextColor(TFT_WHITE);
tft.setTextSize(1);
tft.setCursor(MENU_BTN_X + (MENU_BTN_W - 30) / 2, y_pos + 15);
tft.print("COLOR");
tft.setCursor(MENU_BTN_X + (MENU_BTN_W - 36) / 2, y_pos + 35);
tft.print("CHANGE");
}
else if (i == 2) {// Bottom button - NEW GAME
tft.setTextColor(TFT_WHITE);
tft.setTextSize(1);
tft.setCursor(MENU_BTN_X + (MENU_BTN_W - 18) / 2, y_pos + 20);
tft.print("NEW");
tft.setCursor(MENU_BTN_X + (MENU_BTN_W - 24) / 2, y_pos + 40);
tft.print("GAME");
}
}
}
void handleGameTouch() {
int tileX = (pixel_x - GRID_START_X) / (TILE_SIZE + TILE_SPACING);
int tileY = (pixel_y - GRID_START_Y) / (TILE_SIZE + TILE_SPACING);
if (tileX >= 0 && tileX < GRID_SIZE && tileY >= 0 && tileY < GRID_SIZE) {
if (isValidMove(tileX, tileY)) {
moveTile(tileX, tileY);
moves++;
updateMovesDisplay();
if (checkWin()) {
gameWon();
}
}
}
}
void checkMenuButtons() {
unsigned long currentTime = millis();
if (currentTime - lastButtonPress < BUTTON_DEBOUNCE_TIME) {
return;
}
if (pixel_x >= MENU_BTN_X && pixel_x < (MENU_BTN_X + MENU_BTN_W)) {
if (pixel_y >= MENU_BTN_Y1 && pixel_y < (MENU_BTN_Y1 + MENU_BTN_H)) {
playSound(0);
lastButtonPress = currentTime;
while(tft.getTouch(&pixel_x, &pixel_y)) {
delay(10);
}
}
else if (pixel_y >= MENU_BTN_Y2 && pixel_y < (MENU_BTN_Y2 + MENU_BTN_H)) {
playSound(0);
changeBoardColor();
lastButtonPress = currentTime;
while(tft.getTouch(&pixel_x, &pixel_y)) {
delay(10);
}
}
else if (pixel_y >= MENU_BTN_Y3 && pixel_y < (MENU_BTN_Y3 + MENU_BTN_H)) {
iEnableButtons = 0;
while(tft.getTouch(&pixel_x, &pixel_y)) {
delay(10);
}
playSound(1);
delay(GAME_START_DELAY);
initializeGame();
shuffleBoard();
gameStarted = true;
while(tft.getTouch(&pixel_x, &pixel_y)) {
delay(10);
}
delay(250);
iEnableButtons = 1;
lastButtonPress = currentTime;
}
}
}
bool isValidMove(int x, int y) {
return (
(abs(x - emptyTileX) == 1 && y == emptyTileY) ||
(abs(y - emptyTileY) == 1 && x == emptyTileX)
);
}
void moveTile(int x, int y) {
gameBoard = gameBoard;
gameBoard = 0;
drawTile(emptyTileX, emptyTileY);
drawTile(x, y);
emptyTileX = x;
emptyTileY = y;
playSound(0);
}
void silentMoveTile(int x, int y) {
gameBoard = gameBoard;
gameBoard = 0;
emptyTileX = x;
emptyTileY = y;
}
void shuffleBoard() {
iEnableButtons = 0;
soundEnabled = false;
randomSeed(analogRead(34));
for (int i = 0; i < 200; i++) {
int direction = random(4);
int newX = emptyTileX;
int newY = emptyTileY;
switch (direction) {
case 0: newY--; break;
case 1: newY++; break;
case 2: newX--; break;
case 3: newX++; break;
}
if (newX >= 0 && newX < GRID_SIZE && newY >= 0 && newY < GRID_SIZE) {
silentMoveTile(newX, newY);
}
}
drawBoard();
delay(250);
soundEnabled = true;
iEnableButtons = 1;
}
bool checkWin() {
int value = 1;
for (int y = 0; y < GRID_SIZE; y++) {
for (int x = 0; x < GRID_SIZE; x++) {
if (y == GRID_SIZE-1 && x == GRID_SIZE-1) {
if (gameBoard != 0) return false;
} else {
if (gameBoard != value++) return false;
}
}
}
return true;
}
void gameWon() {
tft.fillScreen(TFT_BLACK);
drawFrame(10, TFT_GREEN);
tft.setTextColor(TFT_YELLOW);
tft.setTextSize(2);
tft.setCursor(60, 60);
tft.print("PUZZLE SOLVED!");
tft.setTextSize(2);
tft.setCursor(80, 120);
tft.print("Moves: ");
tft.print(moves);
tft.setTextSize(1);
tft.setCursor(85, 180);
tft.print("Touch screen to continue");
playSound(2);
while(tft.getTouch(&pixel_x, &pixel_y)) {
delay(10);
}
while(!tft.getTouch(&pixel_x, &pixel_y)) {
delay(10);
}
gameStarted = false;
moves = 0;
showWelcomeScreen();
}
【Arduino 动手做】DIY ESP32 TFT 触摸显示屏上的 15 益智游戏
附录
【Arduino 动手做】DIY ESP32 TFT 触摸显示屏上的 15 益智游戏
项目链接:https://www.hackster.io/mircemk/diy-esp32-15-puzzle-game-on-tft-touch-dispaly-8c5817
项目作者:北马其顿 米尔塞姆克(Mirko Pavleski)
项目视频 :https://www.youtube.com/watch?v=3YXgka5V8iE
项目代码:https://www.hackster.io/code_files/668381/download
页:
[1]