前の記事
Longan Nanoを使ってみる 13 ~ステージの遷移とゲームオーバー
全体の目次
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 ~とりあえずのまとめ~
はじめに
ここでは、ボールが反射したときなどに、音を出す処理を追加していく。前回はまた電子工作とはあまり関係がなかったが、今度はまた少しだけ電子工作っぽくなる。
今回は、圧電スピーカー(圧電サウンダ)を、Longan-Nanoに追加し、PWMという方法で音を出していく。
注意
このページは、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のページで参照してください。
用意するもの
これまでの作業で必要だったもの
- Windows PC (Windows10/11)
- Longan Nano 本体
- microSD (16GBytes程度あれば十分すぎる)
- USB Type-Cケーブル (PCとLongan Nanoを接続する)
- Sipeed Risk V Debugger
- 接続ケーブル。Debugger付属、もしくは自作
- 可変抵抗器(10KΩ、Bカーブ)
- ボタン(プッシュスイッチ、タクトスイッチなど。)モーメンタリ動作のもの。
- ユニバーサル基盤。小さくてよい。45mm程度あれば十分。
- 抵抗。プルアップ用。10KΩ程度。家にあるので良い。
- 半田ごて、線材など
このページで必要になるもの
圧電スピーカーには、スピーカータイプとブザータイプが存在する。ブザータイプは電源をつなぐだけで「ブー」と鳴る。今回はこれは使用できない。必ず、スピーカー型を使用する。秋月の場合「電子ブザー」など、はっきりとわかるように書いてあるが、わかりにくい(「他励式(スピーカー)」「自励式(ブザー」など)場合もあるので注意。
接続回路
今回は、GPIOAのポート3 (A3)に圧電スピーカーを接続する。圧電スピーカーは音質が悪く音量が小さいが、頑丈で鳴らすのが簡単、電気をほとんど消費せず、インピーダンスが高い(接続してもほかの回路への影響が少ない)。
本当は、圧電スピーカと直列に抵抗を入れてLonganNano側を保護するが、なくても問題なく動作する。
というわけで今回の接続図は次の通り。
PWMの使い方
概要
サウンドを鳴らすには、GPIO A3に、一定間隔でON-OFFを出力する。ソフトウェアで実行することもできるが、それをやってしまうとオン/オフのために相当の能力を犠牲にすることになってしまう。
そこで、PWM(pulse wave modulation)という機能を使用する。この機能は、事前に設定したパラメータで、あるポートにオン/オフの繰り返し信号を出し続けるという機能。
この機能を使うと、事前に設定を行った後、一度オンにするだけで、自動的にオン/オフが繰り返される。下の図では、赤い矢印のところで、プログラムから指定を行うだけで、あとは何もしないでも一定間隔の矩形波を出力することができる。
PWMには、非常に多くのパラメータがあるが、PWMをサウンド出力(矩形波)に使うとき、気にしなければならないパラメータは2つだけ、パルス幅と、コンペア値。
このパルス幅、コンペア値はそれぞれ、周波数とデユーティ比に相当する。TimerX_CARで指定したパルス幅の1/2の値を、TIMERX_CHxCVで指定するコンペア値に指定すれば、デューティ比50%、1/4を指定すれば、デューティ比25%になる。(Xやxは、使用するタイマーとチャンネルにより異なる。例えば、今回使用するTIMER4、チャンネル3であれば、TIMER4_CARとTIMER4_Ch3CVになる)
具体的な動作として、PWMは、プリスケーラ(タイマーの使い方でも設定した)で指定した分周比で動くカウンターを使用し、次のような動作を繰り返す。
(PWM MODE 0、EAPWM、アップカウントモードの場合)
(1) カウンターがゼロになると、出力をオンにする。
(2) カウンターが一定の値(CAR)になるとカウンターをリセットして0にする。
(3) カウンターがコンペア値として一定の値(CHxCV)になると、出力はオフになる。
この動作は、GD32VF103 User Manualの Figure 15-16に図示されている。左側が、アップカウント―モードの様子で、階段状にカウンタがアップしていくと、それに応じてPWM出力が変化する。
PWMでは、そのほか、最初のカウントが始まるまでの遅延時間や、繰り返しの条件など、さまざまに指定できるが、音を出すために使用するのは、パルス幅、コンペア値と、そのおおもとになる分周比だけでよい。
いわゆる、「ピコピコ音」と表現されるレトロなゲーム音は、今回PWMで作成する、デューティ比50%の矩形波になる。
初期化
PWMを使うには、まずタイマーを用意し、一定間隔でカウンタを回す必要がある。これは、タイマーの使い方と同じ処理となる。
- メインの処理を回すのにタイマー割込みでTIMER5を使用したので、TIMER4をPWMに使用する。TIMER4について、これも54MHzのバスに接続されているが、108Mhzとして計算される。
- TIMER4は、200kHzくらいでカウントさせる。この周波数に深い意味はないが、例えば65.406Hz(C2、ドの音)を出したい場合、200Hz / 65.406 ≒ 3058 となるので、CARに3058を指定する。200Khzをあまり低くしてしまうと、計算誤差が大きくなって正確な音が出しにくくなる。
これらの指定をAPIで行うのは次のような処理になる。
timer_parameter_struct timer_initpara; // タイマーパラメータ構造体の宣言
timer_deinit(TIMER4); // TIMER4の初期化
timer_struct_para_init(&timer_initpara); // タイマーパラメータ構造体の初期化
rcu_periph_clock_enable(RCU_TIMER4); // TIMER4にクロックを供給
timer_initpara.prescaler = 539; // 108MHz/540 で、およそ200KHzが得られるハズ。
timer_initpara.alignedmode = TIMER_COUNTER_EDGE; // カウンタのアラインメントモードをEDGE(0,1,2…9,0,1,2…)にする
timer_initpara.counterdirection = TIMER_COUNTER_UP; // アップカウントモードにする
timer_initpara.period = 3; // 200KHz/4 で、おおよそ50KHzが得られるはず。これはテスト用の値で、実際は使用時に出したい音に合わせて変更される。
timer_initpara.clockdivision = TIMER_CKDIV_DIV1; // デッドタイムで使用する。ここでは使わない。
timer_initpara.repetitioncounter = 0; // 繰り返しカウンタ。毎回イベントが発生するよう0を指定
timer_init(TIMER4, &timer_initpara); // Apply settings to timer
この例では、プリスケーラによって200KHzごとにカウンタが+1されていき、4まで上がったら0に戻る。つまり、200KHzの4回に1回の割合、50KHzでPWM信号が変化することになる。
タイマー割込みだと、あとは割り込みの指定をするだけだったが、PWMの場合、作ったタイマーを、どのように出力するかを指定する。ここでは、ほとんど指定するものはない。特に、TIMER0以外では、使用できる機能がないので指定する内容は「タイマーを使用する」というものだけ。
timer_channel_output_struct_para_init(&timer_ocinitpara); // タイマーチャンネル出力構造体を初期化
timer_ocinitpara.outputstate = TIMER_CCX_ENABLE; // このチャンネルを使用するフラグ(チャンネルはtimer_channel_output_configの引数で指定される)
timer_ocinitpara.outputnstate = TIMER_CCXN_DISABLE; // コンプリメンタりのチャネルは使用しない。コンプリメンタリのチャネルはTIMER_0にだけ用意されているのでTIMER3を使用する場合は無視される。
timer_ocinitpara.ocpolarity = TIMER_OC_POLARITY_HIGH; // TIMER_0以外は使用できない。チャンネルの出力極性。アクティブでHigh。
timer_ocinitpara.ocnpolarity = TIMER_OCN_POLARITY_HIGH; // TIMER_0以外は使用できない。コンプリメンタルチャンネルの出力極性。アクティブでHigh。
timer_ocinitpara.ocidlestate = TIMER_OC_IDLE_STATE_LOW; // TIMER_0以外は使用できない。チャンネルの出力極性。アイドル状態でLow。
timer_ocinitpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW; // TIMER_0以外は使用できない。コンプリメンタルチャンネルの出力極性。アイドルでLOW
timer_channel_output_config(TIMER4, TIMER_CH_3,&timer_ocinitpara); // この設定(実質的にチャンネルを使用するというフラグをオンのみ)をTIMER_4のチャンネル3に適用する
基本的な設定を終えたら、PWMとしてタイマーを使用する。timer_channel_output_pulse_value_configでは、デューティ比をのための値(CH3VAL)を指定するが、あとでどうせ別の値を上書きするので今はゼロになっている。デューティ比を50%にする場合、timer_initpara.period で指定した値の1/2の値をここに指定することになる。
timer_channel_output_mode_configはchannel output compare modeを指定する。タイマー4のチャンネル3を、PWMモード0に指定する。モード0 とモード1では波形が逆になる。サウンドで使う分にはどちらでも同じ。ここには、PWM以外様々なモードが指定できるが、よくわからない。
timer_channel_output_shadow_configについてはよくわからない。
timer_auto_reload_shadow_enable(TIMER4);で、TIMER4の自動リロードの時のシャドウレジスタを有効にする。自動リロードは、タイマーのカウンタがオーバーフローして割り込みがかかり、その後0にリセットされるときに自動的に一定の値を足す機能。これによりタイマーの時間を調整できる。
(推測)
オートリロードレジスタは、カウンタが動作中でも読み書き出るが、これを実現するために値はプリロードされており、読み書きの際は本当のレジスタではなく、プリロードされたシャドウレジスタを読み書きしている。この機能をTIMER4で有効にする。
timer_channel_output_pulse_value_config(TIMER4,TIMER_CH_3,0); // デューティ比を指定する。後でどうせ指定するので、ここでは0を指定している。意味がないが説明のため残す。
timer_channel_output_mode_config(TIMER4,TIMER_CH_3,TIMER_OC_MODE_PWM0); // TIMER4のチャンネル3をPWMモード0として使用することを宣言、
timer_channel_output_shadow_config(TIMER4,TIMER_CH_3,TIMER_OC_SHADOW_DISABLE);
/* auto-reload preload enable */
timer_auto_reload_shadow_enable(TIMER4);
/* start the timer */
timer_enable(TIMER4);
サウンドの出力
初期化が終わったら、実際に音を出すための処理を記述する。音を出すためには、音の周波数とデューティ比(50%)を指定し、タイマー4を作動させればよい。
音の周波数とデューティ比をセットするのは、次のような処理になる。周波数について、APIではtimer_parameter_struct構造体の .periodメンバーに値を指定(timer_initpara.period)し、timer_initを呼び出せば指定できるが、たった一つのパラメータを変えるために、ほかのメンバーも正しく設定する必要があり面倒。そのため、今回はAPIが用意しているマクロを使って、直接書き換えることにする。(デューティ比についても、timer_channel_output_pulse_value_config関数で書き換えることができるが、同じようにマクロを使用する)
マクロは、gd32vf103_timer.h に含まれているが、ドキュメンテーションを見つけることができない。SDKのソースコードから自力で探すか、ユーザーマニュアルから当たりをつけて見つけるしかないのかもしれない。
APIの中の、タイマー関連のヘッダをファイル名(gd32vf103_timer.h)で見つけ、その中を見る。 マニュアルで、Address offset : 0x40となっているので、0x40を探すと、
#define TIMER_CH3CV(timerx) REG32((timerx) + 0x40U)
という行が見つかる。マニュアルでTIMERx_CH3CVとなっているところから、CH3CVなどをキーワードにして探すなどの方法。
あるいは、timer_channel_output_pulse_value_config関数の中を見て、次のコードからTIMER_CH3CVというマクロを探しても良い。
こうして、周波数とデューティ比を指定するための2つのマクロ、TIMER_CH3CV (デューティ比)、TIMER_CAR(周波数)を求め、値を指定する。
例えば、次のように指定すると、ベースの周波数、200Khzを800(799+1、ゼロバウンダリーなので)で割った、250Hz、デューティ比約50%の矩形波が得られることになる。
TIMER_CH3CV(TIMER4) = 800 / 2;
TIMER_CAR(TIMER4) = 799;
周波数の指定が終わったら、タイマー4を有効にすることで音が出力され、無効にすることで音が止まる。
timer_enable(TIMER4); // 音を出す
timer_disable(TIMER4); // 音を止める
サウンド関連コードの追加
圧電サウンダの接続と、PWMの使用方法が確認出来たら、プロジェクトに、sound.cとsound.hを追加し、サウンド関連の初期化コードを記述する。
初期化、音程の設定、サウンドのスタート、ストップをそれぞれ別の関数にして、音程は引数で指定できるようにしておく。
sound.h
#ifndef __sound_h__
#define __sound_h__
void sound_pwm_init();
void StartSound(void);
void StopSound(void);
void SoundSet(int Period);
#endif
sound.c
#include "lcd/lcd.h"
#include "led.h"
#include "memory.h"
#include "gd32vf103.h"
#include "sound.h"
void sound_pwm_init()
{
timer_oc_parameter_struct timer_ocinitpara;
timer_parameter_struct timer_initpara; // タイマーパラメータ構造体の宣言
timer_deinit(TIMER4);
timer_struct_para_init(&timer_initpara);
// rcu_periph_clock_enable(RCU_AF);
gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_3);
rcu_periph_clock_enable(RCU_TIMER4);
timer_initpara.prescaler = 539; // 108MHz/540 で、およそ200KHzが得られるハズ。
timer_initpara.alignedmode = TIMER_COUNTER_EDGE; // count alignment edge = 0,1,2,3,0,1,2,3... center align = 0,1,2,3,2,1,0
timer_initpara.counterdirection = TIMER_COUNTER_UP; // Counter direction
timer_initpara.period = 3; // 200KHz/4 で、おおよそ50KHzが得られるハズ。ただ、これはSoundSetで動的に書き換えられちゃうけどね。
timer_initpara.clockdivision = TIMER_CKDIV_DIV1; // This is used by deadtime, and digital filtering (not used here though)
timer_initpara.repetitioncounter = 0; // Runs continiously
timer_init(TIMER4, &timer_initpara); // Apply settings to timer
timer_channel_output_struct_para_init(&timer_ocinitpara);
timer_ocinitpara.outputstate = TIMER_CCX_ENABLE; // Channel enable
timer_ocinitpara.outputnstate = TIMER_CCXN_DISABLE; // Disable complementary channel
timer_ocinitpara.ocpolarity = TIMER_OC_POLARITY_HIGH; // Active state is high
timer_ocinitpara.ocnpolarity = TIMER_OCN_POLARITY_HIGH;
timer_ocinitpara.ocidlestate = TIMER_OC_IDLE_STATE_LOW; // Idle state is low
timer_ocinitpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;
timer_channel_output_config(TIMER4, TIMER_CH_3,&timer_ocinitpara); // Apply settings to channel
timer_channel_output_pulse_value_config(TIMER4,TIMER_CH_3,0); // Set pulse width
timer_channel_output_mode_config(TIMER4,TIMER_CH_3,TIMER_OC_MODE_PWM0); // Set pwm-mode
timer_channel_output_shadow_config(TIMER4,TIMER_CH_3,TIMER_OC_SHADOW_DISABLE);
/* auto-reload preload enable */
timer_auto_reload_shadow_enable(TIMER4);
/* start the timer */
timer_enable(TIMER4);
}
bool isSound = TRUE;
void StartSound(void) {
if (isSound == FALSE) return;
timer_enable(TIMER4);
}
void StopSound(void) {
if (isSound == FALSE) return;
timer_disable(TIMER4);
}
void SoundSet(int Period)
{
TIMER_CH3CV(TIMER4) = Period / 2;
TIMER_CAR(TIMER4) = Period;
}
ボールミス時のサウンド
ゲームのメインプログラムから、サウンドを鳴らして実際に動作するかをテストする。
最も簡単なケースとして、ボールをミスしたとき、「ブー」と鳴らすことにする。音を鳴らす処理は実際には、音を鳴らす、止めるのペアの処理となる。そして、その間にはしばらく時間が必要になるが、その間、プログラムの状態ループを止めることができないということに注意が必要になる。
現在、game.c内のボールをミスしたときは、STATE_BALLLOSS状態として定義されている。ボールロスが発生するとSTATE_BALLLOSSに遷移し、画面"MISS"と表示してカウンタを増やし、再度STATE_BALLLOSS状態に遷移する。カウンタが一定値になるまでこのSTATE_BALLLOSS状態に入ったり出たりを繰り返す。
つまり、STATE_BALLLOSSに入った最初の回で音を出し、STATE_BALLLOSSから抜けたときに音を止めればよい。
STATE_BALLLOSSに入った最初の回、というのはwaitCnt が 0 の回になる。したがって、次のように処理を加えることで、ボールロスが起こった時にブーという音を出すことができる。
game.c
#include "lcd/lcd.h"
:
#include "button.h"
#include "sound.h" // 追加
:
:
case STATE_BALLLOSS:{ // ボールロス
static u16 waitCnt = 0;
// ライフがないならゲームオーバーに遷移
if (LifeCnt == 0) {
gameState = STATE_GAMEOVER;
break;
}
// 画面に"-- MISS! --"と表示させる
if (waitCnt == 0) { // 変更
LCD_ShowString(5,80,(const unsigned char *)"-- MISS! --",WHITE);
SoundSet(800); // 音程を決めて
StartSound(); // 音を出す
}
// 一定時間が経過するか、ボタンが押されたらゲームの再開に遷移する
waitCnt++;
if (waitCnt == 0x100 || CheckP1Button()) {
LCD_Clear(BLACK);
waitCnt = 0;
StopSound(); // 追加。状態が遷移するときに音を止める
gameState = STATE_STARTGAME;
}
break;
}
:
:
呼び出している SoundSet(800);というのは、200KHzに対する分周なので200KHz / 800 で、250Hzの音が出力されることになる。
効果音の出力
音は出るようになったが、まだ一つだけの音を、ゲームのプレーが中断している間に鳴らしただけで、ゲーム中の音というわけではない。
今回は、プレイ中(ボールが動いている間)にボールが反射したときに、音を出すような処理を加えていく。
また、「ブ」だけだと貧弱なので、ブロックに当たると「ピポ」、パドルに当たると「ポピ」と2音を鳴らすようにする。
サウンドを管理するためのデータ
音が鳴り始める瞬間は、ボールがブロックやパドルに当たった瞬間、音が鳴り終わるのは、そこから一定の時間経過後になる。
そのため、次のようなデータを持ち、管理させる。
static bool bSound[2]; // ボールの音が出てるかのフラグ。[0]がポピ、[1]がピポ。
static int iSoundIdx=0; // ボールの音がどこまで進んでいるか。
// 例えば「ポピ」を出しているなら、0でポ、1がピ。
static int iSoundStep[2][2]; // 今出している音の進行状況。例えば、「ポピ」の時であれば、
// [0][0]が「ポ」の間増やされていき、一定の値になったら
//「ピ」に変えて[0][1]の値を増やしていく
static int iSoundTim[2][2] = {{400,800},{1000,600}}; //200Hzに対して、これで分週する。400なら、500Hzになる。
サウンド処理
- 初期化処理では、iSoundStep とiSoundStepは0に初期化される。音階のインデックスであるiSoundIdxもゼロになる。
- ボールがパドルに衝突(サウンド番号1)したとき、bSound[1]をTRUEにして、サウンド番号1が鳴っている状態にする。
- ボールを鳴らし続ける処理は、一定間隔で常に呼び出される。この中では、bSound[1]がTRUEであればサウンドが鳴っている状態と判断する。サウンドが鳴っていて、iSoundStep[1…サウンド番号][0…音階のインデックス]が0 (初期化処理で0になっている)なら初期化直後なので「音の鳴り始め」として、PWMのタイマーを有効にする。この時点で音が鳴り始める。
iSoundStep[1][0]が0より大きければ、音が鳴り続けるということなので、iSoundStep[1][0]の値を増やす。この値が一定以上になったら、次の音階に移るため、iSoundIdxを増やす。音階が終わりなら、サウンドを止める - サウンドを止める処理は、初期化処理と同じでよい。
sound.h
void BallSoundInit(void);
void BallSoundStart(int sndNo);
void BallSoundStop(void);
void BallSoundTick(void);
#endif
sound.c
:
void BallSoundInit(void)
{
// 変数を初期化して
for (u8 i = 0;i<2;i++) {
iSoundIdx = 0;
bSound[i] = FALSE;
iSoundStep[i][0] = iSoundStep[i][1] = 0;
}
timer_disable(TIMER4); //音を止める
}
// 音を出し始める。引数に音のインデックス「ピポ」「ポピ」を指定
void BallSoundStart(int sndNo)
{
BallSoundStop();
bSound[sndNo] = TRUE;
}
// 今出ている音をすべて止める。初期化と同じ処理でよい
void BallSoundStop(void)
{
BallSoundInit();
}
// 音を出す主処理。タイマーで一定間隔に呼び出される必要がある
void BallSoundTick(void)
{
for (u8 i = 0 ; i < 2;i++){
if (bSound[i]) {
if (iSoundStep[i][iSoundIdx] == 0) { // 音のなり始め
SoundSet(iSoundTim[i][iSoundIdx]);
timer_enable(TIMER4);
iSoundStep[i][iSoundIdx]++;
} else if (iSoundStep[i][iSoundIdx]<3) {
iSoundStep[i][iSoundIdx]++;
} else if (iSoundStep[i][iSoundIdx] == 3) { // 次の音へ
if (iSoundIdx == 1) { // 全部の音を出し終わったら終了
BallSoundStop();
} else {
iSoundStep[i][iSoundIdx] = INT16_MAX;
iSoundIdx++;
}
}
}
}
}
音を出す処理を作成したら、次に、ボールが壁やパドルに当たった時に音を出し始める処理を入れる。ball.cでは、パドルの衝突が記述されている。ボールは、パドルやブロックの象限を判定するGetOrthant(chkx,chky,pi->x1,pi->y1,pi->x2,pi->y2);
で5(内側にボールが入っている)が返された場合に衝突と判断しているので、この処理で音を出し始めればよい。
ball.c
#include "lcd/lcd.h"
#include "led.h"
:
#include "paddle.h"
#include "sound.h" // 追加
:
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) {
:
:
bi->dxBase = -bi->dxBase;
}
// ポピ音を出す
BallSoundStart(1); // 追加
}
}
}
block.c
#include "lcd/lcd.h"
:
#include "paddle.h"
#include "sound.h"// 追加
:
:
void blockCheck(struct BALLINFO* bi)
{
:
:
// 現在のボールの位置が、ブロックの中にある場合、衝突処理を行う
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) {
:
:
Score = Score + 1;
}
// ピポ音を出す
BallSoundStart(0); // 追加
}
}
音を出し始めたら、これを出し続け、音程を変えたり時間が来たら止める必要がある。そのため、メインループ内で一定間隔で音を鳴らし続ける処理を呼び出す。
game.c
#include "lcd/lcd.h"
:
#include "button.h"
#include "sound.h" // 追加
:
:
void Game(bool isDemo)
{
rcu_periph_clock_enable(RCU_GPIOB);
:
:
Adc_init();
sound_pwm_init();
BallSoundInit(); // 追加 ボール音の初期化
:
while (TRUE) {
timer_enable(TIMER5); // タイマーを有効にする
:
:
heartBeat = (heartBeat+1) & 0x7FFF;
BallSoundTick(); // 追加 サウンド処理を実行
switch (gameState) {
case STATE_IDLE:{
ここまででできたこと
PWMを使用して音を出せるようにした。ボールロスの際に、ブーという音を出した。ボールが反射するときに音を出し、ゲームを続けながら音を止めることができた
次の記事に進む
今回の追加を反映したソースコード
sound.h
#ifndef __sound_h__
#define __sound_h__
void sound_pwm_init();
void StartSound(void);
void StopSound(void);
void SoundSet(int Period);
void BallSoundInit(void);
void BallSoundStart(int sndNo);
void BallSoundStop(void);
void BallSoundTick(void);
#endif
sound.c
#include "lcd/lcd.h"
#include "led.h"
#include "memory.h"
#include "gd32vf103.h"
#include "sound.h"
void sound_pwm_init()
{
timer_oc_parameter_struct timer_ocinitpara;
timer_parameter_struct timer_initpara; // タイマーパラメータ構造体の宣言
timer_deinit(TIMER4);
timer_struct_para_init(&timer_initpara);
// rcu_periph_clock_enable(RCU_AF);
gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_3);
rcu_periph_clock_enable(RCU_TIMER4);
timer_initpara.prescaler = 539; // タイマーの16ビットプリスケーラには54MHzが入力されるので、54Mhz/540・・・
// のはずだが、108MHzが入力されるように振舞っている。
// なので108MHz/540 で、およそ200KHzが得られるハズ。
timer_initpara.alignedmode = TIMER_COUNTER_EDGE; // count alignment edge = 0,1,2,3,0,1,2,3... center align = 0,1,2,3,2,1,0
timer_initpara.counterdirection = TIMER_COUNTER_UP; // Counter direction
timer_initpara.period = 3; // 200KHz/4 で、おおよそ50KHzが得られるハズ。ただ、これはSoundSetで動的に書き換えられちゃうけどね。
timer_initpara.clockdivision = TIMER_CKDIV_DIV1; // This is used by deadtime, and digital filtering (not used here though)
timer_initpara.repetitioncounter = 0; // Runs continiously
timer_init(TIMER4, &timer_initpara); // Apply settings to timer
timer_channel_output_struct_para_init(&timer_ocinitpara);
timer_ocinitpara.outputstate = TIMER_CCX_ENABLE; // Channel enable
timer_ocinitpara.outputnstate = TIMER_CCXN_DISABLE; // Disable complementary channel
timer_ocinitpara.ocpolarity = TIMER_OC_POLARITY_HIGH; // Active state is high
timer_ocinitpara.ocnpolarity = TIMER_OCN_POLARITY_HIGH;
timer_ocinitpara.ocidlestate = TIMER_OC_IDLE_STATE_LOW; // Idle state is low
timer_ocinitpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;
timer_channel_output_config(TIMER4, TIMER_CH_3,&timer_ocinitpara); // Apply settings to channel
timer_channel_output_pulse_value_config(TIMER4,TIMER_CH_3,0); // Set pulse width
timer_channel_output_mode_config(TIMER4,TIMER_CH_3,TIMER_OC_MODE_PWM0); // Set pwm-mode
timer_channel_output_shadow_config(TIMER4,TIMER_CH_3,TIMER_OC_SHADOW_DISABLE);
/* auto-reload preload enable */
timer_auto_reload_shadow_enable(TIMER4);
/* start the timer */
timer_enable(TIMER4);
}
bool isSound = TRUE;
void StartSound(void) {
if (isSound == FALSE) return;
timer_enable(TIMER4);
}
void StopSound(void) {
if (isSound == FALSE) return;
timer_disable(TIMER4);
}
void SoundSet(int Period)
{
TIMER_CH3CV(TIMER4) = Period/2;
TIMER_CAR(TIMER4) = Period;
}
static bool bSound[2]; // ボールの音が出てるかのフラグ。[0]がポピ、[1]がピポ。
static int iSoundIdx=0; // ボールの音がどこまで進んでいるか。
// 例えば「ポピ」を出しているなら、0でポ、1がピ。
static int iSoundStep[2][2]; // 今出している音の進行状況。例えば、「ポピ」の時であれば、
// [0][0]が「ポ」の間増やされていき、一定の値になったら
//「ピ」に変えて[0][1]の値を増やしていく
static int iSoundTim[2][2] = {{400,800},{1000,600}}; //200Hzに対して、これで分週する。400なら、500Hzになる。
void BallSoundInit(void)
{
// 変数を初期化して
for (u8 i = 0;i<2;i++) {
iSoundIdx = 0;
bSound[i] = FALSE;
iSoundStep[i][0] = iSoundStep[i][1] = 0;
}
timer_disable(TIMER4); //音を止める
}
// 音を出し始める。引数に音のインデックス「ピポ」「ポピ」を指定
void BallSoundStart(int sndNo)
{
BallSoundStop();
bSound[sndNo] = TRUE;
}
// 今出ている音をすべて止める。初期化と同じ処理でよい
void BallSoundStop(void)
{
BallSoundInit();
}
// 音を出す主処理。タイマーで一定間隔に呼び出される必要がある
void BallSoundTick(void)
{
for (u8 i = 0 ; i < 2;i++){
if (bSound[i]) {
if (iSoundStep[i][iSoundIdx] == 0) { // 音のなり始め
SoundSet(iSoundTim[i][iSoundIdx]);
timer_enable(TIMER4);
iSoundStep[i][iSoundIdx]++;
} else if (iSoundStep[i][iSoundIdx]<3) {
iSoundStep[i][iSoundIdx]++;
} else if (iSoundStep[i][iSoundIdx] == 3) { // 次の音へ
if (iSoundIdx == 1) { // 全部の音を出し終わったら終了
BallSoundStop();
} else {
iSoundStep[i][iSoundIdx] = INT16_MAX;
iSoundIdx++;
}
}
}
}
}
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"
#include "sound.h"
enum GAMESTATE gameState; // ゲームの状態
volatile u8 WakeFlag = 0; // このフラグが1になると、処理が開始される
int LifeCnt = 0;
int Score = 0;
int Stage= 1; // ステージ
//
// 割り込みハンドラ。タイマーにより指定した周期で非同期に呼び出される
//
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();
sound_pwm_init();
BallSoundInit(); // ボール音の初期化
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;
BallSoundTick(); // サウンド処理を実行
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;
Stage = 1;
InitBlock(Stage);
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;
} else if (ret == 2) { // ブロックがすべてなくなったら
gameState = STATE_NEXTSTAGE;
break;
}
drawDeleteBall(TRUE); // ひとつ前のボールを消す
breakout_PaddleCtrl(isDemo); // パドルを動かす
break;
}
case STATE_NEXTSTAGE:{
static u16 waitNextCnt = 0;
// 画面に"WELL DONE!"と表示させる
LCD_ShowString(5,80,(const unsigned char *)"WELL DONE!",WHITE);
// 一定時間が経過するか、ボタンが押されたらブロックを初期化してゲームの再開に遷移する
waitNextCnt++;
if (waitNextCnt == 0x100 || CheckP1Button()) {
LCD_Clear(BLACK);
waitNextCnt = 0;
Stage++;
InitBlock(Stage);
gameState = STATE_STARTGAME;
}
break;
}
case STATE_BALLLOSS:{ // ボールロス
static u16 waitCnt = 0;
// ライフがないならゲームオーバーに遷移
if (LifeCnt == 0) {
gameState = STATE_GAMEOVER;
break;
}
// 画面に"-- MISS! --"と表示させる
if (waitCnt == 0) {
LCD_ShowString(5,80,(const unsigned char *)"-- MISS! --",WHITE);
SoundSet(800);
StartSound();
}
// 一定時間が経過するか、ボタンが押されたらゲームの再開に遷移する
waitCnt++;
if (waitCnt == 0x100 || CheckP1Button()) {
LCD_Clear(BLACK);
waitCnt = 0;
StopSound();
gameState = STATE_STARTGAME;
}
break;
}
case STATE_GAMEOVER:{
/*ゲーム-オーバー処理*/
static u16 waitGameOverCnt = 0;
// 画面に"-- MISS! --"と表示させる
LCD_ShowString(5,80,(const unsigned char *)"-GAMEOVER-",WHITE);
// 一定時間が経過するか、ボタンが押されたらゲームの再開に遷移する
waitGameOverCnt++;
if (waitGameOverCnt == 0x100 || CheckP1Button()) {
LCD_Clear(BLACK);
waitGameOverCnt = 0;
gameState = STATE_IDLE;
}
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"
#include "sound.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;
}
// ポピ音を出す
BallSoundStart(1);
}
}
}
// ボールを動かす。
// 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;
}
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"
#include "paddle.h"
#include "sound.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(int Stage)
{
// ステージごとのブロック位置
static int BlockStart[] = {3,4,5,2,2,1,0,0};
static int BlockEnd[] = {6,7,8,7,8,9,8,9};
int stageWork = (Stage-1) & 0x7; // ステージは0~7を繰り返す
// ブロックテーブルを初期化する。
memset((void *)blockmtx,0,sizeof(blockmtx));
blkCnt = 0;
for (int i = 0;i<BLOCK_CNT_H;i++) {
for (int j = BlockStart[stageWork];j<BlockEnd[stageWork];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;
}
// ピポ音を出す
BallSoundStart(0);
}
}