前の記事
全体の目次
Longan Nanoを使ってみる 1 ~ビルド環境の構築~
Longan Nanoを使ってみる 2 ~デバッガの環境設定~
Sipeed RISC-V Debugger
Longan Nanoを使ってみる 3 ~デバッガの使用方法~
Longan Nanoを使ってみる 4 ~printfを使ったデバッグ~
Longan Nanoを使ってみる 5 ~ゲームのプロジェクトを作成~
Longan Nanoを使ってみる 6 ~文字出力~
Longan Nanoを使ってみる ~FONTX2ファイルを作る~
Longan Nanoを使ってみる 7 ~外枠とブロックを書く~
Longan Nanoを使ってみる ~謎の画像表示関数~
Longan Nanoを使ってみる 8 ~ボールを動かす~
Longan Nanoを使ってみる 9 ~A/Dコンバータから入力~
Longan Nanoを使ってみる 10 ~パドルを動かす~
Longan Nanoを使ってみる 11 ~ボタンの入力
Longan Nanoを使ってみる 12 ~ボールのロス~
Longan Nanoを使ってみる 13 ~ステージの遷移とゲームオーバー~
Longan Nanoを使ってみる 14 ~PWMとサウンド~
Longan Nanoを使ってみる 15 ~音楽を鳴らす~
Longan Nanoを使ってみる 16 ~とりあえずのまとめ~
グラフィックスライブラリ
文字の出力ができる環境であれば、基本的なグラフィックス機能はすでに使用可能になっている。
注意
このページは、quiita.com で公開されています。URLがqiita.com以外のサイト、例えばjpdebug.comなどのページでご覧になっている場合、悪質な無許可転載サイトで記事を見ています。正しいURLは、https://qiita.com/BUBUBB/items/7ce85ada67a3f6d1944d です。
無許可転載サイトでの権利表記(CC BY SA 2.5、CC BY SA 3.0とCC BY SA 4.0など)は、不当な表示です。
正確な内容は、qiitaのページで参照してください。
グラフィック関数
LCD_Init();とLCD_Clear(BLACK);を実行した後であれば、lcd.cに含まれる様々な関数が使用できる。今回のプログラムでは、次の関数を使用する。
void LCD_Clear(u16 Color)
関数説明: 画面を指定色で塗りつぶす
パラメータ: Color = 描画色
戻り値: なし
void LCD_Fill(u16 xsta,u16 ysta,u16 xend,u16 yend,u16 color)
関数説明: 矩形領域を塗りつぶす
パラメータ:xsta,ysta = 左上座標
xend,yend = 右下座標
color = 描画色
戻り値: なし
void LCD_DrawLine(u16 x1,u16 y1,u16 x2,u16 y2,u16 color)
関数説明: 直線を描画する
パラメータ:x1,y1 = 始点座標
x2,y2 = 終点座標
color = 描画色
戻り値: なし
void LCD_ShowString(u16 x,u16 y,const u8 *p,u16 color)
関数説明: 文字列を表示する(2バイト文字に対応するには USE_UTF8STR を定義)
パラメータ:x,y = 表示開始(左上)座標
p = 文字列へのポインタ
color = 描画色
戻り値: なし
ゲームのフィールドとブロックを表示させる
今回、ブロック崩しを作るので、画面に次の物体を表示させる。
- スコアとライフ
- 外枠
- ブロック
当然、画面に絵をかくのは簡単なのだが、特にブロックはその衝突確認などの処理が必要になるため、絵を描くだけでは済まない。また、これらは書き込むタイミングにも注意が必要。
これを含めて、いくつかの関数を追加する。
外枠などの固定領域を表示する
DrawBORDER関数。
ここでは、固定領域を表示している。あとからの変更が容易になるように、ゲーム領域は#defineで定義した値にしてある。
この値は、表示だけではなく、ボールが壁に跳ね返る、といった処理でも判定に使用される。
使用されているグラフィック関数は、線を引く LCD_DrawLIne、文字を出力するLCD_ShowString。
game.h
:
#define GAMEAREA_X0 0
#define GAMEAREA_Y0 20
#define GAMEAREA_X1 79
#define GAMEAREA_Y1 148
:
extern int LifeCnt; // ライフ残数
extern int Score; // スコア
game.c
int LifeCnt = 0; // ライフ残数
int Score = 0; // スコア
// 外枠とスコア、ライフ残を表示する
void DrawBORDER()
{
LCD_DrawLine(GAMEAREA_X0,GAMEAREA_Y1,GAMEAREA_X0,GAMEAREA_Y0,WHITE);
LCD_DrawLine(GAMEAREA_X0,GAMEAREA_Y0,GAMEAREA_X1,GAMEAREA_Y0,WHITE);
LCD_DrawLine(GAMEAREA_X1,GAMEAREA_Y0,GAMEAREA_X1,GAMEAREA_Y1,WHITE);
LCD_ShowString(0,0,(const u8 *)"SCORE:",WHITE);
LCD_ShowString(10,160-12,(const u8 *)"LIFE:",WHITE);
char life[3];
sprintf(life,"%1d",LifeCnt);
LCD_ShowString(55,160-12,(u8 *)life,WHITE);
u8 scr[12];
sprintf((char *)scr,"%5d0",Score);
LCD_ShowString(38,0,scr,WHITE);
}
ブロックの初期化と表示
次に、画面にブロックを表示させる。ブロックは、外枠やスコアの文字などとは違い、ただ表示させるだけではダメで、管理する必要がある。ブロックは存在する場所もあるし、しない場所もある。ボールとぶつかれば消える。こうした処理を実現するためには、画面に描くだけというわけにはいかない。
そこで、データ構造を作り、データを設定し、それをブロックとして表示する。
ブロックのデータ構造
ここでは、画面のうち、ゲームに使う部分(外側の枠線と、1ドットのマージンを引いた区画)全体をBLOCK_CNT_H x BLOCK_CNT_Vの領域に分けて、それをBLOCK_INFOという構造体に情報を分けていく。
それぞれのマス目は、blockmtx[x][y]に対応し、ブロックが存在する場合、ブロックの種類(ここでは1種類しかないので不要だが、将来のために用意した)と、blockmtx[x][y]の中にブロックの座標を格納する。ブロックの座標は、マス目の左上から、マス目のサイズより少し小さくした値を指定する。(ここでは、横方向に2ドット、縦方向に3ドット小さくしている。
ソースコードでは、次のようになる。
block.h
// 表示されるブロックの規定値
#define BLOCK_CNT_H 7 // ブロックは横に7個
#define BLOCK_CNT_V 21 // ブロックは縦に最大21個
#define BLOCK_SIZE_W 11 // ブロックの横幅は11ドット
#define BLOCK_SIZE_H 6 // ブロックの縦は6ドット
// ブロックのテーブル
// 画面上はBLOCK_CNT_H x BLOCK_CNT_Vのマス目に分類され、そこにあるブロックの
// 種類が item に入っている。
// テーブルには、事前にこのブロックが存在する矩形の情報が入っており、ボールとの
// 衝突判定ではこの座標が使われる。
struct BLOCKINFO {
u8 item;
u8 x1;
u8 y1;
u8 x2;
u8 y2;
} blockmtx[BLOCK_CNT_H][BLOCK_CNT_V];
ブロックの初期化
ブロックの初期化処理では、画面全体の領域から、上3つめから7つ目までにブロックを配置している。
各ブロック左上のX座標は、 j * BLOCK_SIZE_W という、ブロックの幅と横方向のインデックスの積に、(GAMEAREA_X0 + 2)として、枠線部分と2ドットのマージンを足している。Y座標も同様にマージンの足しこみが行われている。
各ブロック右下の座標は、左上の座標にマス目の横幅を足し、マージンを引いている。こうすることで、ブロックとブロックに隙間ができて、ブロック崩しっぽくなる。隙間は自由に調整できるが、この値は見た目の良しあしに結構影響するので、変に調整すると出来が悪く見えてしまう。
データ構造の定義部分。このデータ構造をblockmtxという配列で用意する。合わせて、画面に存在するブロック数や残りブロック数の変数も使いそうなので用意しておく。ブロックを初期化するのに合わせて、ブロックの総数なども初期化しておく。
block.h
block.hには、外部から呼び出すための関数プロトタイプを追加する。
//全体のブロック数と、残りのブロック数。ブロックが少なくなったらボールの速度を上げる、などに使用する。
extern int blkCnt; // 総ブロック数
extern int blkBrk; // 残りブロック数
:
void DrawBlock();
:
block.c
int blkCnt = 0; // 総ブロック数
int blkBrk = 0; // 残りブロック数
void InitBlock()
{
// ブロックテーブルを初期化する。
memset((void *)blockmtx,0,sizeof(blockmtx));
blkCnt = 0;
for (int i = 0;i<BLOCK_CNT_H;i++) {
for (int j = 3;j<7;j++) {
blockmtx[i][j].item = 1;
blockmtx[i][j].x1 = i * BLOCK_SIZE_W + (GAMEAREA_X0 + 2);
blockmtx[i][j].y1 = j * BLOCK_SIZE_H + (GAMEAREA_Y0 + 2);
blockmtx[i][j].x2 = blockmtx[i][j].x1 + BLOCK_SIZE_W - 2 ;
blockmtx[i][j].y2 = blockmtx[i][j].y1 + BLOCK_SIZE_H - 3 ;
blkCnt++;
}
}
blkBrk = blkCnt;
}
ブロックの表示
ブロックを画面に表示させるのは非常に簡単。データの中にブロックの座標が含まれているので、これをそのまま表示していくだけでよい。ブロック崩しの場合、ブロックは途中で増えたり移動したりしない。消えるだけ。そのため、1つを表示する、という処理は不要で、すべてを表示するという処理を最初に一回呼べばよいことになる。
それっぽく見せるため、色を変えている。 u16 col = colTbl[j % 6]; が意味不明、と言われたので補足すると、色は colTblで6色定義されているので、0~5で繰り返し色を選択させたい。 if (color > 5) {color = 0;}などとやるのが面倒なので、剰余を使って0~5の繰り返しを生成している。
block.h
block.hには、外部から呼び出すための関数プロトタイプを追加する。
:
void DrawBlock();
:
block.c
// ブロックすべてを描画する
// ブロック崩しでは、ブロックが1つだけ表示される、ということはない(消えていくだけ)ので、
// 表示は無条件で全ブロックを表示させれば良いことになる。
void DrawBlock()
{
static u16 colTbl[] = {RED, BLUE, GREEN ,MAGENTA,CYAN, YELLOW};
for (u8 i = 0 ; i< BLOCK_CNT_H;i++) {
for (u8 j = 0; j<BLOCK_CNT_V;j++) {
u16 col = colTbl[j % 6];
if (blockmtx[i][j].item == 1) {
LCD_Fill(blockmtx[i][j].x1,blockmtx[i][j].y1,blockmtx[i][j].x2,blockmtx[i][j].y2,col);
}
}
}
}
メインループからの呼び出し
今回のプログラムは、タイマーで駆動されるステートマシンになっている。それを踏まえて、ゲームのメイン処理中に今回の処理を加えていく。
今回実装した、固定部分の表示、ブロック配置の初期化、ブロックの描画は、ゲームの開始前に一度だけ実行することになる。そこで、STATE_STARTGAME状態の処理中に、これらの処理を呼び出し、処理が終わったらゲームを開始する。
game.c
void Game(void)
{
:
:
gameState = STATE_STARTGAME; //初期化処理が終了したのでゲーム開始処理を行う
:
while (gameState != STATE_GAMEOVER) { //ゲームオーバーになるまで繰り返す
:
switch (gameState) {
case STATE_STARTGAME:{
/*ゲームの開始処理 */
Score = 0;
LifeCnt = 3;
DrawBORDER(); //外枠とライフ残、スコアを画面に表示させる
InitBlock();
DrawBlock();
gameState = STATE_INGAME;
break;
}
case STATE_INGAME:{
/*ゲームのメイン処理*/
:
:
実際には、そのほかの処理もSTATE_STARTGAME中に書かなければならないが、今のところはこの状態。
(そのほかの処理…パドルを初期化する、ボールを初期化するなど)
今回の追加で表示される画面
今回の追加の結果、プログラムを実行すると次のように画面に表示される。俄然ゲームっぽくなってくる。

ここまででできたこと
基本的な画面への描画を行った。 ブロック崩しの「ブロック」について、そのデータの構造を決め、それに基づいて画面にブロックを描画した。
次の記事に進む
関連記事
今回の追加を反映したソースコード
game.h
#ifndef __GAME_H__
#define __GAME_H__
extern void Game(void);
// ゲームの表示領域
#define GAMEAREA_X0 0
#define GAMEAREA_Y0 20
#define GAMEAREA_X1 79
#define GAMEAREA_Y1 148
extern int LifeCnt;
extern int Score;
#endif
game.c
#include "lcd/lcd.h"
#include "led.h"
#include "memory.h"
#include "gd32vf103.h"
#include "game.h"
#include "ball.h"
#include "block.h"
#include "paddle.h"
enum GAMESTATE gameState; // ゲームの状態
volatile u8 WakeFlag = 0; // このフラグが1になると、処理が開始される
int LifeCnt = 0;
int Score = 0;
//
// 割り込みハンドラ。タイマーにより指定した周期で非同期に呼び出される
//
void TIMER5_IRQHandler(void)
{
if(SET == timer_interrupt_flag_get(TIMER5, TIMER_INT_FLAG_UP)){
timer_interrupt_flag_clear(TIMER5, TIMER_INT_FLAG_UP);
WakeFlag = 1;
}
}
//
// タイマーの初期化
//
void timer5_config(int Cnt)
{
// タイマーのパラメータを設定する。
// タイマーの16ビットプリスケーラには54MHzが入力される・・・はずだが、108MHzが入力されるように振舞っている。
// それを、10000分周(timer_initpara.prescaler = 10000-1)すると、おおよそ108,000,000/10000= 10.8Khz
// それを、引数の cnt回数えて(timer_initpara.period = Cnt;)タイマーの周期を決める。
// 例えば、cntが30の時は、10,800 / 30 =360で、360Hzとなる。
timer_parameter_struct timer_initpara;
rcu_periph_clock_enable(RCU_TIMER5);
timer_deinit(TIMER5);
timer_struct_para_init(&timer_initpara);
timer_initpara.prescaler = 10000 - 1;
timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
timer_initpara.counterdirection = TIMER_COUNTER_UP;
timer_initpara.period = Cnt;
timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
timer_init(TIMER5, &timer_initpara);
timer_auto_reload_shadow_enable(TIMER5);
timer_interrupt_enable(TIMER5, TIMER_INT_UP);
// 割り込みを有効にして、タイマー5を設定する
eclic_global_interrupt_enable();
eclic_set_nlbits(ECLIC_GROUP_LEVEL3_PRIO1);
eclic_irq_enable(TIMER5_IRQn,1,0);
// タイマーを開始する
timer_enable(TIMER5);
}
// 外枠とスコア、ライフ残を表示する
void DrawBORDER()
{
LCD_DrawLine(GAMEAREA_X0,GAMEAREA_Y1,GAMEAREA_X0,GAMEAREA_Y0,WHITE);
LCD_DrawLine(GAMEAREA_X0,GAMEAREA_Y0,GAMEAREA_X1,GAMEAREA_Y0,WHITE);
LCD_DrawLine(GAMEAREA_X1,GAMEAREA_Y0,GAMEAREA_X1,GAMEAREA_Y1,WHITE);
LCD_ShowString(0,0,(const u8 *)"SCORE:",WHITE);
LCD_ShowString(10,160-12,(const u8 *)"LIFE:",WHITE);
char life[3];
sprintf(life,"%1d",LifeCnt);
LCD_ShowString(55,160-12,(u8 *)life,WHITE);
u8 scr[12];
sprintf((char *)scr,"%5d0",Score);
LCD_ShowString(38,0,scr,WHITE);
}
//
// メイン処理
//
void Game(void)
{
// 初期化処理
gameState = STATE_INIT; // ステータスを初期化にする
timer5_config(30); // タイマーの初期化を行う
LCD_Init();
LCD_Clear(BLACK);
u16 tick = 0; // LEDを点滅させるためのカウンターを初期化
gameState = STATE_STARTGAME; //初期化処理が終了したのでゲーム開始処理を行う
while (TRUE) { //
timer_enable(TIMER5); // タイマーを有効にする
// タイマーのウェイト処理。wakeFlagが割り込みルーチン内で1になるまで無限ループする
while(WakeFlag == 0) {
}
tick = (tick + 1) & 0x8FFF; // LEDの点滅用カウンタのインクリメント
WakeFlag = 0; // タイマーのウエイトフラグを初期化する
switch (gameState) {
case STATE_STARTGAME:{
/*ゲームの開始処理 */
Score = 0;
LifeCnt = 3;
DrawBORDER(); //外枠とライフ残、スコアを画面に表示させる
InitBlock();
DrawBlock();
gameState = STATE_INGAME;
break;
}
case STATE_INGAME:{
/*ゲームのメイン処理*/
/*ゲームオーバーになったら、 gameState = STATE_GAMEOVER; を実行*/
if ((tick % 100) < 50) {
//LCD_ShowString(0,12,"LED オン",RED);
led_on(LED_R);
} else {
//LCD_ShowString(0,12,"LED オフ",WHITE);
led_off(LED_R);
}
break;
}
case STATE_GAMEOVER:{
/*ゲーム-オーバー処理*/
break;
}
}
}
}
block.h
#ifndef __block_h__
#define __block_h__
// 表示されるブロックの規定値
#define BLOCK_CNT_H 7 // ブロックは横に7個
#define BLOCK_CNT_V 21 // ブロックは縦に最大21個
#define BLOCK_SIZE_W 11 // ブロックの横幅は11ドット
#define BLOCK_SIZE_H 6 // ブロックの縦は6ドット
// ブロックのテーブル
// 画面上はBLOCK_CNT_H x BLOCK_CNT_Vのマス目に分類され、そこにあるブロックの
// 種類が item に入っている。
// テーブルには、事前にこのブロックが存在する矩形の情報が入っており、ボールとの
// 衝突判定ではこの座標が使われる。
struct BLOCKINFO {
unsigned char item;
unsigned char x1;
unsigned char y1;
unsigned char x2;
unsigned char y2;
};
//全体のブロック数と、残りのブロック数。ブロックが少なくなったらボールの速度を上げる、などに使用する。
extern int blkCnt; // 総ブロック数
extern int blkBrk; // 残りブロック数
void InitBlock();
void DrawBlock();
#endif
block.c
#include "lcd/lcd.h"
#include "led.h"
#include "memory.h"
#include "gd32vf103.h"
#include "game.h"
#include "ball.h"
#include "wall.h"
#include "block.h"
// ブロックのテーブル
// 画面上はBLOCK_CNT_H x BLOCK_CNT_Vのマス目に分類され、そこにあるブロックの
// 種類が item に入っている。
// テーブルには、事前にこのブロックが存在する矩形の情報が入っており、ボールとの
// 衝突判定ではこの座標が使われる。
struct BLOCKINFO blockmtx[BLOCK_CNT_H][BLOCK_CNT_V];
//全体のブロック数と、残りのブロック数。ブロックが少なくなったらボールの速度を上げる、などに使用する。
int blkCnt = 0; // 総ブロック数
int blkBrk = 0; // 残りブロック数
// ブロックのテーブルを初期化する
//
void InitBlock()
{
// ブロックテーブルを初期化する。
memset((void *)blockmtx,0,sizeof(blockmtx));
blkCnt = 0;
for (int i = 0;i<BLOCK_CNT_H;i++) {
for (int j = 3;j<7;j++) {
blockmtx[i][j].item = 1;
blockmtx[i][j].x1 = i * BLOCK_SIZE_W + (GAMEAREA_X0 + 2);
blockmtx[i][j].y1 = j * BLOCK_SIZE_H + (GAMEAREA_Y0 + 2);
blockmtx[i][j].x2 = blockmtx[i][j].x1 + BLOCK_SIZE_W - 2 ;
blockmtx[i][j].y2 = blockmtx[i][j].y1 + BLOCK_SIZE_H - 3 ;
blkCnt++;
}
}
blkBrk = blkCnt;
}
// ブロックすべてを描画する
// ブロック崩しでは、ブロックが1つだけ表示される、ということはない(消えていくだけ)ので、
// 表示は無条件で全ブロックを表示させれば良いことになる。
void DrawBlock()
{
static u16 colTbl[] = {RED, BLUE, GREEN ,MAGENTA,CYAN, YELLOW};
for (u8 i = 0 ; i< BLOCK_CNT_H;i++) {
for (u8 j = 0; j<BLOCK_CNT_V;j++) {
u16 col = colTbl[j % 6];
if (blockmtx[i][j].item == 1) {
LCD_Fill(blockmtx[i][j].x1,blockmtx[i][j].y1,blockmtx[i][j].x2,blockmtx[i][j].y2,col);
}
}
}
}