Intern

M5Stackでテトリス

今回はM5STACKを使ってテトリスを作ってみようと思います。

使うものはVScodeとM5Stack、それを繋ぐコードです。

VScodeにてテトリスをするためのコードを書きます。

#include <M5Stack.h>

#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
#define BLOCK_SIZE 20
#define GRID_WIDTH 10
#define GRID_HEIGHT 12
#define OFFSET_X ((SCREEN_WIDTH - GRID_WIDTH * BLOCK_SIZE) / 2)
#define OFFSET_Y ((SCREEN_HEIGHT - GRID_HEIGHT * BLOCK_SIZE) / 2)

uint8_t grid[GRID_HEIGHT][GRID_WIDTH] = {0};

const uint16_t colors[] = {
  BLACK, RED, GREEN, BLUE, YELLOW, CYAN, MAGENTA, ORANGE
};

int tetromino[4][4];
int tX = 3, tY = 0;
int type = 0;
int score = 0;

void drawGrid();
void drawScore(); 

const int shapes[7][4][4] = {
  {{0,1,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}}, // T
  {{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}}, // I
  {{1,1,0,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}}, // Z
  {{0,1,1,0},{1,1,0,0},{0,0,0,0},{0,0,0,0}}, // S
  {{1,0,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}}, // J
  {{0,0,1,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}}, // L
  {{1,1,0,0},{1,1,0,0},{0,0,0,0},{0,0,0,0}}  // O
};

void drawBlock(int x, int y, int color) {
  M5.Lcd.fillRect(OFFSET_X + x * BLOCK_SIZE, OFFSET_Y + y * BLOCK_SIZE, BLOCK_SIZE - 1, BLOCK_SIZE - 1, colors[color]);
}

void drawFrame() {
  M5.Lcd.drawRect(OFFSET_X - 1, OFFSET_Y - 1, GRID_WIDTH * BLOCK_SIZE + 2, GRID_HEIGHT * BLOCK_SIZE + 2, WHITE);
}

bool checkCollision(int newX, int newY, int shape[4][4]) {
  for (int y = 0; y < 4; y++) {
    for (int x = 0; x < 4; x++) {
      if (shape[y][x]) {
        int gx = newX + x;
        int gy = newY + y;
        if (gx < 0 || gx >= GRID_WIDTH || gy >= GRID_HEIGHT || (gy >= 0 && grid[gy][gx])) {
          return true;
        }
      }
    }
  }
  return false;
}

void mergeBlock() {
  for (int y = 0; y < 4; y++) {
    for (int x = 0; x < 4; x++) {
      if (tetromino[y][x]) {
        grid[tY + y][tX + x] = type + 1;
      }
    }
  }
}

void rotate() {
  int temp[4][4];
  for (int y = 0; y < 4; y++)
    for (int x = 0; x < 4; x++)
      temp[y][x] = tetromino[3 - x][y];
  if (!checkCollision(tX, tY, temp))
    memcpy(tetromino, temp, sizeof(tetromino));
}

void clearLines() {
  int linesCleared = 0;
  for (int y = GRID_HEIGHT - 1; y >= 0; y--) {
    bool full = true;
    for (int x = 0; x < GRID_WIDTH; x++) {
      if (!grid[y][x]) {
        full = false;
        break;
      }
    }
    if (full) {
      linesCleared++;
      for (int yy = y; yy > 0; yy--) {
        for (int x = 0; x < GRID_WIDTH; x++) {
          grid[yy][x] = grid[yy - 1][x];
        }
      }
      for (int x = 0; x < GRID_WIDTH; x++) {
        grid[0][x] = 0;
      }
      y++;  // 同じ行を再チェック
    }
  }

  switch (linesCleared) {
    case 1: score += 100; break;
    case 2: score += 300; break;
    case 3: score += 500; break;
    case 4: score += 800; break;
  }

  if (linesCleared > 0) {
    drawGrid();
    drawScore();
  }
}

void drawGrid() {
  for (int y = 0; y < GRID_HEIGHT; y++) {
    for (int x = 0; x < GRID_WIDTH; x++) {
      drawBlock(x, y, grid[y][x]);
    }
  }
}

void drawCurrent() {
  for (int y = 0; y < 4; y++) {
    for (int x = 0; x < 4; x++) {
      if (tetromino[y][x]) {
        drawBlock(tX + x, tY + y, type + 1);
      }
    }
  }
}

void drawScore() {
  M5.Lcd.fillRect(200, 0, 120, 30, BLACK);  // スコア表示エリアを広く確保
  M5.Lcd.setCursor(205, 5);                 // 左上に表示
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(2);
  M5.Lcd.printf("Score:%d", score);
}

void spawn() {
  type = random(0, 7);
  memcpy(tetromino, shapes[type], sizeof(tetromino));
  tX = 3;
  tY = 0;
  if (checkCollision(tX, tY, tetromino)) {
    M5.Lcd.setTextColor(RED);
    M5.Lcd.setTextSize(3);
    M5.Lcd.setCursor(SCREEN_WIDTH/2 - 80, SCREEN_HEIGHT/2 - 10);
    M5.Lcd.print("Game Over");
    while (true) delay(1000);
  }
}

void setup() {
  M5.begin();
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setRotation(1);
  randomSeed(esp_random());
  drawScore();  // 初期スコア描画
  spawn();
}

unsigned long lastFall = 0;
int fallDelay = 500;

void loop() {
  M5.update();

  if (millis() - lastFall > fallDelay) {
    lastFall = millis();
    if (!checkCollision(tX, tY + 1, tetromino)) {
      tY++;
    } else {
      mergeBlock();
      clearLines();
      spawn();
    }
  }

  if (M5.BtnA.wasPressed()) {
    if (!checkCollision(tX - 1, tY, tetromino)) tX--;
  }
  if (M5.BtnC.wasPressed()) {
    if (!checkCollision(tX + 1, tY, tetromino)) tX++;
  }
  if (M5.BtnB.wasPressed()) {
    rotate();
  }

  M5.Lcd.fillScreen(BLACK);
  drawFrame();
  drawGrid();
  drawCurrent();
  drawScore();  // ← 毎フレーム描画でもOK
  delay(50);
}Code language: PHP (php)

描画処理

drawBlock(int x, int y, int color)

  • 単一のブロック(四角)を描画

drawGrid()

  • 現在配置済みのブロック全体を描画(grid 配列を元に)

drawCurrent()

  • 現在落下中のブロックを描画

drawFrame()

  • フィールド(枠)を白で囲む

drawScore()

  • スコアを右上に表示(幅120px・高さ30px分のエリアを毎回クリアして更新)

drawFrame();でどこからどこまでの範囲にブロックを置けるのかを可視化しています。

if (M5.BtnB.wasPressed())の処理でボタンB(真ん中の物理ボタン)が押されたときブロックが回転するようにしています。

ロジック処理(落下・回転・合体・衝突判定)

checkCollision(newX, newY, shape)

  • 指定座標でブロックを置けるかチェック(壁・床・他ブロックとの衝突)

mergeBlock()

  • 落下して着地したブロックを grid に固定

rotate()

  • テトロミノを右回転(回転後も checkCollision で衝突確認)

ライン消去処理

clearLines()

  • 横一列が全て埋まったら削除
  • 上の段を一段ずつ下にずらす
  • 複数行消しに応じてスコアを加算
  • 消去後に drawGrid()drawScore() で画面更新

ブロックの生成とゲームオーバー

spawn()

  • 新しいテトロミノをランダム生成し、初期位置に配置
  • 開始位置で checkCollision()true なら ゲームオーバー

まとめ

スコアをつけることでゲーム性が出たように感じます。次に来るブロックの表示などまだまだ拡張性があると思いました。