前の記事
全体の目次
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 ~とりあえずのまとめ~
はじめに
今のところ、このブロック崩しはパドルの後ろにも壁があり、ボールが失われることがない。次に、後ろの壁を外し、ボールをミスしたときの処理を加えていく。
ボールがミスすると、次のような処理が必要になる。
- ライフ(ボールの数)を1つ減らす
- ライフがゼロになったら、ゲームオーバー状態にする
- ライフがゼロになっていなかったら、ボールの位置を初期状態に戻してゲームを再開する。
このプログラムは、ステートマシンになっており、プログラムが起動すると、状態がSTATE_INIT→STATE_STARGAME→STATE_INGAMEと進んでいく。ゲームオーバーの処理を加えるためには、状態を増やし、ゲームオーバーになったがゲームが始まっていない、という状態を作っていく必要がある。

注意
このページは、quiita.com で公開されています。URLがqiita.com以外のサイト、例えばjpdebug.comなどのページでご覧になっている場合、悪質な無許可転載サイトで記事を見ています。正しい記事は、https://qiita.com/BUBUBB/items/7ce85ada67a3f6d1944d からリンクしています。
無許可転載サイトでの権利表記(CC BY SA 2.5、CC BY SA 3.0とCC BY SA 4.0など)は、不当な表示です。
正確な内容は、qiitaのページで参照してください。
アイドル状態の作成
今まで、ゲームのメイン処理では状態遷移について変更してこなかった。 今回、状態を複数増やしていく。今回のようなステートマシンの場合、前に書いたような状態の遷移図だけを把握しておけば、状態を増やすのは簡単になる。
状態の数が増えていくと、 switch-caseの条件が増えていきプログラムが縦に長くなっていく。必要に応じて、サブ状態を定義していくこともできる。(例えば、STATE_INGAME(ゲーム実行中)という状態の中に、STATE_SUB_BLOCKHIT、STATE_SUB_PADDLEHITなど、複数の状態を含めるなど)
私見だが、1つの関数は短いに越したことはないが、長くなったからと言って必ずしも醜いプログラムになるとは思わない。switch-caseのように、プログラムの流れが上から下になっている、評価すべき条件式がswitch()の中の一つだけ、というのであればいくら長くてもわかりにくくはならない。(逆に言えば、3重、4重になったループや、複雑なifの組み合わせなどはいくら短くても理解が難しい)
まず、ゲームが始まる前の状態として、STATE_IDLEという状態を増やす。STATE_IDLEでは、画面に「ボタンを押せ」という文字を表示させ、ボタンが押されるまでこの状態を維持する。
game.hでは、STATE_IDLE という状態をenumに追加する。
game.h
:
enum GAMESTATE {
STATE_INIT, // 初期化処理中
STATE_IDLE, // ゲーム開始待ち (今回追加)
STATE_STARTGAME, // ゲーム開始(初期化中)
STATE_INGAME, // ゲーム中
STATE_GAMEOVER, // ゲームオーバー処理中
};
:
game.c内では、switchの中に、STATE_IDLEのcaseを追加する。
画面に動きがないのも面白くないので、idleCntというカウンタを使って、文字を白赤で点滅させてみることにする。文字が表示される、されないを状態にすることもできるが、さすがにそれは面倒くさい。そこで、状態遷移のループを回すときに汎用的に使えるカウンタ、heartBeatを用意し、様々なことに使うことにする。
game.c
:
gameState = STATE_IDLE; //初期化処理が終了したのでゲーム開始処理を行う
static u16 heartBeat = 0; // 状態遷移ループのカウンタ (追加)
while (gameState != STATE_GAMEOVER) { //ゲームオーバーになるまで繰り返す
:
while(WakeFlag == 0) {
delay_1ms(10);
break;
}
:
:
heartBeat = (heartBeat+1) & 0x7FFF; // 状態遷移ループのカウンタを増やす (追加)
switch (gameState) {
case STATE_IDLE:{
:
heartBeat = (heartBeat+1) & 0x7FFF; // 状態遷移ループのカウンタを増やす (追加)
という行は、0~0x7FFFまでを繰り返す。
heartBeat= heartBeat + 1;
if (heartBeat> 0x7FFF) heartBeat = 0;
の2行と同じ動作になる。カウンタをある値でゼロに戻す、という場合、ある値、というのが2進数での桁上がりのタイミングであれば、コストの高い比較を使わずに処理できる。
用意したカウンタ heartBeatを使って、STATE_IDLE内の処理を記述する。この処理の中で、ボタンの入力を待っているのは次の部分。
if (CheckP1Button()) {
:
}
これはLongan Nanoを使ってみる 11 ~ボタンの入力~で実装したボタンの入力処理になる。ボタンが押されたら、状態を表す変数 gameStateにSTATE_STARTGAMEを設定する。
game.c
:
switch (gameState) {
case STATE_IDLE:{
u16 cnt = heartBeat & 0xFF;
if (cnt == 0x00) {
LCD_ShowString(5,40,(const unsigned char *)"PUSH BUTTON",WHITE);
LCD_ShowString(5,60,(const unsigned char *)" TO START ",WHITE);
} else if (cnt == 0x80) {
LCD_ShowString(5,40,(const unsigned char *)"PUSH BUTTON",RED);
LCD_ShowString(5,60,(const unsigned char *)" TO START ",RED);
}
if (CheckP1Button()) {
gameState = STATE_STARTGAME;
break;
}
break;
}
case STATE_STARTGAME:{
/*ゲームの開始処理 */
:
後ろの壁を取り除く
現在、このプログラムはパドルの後ろの壁にも反射するようになっており、ボールが失われることは無い。ボールロスの処理を作る前に、まずはパドルの後ろにある壁を取りぞのく必要がある。
wall.c の checkWallでは、ボールが壁に当たった時に反射する処理が書かれている。この関数は、戻り値として
- 0 .. ミス。すべてのボールがなくなった
- 1 .. ゲーム継続
- 2 .. そのボールは失われたがまだボールが残っている
を返す仕様。
// 横の壁のチェック。ボールは3x3ドットなので、衝突判定はその範囲を考慮して、1ドット分広くチェックする。
if ((x-1) <= GAMEAREA_X0 || (x+1) >=GAMEAREA_X1) {
:
横の壁に当たった時の処理
:
}
if ((y-1) <=GAMEAREA_Y0 || (y+1) >=GAMEAREA_Y1) {
:
前後の壁に当たった場合の処理
:
if ((y-1) <= GAMEAREA_Y0) {
:
正面の壁に当たった時の処理
:
} else {
:
パドルの後ろの壁に当たった時の処理
:
}
}
現在、パドルの後ろの壁に当たった時の処理は「なにもしない」になっている。この部分を書いていくことになる。
すべきことは、次の通り。
- 今処理中のボールのデータをクリアする。
- 画面上にボールが1つも無くなったらボールロスとして0を返す。そうでなければ2を返す。
ball.cでは、すでにボールを削除する関数(BallDead)が存在するが、削除した後に何個ボールが生きているかを知る関数はない。まず、ball.cとball.hに、今生きているボールの数を返す関数を追加する。
ball.h
:
int GetBallCount();
:
ball.c
// 生きているボールの数を返す
int GetBallCount()
{
int ballCount = 0;
for (u8 i = 0 ; i < 5;i++) {
if (ball[i].x != 0 && ball[i].y != 0) {
ballCount++;
}
}
return ballCount;
}
この関数を使って、パドルの後ろの壁に当たった処理を記述する。
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 (GetBallCount() == 0) { // すべてのボールがなくなった
return 0;
} else { // まだボールは残っている
return 2;
}
}
}
return 1;
}
これで、パドルの後ろの壁がなくなり、パドルの後ろ側にボールが行ってしまったらcheckWall関数から0が戻されることになる。
ball.c側では、すでにこの戻り値に対する処理が実装済みになっており、ボールミスした場合には、moveBall関数から 0 が返るようになっている。
ball.c
unsigned char moveBall()
{
:
// 壁反射チェック
{
u8 ret = checkWall(bi);
if (ret == 0) {
return 0;
} else if (ret == 2) {
continue;
}
}
:
ボールをロスしたときの状態を追加
まず、STATE_BALLLOSS状態を作る。状態の作り方は、アイドル状態と同じ。状態を示す列挙値 STATE_BALLLOSSを追加し、それに合わせてcase も増やす。
game.h
enum GAMESTATE {
STATE_INIT, // 初期化処理中
STATE_IDLE,
STATE_STARTGAME, // ゲーム開始(初期化中)
STATE_INGAME, // ゲーム中
STATE_BALLLOSS, // ボールのロス (追加)
STATE_GAMEOVER, // ゲームオーバー処理中
};
main.cでは、今まで STATE_STARTGAME 内で行っていた、ライフを3にする処理と、スコアを0にする処理、ブロックを初期化する処理を、STATE_IDLEに移動させる。これは、ボールロスが起こった時、ライフを1減らして再度ゲームを開始するため。
STATE_BALLLOSS → STATE_STARTGAMEに状態を遷移させるが、STATE_STARTGAME内でライフやスコアをリセットさせてしまっては意味がない。
そこで、ライフやスコアの初期化などはSTATE_IDLE側に、それ以外の今あるブロックの描画などはSTATE_STARTGAME側にする。
game.c
:
switch (gameState) {
case STATE_IDLE:{
u16 cnt = heartBeat & 0xFF;
if (cnt == 0x00) {
LCD_ShowString(5,40,(const unsigned char *)"PUSH BUTTON",WHITE);
LCD_ShowString(5,60,(const unsigned char *)" TO START ",WHITE);
} else if (cnt == 0x80) {
LCD_ShowString(5,40,(const unsigned char *)"PUSH BUTTON",RED);
LCD_ShowString(5,60,(const unsigned char *)" TO START ",RED);
}
if (CheckP1Button()) {
LCD_Clear(BLACK);
Score = 0;
LifeCnt = 3;
InitBlock();
gameState = STATE_STARTGAME;
break;
}
break;
}
case STATE_INGAME:{
drawDeleteBall(FALSE); // ひとつ前のボールを消す
u8 ret= moveBall();
if (ret == 0) { // すべてのボールがなくなったら
}
drawDeleteBall(TRUE); // ひとつ前のボールを消す
breakout_PaddleCtrl(isDemo); // パドルを動かす
break;
}
case STATE_BALLLOSS:{ // ボールロス
/* この部分を実装していく */
break;
}
case STATE_GAMEOVER:{
/*ゲーム-オーバー処理*/
break;
}
}
}
ボールロスによる状態の遷移を実装
STATE_BALLLOSS状態を新しく作った(といっても、caseを追加しただけだが)ので、次にここに状態を遷移させる処理を追加する。現時点で、すでにmoveBallの戻り値は評価されているので、この部分に状態遷移の処理を追加する。具体的な処理としては、ライフを一つ減らして、ボールロスの状態に遷移させればよい。
game.c
:
case STATE_INGAME:{
drawDeleteBall(FALSE); // ひとつ前のボールを消す
u8 ret= moveBall();
if (ret == 0) { // すべてのボールがなくなったら
LifeCnt--; // ライフを一つ減らして
gameState = STATE_BALLLOSS; // ボールロスの状態に遷移させる
break;
}
:
これで、すべてのボールがなくなったらボールロスの状態に遷移するようになる。
ボールロス状態の実装
ボールロス状態になったら場合の処理は、次のようにする。
- ライフがゼロになったらゲームオーバー状態(STATE_GAMEOVER)に遷移させる。
- 画面に、”-- MISS --"などと表示する。
- 一定時間、もしくはボタンが押されたらSTATE_STARTGAME状態に遷移させてゲームを再開する
これを踏まえて、case STATE_BALLLOSS: の中を実装する。
この中で、「一定時間が経過すると」というのが意外と難しい。ステートマシンでは、この状態の中で待ちを入れることはできない。そのため、"--MISS!--"などと表示した後に _delay(100)で待つ、というコードは書けず、状態遷移ループを回し続ける必要がある。
一つの方法は、STATE_BALLLOSSでは"MISS"と表示するなどの処理にとどめ、STATE_BALLLOSSWAITという状態を回して待つという方針がある。この方法は、ステートマシンのコードとしては素直でわかりやすいが、面倒になってしまう。そこで、今回はwaitCnt という変数でカウンタを使ってSTATE_BALLLOSSを複数回回している。
game.c
:
case STATE_INGAME:{
drawDeleteBall(FALSE); // ひとつ前のボールを消す
u8 ret= moveBall();
if (ret == 0) { // すべてのボールがなくなったら
LifeCnt--;
gameState = STATE_BALLLOSS; // ボールロスの状態に遷移させる
break;
}
drawDeleteBall(TRUE); // ひとつ前のボールを消す
breakout_PaddleCtrl(isDemo); // パドルを動かす
break;
}
case STATE_BALLLOSS:{ // ボールロス
static u16 waitCnt = 0;
// ライフがないならゲームオーバーに遷移
if (LifeCnt == 0) {
gameState = STATE_GAMEOVER;
break;
}
// 画面に"-- MISS! --"と表示させる
LCD_ShowString(5,80,(const unsigned char *)"-- MISS! --",WHITE);
// 一定時間が経過するか、ボタンが押されたらゲームの再開に遷移する
waitCnt++;
if (waitCnt == 0x100 || CheckP1Button()) {
LCD_Clear(BLACK);
waitCnt = 0;
gameState = STATE_STARTGAME;
}
break;
}
case STATE_GAMEOVER:{
:
動作の確認
いま、プログラムではパドルが自動的に動くようになっている。今回行った追加をテストするためには、パドルが自動的に動く処理をやめる必要がある。(自動的に動かす場合、あまりミスしないので)
デモモードの実装でmain.cに加えたデモモード指定を変更して、手動操作に変えておく。
main.c
int main( void )
{
printf_debug_init();
led_init();
Game(FALSE); // TRUEからFALSEに変更
while(1){
}
};
これでビルド/実行すると、ボールをミスすると、画面に --MISS-- と表示され、LIFEが減っていくようになる。 LIFEが1の時にミスすると、そこで画面は止まる。(STATE_GAMEOVERを実装していないので)
ここまででできたこと
プログラムに複数の状態を追加し、その状態をボタン入力や、時間経過、ゲーム上のイベント(ボールのミス)で遷移させることができるようになった。
次の記事に進む
Longan Nanoを使ってみる 13 ~ステージの遷移とゲームオーバー~
今回の追加を反映したソースコード
game.h
#ifndef __GAME_H__
#define __GAME_H__
extern void Game(bool);
// ゲームの表示領域
#define GAMEAREA_X0 0
#define GAMEAREA_Y0 20
#define GAMEAREA_X1 79
#define GAMEAREA_Y1 148
enum GAMESTATE {
STATE_INIT, // 初期化処理中
STATE_IDLE,
STATE_STARTGAME, // ゲーム開始(初期化中)
STATE_INGAME, // ゲーム中
STATE_BALLLOSS, // ボールのロス
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"
#include "paddle.h"
#include "button.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;
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(bool isDemo)
{
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);
Adc_init();
u16 tick = 0; // LEDを点滅させるためのカウンターを初期化
gameState = STATE_IDLE; //初期化処理が終了したのでゲーム開始処理を行う
static u16 heartBeat = 0; // 状態遷移ループのカウンタ
while (TRUE) {
timer_enable(TIMER5); // タイマーを有効にする
// タイマーのウェイト処理。wakeFlagが割り込みルーチン内で1になるまで無限ループする
while(WakeFlag == 0) {
delay_1ms(10);
break;
}
tick = (tick + 1) & 0x8FFF; // LEDの点滅用カウンタのインクリメント
WakeFlag = 0; // タイマーのウエイトフラグを初期化する
heartBeat = (heartBeat+1) & 0x7FFF;
switch (gameState) {
case STATE_IDLE:{
u16 cnt = heartBeat & 0xFF;
if (cnt == 0x00) {
LCD_ShowString(5,40,(const unsigned char *)"PUSH BUTTON",WHITE);
LCD_ShowString(5,60,(const unsigned char *)" TO START ",WHITE);
} else if (cnt == 0x80) {
LCD_ShowString(5,40,(const unsigned char *)"PUSH BUTTON",RED);
LCD_ShowString(5,60,(const unsigned char *)" TO START ",RED);
}
if (CheckP1Button()) {
LCD_Clear(BLACK);
Score = 0;
LifeCnt = 3;
InitBlock();
gameState = STATE_STARTGAME;
}
break;
}
case STATE_STARTGAME:{
/*ゲームの開始処理 */
DrawBORDER(); //外枠とライフ残、スコアを画面に表示させる
DrawBlock();
InitBallPos(0,NULL);
InitPaddle();
gameState = STATE_INGAME;
break;
}
case STATE_INGAME:{
drawDeleteBall(FALSE); // ひとつ前のボールを消す
u8 ret= moveBall();
if (ret == 0) { // すべてのボールがなくなったら
LifeCnt--;
gameState = STATE_BALLLOSS; // ボールロスの状態に遷移させる
break;
}
drawDeleteBall(TRUE); // ひとつ前のボールを消す
breakout_PaddleCtrl(isDemo); // パドルを動かす
break;
}
case STATE_BALLLOSS:{ // ボールロス
static u16 waitCnt = 0;
// ライフがないならゲームオーバーに遷移
if (LifeCnt == 0) {
gameState = STATE_GAMEOVER;
break;
}
// 画面に"-- MISS! --"と表示させる
LCD_ShowString(5,80,(const unsigned char *)"-- MISS! --",WHITE);
// 一定時間が経過するか、ボタンが押されたらゲームの再開に遷移する
waitCnt++;
if (waitCnt == 0x100 || CheckP1Button()) {
LCD_Clear(BLACK);
waitCnt = 0;
gameState = STATE_STARTGAME;
}
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);
int GetBallCount();
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"
#include "paddle.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;
}
// 生きているボールの数を返す
int GetBallCount()
{
int ballCount = 0;
for (u8 i = 0 ; i < 5;i++) {
if (ball[i].x != 0 && ball[i].y != 0) {
ballCount++;
}
}
return ballCount;
}
// ボールの速度を調整する
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);
}
}
}
void CheckPaddle(struct BALLINFO *bi)
{
int chkx = CVT_AXIS(bi->x);
int chky = CVT_AXIS(bi->y);
for (int j=0 ; j<2;j++) {
struct PADDLEINFO* pi = GetPaddleInfo(j);
if (pi == NULL) continue;
u8 pos = GetOrthant(chkx,chky,pi->x1,pi->y1,pi->x2,pi->y2);
if (pos == 5) {
//ひとつ前のボール座標が、パドルのどこにあったかを求める。
chkx = CVT_AXIS(bi->oldx);
chky = CVT_AXIS(bi->oldy);
u8 prevpos = GetOrthant(chkx,chky,pi->x1,pi->y1,pi->x2,pi->y2);
// ボールの新しい位置は、パドルの内側なので、ボールの座標をもとに戻さないといけない
BallBack(bi);
// パドルにボールが反射する処理
if (prevpos == 2 || prevpos == 8) { // パドルの長径に当たった場合、y座標を反転
bi->dyBase = -bi->dyBase;
u8 pdlcx = pi->x1 + pi->Width/2;
u8 ballpdldif = abs(chkx - pdlcx);
if (ballpdldif > ( pi->Width/2) / 3) { // 端だったら角度を増やす
int cx = pi->x1 + pi->Width / 2; // パドルの中央位置
int ballX = CVT_AXIS(bi->x); // ボールの位置
if (bi->dxBase > 0) { // ボールは右に移動中
bi->dxBase += (ballX > cx) ? 1 : -1;
} else { // ボールは左に移動中
bi->dxBase += (ballX > cx) ? -1 : 1;
}
if (abs(bi->dxBase) < BALL_DX_MIN) {
bi->dxBase = bi->dxBase > 0 ? BALL_DX_MIN : -BALL_DX_MIN;
} else if (abs(bi->dxBase) > BALL_DX_MAX) {
bi->dxBase = bi->dxBase > 0 ? BALL_DX_MAX : -BALL_DX_MAX;
}
}
} else if (prevpos == 4 || prevpos == 6) { // パドルの横に当たった時
// x反転
bi->dxBase = -bi->dxBase;
} else { // それ以外。パドルの角に当たった時
bi->dyBase = -bi->dyBase;
bi->dxBase = -bi->dxBase;
}
}
}
}
// ボールを動かす。
// 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;
}
CheckPaddle(bi);
updateBallSpeed(bi);
}
return 1;
}
wall.c
#include "lcd/lcd.h"
#include "led.h"
#include "memory.h"
#include "gd32vf103.h"
#include "game.h"
#include "ball.h"
// 壁反射のチェック
// 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 (GetBallCount() == 0) { // すべてのボールがなくなった
return 0;
} else { // まだボールは残っている
return 2;
}
}
}
return 1;
}
main.c
int main( void )
{
printf_debug_init();
led_init();
Game(FALSE); // TRUEからFALSEに変更
while(1){
}
};