前の記事
Longan Nanoを使ってみる 9 ~A/Dコンバータから入力~
全体の目次
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などのページでご覧になっている場合、悪質な無許可転載サイトで記事を見ています。正しい記事は、https://qiita.com/BUBUBB/items/7ce85ada67a3f6d1944d からリンクしています。
無許可転載サイトでの権利表記(CC BY SA 2.5、CC BY SA 3.0とCC BY SA 4.0など)は、不当な表示です。
正確な内容は、qiitaのページで参照してください。
パドルのデータ
まず、パドルを表示させるためのデータを定義する。
ヘッダファイルでは、データの構造を定義する。 描画と衝突判定のため、パドルの矩形の左上と右下の座標。描画のため移動前のパドルの左側X座標、右側y座標、パドルの大きさを変化させるとき、左上、右下の座標を決定させるためのパドルの横幅をデータとして持たせる。
Cプログラム側では、パドルは、上下に最大2個出せるように配列で持たせたデータ本体と、データ本体にアクセスさせるための関数を用意しておく。
paddle.h
//パドルの位置に関する情報
struct PADDLEINFO {
unsigned char x1;
unsigned char y1;
unsigned char x2;
unsigned char y2;
unsigned char prevx1;
unsigned char prevx2;
unsigned char Width;
};
struct PADDLEINFO* GetPaddleInfo(int idx);
void InitPaddle(void);
paddle.c
:
// パドルの情報
struct PADDLEINFO Paddle[2];
// パドル位置の初期化
void InitPaddle(void)
{
memset(Paddle,0,sizeof(Paddle));
Paddle[0].Width = 20;
Paddle[0].x1 = (GAMEAREA_X1 /2) - Paddle[0].Width /2 ;
Paddle[0].y1 = GAMEAREA_Y1 - 15;
Paddle[0].x2 = (GAMEAREA_X1 /2) + Paddle[0].Width /2 ;
Paddle[0].y2 = GAMEAREA_Y1 - 10;
}
// 外部からパドルの位置などの情報にアクセスさせるための関数
struct PADDLEINFO* GetPaddleInfo(int idx)
{
if (Paddle[idx].y1 == 0) return NULL;
return &Paddle[idx];
}
:
パドルの表示
パドルは、移動したときに描画される。このとき、いったん古いパドルを消して、新しいパドルを描画する必要がある。
ただ、ボールに比べてパドルは大きさが大きく、全体を消して再描画する場合、ちらつきが見えてしまったり、速度が遅くなってしまう。そのため、少しだけ工夫する必要がある。幸い、パドルは左右にしか動かず、しかも矩形なので差分だけを消すことが簡単にできる。
パドルは左右にしか動かないので、下の図のように、パドルが右にしか動かないときには、動く前のx1から、動いた後のx1までの矩形を背景色で塗りつぶすだけでよい。逆に、パドルが左に動いたときは、現在のx2から、動いたあとのx2までの矩形を塗りつぶす。
こうすることで、パドル全体が消える瞬間がなくなり、パドルがちらつかなくなる。もちろん、負荷も低くなるだろう。
paddle.c
// パドルの描画
void drawPaddle()
{
for (u8 i = 0 ; i < 2;i++) {
struct PADDLEINFO* pi = &Paddle[i];
if (pi->y1 == 0) continue;
LCD_Fill(pi->x1,pi->y1,pi->x2,pi->y2, WHITE);
if (pi->prevx1 >= (GAMEAREA_X0+1) && pi->x1 > pi->prevx1) {
LCD_Fill(pi->prevx1,pi->y1,pi->x1,pi->y2,BLACK);
} else if (pi->x1 < pi->prevx1) {
LCD_Fill(pi->x2,pi->y1,pi->prevx2,pi->y2,);
}
}
}
パドルを動かす
パドルを、A/Dコンバーターから得た値に基づいて動かす。ここでは、関数を2つに分ける。
- パドルの番号(最大2)と、パドルの中心座標を指定して、パドル情報 Paddle[2]の配列を更新する。
- ゲームの主処理から呼び出され、パドルの情報を読み込み、パドル情報を更新し、描画するという一連の処理を実行する。
パドルの中心のX座標としてボリュームから入力した値を使い、保存する情報はパドルの4隅の座標、というのは面倒なようだがボリュームを動かすときだけ4隅の計算をすればよいので、実行速度としては有利だと思うのでこの方法にした。(常に中心座標とパドルの幅だけを持つ方法もあった。パドルの側面への衝突などを含め、ブロックと同じように扱いたかったので、ブロックと同じ四隅座標を持つようにした)
setPaddle(int pdlId , int cx)は、paddle.c以外から呼び出されることがないので、ヘッダファイルへのプロトタイプ宣言を外してある。
paddle.h
void breakout_PaddleCtrl(void);
paddle.c
// パドルの中心座標(X)を指定し、その位置にパドルを移動させる
void setPaddle(int pdlId , int cx) {
Paddle[pdlId].prevx1 = Paddle[pdlId].x1;
Paddle[pdlId].prevx2 = Paddle[pdlId].x2;
u8 cxMin = GAMEAREA_X0 + (Paddle[pdlId].Width / 2)+1;
u8 cxMax = GAMEAREA_X1 - (Paddle[pdlId].Width / 2)-1;
if (cx >= cxMax) cx = cxMax;
if (cx <= cxMin) cx = cxMin;
Paddle[pdlId].x1 = cx - Paddle[pdlId].Width / 2;
Paddle[pdlId].x2 = cx + Paddle[pdlId].Width / 2;
}
// パドルを動かす
void breakout_PaddleCtrl(void)
{
u16 padPos = getPaddlePos();
for (u8 i = 0 ; i < 2;i++) {
struct PADDLEINFO* pi = &Paddle[i];
if (pi->y1 == 0) continue;
u8 newpadx = (GAMEAREA_X1 - GAMEAREA_X0) * padPos / 1024;
setPaddle(i,newpadx);
}
drawPaddle();
}
ゲームの主処理からパドルの移動を呼び出す
前の章では、ADコンバーターからの入力を、ゲームの主処理で取得し表示していた。
case STATE_INGAME:{
:
drawDeleteBall(TRUE); // ひとつ前のボールを消す
uint16_t val = getPaddlePos();
printf("VAL:%5d \r\n",val);
break;
:
この部分の処理を削除し、パドルを動かすように書き換える。また、ゲームの開始処理では、パドルの位置の初期化も加える。
game.c
void Game(void)
{
:
:
while (TRUE) { //
timer_enable(TIMER5); // タイマーを有効にする
:
:
switch (gameState) {
case STATE_STARTGAME:{
/*ゲームの開始処理 */
Score = 0;
LifeCnt = 3;
DrawBORDER(); //外枠とライフ残、スコアを画面に表示させる
InitBlock();
DrawBlock();
InitBallPos(0,NULL);
InitPaddle(); // パドル位置の初期化を行う
gameState = STATE_INGAME;
break;
}
case STATE_INGAME:{
drawDeleteBall(FALSE); // ひとつ前のボールを消す
u8 ret= moveBall();
if (ret == 0) {
}
drawDeleteBall(TRUE); // ひとつ前のボールを消す
breakout_PaddleCtrl(); // パドルを動かす
break;
}
ボールとの衝突処理
ここまでの処理で、画面にはパドルが表示され、ボリュームを動かすと画面上のパドルが動くようになっている。
次に、ボールとパドルの衝突判定を行い、ボールをパドルで反射させる。基本的な考え方は、ブロックとの衝突と同じで、違うところはぶつかってもパドルは消えない、という点。
衝突の判定は、game.c内で定義されている、GetOrthant(int x , int y , int x1, int y1 , int x2 , int y2);関数が汎用的に使用できる。これは、矩形とボールの位置を与え、ボールが前の図のどの点に存在するかを判定する。
この処理は、paddle.cではなくball.c側に実装する。
CheckPaddle 関数は、ボール情報へのポインタを与えられ、そのボールがパドル(最大2個)と衝突したかをGetOrthantで調べる。現在の座標が、⑤にある場合、ボールはパドルと衝突した、と判断し、ボールの位置を1つ前(衝突の直前)に戻し、ボールの反転処理を行う。
また、ボールの反射処理では、ゲーム中にボールの動きに変化を持たせるため、②の場所に当たった場合、パドルの位置によってボールの角度が変わるようになっている。
ball.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;
}
}
}
}
パドルの禁則処理
実際に、このプログラムには、ひとつ大きなバグがある。ボールがパドルと壁の間に挟まれた時の処理が考慮されていない。
挟まれた時には、2つのパターンが考えられる。
ボールが壁とパドルの間に挟まれて「捕まって」しまう。
さらに、この状態でパドルが左に動くと、パドルの中にボールが入り込んでしまう。こうなると、ボールはパドルの中から出られなくなってしまい、ボールが画面から消えてしまう。
そこで、ボールがパドルと横並びになっているとき、パドルを壁から強制的に少し離してボールが挟まれないようにする。こうすると、仮にボールがパドルの中に入ってしまってもパドルが移動するのでボールは外に出ることができる。
かなり場当たり的な処理だが。
// パドルの中心座標(X)を指定し、その位置にパドルを移動させる
void setPaddle(int pdlId , int cx) {
Paddle[pdlId].prevx1 = Paddle[pdlId].x1;
Paddle[pdlId].prevx2 = Paddle[pdlId].x2;
u8 cxMin = GAMEAREA_X0 + (Paddle[pdlId].Width / 2)+1;
u8 cxMax = GAMEAREA_X1 - (Paddle[pdlId].Width / 2)-1;
if (cx >= cxMax) cx = cxMax;
if (cx <= cxMin) cx = cxMin;
Paddle[pdlId].x1 = cx - Paddle[pdlId].Width / 2;
Paddle[pdlId].x2 = cx + Paddle[pdlId].Width / 2;
// ボールが壁とパドルに挟まれないように、ボールの位置によってパドルの動きを制限する
for (u8 i = 0 ; i < 5;i++) {
struct BALLINFO *bi = GetBallInfo(i);
if (bi == NULL) continue;
// パドルのy座標とボールのy座標を比べ、横並びになっていたら
if ( Paddle[pdlId].y1 <= CVT_AXIS(bi->y) && CVT_AXIS(bi->y) <= Paddle[pdlId].y2) {
// ボールがパドルと横並び
if (CVT_AXIS(bi->x) <=4 && Paddle[pdlId].x1 <=4) { //ボールが左端4ドット内にいる場合
Paddle[pdlId].x1 = 4;
Paddle[pdlId].x2 = Paddle[pdlId].x1 + Paddle[pdlId].Width;
} else if (CVT_AXIS(bi->x) >= (GAMEAREA_X1 - 4) && Paddle[pdlId].x2 >= (GAMEAREA_X1 - 4)) {
Paddle[pdlId].x2 = (GAMEAREA_X1 - 4);
Paddle[pdlId].x1 = Paddle[pdlId].x2 - Paddle[pdlId].Width;
}
}
}
}
デモモードの実装
パドルを動くようにしたが、毎回テストのたびにボリュームを動かすのは面倒だ。
そこで、ボールの位置に応じて、パドルを自動的に動かす「デモモード」を実装する。デモモードは、グローバル変数などではなく、main.c内で引数で呼び出すようにする。これは、将来、デモモードとプレイモードを動的に切り替えるようにするため。(main.c内で、メニューを表示させる、など)
パドルは、基本的にボールのx座標と、パドルのx座標を合わせるように動かす。ただ、今回のプログラムでは、ボールは最大5つ表示できるようになっており、どのボールを追いかけるかを決める必要がある。
このプログラムでは、ボールとパドルの「壁を考えた距離」を基本とする。ボールがパドルから遠ざかっているときは、ボールy位置+壁とパドルとの距離(ボールは、奥の壁に跳ね返って帰ってくる)と考え、ボールがパドルから近づいているときは、ボールy位置とパドルのy位置の距離と考える。
この距離が近いものを追いかけるようにする。
paddle.h
:
void breakout_PaddleCtrl(bool);
:
paddle.c
// パドルを動かす
void breakout_PaddleCtrl(bool isDemo)
{
u16 padPos = getPaddlePos();
for (u8 i = 0 ; i < 2;i++) {
struct PADDLEINFO* pi = &Paddle[i];
if (pi->y1 == 0) continue;
if (isDemo) {
//追いかけるべきボールを探す
// ボールの実質的な距離をベースにする
int distMax = INT16_MAX;
int ballIdx = -1;
for (int j =0;j <5; j++) {
struct BALLINFO* bi = GetBallInfo(j);
if (bi == NULL) continue;
int dist;
if (bi->dy < 0) { // ボールが遠ざかっているなら
dist = CVT_AXIS(bi->y) + pi->y1;
} else { // ボールが近づいているなら
dist = CVT_AXIS(pi->y1) - bi->y;
}
if (dist < distMax) {
ballIdx = j;
distMax = dist;
}
}
struct BALLINFO* bi = GetBallInfo(ballIdx);
u8 cx = pi->x1 + (pi->Width / 2);
if (CVT_AXIS(bi->x) > cx) {
setPaddle(i,cx+1);
} else if (CVT_AXIS(bi->x) < cx) {
setPaddle(i,cx-1);
}
} else {
u8 newpadx = (GAMEAREA_X1 - GAMEAREA_X0) * padPos / 1024;
setPaddle(i,newpadx);
}
}
drawPaddle();
}
game.h
extern void Game(bool);
game.c
//
// メイン処理
//
void Game(bool isDemo)
{
rcu_periph_clock_enable(RCU_GPIOB);
gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_8); // B8をデバッグに使う
:
case STATE_INGAME:{
:
:
drawDeleteBall(TRUE); // ひとつ前のボールを消す
breakout_PaddleCtrl(isDemo); // パドルを動かす
break;
}
:
main.c
int main( void )
{
printf_debug_init();
led_init();
Game(TRUE);
while(1){
}
};
ここまででできたこと
画面上にパドルを表示させ、ボールを反射させる処理を追加した。
次の記事に進む
今回の追加を反映したソースコード
main.c
#include "lcd/lcd.h"
#include "gd32vf103.h"
#include "fatfs/tf_card.h"
#include "systick.h"
#include <stdio.h>
#include "game.h"
void printf_debug_init()
{
// GPIOのAが、USART0なのでGPIOAにクロックを供給する
rcu_periph_clock_enable(RCU_GPIOA);
// USART0にクロックを供給
rcu_periph_clock_enable(RCU_USART0);
// TX、RXがGPIOAの9と10に出ているので、それぞれを初期化する。
gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9); // USART0 TX
gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_10); // USART0 RX
// TXの設定を行う。デバッグでは、ターミナルへの出力だけを行うので、最低限の設定でよい。
usart_deinit(USART0);
usart_baudrate_set(USART0, 115200U);
usart_word_length_set(USART0, USART_WL_8BIT);
usart_stop_bit_set(USART0, USART_STB_1BIT);
usart_parity_config(USART0, USART_PM_NONE);
usart_hardware_flow_rts_config(USART0, USART_RTS_DISABLE);
usart_hardware_flow_cts_config(USART0, USART_CTS_DISABLE);
usart_receive_config(USART0, USART_RECEIVE_ENABLE);
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
//USARTを有効にする
usart_enable(USART0);
}
int _put_char(int ch)
{
usart_data_transmit(USART0, (uint8_t) ch );
while ( usart_flag_get(USART0, USART_FLAG_TBE)== RESET){
}
return ch;
}
int main( void )
{
printf_debug_init();
led_init();
Game(TRUE);
while(1){
}
};
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_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"
#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;
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_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);
InitPaddle();
gameState = STATE_INGAME;
break;
}
case STATE_INGAME:{
drawDeleteBall(FALSE); // ひとつ前のボールを消す
u8 ret= moveBall();
if (ret == 0) {
}
drawDeleteBall(TRUE); // ひとつ前のボールを消す
breakout_PaddleCtrl(isDemo); // パドルを動かす
break;
}
case STATE_GAMEOVER:{
/*ゲーム-オーバー処理*/
break;
}
}
}
}
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;
}
// ボールの速度を調整する
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;
}
paddle.h
#ifndef __paddle_h__
#define __paddle_h__
//パドルの位置に関する情報
struct PADDLEINFO {
unsigned char x1;
unsigned char y1;
unsigned char x2;
unsigned char y2;
unsigned char prevx1;
unsigned char prevx2;
unsigned char Width;
};
struct PADDLEINFO* GetPaddleInfo(int idx);
void Adc_init(void) ;
uint16_t Get_adc(int ch);
unsigned int getPaddlePos();
void breakout_PaddleCtrl(bool);
void InitPaddle(void);
#endif
paddle.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"
// ボリューム入力の初期化
void Adc_init(void)
{
// GPIOポートA を、アナログ入力モード、50MHz、ピン0とピン1
gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0|GPIO_PIN_1);
// ADCのクロックプリスケーラを APB2/12 に設定する
rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV12);
// ADC0にクロックを供給する
rcu_periph_clock_enable(RCU_ADC0);
// ADCをクリアする
adc_deinit(ADC0);
// ADCをフリーモード(全ADCを独立して動作させる)にする。(ADC_CTL0のSYNCMを0b000)
adc_mode_config(ADC_MODE_FREE);
// ADCのデータを右詰めにする
adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT);
// チャンネルグループのデータ長を1に設定する。(単発)
adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL,1);
// ADCの外部トリガーソースを無効にする
adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC0_1_EXTTRIG_INSERTED_NONE);
// ADCの外部トリガーのレギュラーチャンネルを有効にする
adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE);
// ADCを有効にする
adc_enable(ADC0);
delay_1ms(1);
// キャリブレーションを行う。この関数は、中でキャリブレーションの完了を待つので、呼び出すだけでよい
adc_calibration_enable(ADC0);
}
//指定されたチャンネルからデータを読みだす
uint16_t Get_adc(int ch)
{
// 読みだすチャンネルを指定する
adc_regular_channel_config(ADC0, 0 , (uint8_t)ch, ADC_SAMPLETIME_7POINT5);
// 読み出しを開始する
ADC_CTL1(ADC0)|=ADC_CTL1_ADCON;
// データが出そろうのを待つ
while(adc_flag_get(ADC0 , ADC_FLAG_EOC) == RESET);
// データを読みだす
int ret = adc_regular_data_read(ADC0);
// フラグをクリアする
adc_flag_clear(ADC0,ADC_FLAG_EOC);
return ret;
}
// ADCの値を読み出し、パドルの位置に変更する。
#define MAX_PADDLE_SPEED 100
unsigned int getPaddlePos()
{
static unsigned int actualValue = -1;
// 0~4096の値を、4で割って0~1024くらいにしておく。端っこのほうが怪しいから。
short CurrentPos = Get_adc(0);
CurrentPos = CurrentPos >> 2;
if (actualValue == -1) {
actualValue = CurrentPos;
} else {
int dif = abs(actualValue - CurrentPos);
if (dif < MAX_PADDLE_SPEED) {
actualValue = CurrentPos;
} else {
if (actualValue > CurrentPos) {
actualValue-= MAX_PADDLE_SPEED;
} else {
actualValue+= MAX_PADDLE_SPEED;
}
}
}
return actualValue;
}
// パドルの情報
struct PADDLEINFO Paddle[2];
void InitPaddle(void)
{
memset(Paddle,0,sizeof(Paddle));
Paddle[0].Width = 20;
Paddle[0].x1 = (GAMEAREA_X1 /2) - Paddle[0].Width /2 ;
Paddle[0].y1 = GAMEAREA_Y1 - 15;
Paddle[0].x2 = (GAMEAREA_X1 /2) + Paddle[0].Width /2 ;
Paddle[0].y2 = GAMEAREA_Y1 - 10;
}
// 外部からパドルの位置などの情報にアクセスさせるための関数
struct PADDLEINFO* GetPaddleInfo(int idx)
{
if (Paddle[idx].y1 == 0) return NULL;
return &Paddle[idx];
}
// パドルの描画
void drawPaddle()
{
for (u8 i = 0 ; i < 2;i++) {
struct PADDLEINFO* pi = &Paddle[i];
if (pi->y1 == 0) continue;
LCD_Fill(pi->x1,pi->y1,pi->x2,pi->y2, WHITE);
if (pi->prevx1 >= (GAMEAREA_X0+1) && pi->x1 > pi->prevx1) {
LCD_Fill(pi->prevx1,pi->y1,pi->x1,pi->y2,BLACK);
} else if (pi->x1 < pi->prevx1) {
LCD_Fill(pi->x2,pi->y1,pi->prevx2,pi->y2,BLACK);
}
}
}
// パドルの中心座標(X)を指定し、その位置にパドルを移動させる
void setPaddle(int pdlId , int cx) {
Paddle[pdlId].prevx1 = Paddle[pdlId].x1;
Paddle[pdlId].prevx2 = Paddle[pdlId].x2;
u8 cxMin = GAMEAREA_X0 + (Paddle[pdlId].Width / 2)+1;
u8 cxMax = GAMEAREA_X1 - (Paddle[pdlId].Width / 2)-1;
if (cx >= cxMax) cx = cxMax;
if (cx <= cxMin) cx = cxMin;
Paddle[pdlId].x1 = cx - Paddle[pdlId].Width / 2;
Paddle[pdlId].x2 = cx + Paddle[pdlId].Width / 2;
// ボールが壁とパドルに挟まれないように、ボールの位置によってパドルの動きを制限する
for (u8 i = 0 ; i < 5;i++) {
struct BALLINFO *bi = GetBallInfo(i);
if (bi == NULL) continue;
// パドルのy座標とボールのy座標を比べ、横並びになっていたら
if ( Paddle[pdlId].y1 <= CVT_AXIS(bi->y) && CVT_AXIS(bi->y) <= Paddle[pdlId].y2) {
// ボールがパドルと横並び
if (CVT_AXIS(bi->x) <=4 && Paddle[pdlId].x1 <=4) { //ボールが左端4ドット内にいる場合
Paddle[pdlId].x1 = 4;
Paddle[pdlId].x2 = Paddle[pdlId].x1 + Paddle[pdlId].Width;
} else if (CVT_AXIS(bi->x) >= (GAMEAREA_X1 - 4) && Paddle[pdlId].x2 >= (GAMEAREA_X1 - 4)) {
Paddle[pdlId].x2 = (GAMEAREA_X1 - 4);
Paddle[pdlId].x1 = Paddle[pdlId].x2 - Paddle[pdlId].Width;
}
}
}
}
// パドルを動かす
void breakout_PaddleCtrl(bool isDemo)
{
u16 padPos = getPaddlePos();
for (u8 i = 0 ; i < 2;i++) {
struct PADDLEINFO* pi = &Paddle[i];
if (pi->y1 == 0) continue;
if (isDemo) {
//追いかけるべきボールを探す
// ボールの実質的な距離をベースにする
int distMax = INT16_MAX;
int ballIdx = -1;
for (int j =0;j <5; j++) {
struct BALLINFO* bi = GetBallInfo(j);
if (bi == NULL) continue;
int dist;
if (bi->dy < 0) { // ボールが遠ざかっているなら
dist = CVT_AXIS(bi->y) + pi->y1;
} else { // ボールが近づいているなら
dist = CVT_AXIS(pi->y1) - bi->y;
}
if (dist < distMax) {
ballIdx = j;
distMax = dist;
}
}
struct BALLINFO* bi = GetBallInfo(ballIdx);
u8 cx = pi->x1 + (pi->Width / 2);
if (CVT_AXIS(bi->x) > cx) {
setPaddle(i,cx+1);
} else if (CVT_AXIS(bi->x) < cx) {
setPaddle(i,cx-1);
}
} else {
u8 newpadx = (GAMEAREA_X1 - GAMEAREA_X0) * padPos / 1024;
setPaddle(i,newpadx);
}
}
drawPaddle();
}