LoginSignup
0
0

More than 1 year has passed since last update.

Longan Nanoを使ってみる 8 ~ボールを動かす~

Last updated at Posted at 2022-02-22

前の記事

Longan Nanoを使ってみる 7 ~外枠とブロックを書く~

全体の目次

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 ~とりあえずのまとめ~

はじめに

このパートでは、一切、マイコン関連の話は出てこない。(前の記事でもほとんどマイコンぽくはなかったが、それでもSipeedの提供するグラフィックスライブラリくらいは使ったし、関連記事では低レベルのAPIを使って画像を出力した)

もし、興味がない場合、そのまま次の記事に進む。

注意

このページは、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のページで参照してください。

ボールを動かすには

ボールは、3x3ドットの四角で表現する。ボールを表示するのは LCD_Fill(u16 xsta,u16 ysta,u16 xend,u16 yend,u16 color) を使えばよいので簡単だが、実際にはいろいろ考える必要がある。

  • ボールを移動させたときは、古いボールを消し、新しいボールを書く、という作業が必要になる。
  • ボールは衝突判定が必要で、壁やブロック、パドル(まだ実装していない)に反射する
  • ボールは、斜めに移動する。45度単位などであれば、X座標に+1、Y座標に+1などとすればよいが、30度などの場合、X座標に+1、2回に1回だけY座標に+1などと実行する必要がある。
  • ボールは1つだけだと寂しいので、複数表示できるようにしたい。

ボールの移動

45度でボールが移動しているときは、Xが1増えるとき、yも1増えれば問題ない(dx=1、dy=1)。しかし、ボールの角度が45以外の時には、X座標が1移動したとき、y座標は2移動することになる(dx=1、dy=2)。
しかも、X座標をベースとして考えると、45度の時の移動距離が1.414ドット(ピタゴラスの定理)なのに対して、移動距離が2.23ドットとなり、角度によりボールの速さが異なる、となってしまう。かといって、横を0.5ドットで動かす(dx=0.5、dy=1)・・・ことはできない。

ball1.png

これについて、解決方法はいくつかある。

  • 座標を浮動小数点で計算する。この方法は最も安直な方法で、現在のPCであれば最も高速な方法かもしれない。Longan NANOでも、浮動小数点の計算はできる。ただ、なんとなく負けのような気がするので避けることにする。
  • DDA(デジタル微分解析)を使用する。プレゼンハムの方法を使用すると、1回の処理で1ドット、きれいに直線を任意の方向に伸ばすことができる。この直線の描画方法は、lcd.cの中のLCD_DrawLine(u16 x1,u16 y1,u16 x2,u16 y2,u16 color)でも使用されている方法で、太古の昔からいまだに使われ続けている。この方法は最も優れた方法だと思われる。ただ、ボールを動かすときにこの方法を使うと、1フレームにつき1ドット必ず動くことになり、ボールが早くなった時、遅くなった時などに、ボールのフレームを飛ばす必要があり、処理が面倒になる。(面倒なだけで可能)
  • 固定小数点を使用する。整数で計算するが、ボールの座標や、増分は8倍して計算する。実際に、ボールを表示するときには、ボールの座標を1/8 して表示する。これは、ボールがX=10,y=5にある場合に「x=10、y=5、dx=0.5、dy=1」をすべて8倍して、「x=80、y=40、dx=4、dy=8」と考えて計算する、というもの。 10倍して考えたほうが楽なのでは?と思うかもしれないが、8で割る、という処理は現在のPCにとって10で割るのよりも全然簡単なので、8倍している。別に、16倍でも 32倍でもよいが、10倍に一番ちかいのが 8倍なので8にした。

ボールのデータ

これを踏まえて、ボールのデーターは次のような構造体にしておく。

ball.h

// ボールの速度を変える条件を満たしたかのフラグ
#define SPDMSK_BACKWALL 0x01        // ボールが奥の壁にヒットしたら速度が上がる
#define SPDMSK_BLOCKCNT_1 0x02      // ブロック数が半分になったら速度が上がる
#define SPDMSK_BLOCKCNT_2 0x04      // ブロック数が1/4になったら速度が上がる

struct BALLINFO {
	int oldx;	    // 8倍された、ボールの直前のX座標
	int oldy;	    // 8倍された、ボールの直前のY座標
	int x;		    // 8倍された、ボールのX座標
	int y;		    // 8倍された、ボールのY座標
	int dx;         // 8倍された、ボールの現在のX増分
	int dy;         // 8倍された、ボールの現在のY増分
	int dxBase;     // 8倍された、ボールのX増分初期値
	int dyBase;     // 8倍された、ボールのY増分初期値
	unsigned char SpeedMask;   // ボールの速度レベルを変える条件を満たしたかのフラグ
};

// 座標を1/8する。
#define CVT_AXIS(__x) ((__x) >> 3)

ボールのデータを初期化

ボールを初期化し、よく使う処理を関数化しておく。ここで用意したのは、次の3つの関数。これらの処理は、純粋に BALLINFO構造体のデータを書き換える処理しか含まない。(ボールの残数だけは変えるが) C++だったらメソッドにするべき、構造体と密接に関連する関数群となる。

  • InitBallPos ・・・ 構造体を初期化する関数。この関数では1番目の引数ではボールすべてを初期化し、最初のボールを追加する。引数が1のときには、5つの配列のうち、開いているところを探し、ボールを追加する。
  • GetBallInfo ・・・ボールのオブジェクトへのポインタを返す関数。ボールの配列をグローバル変数にせず、この関数を経由してポインタを与えてデータを参照させる。
  • BallStep・・・ボールを次の位置に進める関数。ボールの座標に、dx、dyを加える。
  • BallBack・・・ボールを1つ前の位置に戻す関数。ボールの衝突が検知された場合、ひとつ前の座標に戻して反射させる。そのため、ひとつ前に戻すという処理は、壁やブロック、パドルで使われる。
  • BallSwapX、BallSwapY・・・ボールのdx、dyを逆にする関数
  • BallDead・・・ボールがミスしたときに消す関数。
  • updateBallSpeed・・・ボールの速度を調整する関数。。の処理はゲーム中、例えばボールが奥の壁に反射して、速度が増す場合などに呼び出されるので、InitBallPosの中に書くのは適切ではないので、別関数にして InitBallPosから呼び出すようにしておく。

ball.h

ball.hでは、これらの関数のプロとタイプと、座標を 1/8 するマクロ CVT_AXIS を定義しておく

// 座標を1/8する。
#define CVT_AXIS(__x) ((__x) >> 3)
struct BALLINFO* GetBallInfo(unsigned int idx);
void BallStep(struct BALLINFO *bi) ;
void BallBack(struct BALLINFO *bi);
void BallSwapX(struct BALLINFO *bi);
void BallSwapY(struct BALLINFO *bi);
void BallDead(struct BALLINFO *bi) ;
void InitBallPos(u8 mode, struct BALLINFO *bi);

ball.c

//ボールの座標
struct BALLINFO ball[5];             // 最大5個のボールが出現するので配列にしておく
// ボールの情報にアクセスするための関数
struct BALLINFO* GetBallInfo(unsigned int idx)
{
    if (ball[idx].x == 0 && ball[idx].y == 0) return NULL;
    return &ball[idx];
}

u8 ballLive = 0;       // ボールの数
   :

//ボールを1つ進める
void BallStep(struct BALLINFO *bi) 
{
	bi->oldx = bi->x;
	bi->oldy = bi->y;
	bi->x += bi->dx;
	bi->y += bi->dy;
}
//ボールを1つ前の位置に戻す
void BallBack(struct BALLINFO *bi) 
{
	bi->x = bi->oldx;
	bi->y = bi->oldy;
}
// ボールのdxを逆にする
void BallSwapX(struct BALLINFO *bi) 
{
    bi->dx = -bi->dx;
    bi->dxBase = -bi->dxBase;
}
// ボールのdyを逆にする
void BallSwapY(struct BALLINFO *bi) 
{
    bi->dy = -bi->dy;
    bi->dyBase = -bi->dyBase;
}
// ボールが失われた時の処理
void BallDead(struct BALLINFO *bi) 
{
	if (bi->x != 0) {
		ballLive--;
	}
	bi->oldx = 0;
	bi->oldy = 0;
	bi->x = 0;
	bi->y = 0;
}
// ボールの速度を調整する

void updateBallSpeed(struct BALLINFO *bi)
{
	int speedlvl = 1;
	if (bi->SpeedMask & SPDMSK_BACKWALL) speedlvl += 1; 
	if (bi->SpeedMask & SPDMSK_BLOCKCNT_1) speedlvl += 1;
	if (bi->SpeedMask & SPDMSK_BLOCKCNT_2) speedlvl += 1;
	bi->dx = bi->dxBase * speedlvl;
	bi->dy = bi->dyBase * speedlvl;
}


// ボールを初期化する
void InitBallPos(u8 mode, struct BALLINFO *bi)
{
	if (mode ==  0) {		// 完全初期化して最初のボールを生きにする。 ballIdxは使われない
		memset(ball,0,sizeof(ball));
		ball[0].x = (GAMEAREA_X0 + GAMEAREA_X1/2)*8;
		ball[0].y = (GAMEAREA_Y0 + (GAMEAREA_Y1-GAMEAREA_Y0)/2)*8;
		ball[0].dxBase = BALL_DX_DEFAULT;
		ball[0].dyBase = BALL_DY_DEFAULT;
		updateBallSpeed(&ball[0]);
		ballLive=1;	
	} else if (mode == 1)  { // ballIdxの位置を元にしてボールを1つ追加する。 ボールの速度・角度は元のボールと変える。
		for (int j = 1; j < 5;j++ ) {
			if (ball[j].x == 0) {			// このボールで行こう
				ball[j].x = bi->x;
				ball[j].y = bi->y;
				ball[j].dyBase = BALL_DY_DEFAULT;
				ball[j].dx = -bi->dx;
				ball[j].dxBase = -bi->dxBase;
				ball[j].SpeedMask = 0;
				updateBallSpeed(&ball[j]);
				ballLive++;
				break;
			}
		}
	}
}

古いボールを消し、新しいボールを書く

これは比較的簡単。ブロック崩しでは、ほかのオブジェクト(ブロックや壁)と重なることはない。(反射したりブロックが消えるので)また、背景画像もなく、ボールも単純な矩形である。そのため、ボールの新しい座標を計算する前に、前回表示した座標の矩形を背景色で塗りつぶすだけでよい。

ball.h

void drawDeleteBall(bool isDraw);

ball.c

//
// ボールを消す、または表示する
//  true... 表示する、false ...消す
void drawDeleteBall(bool isDraw)
{
	for (u8 i = 0 ; i < 5;i++) {
		struct BALLINFO *bi = &ball[i];
		if(bi->x !=0) {
			u16 c;
			if (isDraw == FALSE) {
                c = BLACK;
			} else {
                c = WHITE;
			}
			LCD_Fill(CVT_AXIS(bi->x)-1 ,CVT_AXIS(bi->y)-1 ,CVT_AXIS(bi->x)+1,CVT_AXIS(bi->y)+1,c);

		}
	}
}

衝突判定

ボールは、壁やパドル当たると跳ね返り、ブロックに当たると消える。

パドルとの衝突判定

まだ今のところパドルがないので、ボールはゲーム領域の4辺どこに当たっても反射するようにしておく。

ブロックとの衝突判定

ブロックは、ブロックの初期化と表示で実装したように、すべてのブロックが、その占有領域の矩形の座標を持っている。

ボールの座標を計算し、そのボールの新しい座標が、占有領域に入ったかを調べればよいとこになる。

ただ、ブロックだけは、衝突する方向がたくさんあり、それぞれにより動作が違う。

block3.png

また、ブロックはたくさんあるので、ボールが動くたびに、すべてのブロックと衝突判定するのは無駄が多い。PCであれば楽な作業だが、CPUパワーが少ないマイコンではなるべく無駄は避ける必要がある。

衝突判定

衝突判定は、基本的に「移動後の座標を計算しその座標がブロックの矩形に入っていたら衝突」とする。これは壁への衝突と変わらない。

ただ、衝突したとき、移動前の座標がどこにあったかを判断し、衝突した辺(もしくは角)を決定する。

block4.png

上の図は、■で移動前のボールの座標、□で移動後のボールの座標を示している。ブロックに対して、その外側の領域を9つに分け、象限と呼ぶことにする。

この図で、ボールがどの象限にあるのかは、簡単に判定できる。例えば、X1軸(ブロックの向かって左側)よりもボールの座標が小さければ、①、④、⑦象限のどれか、さらにy1軸(ブロックの上辺)よりも小さければ①象限となる。

この処理は、ブロックとの衝突、パドルとの衝突などでも共通して使用するので、game.cで実装して関数にしておく。

game.h

  :
unsigned char GetOrthant(int x , int y , int x1, int y1 , int x2 , int y2);
  :

game.c

// 座標が、矩形の外側に対して、どの象限にいるのかを返す関数
//    1         2        3
//        +---------+
//   4    |    5    |    6
//        +---------+
//   7         8         9    

u8  GetOrthant(int x , int y , int x1, int y1 , int x2 , int y2)
{
	bool bLowerX1 =  (x < x1);
	bool bUpperX2 =  (x > x2);
	bool bLowerY1 =  (y < y1);
	bool bUpperY2 =  (y > y2);

    
	if (bLowerX1) {
        return bLowerY1 ? 1: (bUpperY2 ? 7:4);
	} else if (bUpperX2) {
        return bLowerY1 ? 3: (bUpperY2 ? 9:6);
	} else {
        return bLowerY1 ? 2: (bUpperY2 ? 8:5);
	}
}

不要な計算の排除

衝突判定を全ブロックで実行するのは無駄なので、何とか省力化したい。そこで、ブロックのデータ構造 で利用した、画面全体を区分けする方法を使用する。

ブロックは、ゲームの領域全体を、横BLOCK_H_CNT、縦BLOCK_V_CNTの領域に分けて配置されている。

block1.png

ブロックを初期配置するときに使用した計算方法を使い、ボール移動後の中心位置がどの区画に属しているかを計算する。衝突判定は、ボールが属している区画についてだけ行えばよいことになる。

	int xidx = ( ボールのX座標 - (ゲーム領域の左端+マージン)) /ブロックの横幅;
	int yidx = ( ボールのY座標 - (ゲーム領域の上端+マージン) / ブロックの縦幅;

このプログラムでは、マージンを2として描画した。この式で、ボールがどのブロックに属しているかが一意に決まる。

これらを踏まえて、ブロックの反射チェック処理はこうなる。

block.h

 :
void blockCheck(struct BALLINFO* bi);
 :

block.c

//
// ブロック反射チェック
// この時点では、BALLINFOの座標は移動済みの座標になっている。
//
void blockCheck(struct BALLINFO* bi)
{

	int chkx = CVT_AXIS(bi->oldx);
	int chky = CVT_AXIS(bi->oldy);
	// 座標がある位置のブロック番号を求める
	int xNow = CVT_AXIS(bi->x);
	int yNow = CVT_AXIS(bi->y);
	int xidx = (xNow - (GAMEAREA_X0+2)) /BLOCK_SIZE_W;
	int yidx = (yNow - (GAMEAREA_Y0+2)) / BLOCK_SIZE_H;

	// 現在のボールの位置が、ブロックの中にある場合、衝突処理を行う
	if (blockmtx[xidx][yidx].item != 0 && GetOrthant(xNow,yNow, blockmtx[xidx][yidx].x1 ,blockmtx[xidx][yidx].y1,blockmtx[xidx][yidx].x2,blockmtx[xidx][yidx].y2) == 5) {
		blkBrk--;							// 残ブロック数を1つ減らす
		// ボールの新しい位置は、ブロックの内側なので、座標はひとつ前の位置に戻さないといけない。
		BallBack(bi);

		// 難易度調整
		if (blkBrk <= (blkCnt / 2)) {				// 残ブロックスが全ブロック数の半分以下になったら
			bi->SpeedMask  |= SPDMSK_BLOCKCNT_1;	// スピードレベル1
		} 
		if (blkBrk <= (blkCnt / 4)) {				// 残ブロックスが全ブロック数の1/4分以下になったら
			bi->SpeedMask |= SPDMSK_BLOCKCNT_2;		// スピードレベル2
		}

		// ボールを跳ね返す。
		u8 pos = GetOrthant(chkx,chky, blockmtx[xidx][yidx].x1 ,blockmtx[xidx][yidx].y1,blockmtx[xidx][yidx].x2,blockmtx[xidx][yidx].y2);
		if (pos == 2 || pos== 8) {
            BallSwapY(bi);
		} else if (pos == 4 || pos == 6) {
            BallSwapX(bi);
		} else {
            BallSwapX(bi);
            BallSwapY(bi);
		}

		// ブロックは消す
		blockmtx[xidx][yidx].item = 0;	 
		LCD_Fill(blockmtx[xidx][yidx].x1,blockmtx[xidx][yidx].y1,blockmtx[xidx][yidx].x2,blockmtx[xidx][yidx].y2,BLACK);

		// スコアの処理
		if (bi->SpeedMask & SPDMSK_BACKWALL) {		// 裏に入っていたらスコアは増量
			Score = Score + 2;
		} else {
			Score = Score + 1;
		}
	}
}

壁との衝突判定

壁の衝突判定は、外枠を書くで使った、次の定数を使う。この値は、game.h内で定義されている。

#define GAMEAREA_X0   0
#define GAMEAREA_Y0   20
#define GAMEAREA_X1   79
#define GAMEAREA_Y1  148

ボールの新しい座標を計算し、GAMEAREA_X0 (0になっている)と等しいか小さいなら、ボールは横の壁に反射した、と判断すればよいことになる。
ボールは、横の壁に反射すると、Xの増減の符号が逆になる。 同じようにGAMEARETA_Y1(148)を超えたら、Y方向の増分の符号を反転させる。

壁判定のチェックでは、ボールの大きさを意識する必要がある。ブロックの衝突チェックでは、衝突後にブロックは消えるので、ボールがブロックにめり込んでも問題ないが、壁はボールがめり込むと絵が乱れてしまう。そのため、ブロックの衝突チェックでは、3x3であることを意識した判定をおこなう。

それ以外は、ほかの衝突処理とやることはあまり変わらない。現在の点がブロックの外の場合、現在の座標をひとつ前に戻し、ボールを反転させる、という処理になる。下方向の壁については、衝突ではなくボールのロスになるが、当面の間、ボールの反射として処理する。

wall.h

unsigned char checkWall(struct BALLINFO* bi);

wall.c

// 壁反射のチェック
// 0 .. ミス。すべてのボールが亡くなった
// 1 .. ゲーム継続
// 2 .. そのボールは失われたがまだボールが残っている
u8 checkWall(struct BALLINFO* bi)
{
	int x = CVT_AXIS(bi->x);
	int y = CVT_AXIS(bi->y);

	// 横の壁のチェック。ボールは3x3ドットなので、衝突判定はその範囲を考慮して、1ドット分広くチェックする。
	if ((x-1) <= GAMEAREA_X0 || (x+1) >=GAMEAREA_X1) {
		// ひとつ前の場所に戻して
		BallBack(bi);
		// X方向を逆にする
        BallSwapX(bi);
	} 
	if ((y-1) <=GAMEAREA_Y0 || (y+1) >=GAMEAREA_Y1) {
		// ひとつ前の場所に戻して
		BallBack(bi);
		// Y方向を逆にする。
        BallSwapY(bi);
		if ((y-1) <= GAMEAREA_Y0) {
			bi->SpeedMask |= SPDMSK_BACKWALL;
		} else {
            //今のところボールロスにはしない
            /*
			BallDead(bi);
			if (ballLive == 0) {
				return 0;
			}
			return 1;
            */
		}
	}
	return 1;
}

ボールを動かす

ここまでで以下の処理を記述した。

  • ボールのデータを作り、関連の関数を用意した。
  • ブロックとの衝突の関数を作成した。
  • 壁との衝突の関数を作成した。

これで役者が揃ったので、ボールを動かす処理を作成する。やることはほとんどすべて、関数で用意されている。ボールを動かし、壁に反射したかをチェックし、ブロックに反射したかをチェックする。ボールを動かしたときに、ボールを後逸したか、ブロックが全部消されたか(面クリア)がわかるので、これを戻す関数とする。

ball.h

unsigned char  moveBall();

ball.c

//  ボールを動かす。
// 0... ミス
// 1... 継続
// 2... クリア
u8 moveBall()
{
	for (u8 i = 0 ; i < 5;i++) {
		struct BALLINFO *bi = &ball[i];		
		if (bi->x == 0) continue;

		//ボールを動かす
        BallStep(bi);
		// 壁反射チェック
		{
			u8 ret = checkWall(bi);
			if (ret == 0) {
				return 0;
			} else if (ret == 2) {
				continue;
			}
		}


		//ブロック反射チェック
		// ボールの進行方向の隅がブロックに接しているかを調べる
		blockCheck(bi);
		if (blkBrk == 0) {
			return 2;
		}
    	updateBallSpeed(bi);
    }
	return 1;
}

メインループへの組み込み

現在、メインの処理はステートマシンになっており、ゲームの主要処理はSTATE_INGAMEで行われている。

void Game(void) 
{
          :
          :
    gameState = STATE_STARTGAME;        //初期化処理が終了したのでゲーム開始処理を行う
          :    
    while (TRUE) {
          :
        switch (gameState) {
            case STATE_STARTGAME:{
                /*ゲームの開始処理 */
                     :  
                break;
            }
            case STATE_INGAME:{
                /*ゲームのメイン処理*/
                     :
                break;
            }

今回作った処理を、各ゲームの最初に一度だけ実行すればよい処理を STATE_STARTGAMEに、毎回実行する処理をSTATE_INGAMEから呼び出せばよい。ブロックとの衝突、壁の反射などはすべて、ボールを動かす処理にまとめられているので、ゲーム中はそれを呼び出すことになる。

main.c

void Game(void) 
{
       :
    gameState = STATE_STARTGAME;        //初期化処理が終了したのでゲーム開始処理を行う
    
    while (TRUE) {   //ゲームオーバーになるまで繰り返す
           :
        switch (gameState) {
            case STATE_STARTGAME:{
                /*ゲームの開始処理 */
                Score = 0;
                LifeCnt = 3;
                DrawBORDER();               //外枠とライフ残、スコアを画面に表示させる
                InitBlock();
                DrawBlock();
                InitBallPos(0,NULL);        // 今回追加。ボールの位置を初期化する
                gameState = STATE_INGAME;
                break;
            }
            case STATE_INGAME:{
        		drawDeleteBall(FALSE);		// 今回追加。ひとつ前のボールを消す
	            u8 ret= moveBall();         // 今回追加。ボールを動かす
	            if (ret == 0) {

                }
        		drawDeleteBall(TRUE);			// ひとつ前のボールを消す
                break;                
            }
            case STATE_GAMEOVER:{
                /*ゲーム-オーバー処理*/
                break;
            }
        }

    }
}

ここまででできたこと

画面上に動くオブジェクト(ボール)を配置し、壁やブロックと衝突の判定や衝突処理を追加した。

次の記事

Longan Nanoを使ってみる 9 ~パドルの入力~

今回の追加を反映したソースコード

今回の追加処理はかなり多くなるが、それらを反映したソースコードは次のようになる。

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

enum GAMESTATE {
	STATE_INIT,			// 初期化処理中
	STATE_STARTGAME,	// ゲーム開始(初期化中)
	STATE_INGAME,		// ゲーム中
	STATE_GAMEOVER,		// ゲームオーバー処理中
};

extern int LifeCnt;
extern int Score;

unsigned char GetOrthant(int x , int y , int x1, int y1 , int x2 , int y2);
#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"

// ゲームの表示領域
#define GAMEAREA_X0   0
#define GAMEAREA_Y0   20
#define GAMEAREA_X1   79
#define GAMEAREA_Y1  148


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;
	    
        static u8 oeFlag;
        if (oeFlag == 0) {
            gpio_bit_reset(GPIOB, GPIO_PIN_8); //OE#
            oeFlag = 1;
        } else {
            gpio_bit_set(GPIOB, GPIO_PIN_8); //OE#
            oeFlag = 0;
        }
    }
}

//
// タイマーの初期化
//
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);	
}


// 座標が、矩形の外側に対して、どの象限にいるのかを返す関数
//    1         2        3
//        +---------+
//   4    |    5    |    6
//        +---------+
//   7         8         9    
unsigned char GetOrthant(int x , int y , int x1, int y1 , int x2 , int y2)
{
	bool bLowerX1 =  (x < x1);
	bool bUpperX2 =  (x > x2);
	bool bLowerY1 =  (y < y1);
	bool bUpperY2 =  (y > y2);

    
	if (bLowerX1) {
        return bLowerY1 ? 1: (bUpperY2 ? 7:4);
	} else if (bUpperX2) {
        return bLowerY1 ? 3: (bUpperY2 ? 9:6);
	} else {
        return bLowerY1 ? 2: (bUpperY2 ? 8:5);
	}
}


//
// メイン処理
//
void Game(void) 
{
    rcu_periph_clock_enable(RCU_GPIOB);
    gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_8);	// B8をデバッグに使う

    // 初期化処理
    gameState = STATE_INIT;             // ステータスを初期化にする
    timer5_config(100);                  // タイマーの初期化を行う
    LCD_Init();
    LCD_Clear(BLACK);

    u16 tick = 0;                       // LEDを点滅させるためのカウンターを初期化
    gameState = STATE_STARTGAME;        //初期化処理が終了したのでゲーム開始処理を行う
    
    while (TRUE) {   //ゲームオーバーになるまで繰り返す
		timer_enable(TIMER5);               // タイマーを有効にする
        // タイマーのウェイト処理。wakeFlagが割り込みルーチン内で1になるまで無限ループする
		
        while(WakeFlag == 0) {
            delay_1ms(10);
            break;
        }

        tick = (tick + 1) & 0x8FFF;     // LEDの点滅用カウンタのインクリメント
		WakeFlag = 0;                   // タイマーのウエイトフラグを初期化する
        
        switch (gameState) {
            case STATE_STARTGAME:{
                /*ゲームの開始処理 */
                Score = 0;
                LifeCnt = 3;
                DrawBORDER();               //外枠とライフ残、スコアを画面に表示させる
                InitBlock();
                DrawBlock();
                InitBallPos(0,NULL);
                gameState = STATE_INGAME;
                break;
            }
            case STATE_INGAME:{
        		drawDeleteBall(FALSE);			// ひとつ前のボールを消す
	            u8 ret= moveBall();
        		drawDeleteBall(TRUE);			// ひとつ前のボールを消す
                break;                
            }
            case STATE_GAMEOVER:{
                /*ゲーム-オーバー処理*/
                break;
            }
        }

    }
}

ball.h

#ifndef __ball_h__
#define __ball_h__
// ボールの速度を変える条件を満たしたかのフラグ
#define SPDMSK_BACKWALL 0x01        // ボールが奥の壁にヒットしたら速度が上がる
#define SPDMSK_BLOCKCNT_1 0x02      // ブロック数が半分になったら速度が上がる
#define SPDMSK_BLOCKCNT_2 0x04      // ブロック数が1/4になったら速度が上がる

struct BALLINFO {
	int oldx;	    // 8倍された、ボールの直前のX座標
	int oldy;	    // 8倍された、ボールの直前のY座標
	int x;		    // 8倍された、ボールのX座標
	int y;		    // 8倍された、ボールのY座標
	int dx;         // 8倍された、ボールの現在のX増分
	int dy;         // 8倍された、ボールの現在のY増分
	int dxBase;     // 8倍された、ボールのX増分初期値
	int dyBase;     // 8倍された、ボールのY増分初期値
	unsigned char SpeedMask;   // ボールの速度レベルを変える条件を満たしたかのフラグ
};

// 座標を1/8する。
#define CVT_AXIS(__x) ((__x) >> 3)
struct BALLINFO* GetBallInfo(unsigned int idx);
void BallStep(struct BALLINFO *bi);
void BallBack(struct BALLINFO *bi);
void BallSwapX(struct BALLINFO *bi);
void BallSwapY(struct BALLINFO *bi);
void BallDead(struct BALLINFO *bi) ;
void InitBallPos(u8 mode, struct BALLINFO *bi);
void drawDeleteBall(bool isDraw);
unsigned char  moveBall();
#endif

ball.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"



// ボールの角度を示すデフォルト値と、角度の最大・最小値
#define BALL_DX_DEFAULT 1*2
#define BALL_DY_DEFAULT 2*2
#define BALL_DX_MIN  (1*2)
#define BALL_DX_MAX  (4*2)
#define BALL_DY_MIN  (1*2)
#define BALL_DY_MAX  (4*2)

//ボールの座標
struct BALLINFO ball[5];             // 最大5個のボールが出現するので配列にしておく

// ボールの情報にアクセスするための関数
struct BALLINFO* GetBallInfo(unsigned int idx)
{
    if (ball[idx].x == 0 && ball[idx].y == 0) return NULL;
    return &ball[idx];
}


u8 ballLive = 0;       // ボールの数




//ボールを1つ進める
void BallStep(struct BALLINFO *bi) 
{
	bi->oldx = bi->x;
	bi->oldy = bi->y;
	bi->x += bi->dx;
	bi->y += bi->dy;
}
//ボールを1つ前の位置に戻す
void BallBack(struct BALLINFO *bi) 
{
	bi->x = bi->oldx;
	bi->y = bi->oldy;
}
// ボールのdxを逆にする
void BallSwapX(struct BALLINFO *bi) 
{
    bi->dx = -bi->dx;
    bi->dxBase = -bi->dxBase;
}
// ボールのdyを逆にする
void BallSwapY(struct BALLINFO *bi) 
{
    bi->dy = -bi->dy;
    bi->dyBase = -bi->dyBase;
}
// ボールが失われた時の処理
void BallDead(struct BALLINFO *bi) 
{
	if (bi->x != 0) {
		ballLive--;
	}
	bi->oldx = 0;
	bi->oldy = 0;
	bi->x = 0;
	bi->y = 0;
}
// ボールの速度を調整する

void updateBallSpeed(struct BALLINFO *bi)
{
	int speedlvl = 1;
	if (bi->SpeedMask & SPDMSK_BACKWALL) speedlvl += 1; 
	if (bi->SpeedMask & SPDMSK_BLOCKCNT_1) speedlvl += 1;
	if (bi->SpeedMask & SPDMSK_BLOCKCNT_2) speedlvl += 1;
	bi->dx = bi->dxBase * speedlvl;
	bi->dy = bi->dyBase * speedlvl;
}


// ボールを初期化する
void InitBallPos(u8 mode, struct BALLINFO *bi)
{
	if (mode ==  0) {		// 完全初期化して最初のボールを生きにする。 ballIdxは使われない
		memset(ball,0,sizeof(ball));
		ball[0].x = (GAMEAREA_X0 + GAMEAREA_X1/2)*8;
		ball[0].y = (GAMEAREA_Y0 + (GAMEAREA_Y1-GAMEAREA_Y0)/2)*8;
		ball[0].dxBase = BALL_DX_DEFAULT;
		ball[0].dyBase = BALL_DY_DEFAULT;
		updateBallSpeed(&ball[0]);
		ballLive=1;	
	} else if (mode == 1)  { // ballIdxの位置を元にしてボールを1つ追加する。 ボールの速度・角度は元のボールと変える。
		for (int j = 1; j < 5;j++ ) {
			if (ball[j].x == 0) {			// このボールで行こう
				ball[j].x = bi->x;
				ball[j].y = bi->y;
				ball[j].dyBase = BALL_DY_DEFAULT;
				ball[j].dx = -bi->dx;
				ball[j].dxBase = -bi->dxBase;
				ball[j].SpeedMask = 0;
				updateBallSpeed(&ball[j]);
				ballLive++;
				break;
			}
		}
	}
}

//
// ボールを消す、または表示する
//  true... 表示する、false ...消す
void drawDeleteBall(bool isDraw)
{
	for (u8 i = 0 ; i < 5;i++) {
		struct BALLINFO *bi = &ball[i];
		if(bi->x !=0) {
			u16 c;
			if (isDraw == FALSE) {
                c = BLACK;
			} else {
                c = WHITE;
			}
			LCD_Fill(CVT_AXIS(bi->x)-1 ,CVT_AXIS(bi->y)-1 ,CVT_AXIS(bi->x)+1,CVT_AXIS(bi->y)+1,c);

		}
	}
}

//  ボールを動かす。
// 0... ミス
// 1... 継続
// 2... クリア
unsigned char  moveBall()
{
	for (u8 i = 0 ; i < 5;i++) {
		struct BALLINFO *bi = &ball[i];		
		if (bi->x == 0) continue;

		//ボールを動かす
        BallStep(bi);
		// 壁反射チェック
		{
			u8 ret = checkWall(bi);
			if (ret == 0) {
				return 0;
			} else if (ret == 2) {
				continue;
			}
		}


		//ブロック反射チェック
		// ボールの進行方向の隅がブロックに接しているかを調べる
		blockCheck(bi);
		if (blkBrk == 0) {
			return 2;
		}
    	updateBallSpeed(bi);
    }
	return 1;
}

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();
void blockCheck(struct BALLINFO* bi);
#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);
			}
		}
	}
	

}

//
// ブロック反射チェック
// この時点では、BALLINFOの座標は移動済みの座標になっている。
//
void blockCheck(struct BALLINFO* bi)
{
	// 一つ前の座標
	int chkx = CVT_AXIS(bi->oldx);
	int chky = CVT_AXIS(bi->oldy);
	// 座標がある位置のブロック番号を求める
	int xNow = CVT_AXIS(bi->x);
	int yNow = CVT_AXIS(bi->y);
	int xidx = (xNow - (GAMEAREA_X0+2)) /BLOCK_SIZE_W;
	int yidx = (yNow - (GAMEAREA_Y0+2)) / BLOCK_SIZE_H;

	// 現在のボールの位置が、ブロックの中にある場合、衝突処理を行う
	if (blockmtx[xidx][yidx].item != 0 && GetOrthant(xNow,yNow, blockmtx[xidx][yidx].x1 ,blockmtx[xidx][yidx].y1,blockmtx[xidx][yidx].x2,blockmtx[xidx][yidx].y2) == 5) {
		blkBrk--;							// 残ブロック数を1つ減らす
		// ボールの新しい位置は、ブロックの内側なので、座標はひとつ前の位置に戻さないといけない。
		BallBack(bi);

		// 難易度調整
		if (blkBrk <= (blkCnt / 2)) {				// 残ブロックスが全ブロック数の半分以下になったら
			bi->SpeedMask  |= SPDMSK_BLOCKCNT_1;	// スピードレベル1
		} 
		if (blkBrk <= (blkCnt / 4)) {				// 残ブロックスが全ブロック数の1/4分以下になったら
			bi->SpeedMask |= SPDMSK_BLOCKCNT_2;		// スピードレベル2
		}

		// ボールを跳ね返す。
		u8 pos = GetOrthant(chkx,chky, blockmtx[xidx][yidx].x1 ,blockmtx[xidx][yidx].y1,blockmtx[xidx][yidx].x2,blockmtx[xidx][yidx].y2);
		if (pos == 2 || pos== 8) {
            BallSwapY(bi);
		} else if (pos == 4 || pos == 6) {
            BallSwapX(bi);
		} else {
            BallSwapX(bi);
            BallSwapY(bi);
		}

		// ブロックは消す
		blockmtx[xidx][yidx].item = 0;	 
		LCD_Fill(blockmtx[xidx][yidx].x1,blockmtx[xidx][yidx].y1,blockmtx[xidx][yidx].x2,blockmtx[xidx][yidx].y2,BLACK);

		// スコアの処理
		if (bi->SpeedMask & SPDMSK_BACKWALL) {		// 裏に入っていたらスコアは増量
			Score = Score + 2;
		} else {
			Score = Score + 1;
		}
	}
}

wall.h

#ifndef __wall_h__
#define __wall_h__
#include "ball.h"
unsigned char checkWall(struct BALLINFO* bi);
#endif

wall.c

#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();
void blockCheck(struct BALLINFO* bi);
#endif
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