はじめに
あなたはヘビがリンゴを食べようとして画面上を走る子供時代のヘビゲームを覚えていますか? この記事では、FPGAでのゲームの実装について説明します1 。
図1.ゲームプレイ
まず、自己紹介をし、プロジェクトに取り組んだ理由を説明しましょう。 私たちは3人います: Tymur Lysenko 、 Daniil Manakovskiy 、 Sergey Makarov 。 イノポリス大学の 1年生として、「コンピューターアーキテクチャ」のコースを受講しました。これは専門的に教えられ、学習者がコンピューターの低レベルの構造を理解できるようにします。 コースのある時点で、インストラクターはコースの追加ポイントのためにFPGAのプロジェクトを開発する機会を提供してくれました。 私たちの動機は成績だけではありませんでしたが、ハードウェア設計の経験を増やし、結果を共有し、最後に楽しいゲームをすることに興味があります。
それでは、暗い詳細に進みましょう。
プロジェクトの概要
このプロジェクトでは、簡単に実装できる楽しいゲーム、つまり「Snake」を選択しました。 実装の構造は次のとおりです。まず、入力がSPIジョイスティックから取得され、次に処理され、最後に画像がVGAモニターに出力され、スコアが7セグメントディスプレイ(16進数)に表示されます。 ゲームロジックは直感的で簡単ですが、VGAとジョイスティックは興味深い課題であり、それらの実装は優れたゲーム体験をもたらしました。
ゲームには次のルールがあります。 プレイヤーは、ヘビの頭から始めます。 目標はリンゴを食べることです。リンゴは、前のものが食べられた後に画面上でランダムに生成されます。 さらに、ヘビは空腹を満たした後、1尾伸びています。 尾は頭に続いて次々に動きます。 ヘビは常に動いています。 画面の境界に達すると、ヘビは画面の別の側に移動されます。 頭が尻尾に当たると、ゲームオーバーです。
使用したツール
- 6272論理エレメント、50 MHzクロック、3ビットカラーVGA、8桁7セグメントディスプレイを備えたアルテラCyclone IV(EP4CE6E22C8N)。 FPGAはピンへのアナログ入力を取得できません。
- SPIジョイスティック(KY-023)
- 60 HzのリフレッシュレートをサポートするVGAモニター
- Quartus Prime Lite Edition 18.0.0ビルド614
- Verilog HDL IEEE 1364-2001
- ブレッドボード
- 電気素子:
- 8個のオス-メスコネクタ
- メス-メスコネクタ1個
- オスオスコネクタ1個
- 4個の抵抗(4.7KΩ)
アーキテクチャの概要
プロジェクトのアーキテクチャは、考慮すべき重要な要素です。 図2は、このアーキテクチャをトップレベルの観点から示しています。
図2.デザインの最上位ビュー( pdf )
ご覧のとおり、多くの入力、出力、およびいくつかのモジュールがあります。 このセクションでは、各要素の意味を説明し、ポート用にボード上で使用されるピンを指定します。
主な入力
実装に必要な主な入力はres_x_one 、 res_x_two 、 res_y_one 、 res_y_twoであり、これらはジョイスティックの現在の方向を受信するために使用されます。 図3は、それらの値と方向の間のマッピングを示しています。
入力 | 左 | そうだね | 上へ | ダウン | 方向に変化なし |
---|---|---|---|---|---|
res_x_one(PIN_30) | 1 | 0 | x | x | 1 |
res_x_two(PIN_52) | 1 | 0 | x | x | 0 |
res_y_one(PIN_39) | x | x | 1 | 0 | 1 |
res_y_two(PIN_44) | x | x | 1 | 0 | 0 |
図3.ジョイスティックの入力と方向のマッピング
その他の入力
- clk-ボードクロック(PIN_23)
- リセット -ゲームをリセットして印刷を停止する信号(PIN_58)
- 色 -1の場合、可能なすべての色が画面に出力され、デモ目的のみに使用されます(PIN_68)
メインモジュール
joystick_input
joystick_inputは、ジョイスティックからの入力に基づいて方向コードを生成するために使用されます。
game_logic
game_logicには、ゲームをプレイするために必要なすべてのロジックが含まれています。 モジュールは、ヘビを所定の方向に移動します。 さらに、リンゴの摂食と衝突の検出も行います。 さらに、画面上のピクセルの現在のxおよびy座標を受け取り、その位置に配置されたエンティティを返します。
VGA_Draw
引き出しは、現在の位置( iVGA_X、iVGA_Y )と現在のエンティティ( ent )に基づいて、ピクセルの色を特定の値に設定します。
VGA_Ctrl
VGA出力( V_Sync、H_Sync、R、G、B )への制御ビットストリームを生成します。
SSEG_ディスプレイ2
SSEG_Displayは、7セグメントディスプレイに現在のスコアを出力するドライバーです。
Vga_clk
VGA_clkは50MHzクロックを受信し、25.175 MHzにカットダウンします。
game_upd_clk
game_upd_clkは、ゲームの状態の更新をトリガーする特別なクロックを生成するモジュールです。
出力
- VGA_B -VGA青ピン(PIN_144)
- VGA_G -VGA緑ピン(PIN_1)
- VGA_R -VGA赤ピン(PIN_2)
- VGA_HS -VGA水平同期(PIN_142)
- VGA_VS -VGA垂直同期(PIN_143)
- sseg_a_to_dp -8つのセグメントのどれを点灯するかを指定します(PIN_115、PIN_119、PIN_120、PIN_121、PIN_124、PIN_125、PIN_126、PIN_127)
- sseg_an -4つの7セグメントディスプレイのどれを使用するかを指定します(PIN_128、PIN_129、PIN_132、PIN_133)
実装
SPIジョイスティックによる入力
図4. SPIジョイスティック(KY-023)
入力モジュールを実装しているときに、スティックがアナログ信号を生成することがわかりました。 ジョイスティックには、各軸に3つの位置があります。
- top-〜5V出力
- 中-〜2.5V出力
- 低-〜0V出力
入力は3進システムに非常に似ています。X軸については、 true
(左)、 false
(右)、およびundetermined
状態があり、ジョイスティックは左にも右にもありません。 問題は、FPGAボードがデジタル入力しか処理できないことです。 したがって、何らかのコードを記述するだけでは、この3項論理を2項に変換することはできません。 最初に提案された解決策は、アナログ-デジタルコンバーターを見つけることでしたが、その後、物理学に関する学校の知識を活用し、分圧器を実装することにしました3 。 3つの状態を定義するには、2ビットが必要ですundefined
はfalse
、01はundefined
、11はtrue
です。 いくつかの測定の後、ボード上でゼロと1の間の境界は約1.7Vであることがわかりました。 したがって、次のスキームを作成しました(circuitlab 4を使用して作成されたイメージ):
図5.ジョイスティック用ADCの回路
物理的な実装はArduinoキットアイテムを使用して構築され、次のようになります。
図6. ADCの実装
回路は各軸に1つの入力を取り、2つの出力を生成します。最初の出力はスティックから直接来て、ジョイスティックの出力がzero
場合のみゼロになりzero
。 2番目はundetermined
状態では0ですが、 true
1 true
。 これは、予想したとおりの結果です。
入力モジュールのロジックは次のとおりです。
- 3項論理を各方向の単純な2進ワイヤに変換します。
- 各クロックサイクルで、1つの方向のみが
true
かどうかをチェックしtrue
(ヘビは斜めに進むことはできません)。 - 新しい方向を前の方向と比較して、プレイヤーが方向を反対方向に変更できないようにすることで、ヘビが自分自身を食べないようにします。
reg left, right, up, down; initial begin direction = `TOP_DIR; end always @(posedge clk) begin //1 left = two_resistors_x; right = ~one_resistor_x; up = two_resistors_y; down = ~one_resistor_y; if (left + right + up + down == 3'b001) //2 begin if (left && (direction != `RIGHT_DIR)) //3 begin direction = `LEFT_DIR; end //same code for other directions end end
VGAへの出力
60 FPSで動作する60Hz画面で解像度640x480の出力を作成することにしました。
VGAモジュールは、 ドライバーとドロワーの2つの主要部分で構成されています。 ドライバは、垂直、水平同期信号、およびVGA出力に与えられる色で構成されるビットストリームを生成します。 @SlavikMIPTによって書かれた記事5は、VGAを使用する基本的な原則を説明しています。 ドライバーを記事からボードに適合させました。
画面を16x16ピクセルの正方形で構成される40x30要素のグリッドに分割することにしました。 各要素は1つのゲームエンティティを表します:リンゴ、ヘビの頭、尾、または何もない。
実装の次のステップは、エンティティのスプライトを作成することでした。
Cyclone IVには、VGAの色を表す3ビットのみがあります(赤に1、緑に1、青に1)。 このような制限のため、画像の色を利用可能な色に合わせるためのコンバーターを実装する必要がありました。 そのために、各ピクセルのRGB値を128で割るPythonスクリプトを作成しました。
from PIL import Image, ImageDraw filename = "snake_head" index = 1 im = Image.open(filename + ".png") n = Image.new('RGB', (16, 16)) d = ImageDraw.Draw(n) pix = im.load() size = im.size data = [] code = "sp[" + str(index) + "][{i}][{j}] = 3'b{RGB};\\\n" with open("code_" + filename + ".txt", 'w') as f: for i in range(size[0]): tmp = [] for j in range(size[1]): clr = im.getpixel((i, j)) vg = "{0}{1}{2}".format(int(clr[0] / 128), # an array representation for pixel int(clr[1] / 128), # since clr[*] in range [0, 255], int(clr[2] / 128)) # clr[*]/128 is either 0 or 1 tmp.append(vg) f.write(code.format(i=i, j=j, RGB=vg)) # Verilog code to initialization d.point((i, j), tuple([int(vg[0]) * 255, int(vg[1]) * 255, int(vg[2]) * 255])) # Visualize final image data.append(tmp) n.save(filename + "_3bit.png") for el in data: print(" ".join(el))
オリジナル | スクリプトの後 |
|
|
図7.入力と出力の比較
引き出しの主な目的は、現在の位置( iVGA_X、iVGA_Y )と現在のエンティティ( ent )に基づいてピクセルの色をVGAに送信することです。 すべてのスプライトはハードコーディングされていますが、上記のスクリプトを使用して新しいコードを生成することで簡単に変更できます。
always @(posedge iVGA_CLK or posedge reset) begin if(reset) begin oRed <= 0; oGreen <= 0; oBlue <= 0; end else begin // DRAW CURRENT STATE if (ent == `ENT_NOTHING) begin oRed <= 1; oGreen <= 1; oBlue <= 1; end else begin // Drawing a particular pixel from sprite oRed <= sp[ent][iVGA_X % `H_SQUARE][iVGA_Y % `V_SQUARE][0]; oGreen <= sp[ent][iVGA_X % `H_SQUARE][iVGA_Y % `V_SQUARE][1]; oBlue <= sp[ent][iVGA_X % `H_SQUARE][iVGA_Y % `V_SQUARE][2]; end end end
7セグメントディスプレイへの出力
プレーヤーがスコアを確認できるようにするために、ゲームスコアを7セグメントディスプレイに出力することにしました。 時間不足のため、EP4CE6スターターボードドキュメント2のコードを使用しました。 このモジュールは、ディスプレイに16進数を出力します。
ゲームロジック
開発中にいくつかのアプローチを試しましたが、必要なメモリ量が最小限で、ハードウェアでの実装が簡単で、並列計算のメリットが得られるアプローチになりました。
モジュールはいくつかの機能を実行します。 VGAは左上から右下に移動する各クロックサイクルでピクセルを描画するため、ピクセルの色を生成するVGA_Drawモジュールは、現在の座標に使用する色を識別する必要があります。 これは、ゲームロジックモジュールが出力するものです-指定された座標のエンティティコードです。
さらに、全画面が描画された後にのみゲームの状態を更新する必要があります。 game_upd_clkモジュールによって生成される信号は、更新するタイミングを決定するために使用されます。
ゲームの状態
ゲームの状態は以下で構成されます:
- ヘビの頭の座標
- ヘビの尾の座標の配列。 実装では、配列は128要素に制限されています
- 尾の数
- リンゴの座標
- ゲームオーバーフラグ
- ゲーム勝ちフラグ
ゲームの状態の更新には、いくつかの段階が含まれます。
- 特定の方向に基づいて、ヘビの頭を新しい座標に移動します。 座標が端にあり、さらに変更する必要があることが判明した場合、頭は画面の別の端にジャンプする必要があります。 たとえば、方向は左に設定され、現在のX座標は0です。したがって、新しいX座標は最後の水平アドレスと等しくなります。
- ヘビの頭の新しい座標は、リンゴの座標に対してテストされます。
2.1。 それらが等しく、配列がいっぱいでない場合は、配列に新しいテールを追加し、テールカウンターをインクリメントします。 カウンターが最高値(この場合は128)に達すると、ゲーム勝ちフラグが設定されます。つまり、スネークはそれ以上成長できず、ゲームは続行されます。 新しい尾は、ヘビの頭の以前の座標に配置されます。 リンゴをそこに配置するには、XとYのランダム座標を取得する必要があります。
2.2。 それらが等しくない場合、隣接する尾の座標を順番に交換します。 (n + 1)-thの前にn番目のテールが追加された場合、n番目のテールはn番目の座標を受け取ります。 最初の尾は、頭の古い座標を受け取ります。 - ヘビの頭の新しい座標が尾の座標と一致するかどうかを確認します。 その場合、ゲームオーバーフラグが立てられ、ゲームは停止します。
ランダム座標生成
6ビットの線形フィードバックシフトシフトレジスタ(LFSR) 6によって生成されたランダムビットを取ることによって生成された乱数。 数字を画面に収めるために、それらはゲームグリッドの次元で分割され、残りが取得されます。
おわりに
8週間の作業の後、プロジェクトは正常に実装されました。 ゲーム開発の経験があり、FPGA用の「Snake」ゲームの楽しいバージョンになりました。 ゲームはプレイ可能であり、プログラミング、アーキテクチャの設計、ソフトスキルのスキルが向上しました。
承認されたセグメント
深い知識とそれを実践する機会を与えてくれた教授ムハンマド・ファヒムとアレクサンドル・トルマソフに特別な感謝と感謝を表明したいと思います。 プロジェクトで使用された重要なハードウェアを提供してくれたVladislav Ostankovichと、デバッグを支援してくれたTemur Kholmatovに心から感謝します。 アナスタシヤボイコがゲームのために美しいスプライトを描いたことを忘れないでください。 また、この記事の校正と編集については、 Rabab Maroufに敬意を表したいと思います。
ゲームのテストを助け、記録を立てようとしたすべての人々に感謝します。 あなたがそれを楽しむことを願っています!
参照資料
[1]: Githubのプロジェクト
[2]: [FPGA] EP4CE6スターターボードのドキュメント
[3]: 分圧器
[4]: 回路をモデリングするためのツール
[5]: FPGAアルテラCyclone III用VGAアダプター
[6]: ウィキペディアの線形フィードバックシフトレジスタ(LFSR)
FPGAのLFSR-VHDLおよびVerilogコード
リンゴのテクスチャ
乱数を生成するアイデア
パルニトカル、S。(2003)。 Verilog HDL:デジタル設計および合成ガイド、第2版。