Posts Tagged with "Space Invaders"

既に発行済みのブログであっても適宜修正・追加することがあります。
We may make changes and additions to blogs already published.
posted by sakurai on August 19, 2022 #498

テストケース

設計で最重要なのがテストケース作成です。バグの無い設計のためには、テストケースを網羅的に作成して検証する必要があります。

表498.1 テストケース進捗表
FSM No. テストケース Pass/Fail
No. 内容 V1 (State) V2 (Seq.)
自機音
チャネル(#0)
1 CODE1演奏中にCODE2がプリエンプト可能なこと Pass Pass
2 CODE1演奏中にCODE9がプリエンプト可能なこと Pass Pass
3 CODE9演奏中にCODE1がプリエンプト不可能なこと
(自機増加音が妨げられないこと)
Pass Pass
4 CODE3を無視すること Pass Pass
インベーダ音
チャネル(#1)
5 CODE3演奏中にCODE3がプリエンプト可能なこと
(実際には起こらないため不要)
Pass Pass
6 CODE1を無視すること Pass Pass
インベーダ音
チャネル(#2)
7 CODE4演奏中にCODE5がプリエンプト可能なこと Pass Pass
8 CODE5演奏中にCODE6がプリエンプト可能なこと Pass Pass
9 CODE1を無視すること Pass Pass
UFO音
チャネル(#3)
10 CODE10演奏中にCODE8がプリエンプト可能なこと Pass Pass
11 CODE10演奏後にCODE10OFFが来るまで演奏を継続すること Pass Pass
(12) (11で)CODE10演奏中にプチプチ音が鳴らないこと Pass Pass
13 CODE10演奏中にCODE8がプリエンプトし最後にOFFになること Pass Pass
14 CODE10演奏中にCODE8がプリエンプトしOFFがプリエンプト
した後OFFになること
Pass Pass
15 CODE1を無視すること Pass Pass

プログラムの対称性に鑑み、最小必要パターンのみとしました。パターンは必要に応じ、随時追加削除していきます。


左矢前のブログ 次のブログ右矢

posted by sakurai on August 18, 2022 #497

チャネルとコードの対応

表497.1 音データ、コードとサウンドチャネルの関係
サウンドの説明 CODE番号 サウンドチャネル
ON OFF
自機弾発射音 1 自機音チャネル(#0)
自機爆発音 2
自機増加音 9
インベーダ爆発音 3 インベーダ音チャネル(#1)
インベーダ歩行音1 4 インベーダ音チャネル(#2)
インベーダ歩行音2 5
インベーダ歩行音3 6
インベーダ歩行音4 7
UFO爆発音 8 UFO音チャネル(#3)
UFO飛行音 10 10+16

チャネル起動条件

それぞれのサウンドチャネルは別のFSMにより駆動されます。それらの起動条件を上げると、

  • チャネル0(FSM0) ---- (CODE1_ON || CODE2_ON || CODE9_ON) && !empty
  • チャネル1(FSM1) ---- CODE3_ON && !empty
  • チャネル2(FSM2) ---- (CODE4_ON || CODE5_ON || CODE6_ON || CODE7_ON) && !empty
  • チャネル3(FSM3) ---- ((CODE8_ON || CODE10_ON || CODE10_OFF) && !empty) || UFO
  • 例外処理: チャネル3においてはCODE10_ONの場合、OFFが来るまで演奏し続けるようにUFOのフラグを立てますが、起動時にUFOフラグだった場合はコマンドが無くてもCODE10_ONとして起動します。

チャネル終了条件

それぞれのサウンドチャネルは演奏を終了することがあります。カウンタが0になる場合またはプリエンプションです。具体的な終了条件(もしくは割り込み条件)を上げると、

  • カウンタが0になる (|| 以下のプリエンプション)
  • チャネル0(FSM0) ---- ((CODE1_ON || CODE2_ON || CODE9_ON) && !empty) && (current != CODE9_ON)
  • 例外処理: チャネル0(FSM0) ---- CODE9_ONの場合はプリエンプション禁止のため、currentを見てCODE9_ON(自機増加中)だったら割り込まないようにします。
  • チャネル1(FSM1) ---- CODE3_ON && !empty
  • チャネル2(FSM2) ---- (CODE4_ON || CODE5_ON || CODE6_ON || CODE7_ON) && !empty
  • チャネル3(FSM3) ---- (CODE8_ON || CODE10_ON || CODE10_OFF) && !empty
  • 全チャネル(FSM0~3) --- コードがある場合はキューから取り出し先頭に戻ります。

このように、起動条件の例外はUFOフラグが立っている場合であり、終了(割り込み)条件の例外は自機が増加中に割り込まない場合です。


左矢前のブログ 次のブログ右矢

posted by sakurai on August 17, 2022 #496

設計方針

前回のBSVによるサウンドFSMの設計においては、ステートベース設計手法により設計しました。ステートベース設計とは、ここでは一連のフローを、クロックサイクルで定義されるステートに分解し、ステートのルールを一つずつ書いていく手法を指します。ただ、Verilogでも同じ手法で設計するため工数は同じであり、高級言語のご利益はありません。

前回の設計では、以前にVerilogでステートベースで設計した経験があるので、階層化ルールを用いて階層化FSMをステートベース設計しました。今回は比較のため、シーケンスベースで設計しようと思います。シーケンスベース設計とは、ここではシナリオを人手でステートに分解せずに、BSVに任せることを指します。

使用コンパイラ

ところが、2021年末にアップグレードのため、bscを再コンパイルしたところ(build 9a7d5e05)、Verilog出力が正しくできなくなりました。具体的にはスタックオーバーフローというエラーが出ます。フォーラムで聞いたところソースを送って欲しいとのことでした。

ソースを送って調べてもらいましたが、スタックを広げるコマンドを実行してもエラーが出るらしく、ソースを改善せよとのことです。ソースは巨大なFSMから構成されるため、FSMを分離したいのはやまやまなのですが、教えられた通り実施しても、bscが一個のFSMにまとめようとするためうまく行きませんでした。やむなく、以前のコンパイラ(build 38534dc)を使い続けることにします。

処理フロー

図54.2は以前の記事に示したもので、FSMはこれをデコードし、音声を出力するものです。

図54.2
図54.2 waveフォーマット例

従って、基本的なフローはこのフォーマットどおりのステートベース設計のものを踏襲します。

  • コード待ち ---- FSM0~3に応じたコードを受け付ける)
  • 例外フラグ ---- UFOの場合はONからOFFまでサウンドをならし続ける等の例外への対処
  • コードに応じてROMの先頭+16にポインタを移す
  • フォーマットサイズを取得し、ポインタをフォーマット長分だけ増加
  • "data"をスキップ
  • データサイズを取得しカウンタにセット
  • ポインタをデータの先頭に移す
  • 終了条件でなければループ
  •  音声データを取り出す
  •  データを出力
  •  ポインタを進める
  •  カウンタを1減らす
  • ループ終端

ここで前回は、上記ループ内部において、インターポレーションのため4クロックからなるサウンド出力を4回繰り返していました。が、出力はループ内では変わらないので、16サイクルに1回出力すれば良いことになります。よって上記のループは16サイクルになるように設計します。


左矢前のブログ 次のブログ右矢

posted by sakurai on July 24, 2021 #425

BSVによるSpaceInvaderのVerilogソースを公開」と題する記事をQiitaに投稿しました。

図%%.1
図425.1 Qiita投稿

ボードの説明、Verilogソースの入手法、VivadoによるFPGAの配置配線までをガイドしています。


左矢前のブログ 次のブログ右矢

敵弾の貫通度の修正 (3)

posted by sakurai on June 17, 2021 #423

実行結果

このように修正した結果、以下の図423.1に示すように、敵弾の種類により貫通度を変えることができました。

図%%.1
図423.1 貫通度の違い

図423.1の下部中央のバリケードの

  • 右側に当たったのは、"Rolling"ショットで貫通度=3
  • 左側に当たったのは、"Plunger"ショットで貫通度=7

と深さが異なります。

以下の図はFPGAボードの動画です。最初に右側にRollingショット、次に左側にPlungerショットが着弾しています。

図%%.2
図423.2 貫通度の違い(動画)

以下の図は前記事のオリジナル動画です。最初に左側にPlungerショット、次に右側にRollingショットが着弾しています。

図%%.3
図423.3 貫通度の違い(オリジナル動画)

左矢前のブログ 次のブログ右矢

敵弾の貫通度の修正 (2)

posted by sakurai on June 16, 2021 #422

敵弾種類

敵弾名はインベーダゲームソースの研究から引用しています。図422.1の左から、Squiggly (抵抗器のようなジグザグ)、Rolling、Plunger (パイプつまり直し)と名前がついています。

図%%.1
図422.1 敵弾の種類

図422.2において、敵弾の貫通度は左からそれぞれ3, 3, 7としています。

BSVコードの修正

元々敵弾をランダム(風)に出現させるため、カウンタの値により敵弾タイプを決めていますが、同じように貫通度を決めるテーブルです。

UInt#(3) invBullet_pdval[8] =   { 7,  3, 3,  7, 3,  3,  7, 3}; // pd = penetrating depth

次は貫通度を記憶するためのレジスタであり、敵弾数だけ持つ必要があります。

Reg#(UInt#(3)) invBullet_pd[`INV_BULLET_MAX];

このレジスタを各敵弾にひとつ持たせるために敵弾の同時最大数だけインスタンシエートします。

for (int ii = 0; ii < `INV_BULLET_MAX; ii = ii + 1) begin
 :
   invBullet_pd[ii] <- mkRegU;
 :
end

敵弾発生部において、タイプを決定するのと同じロジックにより、上記の表を引きます。

            invBullet_type[idx] <= invBullet_rand[truncate(counter) & 3'h7];
            invBullet_pd[idx] <= invBullet_pdval[truncate(counter) & 3'h7];

敵弾衝突の一部です。この2つのseq - endseqブロックは元は一つでしたが、base(バリケード)の時だけこの貫通度を使用するように分離します。他の場合は最深(貫通度=7)固定とします。

                endseq else if (fbase) seq
                    eraseInvBullet(invBullet_x[idx], invBullet_y[idx]);     // 現敵弾消去
                    explodeInvBullet(invBullet_x[idx], invBullet_y[idx], invBullet_pd[idx]);   // 次敵弾爆発
                    invBullet_expTimer[idx] <= 1;           // 敵弾爆発タイマスタート
                    voff <= 5; // break 'for'
                 endseq else if (fobj || invBullet_y[idx] >= 225) seq
                    eraseInvBullet(invBullet_x[idx], invBullet_y[idx]);     // 現敵弾消去
                    invBullet_pd[idx] <= 7; // 最深
                    explodeInvBullet(invBullet_x[idx], invBullet_y[idx], 7);   // 次敵弾爆発
                    invBullet_expTimer[idx] <= 1;           // 敵弾爆発タイマスタート
                    voff <= 5; // break 'for'

コール先の爆発ルーチンです。爆発は、爆発マークを表示することで開始します。

// 敵弾爆発
function Stmt explodeInvBullet(
              UInt#(8) destx,
              UInt#(8) desty,
              UInt#(3) pd);
   return (seq
      orArea(43, 16, destx - 3, desty + extend(pd), 8, 8);
   endseq);
endfunction

爆発マーク消去コール部分です。爆発開始から一定時間後に、保存してある貫通度を使用して爆発マークを消去します。

       expEraseInvBullet(invBullet_x[idx], invBullet_y[idx], invBullet_pd[idx]);        // 敵弾爆発マーク消去

コール先の爆発マーク消去ルーチンです。

// 敵弾爆発マーク消去
function Stmt expEraseInvBullet(
              UInt#(8) destx,
              UInt#(8) desty,
              UInt#(3) pd);
   return (seq
      eraseAreaSP(43, 16, destx - 3, desty + extend(pd), 8, 8);
   endseq);
endfunction

左矢前のブログ 次のブログ右矢

敵弾の貫通度の修正

posted by sakurai on June 15, 2021 #420

Youtubeの動画の分析

この動画を観察すると、バリケード(シールド)への貫通度が、敵弾の種類によって異なるように思われます。

下図は左端のインベーダからT字型(Plunger)の敵弾が発射されたところです。

図%%.1
図420.1 敵弾1

敵弾が左端のバリケードに着弾しました。深く潜って(貫通度=7)爆発しています。

図%%.2
図420.2 敵弾1爆発

同じく左端のインベーダからジグザグ型(Squiggly)の敵弾が発射されたところです。

図%%.3
図420.3 敵弾2

敵弾が左端のバリケードに着弾しました。浅く潜って(貫通度=3)爆発しています。明らかに敵弾の種類により貫通度が異なっています。

図%%.4
図420.4 敵弾2爆発

動画を観察した限りでは、T字型の敵弾が7、他の敵弾は貫通度=3で爆発するようでした。一方、最下端では7で爆発するようなので、バリケードで爆発する際にのみ、貫通度(penetration distance)を設定することにします。


左矢前のブログ 次のブログ右矢

オープニングアニメーション (3)

posted by sakurai on June 11, 2021 #418

呼び出し側

前々稿に個別ファンクションを掲載したので、それをオープニングアニメーションとしてまとめるファンクションです。オープニングアニメーション時を示すようにfoaというフラグをTrueにしています。これがTrueの場合は、ウエイトルーチンにおいてボタンが押された場合にウエイトを中止する仕様となっています。

function Stmt openingAnimation;
   return (seq
      // Opening Animation
      foa <= True;
      eraseArea( 0, 41, 255, 199); // erase screen
      eraseArea(25,242, 5, 7); // erase zanki
      stringS1; // PLAY ...
      if (sbutton) break;
      wait_timer(`TICK_WAIT64);
      if (sbutton) break;
      stringS2; // *SCORE ...
      if (sbutton) break;
      wait_timer(`TICK_WAIT32);
      if (sbutton) break;
      stringS3; // =? MYSTERY ...
      if (sbutton) break;
      wait_timer(`TICK_WAIT64);
      if (sbutton) break;
      replaceY; // ^ -> Y
      if (sbutton) break;
      wait_timer(`TICK_WAIT64);
      if (sbutton) break;
      foa <= False;
   endseq);
endfunction 

実行結果

図418.1に実行結果の動画を示します。

図%%.1
図418.1 オープニングアニメーション

左矢前のブログ 次のブログ右矢

オープニングアニメーション (2)

posted by sakurai on June 10, 2021 #417

データ構造

以下にデータ構造を示します。

図%%.1
図417.1 構造体配列

文字一つに対するパラメータは、図417.2のとおりです。

{sx, sy, dx, dy, w, h} = {パターンソースx, パターンソースy, デスティネーションx, デスティネーションy, 幅、高さ}

本来は図417.1及び図417.2のような構造体の配列としたかったのですが、bscでエラーとなるため、やむなく要素毎の配列に分解しました。構造体配列の実現は現在問い合わせ中です。

BSVコード

以下に分解したコードを示します。

UInt#(8) s1sx[19] = { 42, 50, 58, 77, 66, 42, 58, 74, 82, 42, 42, 90, 98,106, 58,114, 82,122, 66},
         s1sy[19] = {137,137,137,126,137,137,137,137,137,129,129,137,137,137,137,137,137,137,137},
         s1dx[19] = {112,120,128,136, 72, 80, 88, 96,104,112,120,128,136,144,152,160,168,176,184},
         s1dy[19] = { 68, 68, 68, 68, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93},
         s1w[19]  = {  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5},
         s1h[19]  = {  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7};

UInt#(8) s2sx[23] = { 42, 50, 58, 66, 74, 82, 90, 98,106, 90,114, 58, 82,122, 90, 82, 90, 82, 42, 52, 68, 51,  2},
         s2sy[23] = {145,145,145,145,145,145,145,145,145,145,145,145,145,145,145,162,162,145,145, 16, 64, 80, 80},
         s2dx[23] = { 48, 56, 64, 72, 80, 88,104,112,120,128,136,144,152,168,176,184,192,200,208, 76, 80, 79, 78},
         s2dy[23] = {125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,142,155,169,184},
         s2w[23]  = {  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5, 16,  8, 11, 12},
         s2h[23]  = {  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  8,  8,  8};
 
UInt#(8) s3sx[40] = { 98,106, 42,114,122, 82, 90, 98,106,122, 98,  1,  9, 42,114,122, 82, 90, 90, 82, 98,106,114, 42, 82, 90, 98,106,114,122, 98,122,114, 42, 82, 90, 98,106,114,122},
         s3sy[40] = {162,162,129,162,162,170,170,170,170,162,162,154,154,129,170,170,178,178,170,170,178,178,178,129,186,186,186,186,186,186,178,178,178,129,186,186,186,186,186,186},
         s3dx[40] = { 96,104,112,120,128,136,144,152,160,168, 96,104,112,120,128,136,144,152,160,168, 96,104,112,120,128,136,144,152,160,168, 96,104,112,120,128,136,144,152,160,168},
         s3dy[40] = {142,142,142,142,142,142,142,142,142,142,156,156,156,156,156,156,156,156,156,156,170,170,170,170,170,170,170,170,170,170,184,184,184,184,184,184,184,184,184,184},
         s3w[40]  = {  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5},
         s3h[40]  = {  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7};

"SPACE INVADERS"の文字列を、最初は"SPACEINVADERS"とタイミングを空けずに場所を空けて表示していました。が、Youtubeを見ると、"SPACE△△INVADERS"と、空白(△)は場所だけでなくタイミングも空いているようだったので、文字列も"SPACE△△INVADERS"と設定しています。

もうひとつ注意点として、オープニングアニメーション中のウエイト中にSキー(Sボタン)が押された場合は直ちにゲームスタートとなることです。


左矢前のブログ 次のブログ右矢

オープニングアニメーション

posted by sakurai on June 9, 2021 #416

Youtube

この動画を観察すると、オープニングアニメーションの際に文字が一文字ずつ表示されているので、これを実装します。文字列は3つの部分から構成されています。

  • "PLAY SPACE INVADERS" --- 一文字ずつゆっくりと表示
  • "*SCORE ADVANCE TABLE *..." --- 一瞬で表示
  • "=? MYSTERY..." --- 一文字ずつゆっくりと表示

これを3つのカタマリとして、3つの表示ルーチンで表示します。最初と最後のルーチンは1文字表示するたびに8/60 sec(=133.3 msec)のウエイトを入れています。文字表示の間にsbuttonを見ているのは、スタートボタンにより、いつでもゲームを開始できるように割り込みを入れるためです。

function Stmt stringS1; // PLAY SPACE INVADERS
   return (seq
      for (str_idx <= 0; str_idx < 19; str_idx <=  str_idx + 1) seq
         copyArea(s1sx[str_idx], s1sy[str_idx], s1dx[str_idx], s1dy[str_idx], s1w[str_idx], s1h[str_idx]);
         wait_timer(`TICK_WAIT8);
         if (sbutton) break;
      endseq
   endseq);
endfunction

 function Stmt stringS2; // *SCORE ADVANCE TABLE* ...
    return (seq
      for (str_idx <= 0; str_idx < 23; str_idx <=  str_idx + 1) seq
         copyArea(s2sx[str_idx], s2sy[str_idx], s2dx[str_idx], s2dy[str_idx], s2w[str_idx], s2h[str_idx]);
      endseq
   endseq);
endfunction

function Stmt stringS3; // =? MYSTERY ...
   return (seq
      for (str_idx <= 0; str_idx < 40; str_idx <=  str_idx + 1) seq
         copyArea(s3sx[str_idx], s3sy[str_idx], s3dx[str_idx], s3dy[str_idx], s3w[str_idx], s3h[str_idx]);
         wait_timer(`TICK_WAIT8);
         if (sbutton) break;
      endseq
   endseq);
endfunction

左矢前のブログ 次のブログ右矢


ページ: