GB ゲーム開発覚え書き: サウンド
Game, C, GameBoy約 2 年半ぶりの更新になってしまった。今回はゲームに音を追加してみる。
用意したサンプルゲームは以下。
src/main.c
#include <gb/gb.h>
#include <stdio.h>
// キャラクターのタイルセット
const unsigned char characters[] =
{
0x7E,0x7E,0xFF,0x81,0xFF,0x81,0xFF,0xA5,
0xFF,0x81,0xFF,0x81,0xFF,0x81,0x7E,0x7E,
0x7E,0x7E,0xFF,0x81,0xFF,0xA5,0xFF,0x81,
0xFF,0x81,0xFF,0x81,0xFF,0x81,0x7E,0x7E,
0x7E,0x7E,0xFF,0x81,0xFF,0x81,0xFF,0x81,
0xFF,0x81,0xFF,0xA5,0xFF,0x81,0x7E,0x7E,
0x7E,0x7E,0xFF,0x81,0xFF,0x81,0xFF,0x8B,
0xFF,0x81,0xFF,0x81,0xFF,0x81,0x7E,0x7E,
0x7E,0x7E,0xFF,0x81,0xFF,0x81,0xFF,0xD1,
0xFF,0x81,0xFF,0x81,0xFF,0x81,0x7E,0x7E,
0x00,0x24,0x00,0x18,0x7E,0x7E,0xFF,0x81,
0xFF,0xA5,0x99,0xE7,0xFF,0x81,0x7E,0x7E
};
// 背景のタイルセット
const unsigned char backgrounds[] =
{
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0xFF,0xFF,0xFD,0x83,0xFD,0x83,0xFD,0x83,
0xFD,0x83,0xFD,0x83,0x81,0xFF,0xFF,0xFF
};
BOOLEAN is_end = FALSE; // ゲームが終わるかの判定
UINT8 player_pos[2] = {0};
UINT8 friend_pos[2] = {0};
// 衝突判定
BOOLEAN is_colliding_friend()
{
return ((player_pos[0] >= friend_pos[0] && player_pos[0] <= friend_pos[0] + 8) && (player_pos[1] >= friend_pos[1] && player_pos[1] <= friend_pos[1] + 8)) || ((friend_pos[0] >= player_pos[0] && friend_pos[0] <= player_pos[0] + 8) && (friend_pos[1] >= player_pos[1] && friend_pos[1] <= player_pos[1] + 8));
}
BOOLEAN is_colliding_x(BYTE delta)
{
if (delta > 0)
{
return player_pos[0] >= 160;
}
else
{
return player_pos[0] <= 8;
}
}
void main()
{
set_sprite_data(0, 6, characters);
// 主人公
set_sprite_tile(0, 0);
player_pos[0] = 8;
player_pos[1] = 152;
move_sprite(0, player_pos[0], player_pos[1]);
// フレンド
set_sprite_tile(1, 5);
friend_pos[0] = 160;
friend_pos[1] = 152;
move_sprite(1, friend_pos[0], friend_pos[1]);
SHOW_SPRITES;
while (!is_end)
{
UINT8 joypad_control = joypad();
// 操作
if (joypad_control & J_RIGHT)
{
if (!is_colliding_x(1))
{
player_pos[0]++;
move_sprite(0, player_pos[0], player_pos[1]);
}
}
else if (joypad_control & J_LEFT)
{
if (!is_colliding_x(-1))
{
player_pos[0]--;
move_sprite(0, player_pos[0], player_pos[1]);
}
}
is_end = is_colliding_friend();
wait_vbl_done();
}
printf("GAME CLEAR");
}
今回は主人公を左右に動かし、画面右端のフレンドに触れればゲームクリアという単純なもの。
このキャラクターが歩く時に、音を鳴らしたいと思う。
ゲームボーイの音
メモリマップで言うと $FF10
から $FF26
がオーディオ、$FF30
から $FF3F
までが波形。
サウンドは 4 チャンネルあり、それぞれ異なる波形を持つ。
- CH1: 矩形波+スイープ
- CH2: 矩形波
- CH3: 波形メモリ音源
- CH4: ノイズ波
また外部入力(VIN) がある。これはカートリッジの pin 31 からの音をルーティングするもの。
実際に使われた商用ゲームはなく GBA では削除されている。
ゲームボーイのサウンドチップは APU と呼ばれる。
CPU の速度と同期するので、クロック数が 2.4 倍速いスーパーゲームボーイ1ではピッチも変わる。
音を鳴らす準備
サウンドに関するレジスタは NR
から始まり、NR5
はサウンドの制御に関するレジスタを指す。
NR52
は APU のオンオフを制御する。7 ビットが 1 で APU がオンになる。
src/main.c
void main()
{
NR52_REG = 0x80; // APU をオンにする(10000000)
// 処理
}
次に、音量とパンを設定する。
左右チャンネルの音量は NR50
で、6〜4 ビットが左、2〜0 ビットが右の音量。
7 と 3 は VIN に関わるものなので、使わない場合は(というか、まず使わないので) 0 にする。
ちなみにゲームボーイのスピーカーはモノラルなので、パンはヘッドホンで聞かないと確認できない。
src/main.c
void main()
{
NR50_REG = 0x77; // 左右チャンネルの音量を MAX にする (01110111)
NR51_REG = 0xFF; // 全てのチャンネルのパンを振る (11111111)
// 処理
}
音を鳴らす
今回は CH1(矩形波)を使う。
そのレジスタは NR10
から NR14
まで。
設定は覚えることが多い。
例えば NR10
はスイープで、NR11
はデューティ比+長さで、NR12
はボリューム+エンベロープ、NR13
と NR14
は Hi,Lo の周波数…など、レジスタごとにそれぞれ異なる役割を持っている。
また、NR10
であれば、6〜4 ビットは 7.8 ms 単位のスイープ時間、3 ビットは増加させるか減少させるか、2〜0 ビットは周波数を変化させる際のシフト回数…という感じで設定される。
これを覚えたところで、直感的に作れるものでもない。
そこで今回は GBDK の examples にある sound というプログラムを使う。
ここで実際の音を確認しながら、望み通りの値に出力できる。
十字キーで各項目の値を操作し、スタートでプレビュー、セレクトで CH の変更。
sound プログラムで色々いじって、出したい音ができたら画面下の NR10-14:
から続く 2 桁ずつの数値をレジスタに割り当てていく。
例えば NR10-14: 15, 80, 94, 67, 86
と表示されていれば、以下の通り。
src/main.c
NR10_REG = 0x15; // 00010101
NR11_REG = 0x80; // 10000000
NR12_REG = 0x94; // 10010100
NR13_REG = 0x67; // 01100111
NR14_REG = 0x86; // 10000110
これを、主人公が歩いている時に一定間隔で鳴らすようにする。
src/main.c
int8 frame_counter = 0;
void walk_sound() {
frame_counter++;
if(frame_counter >= 18) {
NR10_REG = 0x15;
NR11_REG = 0x80;
NR12_REG = 0x94;
NR13_REG = 0x67;
NR14_REG = 0x86;
frame_counter = 0;
}
}
walk_sound
をループの中に加え、最終的なコードは以下となる。
src/main.c
#include <gb/gb.h>
#include <stdio.h>
// キャラクターのタイルセット
const unsigned char characters[] =
{
0x7E,0x7E,0xFF,0x81,0xFF,0x81,0xFF,0xA5,
0xFF,0x81,0xFF,0x81,0xFF,0x81,0x7E,0x7E,
0x7E,0x7E,0xFF,0x81,0xFF,0xA5,0xFF,0x81,
0xFF,0x81,0xFF,0x81,0xFF,0x81,0x7E,0x7E,
0x7E,0x7E,0xFF,0x81,0xFF,0x81,0xFF,0x81,
0xFF,0x81,0xFF,0xA5,0xFF,0x81,0x7E,0x7E,
0x7E,0x7E,0xFF,0x81,0xFF,0x81,0xFF,0x8B,
0xFF,0x81,0xFF,0x81,0xFF,0x81,0x7E,0x7E,
0x7E,0x7E,0xFF,0x81,0xFF,0x81,0xFF,0xD1,
0xFF,0x81,0xFF,0x81,0xFF,0x81,0x7E,0x7E,
0x00,0x24,0x00,0x18,0x7E,0x7E,0xFF,0x81,
0xFF,0xA5,0x99,0xE7,0xFF,0x81,0x7E,0x7E
};
BOOLEAN is_end = FALSE; // ゲームが終わるかの判定
UINT8 player_pos[2] = {0};
UINT8 friend_pos[2] = {0};
// 衝突判定
BOOLEAN is_colliding_friend()
{
return ((player_pos[0] >= friend_pos[0] && player_pos[0] <= friend_pos[0] + 8) && (player_pos[1] >= friend_pos[1] && player_pos[1] <= friend_pos[1] + 8)) || ((friend_pos[0] >= player_pos[0] && friend_pos[0] <= player_pos[0] + 8) && (friend_pos[1] >= player_pos[1] && friend_pos[1] <= player_pos[1] + 8));
}
BOOLEAN is_colliding_x(BYTE delta)
{
if (delta > 0)
{
return player_pos[0] >= 160;
}
else
{
return player_pos[0] <= 8;
}
}
// 歩く時の音
int frame_counter = 0;
void walk_sound() {
frame_counter++;
if(frame_counter >= 18) {
NR10_REG = 0x15; // 00010101
NR11_REG = 0x80; //
NR12_REG = 0x94;
NR13_REG = 0x67;
NR14_REG = 0x86;
frame_counter = 0;
}
}
void main()
{
NR52_REG = 0x80; // サウンドを有効化
NR50_REG = 0x77; // 左右チャンネルの音量を MAX にする
NR51_REG = 0xFF; // 全てのチャンネルのパンを振る
set_sprite_data(0, 6, characters);
// 主人公
set_sprite_tile(0, 0);
player_pos[0] = 8;
player_pos[1] = 152;
move_sprite(0, player_pos[0], player_pos[1]);
// フレンド
set_sprite_tile(1, 5);
friend_pos[0] = 160;
friend_pos[1] = 152;
move_sprite(1, friend_pos[0], friend_pos[1]);
SHOW_SPRITES;
while (!is_end)
{
UINT8 joypad_control = joypad();
// 操作
if (joypad_control & J_RIGHT)
{
if (!is_colliding_x(1))
{
player_pos[0]++;
// 定期的に歩く音を鳴らす
walk_sound();
move_sprite(0, player_pos[0], player_pos[1]);
}
}
else if (joypad_control & J_LEFT)
{
if (!is_colliding_x(-1))
{
player_pos[0]--;
// 定期的に歩く音を鳴らす
walk_sound();
move_sprite(0, player_pos[0], player_pos[1]);
}
} else {
// 次歩いた時に鳴るようにリセット
frame_counter = 18;
}
is_end = is_colliding_friend();
wait_vbl_done();
}
printf("GAME CLEAR");
}
これをビルドして歩くと、音が鳴ると思う。
次はスクロールを実装する。