2016年09月11日

インベーダーゲームを作ってみる(6)サウンド

インベーダーゲームの解説、最後はサウンドです。

インベーダーの効果音は7種類しかありません。

@インベーダーの行進音
A自機弾の発射音
Bインベーダー命中音
CUFO出現音
DUFO命中音
E自機被弾音
F1500点での自機+1の音

オリジナルのインベーダーゲームは、これらの音を全て専用の回路で生成しています。
以下の記事にそれぞれの回路図が出ています。

Space Invaders sound emulation

これによると、@Fは有名なタイマーICのNE556、CはTIのサウンドチップSN76477、ABDEはオペアンプLM3900を使っているそうです。また、AEはノイズ音が混じりますが、これはCMOS ICの発振回路とシフトレジスタで生成しているそうです。
説明には4066とありますが、シフトレジスタなので4006の間違いでしょう。こちらのマニュアルのP8のパーツリストにも4006とあります。)

俗にゲームの音を「ピコピコ音」と言いますが、インベーダーゲームのサウンドは後にピコピコ音の代名詞となるファミコンなどよりも、アナログ回路で生成した電子音の比率が高かったようです。
以下のビデオはおそらく実機だと思いますが、今聴いても味わいがありますね。



まずはオリジナルの効果音を分析してみましょう。

最初は行進音です。

上のビデオの音声トラックを取り出し、Audacityで増幅・ノイズ除去したものを聴いてみます。
前半はそのまま、後半は2オクターブ上にトランスポーズしています。



音階は、「ラ・ソ・ファ・ミ」あるいは「ミ・レ・ド・シ」というように聞こえなくもありません。
いずれにせよ短調ですね。

インベーダーが減るに連れてテンポが上がりますが、音長(ゲートタイム)自体は変化せず、発音の間隔だけが狭まっていきます。
演奏速度としては、最後は最初の約10倍になるように設定しました。


次に、インベーダーに命中したときの「プチュン」という音です。
これも上のビデオから取り出したものを聴いてみます。
最初は通常再生、2番目は1/2倍速のスロー再生です。



波形を図にすると、下のようになっています。
iv10.png

全体で0.3秒ほどの音ですが、前半0.1秒くらいが低い音がさらに低くなる「プン」という音、その後は高い音からだんだん下がっていく「チューーーン」という音になっています。
また、末尾のところで同じ「チューーン」という音が、残響のようにもう一度小さく入っています。
なので、続けて鳴らすと「プチュンチュン」という具合に聴こえます。

このほか、UFO飛来音は4オクターブくらいの音域をサイン波でピッチを上げ下げするとそれらしくなります。
命中したときの音も、音程が低くなるだけで基本的には同様です。
なお、自機弾の発射音や自機爆発音は、ノイズ成分が必要なのでArduboyでは作るのは難しいです。


プログラム内での音声処理は、以下の通りです。
まず、各効果音の周波数の時間変化を配列に入れます。
データの末尾は「-1」とします。
int16_t snd1[5] = {   // laser
NOTE_B6, NOTE_C7, 0, NOTE_B6, -1
};


これとは別に、構造体で音の特性(ゲートタイム、再生速度、リピートするか等)と状態(再生中か等)を保持します。
enum SoundState {
SoundReady,
SoundPlaying,
SoundDone
};

struct sound_fx_t {
int16_t *data; // array of frequencies, use -1 as end marker
uint8_t gate_time; // note gate time in msec
uint8_t clk; // ticks per note
boolean loop; // true for loop play
uint8_t idx;
uint8_t clk_cnt;
enum SoundState status;
};


サウンドの状態は「Ready」→「Playing」→「Done」と遷移します。
それぞれ、再生前、再生中、再生完了を表します。

Playing状態では、再生速度(clk)ごとに配列のデータを1つずつ再生します。
clkが大きくなれば、演奏速度はゆっくりになります。


サウンド関連の関数は以下の6つを用意しています。

void sound_play(struct sound_fx_t *s)
loop()から各サウンドにつき1回ずつ呼び出すことでサウンドの状態を更新します。
音声チャンネルは1つだけですので、全ての効果音の処理をした結果として、基本的には最後に再生された音だけが聴こえます。
逆に言えば、サウンドの優先順位はsound_playを呼び出す順番で制御できます。後に呼び出されたものが優先です。

void sound_start(struct sound_fx_t *s)
サウンドの再生を開始します。(再生前→再生中へ遷移)

void sound_stop(struct sound_fx_t *s)
サウンドの再生を強制的に完了させます。(再生中→再生完了へ遷移)

void sound_restart(struct sound_fx_t *s)
再生が完了または中止されたサウンドを再び再生可能にします。(再生完了→再生前へ遷移)

boolean sound_ready(struct sound_fx_t *s)
サウンドが再生前ならtrue、そうでなければfalseを返します。

void sound_set_tempo(struct sound_fx_t *s, uint8_t t)
サウンドの再生テンポを指定します。再生はt+1 ticks(1/60秒)ごとに1データずつ進行します。0が最速です。
ラベル:Arduboy
posted by boochow at 23:43| Comment(0) | Arduino | このブログの読者になる | 更新情報をチェックする

インベーダーゲームを作ってみる(5)状態遷移

インベーダーゲームに登場するオブジェクトは、これまでに説明したインベーダーと陣地を除くと
@自機
AUFO
B自機の弾
C敵の弾
があります。

これらのオブジェクトはいずれも、
出現→移動→(爆発)→消滅→出現・・・
という状態遷移をします。

プログラム中でも、オブジェクトごとに細かい違いはあるものの、大体この遷移に沿っています。
流れを把握する上で役立つと思いますので、これらの状態遷移を簡単に解説しておきます。

オブジェクトの基本的な状態は以下の2つです。

・READY
オブジェクトが画面に出現していない状態です。
トリガー(たとえば自機弾なら、発射ボタンを押す)によってACTIVEへ移行します。
移行する前に初期設定(座標の設定など)が必要です。

・ACTIVE
オブジェクトが画面に出現し、動くことができる状態です。
ACTIVEなオブジェクトの処理はオブジェクトの種類に依存します。
オブジェクトの消滅(たとえばUFOが画面端へ到達したとき)や爆発により、他の状態へ移行します。
どの状態へ移行しても、消滅したオブジェクトは最終的にREADYへ移行します。

iv09.png


ACTIVE状態からの移行は、以下の3つのパターンがあります。

(1)直接READYへ移行する
オブジェクトを画面から消去済みの場合、単にREADYへ移行します。

(2)RECYCLEへ移行する
次の画面更新のタイミングでオブジェクトを画面から消去する必要がある場合、RECYCLEへ移行します。
RECYCLE状態では、オブジェクトを画面から消去し、READYへ移行します。

(3)EXPLOSIONへ移行する
爆発のイメージを描画する場合はEXPLOSIONへ移行します。
プログラム内では、オブジェクトによって状態の名称は異なりますが、意味合いは同じです。
EXPLOSION状態はオブジェクトの爆発イメージを画面に描画後、タイマーでRECYCLEへ移行します。

状態はプログラム内では整数で表現しており、
EXPLOSION > RECYCLE = 1 > ACTIVE = 0 > READY = -1
となっています。
この整数はEXPLOSIONからRECYCLEへのカウントダウンタイマーを兼ねており(手抜き)、EXPLOSION > 状態 > RECYCLEのときは、ループ内で1ずつカウントダウンしていきます。


オブジェクトの状態遷移は、主に オブジェクト名_update() という関数で処理しています。
例としてufo_update()は以下のようになっています。
void ufo_update(struct ufo_t *u) {
switch (u->status) {
case OBJ_ACTIVE:
if (u->wait_ctr++ > UFO_WAIT) {
u->wait_ctr = 0;
ufo_move(u);
}
break;
case OBJ_READY:
ufo_appear(u);
break;
case OBJ_RECYCLE:
ufo_erase(u);
u->status = OBJ_READY;
break;
case UFO_EXPLOSION:
DRAW_BITMAP(u->x, UFO_TOP, ufo_score_img[u->img_idx], UFO_W, UFO_H, WHITE);
default:
u->status--;
}
}

statusが状態を表す変数です。
UFOは毎秒60回動かすと早すぎるので、wait_ctrでウエイトを入れています。
ufo_appear()はUFO出現条件が整っているかチェックし、整っていればUFOを出現させます。
UFO_EXPLOSIONでの処理は、爆発イメージを描画します。
UFO_EXPLOSIONからOBJ_RECYCLEまでカウントダウンする動作はdefaultの中で行っています。
なお、UFO_EXPLOSIONでの処理は最後にbreakが付いていないので、u->status--;まで実行されることに注意してください。
ラベル:Arduboy
posted by boochow at 14:06| Comment(0) | Arduino | このブログの読者になる | 更新情報をチェックする

2016年09月10日

Arduinoベースのゲーム機:Gamebuino、Tiny Arcade/Tiny Screen他

Arduboyのことを調べていたら、他にもArduinoベースのゲーム機が出ているのを知ったのでメモしておきます。

ゲーム機のようなものをまとめてみる - inajob's blog

中でも、GamebuinoはディスプレイはNokia 5110のLCDで解像度は84×48ドットと、Arduboyよりさらに低いですが、色々なゲームが既に作られています。

Games - Gamebuino Wiki

ArduboyでGamebuinoのゲームを流用するためのライブラリも既に作られており、パックマンやインベーダーが実際に移植されていました。

Arduboy meets Gamebuino?! - Arduboy / Development - Community

Gamebuino ライブラリの移植 - 日本語 - Community

余談ですが、Gamebuino用のインベーダーゲームの作者は、今回作ったインベーダーゲームのキャラクタデザインで参考にしたPixel Invadersの作者さんでした。


いろいろなゲーム機の中では、個人的には、96x64ピクセルのカラーOLEDを使ったTinyScreenに魅力を感じますが、$75とやや高価ですね。

TinyScreen Video Game Kit - TinyCircuits

Arduinoベースではありませんが、ESP8266を用いたものもあります。NodeMCU2.2インチTFT LCDを組み合わせたもののようです。
NodeMCUなので、プログラミングはLuaを使うことになります。

WiFiBoy.Org

他には、Pocket C.H.I.P.というさらにパワフルなゲーム機もあります。
こちらはCPU周りはRaspberry Piと同程度でOSもLinuxです。

が、まあここまで強力なものが欲しければ、スマホやタブレットでいいのでは、という気もします。
ラベル:Arduboy
posted by boochow at 16:47| Comment(0) | Arduino | このブログの読者になる | 更新情報をチェックする

2016年09月06日

インベーダーゲームを作ってみる(4)陣地の破壊

今回は、前回触れた「インベーダーゲームで強く印象に残った処理」のその2、「陣地が敵弾にだんだん崩される様子」の処理です。

陣地が敵弾や自機弾で崩れていく処理はどのように行っているのか、YouTubeのビデオを見ながら考えました。
そして、おそらくこうだろう、と推測した処理が以下です。

(1)弾の当たり判定は、弾の移動先のピクセルの有無で判定する。
(2)当たりと判定したら、弾を中心に爆発の画像を描画する。この際、黒いピクセルは描かず、白いピクセルだけを描く(ORで描画)。
(3)次に、爆発の画像を消去する。このときは、先程描いた白いピクセルの部分だけを、黒いピクセルで描く(NOT ANDで描画)。結果、陣地が爆発の画像の形で型抜きしたようになる。
iv08.png

これを繰り返すことで、陣地がだんだん削られていきます。
陣地そのものの処理は、ゲーム開始時に画面に絵として描画するだけです。

今回作ったインベーダーゲームは、自機弾・敵弾ともに上記の処理を行っており、陣地が8×6ピクセルしかない割には、結構雰囲気のある崩れ方を表現できています。

なお、当たり判定を画面上のドットの有無で行うというのは、一般的には誤判定が起こりやすいため、あまり使われません。
今回は陣地への当たり判定に使っていますが、敵弾と自機弾が衝突した場合も、陣地に衝突したと誤判定して弾が消滅します。
実は、敵弾と自機弾の衝突については専用の判定処理は行っておらず、上記の誤判定をそのまま弾同士の衝突処理として用いています。
ラベル:Arduboy
posted by boochow at 23:50| Comment(0) | Arduino | このブログの読者になる | 更新情報をチェックする

2016年09月05日

インベーダーゲームを作ってみる(3)インベーダーの移動処理

かつてマイコン少年だったころ、インベーダーゲームで強く印象に残った処理が2つありました。

(1)グラフィカルな敵が多数動いている。処理が大変そう
(2)陣地が敵弾にだんだん崩される様子がリアル。あらかじめ用意した画像を使っているのではなさそう

今回は、このうち(1)のインベーダーの移動処理を扱います。

インベーダー群は、左右に行進しつつ、インベーダーが画面の端まで行くと、一段下へ移動します。
画面最下端へインベーダーが到達したらゲームオーバーです。

従って、移動処理では

(1)画面左右端に到達したインベーダーが居るかどうかの判定
(2)画面最下端に到達したインベーダーが居るかどうかの判定

を行う必要があります。

また、画面の端まで到達できるのは生きているインベーダーだけなので、これらの判定処理は、全ての(生きている)インベーダーについて行う必要があります。
また、「どんなゲームだったのか」で書いたとおり、インベーダーは一度に一匹しか動きません。

これらのことを踏まえ、今回はインベーダーの移動処理を以下のように実装しました。

(1)インベーダーの座標は、全体を一括して扱う(一匹ごとの座標は持たない)
(2)インベーダーの生死はフラグ(の配列)で判定する(ブロック崩しと同じ)
(3)インベーダーを一匹移動させる都度、画面左右下端への到達をチェックする
(4)インベーダーがすべて移動し終わったときに、行進方向の変更や下降の処理を行うかどうか判定する

なお、(1)のインベーダーの座標は全体の「現在位置」「次に動くべき位置」で表し、個別のインベーダーの位置は「次に動かすインベーダーの番号」で表現します。
図にすると下のようになります。この図ではインベーダーは右から左へ移動しています。
iv07.png

インベーダー全体としては(cur_left, cur_top)から(nxt_left, nxt_top)へ移動するのですが、実際には0番〜(nxt_update -1)番までが移動済みで、nxt_update版〜49番までが現在位置に留まっています。

インベーダーの番号は左下が0番で、右へ進むごとに+1、最後の49番は右上のインベーダーです。
オリジナルのインベーダーゲームの映像をよく見ると、最初に動き出すインベーダーは左下で、そこから右隣が順次移動し、次は下から2列目の左端から動き始めていますので、そのような順で番号を振りました。

インベーダーが端に到達したかどうかは、インベーダーを1匹移動させるごとに、そのときの描画座標でチェックします。
(オリジナルのインベーダーでは、画面の端にドットがあるかどうかで判定していたらしいです。)

コードはGitHubで公開していますが、上の説明に対応する部分を載せておきます。

まずインベーダーを表すデータ構造です。

struct aliens_t {
boolean exist[ALN_NUM];
uint8_t alive; // number of aliens alive
int16_t cur_left;
int16_t cur_top;
int16_t nxt_left;
int16_t nxt_top;
int16_t v; // left move or right move
uint8_t nxt_update; // which invader will be moved at the next tick
uint8_t pose; // index num for iv?_img[]
int16_t bottom; // be updated in aliens_draw
boolean move_down; // down-move flag (updated in aliens_draw)
boolean touch_down; // an alien touched bottom line flag
int8_t status;
uint8_t hit_idx; // which invader is been hit
} g_aliens;


そして、移動ルーチンが以下です。
動かすべきインベーダーが見つかれば(最初のif文)、その番号をnxt_updateに代入して終わりです。
49番まで行っても見つからなければ、生存しているすべてのインベーダーを移動し終えたので、最下端に到達したか(touch_down)の確認、X座標の更新、左右端へ到達した場合はY座標の更新を行います。
void aliens_move(struct aliens_t *a) {
uint8_t u = a->nxt_update;
do {
u++;
} while ((u < ALN_NUM) && (!a->exist[u]));

if (u < ALN_NUM) {
a->nxt_update = u;
}
else { // all aliens have moved
if (a->bottom >= SCRN_BOTTOM)
a->touch_down = true;
for (u = 0; (u < ALN_NUM) && (!a->exist[u]) ; u++);
a->nxt_update = u;
a->pose = (a->pose + 1) % 2;
a->cur_left = a->nxt_left;
a->nxt_left += a->v;
a->cur_top = a->nxt_top;
if (a->move_down) { // this flag had been set in aliens_draw()
a->move_down = false;
a->v = -a->v;
a->nxt_top = a->cur_top + ALN_VMOVE;
a->nxt_left = a->cur_left + a->v;
}
}
}

一匹のインベーダーを描画する処理は以下の通りです。ほとんど3種類を描き分ける処理だけですが、最後のところで左右端への到達判定(move_down)と、インベーダー群の中で最大(最下端)のY座標の保存(bottom)を行っています。
void aliens_draw(struct aliens_t *a) {
if (!a->exist[a->nxt_update])
return;

uint8_t row = A_ROW(a->nxt_update);
uint8_t col = A_COL(a->nxt_update);
int16_t x = a->nxt_left + col * (ALN_W + ALN_HSPACING);
int16_t y = a->nxt_top + row * (ALN_H + ALN_VSPACING);
uint8_t *img;

switch (row) {
case 0: img = (uint8_t *)aln1_img[a->pose]; break;
case 1:
case 2: img = (uint8_t *)aln2_img[a->pose]; break;
case 3:
case 4: img = (uint8_t *)aln3_img[a->pose]; break;
}
DRAW_BITMAP(x, y, img, ALN_W, ALN_H, WHITE);

if (((x <= SCRN_LEFT) && (a->v < 0)) ||
((x >= SCRN_RIGHT - ALN_W) && (a->v > 0)))
a->move_down = true;

a->bottom = max(a->bottom, y + ALN_H);
}
ラベル:Arduboy
posted by boochow at 02:03| Comment(0) | Arduino | このブログの読者になる | 更新情報をチェックする
人気記事