老眼の私にも優しい「7インチ 800x480 TFT-LCDモジュール」でブロック崩しゲームを作ってみました。CUI上のモノを参考にしたので、ブロックとパドルとボールはキャラクターです。
結構、軽快に動きます。次のような工夫を加えました。
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 用の個別設定ファイル(スケッチと同じフォルダーにコピーしておく)は次です。
#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); // 使用するパネルをセットします。
}
};
スケッチです。
#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); // 処理の一時停止
}
最後まで見ていただきありがとうございました。
