今回でテトリスを完成させたいと思います。完成と言ってもテトリミノが上から降ってきて、揃えて消すくらいのことしかやりません。
emWinを使ってテトリスを作成すると言っても、とても基本的なことしか行っていません。もっと高度なGUIや画面の描画をする場合には、専用のツールなどが必要になると思われます。
ゲームを完成させるとは、本来、ゲームの難易度選択などのメニューやゲームの開始、得点のカウント、ゲーム終了処理などなど実装するものはたくさんあります。
ここで紹介しているテトリスは、その本来のゲームの完成とは程遠いのですがやりたかったことが出来たという状態で完成といたします。
さて、早速、テトリスを完成させて行きましょう。
コンテンツ
今までのおさらい
さて、今回までのおさらいですが、ま〜大したことはやっていません。
- テトリミノのブロックとして、自分でBitmapイメージを作成し、ARM用バイナリーに変換。
- 変換したBitmapイメージをプロジェクトに組み込み、Bitmapイメージを描画しようとすると非常に時間が掛かって失敗。
- 内蔵SRAM(TCMメモリ)からイメージを読込んでみたり関数の実行を行っても描画時間は変わらない。
- emWinの基本図形描画APIを使用して描画することで高速化が可能。それで妥協する。
という内容でした。
今回行うこと
- テトリミノが1秒毎に下に落ちるようにしたい
- ターミナル のキーボード入力からテトリミノの移動を行う
- テトリミノのタイプ毎にブロックの色を変えたい
早速、コードを実装していきます。
テトリミノが1秒毎に下に落ちるようにしたい
まず、今の状態ではテトリミノが落ちてきません。まずは、GUIで画面が描画できるか確認するためのコードしか動かしていないからです。
そこで、前回LPC54102を使用してテトリスを作ってみた記事から下記の部分のコードをコピペしてきました。そして、下に落ちる部分のコードは、下のように一部修正しています。
以前の記事の末尾にgithubへのリンクがあり、ソースコードを公開しています。LPC54102のコードです。今回作成したi.MXRT1060用のソースコードもgithubに公開しています。
以前の記事はこちらから→
ソースコードを全て掲載しても良いのですが、コードだけのページになってしまうので、分割して掲載しています。
/******************************************************************************* * グローバル変数 ******************************************************************************/ bool gptIsrFlag; char field[FIELD_HEIGHT][FIELD_WIDTH]; char dispBuffer[FIELD_HEIGHT][FIELD_WIDTH]={0}; GUI_MEMDEV_Handle hMem; int minoX=5, minoY=1; uint32_t minoType=0, minoAngle=0; int cell_xSize = LCD_HEIGHT/12; int cell_ySize = LCD_WIDTH/22;
/* テトリミノが移動できるかどうかの関数 */ bool isHit(uint32_t _minoX, uint32_t _minoY,uint32_t _minoType, uint32_t _minoAngle){ for(uint32_t y=0; y < MINO_HEIGHT; y++){ for(uint32_t x=0; x <MINO_WIDTH; x++){ if(minoShapes[_minoType][_minoAngle][y][x]&&field[_minoY +y][_minoX +x]) return true; } } return false; } /* 降ってくるテトリミノのランダム選択関数 */ void resetMino(){ minoX = 5, minoY =0; minoType = rand() % MINO_TYPE_MAX; minoAngle = rand() % MINO_ANGLE_MAX; }
下記while文は、main()関数内に配置します。
/* 以下while文はmain()関数内に配置します。 GPTタイマー * が1秒カウントすると全体のブロックを1段下に落とす。 */ while(1){ //if(utickIntFlag){ if(gptIsrFlag){ for (int y=0; y< FIELD_HEIGHT; y++){ field[y][0] = field[y][FIELD_WIDTH-1] = 1; } for(int x=0; x < FIELD_WIDTH; x++) field[FIELD_HEIGHT-1][x]=1; if(isHit( minoX,//uint32_t _minoX, minoY+1,//uint32_t _minoY, minoType,//uint32_t _minoType, minoAngle//uint32_t _minoAngle )){ for(uint32_t y=0; y < MINO_HEIGHT; y++){ for(uint32_t x=0; x <MINO_WIDTH; x++){ field[minoY + y][minoX + x] |= minoShapes[minoType][minoAngle][y][x]; } } for (uint8_t y=0; y<FIELD_HEIGHT-1; y++){ bool lineFill = true; for(uint8_t x=1; x<FIELD_WIDTH-1; x++){ if(!field[y][x]) lineFill=false; } if (lineFill){ //for (uint8_t x=1; x<FIELD_WIDTH-1; x++) // field[y][x]=0; for (uint8_t j = y; 0<j; j--) memcpy(field[j], field[j-1], FIELD_WIDTH); } } resetMino(); }else{ minoY++; } display(); } }
以前使用したマイコンは、LPC54102でした。今回は、i.MXRT1060を使用していて、1秒をカウントするタイマーモジュールは異なります。
i.MXRTの汎用タイマーである「GPTタイマー」を使用したいと思います。
このコピペしてきたコードの中に、「utickIntFlag」というフラグがありますが、これはUTICKタイマーが1秒カウントした時に割込みが発生したことを管理するフラグです。
今回は、UTICKタイマーではなく、GPTタイマーを使用するのでわかり易く「gptIsrFlag」に変更します。
このフラグが立っていたら、テトリミノを下に移動にして、横のラインが揃っていたら消して上のブロックを下に降ろすというコードです。
GPTタイマーの割込みハンドラ内でgptIsrFlag=true;に設定しています。
display()関数の頭で、gptIsrFlag = false; を挿入して、フラグをクリアしておきましょう。そうしないと、常にフラグが立ってしまい、テトリミノが高速で下に落ちることになります。
最後に、テトリミノをリセットして、画面を再描画してその繰り返しですね。
繰り返しなので、この部分はwhile(1)文でループさせます。
GPTタイマーの実装
i.MXRTのGPTタイマーの初期化ですが、NXPのSDKドライバーは、基本的に使用するドライバのコンフィグを設定して、xxx_init()関数に引数としてセットして初期化するだけでOKです。
GPTタイマーを使用するために、SDKのGPTタイマードライバを組込む必要があります。



初期化とハンドラの実装は以下のようにします。
GPTタイマーの初期化
/* GPTタイマーを使用するためヘッダーファイルをインクルードしておく*/ #include "fsl_gpt.h" /* GPTタイマードライバに必要な変数でmain()内で宣言 */ gpt_config_t gpt_config; uint32_t gptFreq; /* gptIsrFlagはクリアして初期化しておきます */ gptIsrFlag = false; /*以下コードは、main()関数ボードの初期化関数の後くらいに記述 */ /***コンフィグ設定のデフォルト値 * config->clockSource = kGPT_ClockSource_Periph; * config->divider = 1U; * config->enableRunInStop = true; * config->enableRunInWait = true; * config->enableRunInDoze = false; * config->enableRunInDbg = false; * config->enableFreeRun = true; * config->enableMode = true; */ /* GPTタイマーのデフォルトコンフィグ設定の取得と初期化*/ GPT_GetDefaultConfig(&gpt_config); GPT_Init(GPT1, &gpt_config); /* GPTタイマーのクロックソースのクロック設定*/ GPT_SetClockDivider(GPT1, 3); /* GPT timerのタイマーのタイマー(時刻:アウトプットコンペア)設定 */ gptFreq= CLOCK_GetFreq(kCLOCK_PerClk); gptFreq /=3; GPT_SetOutputCompareValue(GPT1, kGPT_OutputCompare_Channel1, gptFreq); /* GPTタイマーのアウトプットコンペア割込みをイネーブル */ GPT_EnableInterrupts(GPT1, kGPT_OutputCompare1InterruptEnable); /* GPTタイマーの割込み監視をスタート */ EnableIRQ(GPT1_IRQn); /* GPTタイマーをスタート */ GPT_StartTimer(GPT1);
GPTタイマーの割込みハンドラの実装
/* GPTタイマーが1秒カウントすると割込みが入る。このハンドラでgptIsrFlagを立てる */ void GPT1_IRQHandler(void) { /* GPTタイマーのアウトプットコンペアの割込みフラグをクリアする*/ GPT_ClearStatusFlags(GPT1, kGPT_OutputCompare1Flag); /* 1秒経ったことを判定するフラグgptIsrFlagを立てる */ gptIsrFlag = true; /* ここから下はARMのエラッタに対する修正コードです。 */ /* Add for ARM errata 838869, affects Cortex-M4, Cortex-M4F, Cortex-M7, Cortex-M7F Store immediate overlapping exception return operation might vector to incorrect interrupt */ #if defined __CORTEX_M && (__CORTEX_M == 4U || __CORTEX_M == 7U) __DSB(); #endif }
とりあえず、これらのコードを追加してビルドしてみます。
デバッグをしてみると、無事テトリミノが1秒毎に下に落ちてくると思います。ビルドエラーが出る場合には、この記事の最後にgithubへのリンクがありますので、そのソースをコードを確認してみてください。
ターミナル のキーボード入力からテトリミノの移動を行う
次に、テトリミノの移動を行っていきます。
これも、基本的にはLPC54102のコードがそのまま使用出来ます。ただ、一点異なるのは、UARTのレジスタが異なるので修正しています。ターミナル から入力したキャラクタが入力されたか判定するためにこのレジスタをチェックしています。
そして、下記のコードを先ほどの下に落ちるコードの上に挿入します。テトリミノを移動してから下に落として揃ったか判定するためです。
/* 下記コードはwhile文の中にテトリミノが下に落ちるコードの上に配置します。*/ char ch; //if (USART0->STAT&USART_STAT_RXRDY_MASK){ if (LPUART1->STAT&LPUART_STAT_RDRF_MASK){ //switch(ch=LPUART1->DATA&0xFF){ switch(ch=DbgConsole_Getchar()){ case 'a': if(!isHit( //minoX-1, //uint32_t _minoX, minoX+1, //uint32_t _minoX, minoY, //uint32_t _minoY, minoType, //uint32_t _minoType, minoAngle //uint32_t _minoAngle) )) //minoX--; minoX++; break; case 'd': if(!isHit( //minoX+1, //uint32_t _minoX, minoX-1, //uint32_t _minoX, minoY, //uint32_t _minoY, minoType, //uint32_t _minoType, minoAngle //uint32_t _minoAngle) )) //minoX++; minoX--; break; case 's': if(!isHit( minoX, //uint32_t _minoX, minoY+1, //uint32_t _minoY, minoType, //uint32_t _minoType, minoAngle //uint32_t _minoAngle) )) minoY++; break; case 0x20: if(!isHit( minoX, //uint32_t _minoX, minoY, //uint32_t _minoY, minoType, //uint32_t _minoType, (minoAngle+1) % MINO_ANGLE_MAX//uint32_t _minoAngle) )) minoAngle = (minoAngle+1) % MINO_ANGLE_MAX; break; default: break; } display(); }
ここで注意が必要なのは、LCD画面の横(480ピクセル)をテトリスのゲーム縦画面に、LCD画面の縦(272ピクセル)を横画面にしたいので、XとYを逆にしていて、さらに’a’を押した時はテトリミノが左に(Xを増やす)、’d’を押した時は右に移動して欲しい(Xを減らす)ので、上記のコードのように修正しています。
テトリミノのタイプ毎にブロックの色を変えたい
最後に、テトリミノのブロック毎に色を付けたいと思います。テトリスのWikiペディアには、次のように決められているので、できるかぎり同じ色にしたいと思います。
- I-テトリミノ(水色)
- 4列消し「テトリス」を決めることのできる唯一のテトリミノ。
- O-テトリミノ(黄色)
- 回転させても形の変わらないテトリミノ。
- S-テトリミノ(緑)
- Z-テトリミノ(赤)
- J-テトリミノ(青)
- L-テトリミノ(オレンジ)
- T-テトリミノ(紫)
enum型で色を定義して、minoShapes配列を次のように書き換えます。定義しているテトリミノ毎に今まではブロック有無を0/1で表現していましたが、ブロック無しは0のまま、ブロックが有りの場合は1以上で色毎に列挙した番号を付けて行きます。
そして、display()関数も少し変えて、最後のテトリミノを描画する部分(GUI_FillRect())を関数化して、テトリミノの色毎にGUI_SetColor()で色を設定して描画しています。
enum { MINO_COLOR_GRAY=1, MINO_COLOR_LIGHT_BLUE, MINO_COLOR_YELLOW, MINO_COLOR_GREEN, MINO_COLOR_RED, MINO_COLOR_BLUE, MINO_COLOR_ORANGE, MINO_COLOR_MAGENTA, MINO_COLOR_MAX }; char minoShapes[MINO_TYPE_MAX][MINO_ANGLE_MAX][MINO_WIDTH][MINO_HEIGHT] = { {//MINO_TYPE_I, LIGHT_BLUE //MINO_ANGLE_0, { {0, 2, 0, 0}, {0, 2, 0, 0}, {0, 2, 0, 0}, {0, 2, 0, 0}, }, //MINO_ANGLE_90, { {0, 0, 0, 0}, {0, 0, 0, 0}, {2, 2, 2, 2}, {0, 0, 0, 0}, }, //MINO_ANGLE_180, { {0, 0, 2 ,0}, {0, 0, 2, 0}, {0, 0, 2, 0}, {0, 0, 2, 0}, }, //MINO_ANGLE_270, { {0, 0 ,0 ,0}, {2, 2 ,2 ,2}, {0, 0 ,0 ,0}, {0, 0 ,0 ,0}, }, }, //MINO_TYPE_O,YELLOW { //MINO_ANGLE_0, { {0, 0, 0 ,0}, {0, 3, 3 ,0}, {0, 3, 3 ,0}, {0, 0, 0 ,0}, }, //MINO_ANGLE_90, { {0, 0, 0, 0}, {0, 3, 3, 0}, {0, 3, 3, 0}, {0, 0, 0 ,0}, }, //MINO_ANGLE_180, { {0, 0, 0, 0}, {0, 3, 3, 0}, {0, 3, 3, 0}, {0, 0, 0, 0}, }, //MINO_ANGLE_270, { {0, 0, 0, 0}, {0, 3, 3, 0}, {0, 3 ,3, 0}, {0, 0, 0, 0}, }, }, //MINO_TYPE_S,GREEN { //MINO_ANGLE_0, { {0, 0, 0, 0}, {0, 4, 4, 0}, {4, 4, 0, 0}, {0, 0, 0, 0}, }, //MINO_ANGLE_90, { {0, 4, 0, 0}, {0, 4, 4, 0}, {0, 0, 4, 0}, {0, 0, 0, 0}, }, //MINO_ANGLE_180, { {0, 0, 0 ,0}, {0, 0, 4, 4}, {0, 4, 4 ,0}, {0, 0, 0 ,0}, }, //MINO_ANGLE_270, { {0, 0, 0, 0}, {0, 4, 0, 0}, {0, 4, 4, 0}, {0, 0, 4, 0}, }, }, //MINO_TYPE_Z, { //MINO_ANGLE_0, { {0, 0, 0, 0}, {5, 5, 0, 0}, {0, 5, 5, 0}, {0, 0, 0, 0}, }, //MINO_ANGLE_90, { {0, 0, 0, 0}, {0, 0, 5, 0}, {0, 5, 5, 0}, {0, 5, 0, 0}, }, //MINO_ANGLE_180, { {0, 0, 0, 0}, {0, 5, 5, 0}, {0, 0, 5, 5}, {0, 0, 0, 0}, }, //MINO_ANGLE_270, { {0, 0, 5, 0}, {0, 5, 5, 0}, {0, 5, 0, 0}, {0, 0, 0, 0}, }, }, //MINO_TYPE_J, { //MINO_ANGLE_0, { {0, 0, 6, 0}, {0, 0, 6, 0}, {0, 6, 6, 0}, {0, 0, 0, 0}, }, //MINO_ANGLE_90, { {0, 0, 0, 0}, {6, 6, 6, 0}, {0, 0, 6, 0}, {0, 0, 0, 0}, }, //MINO_ANGLE_180, { {0, 0, 0, 0}, {0, 6, 6, 0}, {0, 6, 0, 0}, {0, 6, 0, 0}, }, //MINO_ANGLE_270, { {0, 0, 0, 0}, {0, 6, 0, 0}, {0, 6, 6, 6}, {0, 0, 0, 0}, }, }, //MINO_TYPE_L, { //MINO_ANGLE_0, { {0, 7, 0, 0}, {0, 7, 0, 0}, {0, 7, 7, 0}, {0, 0, 0, 0}, }, //MINO_ANGLE_90, { {0, 0, 0, 0}, {0, 0, 7, 0}, {7, 7, 7, 0}, {0, 0, 0, 0}, }, //MINO_ANGLE_180, { {0, 0, 0, 0}, {0, 7, 7, 0}, {0, 0, 7, 0}, {0, 0, 7, 0}, }, //MINO_ANGLE_270, { {0, 0, 0, 0}, {0, 7, 7, 7}, {0, 7, 0, 0}, {0, 0, 0, 0}, }, }, //MINO_TYPE_T, { //MINO_ANGLE_0, { {0, 0, 0, 0}, {8, 8, 8, 0}, {0, 8, 0, 0}, {0, 0, 0, 0}, }, //MINO_ANGLE_90, { {0, 0, 0, 0}, {0, 8, 0, 0}, {0, 8, 8, 0}, {0, 8, 0, 0}, }, //MINO_ANGLE_180, { {0, 0, 0, 0}, {0, 0, 8, 0}, {0, 8, 8, 8}, {0, 0, 0, 0}, }, //MINO_ANGLE_270, { {0, 0, 8, 0}, {0, 8, 8, 0}, {0, 0, 8, 0}, {0, 0, 0, 0}, }, }, }; void fieldDraw(int _color, int _y, int _x){ switch(_color){ case MINO_COLOR_GRAY: GUI_SetColor(GUI_GRAY); break; case MINO_COLOR_LIGHT_BLUE: GUI_SetColor(GUI_LIGHTBLUE); break; case MINO_COLOR_YELLOW: GUI_SetColor(GUI_YELLOW); break; case MINO_COLOR_GREEN: GUI_SetColor(GUI_GREEN); break; case MINO_COLOR_RED: GUI_SetColor(GUI_RED); break; case MINO_COLOR_BLUE: GUI_SetColor(GUI_BLUE); break; case MINO_COLOR_ORANGE: GUI_SetColor(GUI_ORANGE); break; case MINO_COLOR_MAGENTA: GUI_SetColor(GUI_MAGENTA); break; default: break; } GUI_FillRect(_y * cell_ySize, _x * cell_xSize, (_y * cell_ySize) + cell_ySize -2 , (_x * cell_xSize) + cell_xSize -2); } void display(){ /* GPTタイマーのフラグをクリア */ gptIsrFlag = false; /* Screen clear */ //PRINTF("\033[2J"); //Escape Sequence Clear screen //system("cls"); //PRINTF("\033[1;1f"); //Cursor moves to (1,1) GUI_MEMDEV_Select(hMem); GUI_Clear(); memcpy(dispBuffer, field, sizeof(field)); for (int y =0; y< MINO_HEIGHT; y++) for(int x =0; x < MINO_WIDTH; x++) dispBuffer[minoY +y][minoX+x] |= minoShapes[minoType][minoAngle][y][x]; /* Drawing */ for (int y =0; y < FIELD_HEIGHT; y++){ for(int x=0; x<FIELD_WIDTH; x++){ int color; if ((color = dispBuffer[y][x])){ //PRINTF("◽"); //int cell_xSize = LCD_HEIGHT/12; //int cell_ySize = LCD_WIDTH/22; /* BMPイメージの描画は時間が掛かりすぎるのでボツ */ //GUI_BMP_Draw(gray,y * cell_ySize, x * cell_xSize); /*GUI_FillRect()を関数化*/ //GUI_FillRect(y * cell_ySize, x * cell_xSize, (y * cell_ySize) + cell_ySize -2 , (x * cell_xSize) + cell_xSize -2); fieldDraw(color, y, x); }else{ //PRINTF(" "); } } //PRINTF("\n\r"); } GUI_MEMDEV_Select(0); GUI_MEMDEV_CopyToLCD(hMem); //PRINTF("Interrupt occurred.\n\r"); }
これで、ターミナルから’a’、’d’、’s’とスペースキーでテトリミノの移動、回転を行うことができ、テトリミノの色もちゃんと付いていると思います。

は〜〜〜出来た〜〜。
でも、rand()関数は、いつも同じ乱数を出力するので、いつも同じ順番でテトリミノが落ちてきます。本来は、srand()関数で現在時刻などを引数としてシードを変化させるのですが、マイコンなのでそれが出来ません。i.MXRTのセキュリティ機能でTrue Random Number Generatorという機能がありますので、もし興味ある方はご自身でTRNGを使用して乱数を取得し、srand()に突っ込んでテトリミノの落ちてくる順番をランダムにしてみてはいかがでしょうか。
最後に
今回は、「emWin」を使用してテトリスを作成してみました。とても簡単なことしか行っていませんので、是非みんさんもemWinを使用してGUIや、私のようにゲームを描画してみて遊んでみてください。
全体のmain()を含むソースコード(emwin_serial_terminal.c)は、下のgithubに公開していますので、ビルドエラーなどが発生する場合は、参考にしてください。
あ〜〜、emwin_support.hのemwinに割当てメモリサイズ設定もお忘れなく。
githubはこちらから→https://github.com/mcuthings/tetris-rt1060
自分で作ったものが画面に表示されると一際、達成感があります。マイコンを使った電子工作はLEDのLチカなどが一般的ですが、LCD画面に映して遊ぶことでもっと楽しめます。