0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

71歳の... ESP32-S3 と TFT-LCDモジュールで、レトロな「ブロック崩し」を作ってみた

Posted at

 老眼の私にも優しい「7インチ 800x480 TFT-LCDモジュール」でブロック崩しゲームを作ってみました。CUI上のモノを参考にしたので、ブロックとパドルとボールはキャラクターです。

Breakout.jpg

 結構、軽快に動きます。次のような工夫を加えました。

1.ブロック最上段の上側(裏側)に隙間を設けました。そこにうまく侵入すると、連続してブロックが崩せ面白い効果を出せました(昔、Linuxのゲームで見たような)。
2.当たり判定とかが厳しくなるので、ブロックやパドルは1文字1行単位で設定、ボールの描画・移動はピクセル単位。
3.周期性(先日手みたいな)が出ないよう、当たった場合に少しずらす。
4.パドルの端で打つと、来た方向に跳ね返す仕様。
5.ゲームオーバー、またはゲームクリアの4秒後に再始動。
6.ディスプレイの 800x480 サイズのうち、使用範囲は「480x480」。縦横のサイズもある程度柔軟に変更できるように。

難易度が上がるなどの仕様はありません、出来そうではありますが。
 環境・構成は次のよう。

1.Windows 11 Pro 24H2
2.Arduino IDE 2.3.6
3.Freenove ESP32-S3 WROOM (8MB Flash,8MB PSRAM) ボード「FNK0085」
    付属カメラなしで使用
    Arduino IDE上では「ESP32S3 Dev Module」
4.ボードマネージャは最新の"esp32 by Espressif Systems 3.3.2"
5.7"インチ800x480 TFTモジュール、16bitパラレル接続
6.「LovyanGFX」ライブラリーを使用
7.次のツールオプションを設定してコンパイル
    Flash Size: "8MB(64Mb)"
    Partition Schem:"Huge APP ...
    PSRAM: "OPI PSRAM"
7.キー入力は「Tera Term」を通して行う
    キー推しっ放しにも対応してくれるこのようなゲームには必須(かな?)

 LovyanGFX 用の個別設定ファイル(スケッチと同じフォルダーにコピーしておく)は次です。

myLovyanGFX.hpp
#pragma once
#define LGFX_USE_V1
#include <LovyanGFX.hpp>

class LGFX : public lgfx::LGFX_Device {
  lgfx::Panel_SSD1963 _panel_instance;
  lgfx::Bus_Parallel16 _bus_instance;  //16bit Parallelのインスタンス(ESP32s3)
  lgfx::Touch_XPT2046  _touch_instance;
public:
  LGFX(void) {  // バス制御の設定を行います。
    auto cfg = _bus_instance.config();  // バス設定用の構造体を取得します。
    // 16ビットパラレルバスの設定
    cfg.freq_write = 20000000;  // 送信クロック(最大20MHz,80MHzを整数割の値に丸める)
    cfg.pin_wr = 46;             // WR を接続しているピン番号
    cfg.pin_rd =  3;            //14;                        // RD を接続しているピン番号
    cfg.pin_rs =  9;            //3;                        // RS(D/C)を接続しているピン番号

    cfg.pin_d0 = 21;            // D0 を接続しているピン番号
    cfg.pin_d1 = 47;            // 0;//36;                        // D1 を接続しているピン番号
    cfg.pin_d2 = 45;            // D2 を接続しているピン番号
    cfg.pin_d3 =  0;             //35;//38;//37;                        // D3 を接続しているピン番号
    cfg.pin_d4 = 41;            // D4 を接続しているピン番号
    cfg.pin_d5 = 42;            //36;//39;//38;                        // D5 を接続しているピン番号
    cfg.pin_d6 =  2;             // D6 を接続しているピン番号s
    cfg.pin_d7 =  1;             //37;//40;//39;                        // D7 を接続しているピン番号

    cfg.pin_d8 =  8;//14;            //2;                        // D8 を接続しているピン番号
    cfg.pin_d9 = 18;//13;            // D9 を接続しているピン番号
    cfg.pin_d10= 17;//12;           //42;                        // D10を接続しているピン番号
    cfg.pin_d11= 16;//11;           // D11を接続しているピン番号
    cfg.pin_d12= 15;//10;            //41;                        // D12を接続しているピン番号
    cfg.pin_d13=  7;//9;            // D13を接続しているピン番号
    cfg.pin_d14=  6;//46;            //40;                        // D14を接続しているピン番号
    cfg.pin_d15=  5;//3;            // D15を接続しているピン番号
    _bus_instance.config(cfg);  // 設定値をバスに反映します。
    _panel_instance.setBus(&_bus_instance);  // バスをパネルにセットします。

    {  // 表示パネル制御の設定
      auto cfg = _panel_instance.config();  // 表示パネル設定用の構造体を取得します。
      cfg.pin_cs = 4;                       //8;           // CSが接続されているピン番号   (-1 = disable)
      cfg.pin_rst = -1;                     //9;      // RSTが接続されているピン番号  (-1 = disable)
      cfg.pin_busy = -1;                    // BUSYが接続されているピン番号 (-1 = disable)
      cfg.memory_width = 800;               // ドライバICがサポートしている最大の幅
      cfg.memory_height = 480;              // ドライバICがサポートしている最大の高さ
      cfg.panel_width = 800;                // 実際に表示可能な幅
      cfg.panel_height = 480;               // 実際に表示可能な高さ
      cfg.offset_x = 0;                     // パネルのX方向オフセット量
      cfg.offset_y = 0;                     // パネルのY方向オフセット量
      cfg.offset_rotation = 0;              // 回転方向の値のオフセット 0~7 (4~7は上下反転)
      cfg.dummy_read_pixel = 8;             // ピクセル読出し前のダミーリードのビット数
      cfg.dummy_read_bits = 1;              // ピクセル以外のデータ読出し前のダミーリードのビット数
      cfg.readable = true;                  // データ読出しが可能な場合 trueに設定
      cfg.invert = false;                   // パネルの明暗が反転してしまう場合 trueに設定
      cfg.rgb_order = true;                 // パネルの赤と青が入れ替わってしまう場合 trueに設定
      cfg.dlen_16bit = true;                // データ長を16bit単位で送信するパネルの場合trueに設定
      cfg.bus_shared = false;               // SDカードとバス共有はtrueに設定
      _panel_instance.config(cfg);
    }
    {  // mode and resolution 18bit/24bit 設定
      auto cfg = _panel_instance.config_timing_params();
      // cfg.data_width = lgfx::Panel_SSD1963::is18bit;         // 18bitパネル
      cfg.data_width = lgfx::Panel_SSD1963::is18bit_dithering;  // 18bitパネル(ディザリング有効)
      // cfg.data_width = lgfx::Panel_SSD1963::is18bit_FRC;     // 18bitパネル(FRC有効)
      // cfg.data_width = lgfx::Panel_SSD1963::is24bit;         // 24bitパネル
      cfg.pll_clock = 160000000;                                // pll_clock設定
      cfg.refresh_rate = 100;                                   // リフレッシュレート設定
      _panel_instance.config_timing_params(cfg);
    }

    {// タッチスクリーン制御の設定を行います。(必要なければ削除)
    auto cfg = _touch_instance.config();
        cfg.x_min      = 0;    // Minimum X value (raw value) obtained from touch screen // タッチスクリーンから得られる最小のX値(生の値)
        cfg.x_max      = 799;  // Maximum X value (raw value) obtained from the touch screen // タッチスクリーンから得られる最大のX値(生の値)
        cfg.y_min      = 0;    // Minimum Y value (raw value) obtained from touch screen // タッチスクリーンから得られる最小のY値(生の値)
        cfg.y_max      = 479;  // Maximum Y value (raw value) obtained from the touch screen // タッチスクリーンから得られる最大のY値(生の値)
        cfg.pin_int    = 14;   // Pin number to which INT is connected // INTが接続されているピン番号
        cfg.bus_shared = false; // Set to true if you are using the same bus as the screen // 画面と共通のバスを使用している場合 trueを設定
        cfg.offset_rotation = 0;// Adjustment when the display and touch orientation do not match Set with a value from 0 to 7 // 表示とタッチの向きのが一致しない場合の調整 0~7の値で設定
        cfg.spi_host = SPI2_HOST;// or SPI3 to use general purpose
        cfg.freq = 2500000;     // Set SPI clock // SPIクロックを設定
        cfg.pin_sclk = 12;     // Pin number to which SCLK is connected // SCLKが接続されているピン番号
        cfg.pin_mosi = 11;     // Pin number to which MOSI is connected // MOSIが接続されているピン番号
        cfg.pin_miso = 13;     // Pin number to which MISO is connected // MISOが接続されているピン番号
        cfg.pin_cs   = 10;     // Pin number to which CS is connected // CSが接続されているピン番号
    _touch_instance.config(cfg);
    _panel_instance.setTouch(&_touch_instance);//タッチスクリーンをパネルにセット
    }

    setPanel(&_panel_instance);  // 使用するパネルをセットします。
  }
};

 スケッチです。

Fn_Breakout.ino
#include <myLovyanGFX.hpp>
#include <string>

LGFX lcd;

#define SC_W 480  // 実際は 800 pixels のところ
#define SC_H 480
#define ROWS 24              // ゲーム画面の行数
#define COLS 60              // ゲーム画面の列数
#define ROW_H (SC_H / ROWS)  // 480/24=20
#define COL_W (SC_W / COLS)  // 480/60= 8。=char width
#define COLM (COLS / 2)
#define BLK_ROWS 6   // ブロック縦数
#define BLK_COLS 12  // ブロック横数
#define BL "block"   // ブロック
#define BL_W 5
#define PA "<paddle>" // パドル
#define PA_W 8
#define DNAVY 0x00042
int blk[BLK_ROWS][BLK_COLS];   // ブロックを管理
int px, py = ROWS - 1;  // パドルの座標
int bx, by = BLK_ROWS * ROW_H; // ボールの座標 pixel
int dx, dy;                    // ボールの移動量 pixel
int score = 0;             // スコア
int stage = 0;             // プレイ中は0、ゲームオーバーは1、ゲームクリアは2
int BLKs = 0;
boolean ad = false;
uint16_t c;

void init() {
  lcd.fillRect(0, 0, SC_W, SC_H, DNAVY);
  BLKs = 0;
  for (int y = 0; y < BLK_ROWS; y++) {  // ブロックの初期化
    for (int x = 0; x < BLK_COLS; x++) {
      if (y == 0) blk[y][x] = 0;  // no BLKs at top ROWS
      else {
        blk[y][x] = 1;
        BLKs++;
      }
    }
  }
  randomSeed(analogRead(19));
  bx = random(0, SC_W - COL_W);
  by = BLK_ROWS * ROW_H;
  dx = ((bx % 2) == 0) ? +2 : -2;
  dy = 2;
  px = COLM - 4,
  stage = 0;
  score = 0;
}

void setup() {
  Serial.begin(115200);
  lcd.begin();
  lcd.setRotation(0);
  lcd.setColorDepth(16);
  lcd.setFont(&fonts::efontJA_16_b);  // Japanese available monospaced font.
  lcd.fillRect(SC_W, 0, 800 - SC_W, SC_H, TFT_BLACK);
  lcd.startWrite();  // effective?
}

void drawBlPa(uint16_t c, String s, int x, int y) {
  lcd.setTextColor(c);
  lcd.drawString(s, x * COL_W, y * ROW_H);
}
void drawBall(int i, int x, int y) {
  if (i == 0) lcd.setTextColor(DNAVY);
  else lcd.setTextColor(TFT_YELLOW);
  lcd.drawString("●", x, y);
}
void draw_String(uint16_t c, String s, int x, int y) {
  lcd.setTextColor(c);
  lcd.drawString(s, x, y);
}
void draw_blks(){
  // ブロックを描画
  for (int y = 0; y < BLK_ROWS; y++) {
    for (int x = 0; x < BLK_COLS; x++) {
      if (blk[y][x] == 1) c = 0xc000 + 0x0111 * y;
      else c = DNAVY;
      drawBlPa(c, BL, x * BL_W, y);
    }
  }
}
int key = 0;
int x, y;
String s;
int bc, br;

void loop(void) {
  // タッチ
  uint16_t touchX, touchY;
  if (lcd.getTouch(&touchX, &touchY)) {
    Serial.print("tX ");
    Serial.print(touchX - 3650);
    Serial.print(" : tY ");
    Serial.println(touchY - 130);
  }

  if (ad == false) {  // Stand by
    init();
    draw_blks();
    draw_String(TFT_YELLOW, "Hit Any Key To Start", (COLM - 10) * COL_W, (ROWS / 2) * ROW_H);
    draw_String(TFT_YELLOW, "c (Hit either key) m", (COLM - 10) * COL_W, (ROWS / 2 + 2) * ROW_H);
    while (Serial.available() == 0) { ; }
    key = Serial.read();
    lcd.fillRect((COLM - 10) * COL_W, (ROWS / 2) * ROW_H, 20 * COL_W, ROW_H, DNAVY);
    lcd.fillRect((COLM - 10) * COL_W, (ROWS / 2 + 2) * ROW_H, 20 * COL_W, ROW_H, DNAVY);
    ad = true;
  }

  draw_blks();
  lcd.fillRect(6 * COL_W, (ROWS / 2) * ROW_H, 3 * COL_W, 17, DNAVY);  // 前スコア消去
  lcd.setTextColor(TFT_WHITE);
  lcd.drawString("SCORE " + (String)score, COL_W, (ROWS / 2) * ROW_H);  // 新スコア表示
  // バーの操作
  drawBlPa(DNAVY, PA, px, py);     // 前パドルを消す
  if (Serial.available() > 0) {    // 受信したデータが存在する
    key = Serial.read();           // 受信データを読み込む
    if (key == 67 || key == 99) {  // Cキーで左へ移動
      px -= PA_W / 2;
      if (px <= 0) px = 0;
    }
    if (key == 77 || key == 109) { // Mキーで右へ移動
      px += PA_W / 2;
      if (px >= COLS - PA_W) px = COLS - PA_W;
    }
  }
  drawBlPa(TFT_CYAN, PA, px, py);  // パドルを描く

  // ボールの移動。ボールはピクセルで描画、移動
  drawBall(0, bx, by);  // 前ボール消す
  bx += dx;
  by += dy;
  bc = bx / COL_W;
  br = (by - 8) / ROW_H;        // minor adjustment
  if (bc < 0) dx = 2;           // 画面左端にきた時
  if (bc >= COLS - 1) dx = -2;  // 画面右端にきた時
  if (br < 0) dy = 2;           // 画面上端にきた時
  if (br == ROWS - 2 &&         // パドルで打ち返した時
      bc >= px - 1 && bc <= px + PA_W) {
    dy = -2;                     // 上向き
    by--;                        // 上にずれる
    if (bx / COL_W == px - 1) {  // バーの左端
      dx = -2;                   // 左向き
      bx -= 3;                   // 左にずれる
    }
    if (bx / COL_W == px + PA_W) {  // バーの右端
      dx = 2;                       // 右向き
      bx += 3;                      // 右にずれる
    }
  }
  // ボールがブロックに当たったか判定
  br = (by + 12) / ROW_H;  // minor adjustment
  if (0 <= br && br < BLK_ROWS) {
    x = bx / (COL_W * BL_W);  // BL_Wはブロックの幅(半角文字数)
    y = by / ROW_H;
    if (blk[y][x] == 1) {                  // ブロックがある
      drawBlPa(TFT_RED, BL, x * BL_W, y);  // 壊した演出
      blk[y][x] = 0;                       // ブロックを存在しない状態にする
      dy = -dy;                            // y軸方向の移動量を反転
      by--;
      score += 1;                   // スコアを増やす
      if (score == BLKs) stage = 2; // 全て壊したらクリア
    }
  }
  if (by / ROW_H == ROWS - 1) {
    if (stage != 2) stage = 1;  // 画面下端に達したらゲームオーバー
  }

  if (stage > 0) {
    switch (stage) {
      case 2:
        s = "Congratulations!"; // ゲームクリア
        break;
      case 1:
        s = "xx GAME OVER xx";  // ゲームオーバー
        break;
    }
    draw_String(TFT_RED, s, (COLM - s.length() / 2) * COL_W, SC_H / 2);
    delay(4000);
    ad = false;
  }

  drawBall(1, bx, by);  // ボールを描く
  delay(1);             // 処理の一時停止
}

 最後まで見ていただきありがとうございました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?