poppyneko
Published

Developing F2 Racing Game with Wio Terminal Tutorial

Based on the hardware features of Wio Terminal and the Arduino open source ecosystem, this project realizes an immersive F1 racing game.

BeginnerProtip3 hours87
Developing F2 Racing Game with Wio Terminal Tutorial

Things used in this project

Hardware components

Wio Terminal
Seeed Studio Wio Terminal
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

Game Screenshot

Code

F2 Racing Game

Arduino
#include <TFT_eSPI.h>
TFT_eSPI tft;

#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
#define CAR_WIDTH 24
#define CAR_HEIGHT 40
#define ROAD_WIDTH 180
#define OBSTACLE_SIZE 20
#define MOVE_SPEED 8

// 三条车道的中心坐标
const int LANES[3] = {
  (SCREEN_WIDTH - ROAD_WIDTH)/2 + ROAD_WIDTH/6,    // 左车道
  SCREEN_WIDTH/2,                                  // 中车道
  (SCREEN_WIDTH - ROAD_WIDTH)/2 + ROAD_WIDTH*5/6   // 右车道
};

int currentLane = 1;  // 当前车道(0:左,1:中,2:右)
int gameSpeed = 3;
int score = 0;
bool gameRunning = true;
unsigned long lastObstacleTime = 0; // 记录上次生成障碍物的时间
const long obstacleInterval = 1200; // 障碍物生成间隔(毫秒)

struct Obstacle {
  int x, y, lane;
  bool active;
};
Obstacle obstacles[3]; // 保持3个障碍物

void setup() {
  Serial.begin(115200);
  tft.begin();
  tft.setRotation(3);
  tft.fillScreen(TFT_BLACK);

  pinMode(WIO_KEY_A, INPUT_PULLUP);
  pinMode(WIO_KEY_C, INPUT_PULLUP);
  pinMode(WIO_KEY_B, INPUT_PULLUP);

  // 初始化障碍物为未激活状态
  for (int i = 0; i < 3; i++) {
    obstacles[i].active = false;
  }
  drawInterface();
}

void loop() {
  if (gameRunning) {
    updateGame();
    drawGame();
    delay(50);
  } else {
    gameOverScreen();
  }
}

void drawInterface() {
  tft.fillScreen(TFT_BLACK);
  
  // 绘制道路边界
  int roadX = (SCREEN_WIDTH - ROAD_WIDTH)/2;
  tft.drawRect(roadX-2, 0, ROAD_WIDTH+4, SCREEN_HEIGHT, TFT_WHITE);
  
  // 绘制按键提示
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setCursor(10, SCREEN_HEIGHT - 40);
  tft.print("Left:A");
  tft.setCursor(SCREEN_WIDTH - 60, SCREEN_HEIGHT - 40);
  tft.print("Right:C");
}

void updateGame() {
  // 车道切换控制
  if (digitalRead(WIO_KEY_A) == LOW && currentLane > 0) {
    currentLane--;
    delay(150); // 防抖
  }
  if (digitalRead(WIO_KEY_C) == LOW && currentLane < 2) {
    currentLane++;
    delay(150);
  }

  // 生成新障碍物(有时间间隔)
  unsigned long currentTime = millis();
  if (currentTime - lastObstacleTime > obstacleInterval) {
    // 查找可用的障碍物槽位
    for (int i = 0; i < 3; i++) {
      if (!obstacles[i].active) {
        resetObstacle(i);
        lastObstacleTime = currentTime;
        break;
      }
    }
  }

  // 移动活跃的障碍物
  for (int i = 0; i < 3; i++) {
    if (obstacles[i].active) {
      obstacles[i].y += gameSpeed;
      
      // 碰撞检测(基于车道位置)
      if (obstacles[i].lane == currentLane && 
          obstacles[i].y > SCREEN_HEIGHT - CAR_HEIGHT - 10) {
        gameRunning = false;
      }
      
      // 重置超出屏幕的障碍物
      if (obstacles[i].y > SCREEN_HEIGHT) {
        obstacles[i].active = false; // 标记为可重新使用
        score++;
        if (score % 5 == 0) gameSpeed++;
      }
    }
  }
}

// 绘制专业赛车图形
void drawRaceCar(int x, int y) {
  // 车身主体(红色)
  tft.fillRect(x - CAR_WIDTH/2, y - CAR_HEIGHT/2, CAR_WIDTH, CAR_HEIGHT, TFT_RED);
  
  // 车头(深红色)
  tft.fillRect(x - CAR_WIDTH/2 + 2, y - CAR_HEIGHT/2 + 2, CAR_WIDTH - 4, 8, TFT_MAROON);
  
  // 驾驶舱(黑色)
  tft.fillRect(x - CAR_WIDTH/4, y - CAR_HEIGHT/2 + 10, CAR_WIDTH/2, 8, TFT_BLACK);
  
  // 车尾(深红色)
  tft.fillRect(x - CAR_WIDTH/2 + 2, y - CAR_HEIGHT/2 + CAR_HEIGHT - 10, CAR_WIDTH - 4, 8, TFT_MAROON);
  
  // 轮胎(黑色)
  tft.fillRect(x - CAR_WIDTH/2 - 3, y - CAR_HEIGHT/2 + 8, 6, 10, TFT_BLACK);
  tft.fillRect(x + CAR_WIDTH/2 - 3, y - CAR_HEIGHT/2 + 8, 6, 10, TFT_BLACK);
  tft.fillRect(x - CAR_WIDTH/2 - 3, y + CAR_HEIGHT/2 - 18, 6, 10, TFT_BLACK);
  tft.fillRect(x + CAR_WIDTH/2 - 3, y + CAR_HEIGHT/2 - 18, 6, 10, TFT_BLACK);
  
  // 车灯(黄色)
  tft.fillCircle(x - CAR_WIDTH/3, y - CAR_HEIGHT/2 + 4, 3, TFT_YELLOW);
  tft.fillCircle(x + CAR_WIDTH/3, y - CAR_HEIGHT/2 + 4, 3, TFT_YELLOW);
  
  // 尾灯(深红色)
  tft.fillCircle(x - CAR_WIDTH/4, y + CAR_HEIGHT/2 - 6, 3, TFT_MAROON);
  tft.fillCircle(x + CAR_WIDTH/4, y + CAR_HEIGHT/2 - 6, 3, TFT_MAROON);
}

// 绘制锥形障碍物
void drawConeObstacle(int x, int y) {
  // 锥形主体(橙色)
  tft.fillTriangle(
    x, y - OBSTACLE_SIZE/2,
    x - OBSTACLE_SIZE/2, y + OBSTACLE_SIZE/2,
    x + OBSTACLE_SIZE/2, y + OBSTACLE_SIZE/2,
    TFT_ORANGE
  );
  
  // 底座(黄色)
  tft.fillRect(x - 2, y + OBSTACLE_SIZE/2, 4, 4, TFT_YELLOW);
  
  // 反光条(白色)
  tft.drawFastHLine(x - OBSTACLE_SIZE/4, y - OBSTACLE_SIZE/4, OBSTACLE_SIZE/2, TFT_WHITE);
}

void drawGame() {
  int roadX = (SCREEN_WIDTH - ROAD_WIDTH)/2;
  
  // 绘制道路
  tft.fillRect(roadX, 0, ROAD_WIDTH, SCREEN_HEIGHT, TFT_DARKGREEN);
  
  // 绘制车道分隔线(三条赛道)
  int laneWidth = ROAD_WIDTH / 3;
  for (int i = 1; i < 3; i++) {
    tft.drawFastVLine(roadX + i * laneWidth, 0, SCREEN_HEIGHT, TFT_WHITE);
  }

  // 绘制赛车(专业赛车图形)
  int carX = LANES[currentLane];
  int carY = SCREEN_HEIGHT - 10; // 赛车底部位置
  drawRaceCar(carX, carY - CAR_HEIGHT/2);
  
  // 绘制障碍物(锥形障碍物)
  for (int i = 0; i < 3; i++) {
    if (obstacles[i].active) {
      drawConeObstacle(obstacles[i].x, obstacles[i].y);
    }
  }
  
  // 显示分数和速度
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setCursor(10, 10);
  tft.print("Score: ");
  tft.print(score);
  tft.setCursor(10, 30);
  tft.print("Speed: ");
  tft.print(gameSpeed);
  
  // 显示当前车道
  tft.setCursor(10, 50);
  tft.print("Lane: ");
  tft.print(currentLane + 1);
  
  // 显示障碍物间隔时间
  tft.setCursor(10, 70);
  tft.print("Interval: ");
  tft.print(obstacleInterval);
  tft.print("ms");
}

void resetObstacle(int index) {
  obstacles[index].lane = random(0, 3);  // 随机分配到某条车道
  obstacles[index].x = LANES[obstacles[index].lane];
  
  // 设置障碍物初始位置(屏幕上方)
  obstacles[index].y = -OBSTACLE_SIZE - 20;
  obstacles[index].active = true;
}

void gameOverScreen() {
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_RED, TFT_BLACK);
  tft.setTextSize(2);
  tft.setCursor(SCREEN_WIDTH/2 - 70, SCREEN_HEIGHT/2 - 20);
  tft.print("GAME OVER");
  
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextSize(1);
  tft.setCursor(SCREEN_WIDTH/2 - 40, SCREEN_HEIGHT/2 + 10);
  tft.print("Score: ");
  tft.print(score);
  
  tft.setCursor(SCREEN_WIDTH/2 - 80, SCREEN_HEIGHT/2 + 40);
  tft.print("Press B to restart");
  
  while (true) {
    if (digitalRead(WIO_KEY_B) == LOW) {
      delay(200);
      resetGame();
      return;
    }
    delay(50);
  }
}

void resetGame() {
  score = 0;
  gameSpeed = 3;
  currentLane = 1;
  gameRunning = true;
  lastObstacleTime = millis(); // 重置障碍物计时器
  
  // 重置所有障碍物
  for (int i = 0; i < 3; i++) {
    obstacles[i].active = false;
  }
  
  // 生成第一个障碍物
  resetObstacle(0);
  drawInterface();
}

Credits

poppy
1 project • 0 followers
neko
1 project • 0 followers

Comments