目次

Pong Game

 Pongとは、TVゲームが出始めた1975年頃に人気があったテニスゲームです。  2人で対戦するタイプとコンピュータと人間の対戦するタイプの2種類が  ありました。コンピュータと対戦する方は、実際は単純なデジタル回路を  組み合わせていたモノが多かったので、厳密にはコンピュータとの対戦に  当てはまらないのですが、言った者勝ちで認知されていました。  現在は、1チップマイコンで作るのが一般的ですが、デジタル回路の  組合せで出来ていたので、CPLD/FPGAで作成できます。  下の写真は、自分がATtiny2313を利用して作成したPongです。  CPLD/FPGAで作るPongは、次のようにして遊びます。  最初は、人間とCPLD/FPGAが対戦するタイプにします。  その後で、人間と人間の対戦に変更します。  大学や企業では、パーソナルコンピュータを使うことが  多いので、液晶ディスプレイを表示器に利用します。  どの液晶ディスプレイでもサポートしているVGAモードを  利用します。  1人遊びでは、1個のボリュームを使い、ラケット(Pongではパドルですが)  を上下に移動させます。  2人対戦では、2個のボリュームを使い、パドルを上下に移動させます。  スピーカには、ボールヒット、ボールミス、ゲームセットなど  で音がでるようにします。  使い方を決めたので、VGAモードを活用する調査をします。

VGA信号生成

 VGA信号を使い、画面に絵を出すためには、いろいろなサイトで  説明がされているので、作成するPongに必要な内容だけを公開  します。  VGA信号を利用する液晶ディスプレイは、走査線で絵をつくります。  1画面の走査線本数は、480本に固定です。  さらに、1走査線の中には、640ピクセルを入れます。  2次元の画面を構成するのが、液晶ディスプレイの仕事です。  液晶ディスプレイが普及する前は、CRTを利用していた関係で  水平と垂直に同期信号が必要です。  同期信号と絵を描画するための2次元画面の構成は、  以下となります。  水平方向には、800ピクセルのデータが存在し、垂直方向には  525ラインあると考えます。  水平方向の0〜639ピクセルに、画像の輝度データを出力します。  640〜799ピクセルは、水平ブランキング期間として扱います。  655〜751ピクセルでは、水平同期信号を出力します。  垂直方向の0〜479ラインに、画像の輝度データを出力します。  480〜524ラインは、垂直ブランキング期間として扱います。  489〜491ラインは、垂直同期信号を出力します。  このような2次元画面を構成するには、クロックを入力して  カウンタを動かします。  出力信号は、VGAコネクタを利用して送受信します。  VGAコネクタでは、水平同期(HS)、垂直同期信号(VS)の他に  RGBのアナログ信号を出力しています。  VGAの画面構成から、どのようなカウンタが必要か考えます。  水平カウンタを考えます。  水平方向は800ピクセルあるので、1023までカウントする  10ビットカウンタが必要になります。  VHDLの定義では、以下となります。 signal iXCNT : std_logic_vector(9 downto 0);  このカウンタを利用して、水平同期信号を生成します。  655〜751ピクセルでは、水平同期信号を出力すればよいので  次のように考えればよいでしょう。 iHSYNC <= '0' when ( conv_integer(iXCNT) > 654 and conv_integer(iXCNT) < 752 ) else '1' ;  ハードウエアから入った技術者は、上のような  記述をしません。論理和で解決します。 iHSYNC0 <= '0' when ( conv_integer(iXCNT) > 654 ) else '1' ; iHSYNC1 <= '1' when ( conv_integer(iXCNT) > 751 ) else '0' ; iHSYNC <= iHSYNC0 or iHSYNC1 ;  考え方は単純です。次の図を見れば、理解できるでしょう。  垂直カウンタを考えます。  垂直方向は525ラインあるので、1023までカウントする  10ビットカウンタが必要になります。  VHDLの定義では、以下となります。 signal iYCNT : std_logic_vector(9 downto 0);  このカウンタを利用して、垂直同期信号を生成します。  489〜491ラインでは、垂直同期信号を出力すればよいので  次のように考えればよいでしょう。 iVSYNC0 <= '0' when ( conv_integer(iYCNT) > 488 ) else '1' ; iVSYNC1 <= '1' when ( conv_integer(iYCNT) > 491 ) else '0' ; iVSYNC <= iVSYNC0 or iVSYNC1 ;  2つのカウンタを動かすための元になるクロックは  25MHzより多少大きい周波数でなければなりません。  ところが、液晶ディスプレイでは、自身で内部動作クロックを  入力されるHSYNCからPLLにより再生成して同期させるので  CPLD/FPGAを25MHzで動かしても構いません。  同期信号を生成するブロックを定義します。  水平カウンタと入力クロックは、同じでよいので  次のように定義します。 signal iCNT : std_logic_vector(9 downto 0) ; signal iXCNT : std_logic_vector(9 downto 0) ; process ( nRESET , CLOCK ) begin if ( nRESET = '0' ) then iCNT <= (others => '0') ; elsif rising_edge( CLOCK ) then if ( conv_integer(iCNT) = 800 ) then iCNT <= (others => '0') ; else iCNT <= iCNT + '1' ; end if ; end if ; end process ; iXCNT <= iCNT ;  水平カウンタができたので、水平同期信号の定義します。 signal iHSYNC0 : std_logic ; signal iHSYNC1 : std_logic ; signal iHSYNC : std_logic ; iHSYNC0 <= '0' when ( conv_integer(iXCNT) > 654 ) else '1' ; iHSYNC1 <= '1' when ( conv_integer(iXCNT) > 751 ) else '0' ; iHSYNC <= iHSYNC0 or iHSYNC1 ;  輝度信号を出力するタイミングを与えないと  色のついた絵を出すことはできません。  水平カウンタを利用して、絵を出すか否かを  フラグで判定できるようにします。 signal iHDISP : std_logic ; iHDISP <= '1' when ( conv_integer(iXCNT) < 640 ) else '0' ;  垂直カウンタは、水平同期信号を基に生成します。  1ラインごとに、水平同期信号が出されることを利用します。 signal iYCNT : std_logic_vector(9 downto 0); signal iXCNTS : std_logic_vector(1 downto 0); signal iVSYNC0 : std_logic ; signal iVSYNC1 : std_logic ; signal iVSYNC : std_logic ; process ( nRESET , CLOCK ) begin if ( nRESET = '0' ) then iXCNTS <= "00" ; iYCNT <= (others => '0') ; elsif rising_edge( CLOCK ) then iXCNTS <= iXCNTS(0) & iHSYNC ; if ( iXCNTS = "10" ) then if ( conv_integer(iYCNT) = 525 ) then iYCNT <= (others => '0') ; else iYCNT <= iYCNT + '1' ; end if ; end if ; end if ; end process ; iVSYNC0 <= '0' when ( conv_integer(iYCNT) > 488 ) else '1' ; iVSYNC1 <= '1' when ( conv_integer(iYCNT) > 491 ) else '0' ; iVSYNC <= iVSYNC0 or iVSYNC1 ;  水平同期信号が1→0と変化する時点を捕まえて  垂直カウンタを動かします。  輝度信号を出力するタイミングを与えないと  色のついた絵を出すことはできません。  垂直カウンタを利用して、絵を出すか否かを  フラグで判定できるようにします。 signal iVDISP : std_logic ; iVDISP <= '1' when ( conv_integer(iYCNT) < 480 ) else '0' ;  絵を出す期間を、ひとつのフラグにまとめます。 signal iDISP : std_logic ; iDISP <= iVDISP and iHDISP ;  全体をまとめると、次のソースコードになります。 library IEEE; use IEEE.STD_LOGIC_1164.ALL; use IEEE.STD_LOGIC_ARITH.ALL; use IEEE.STD_LOGIC_UNSIGNED.ALL; entity pong is generic ( XPIXEL_MAX : Integer := 800 ; XPIXEL : Integer := 640 ; YLINE_MAX : Integer := 525 ; YLINE : Integer := 480 ; HSYNC_0 : Integer := 654 ; HSYNC_1 : Integer := 751 ; VSYNC0 : Integer := 488 ; VSYNC1 : Integer := 491 --; ); port ( -- system nRESET : in std_logic; CLOCK : in std_logic; -- 25MHz -- sync HSYNC : out std_logic ; VSYNC : out std_logic ; -- monitor MDISP : out std_logic --; ); end pong; architecture Behavioral of pong is -- generate internal clock signal iCNT : std_logic_vector(9 downto 0) ; -- horizontal counter signal iXCNT : std_logic_vector(9 downto 0) ; signal iHSYNC0 : std_logic ; signal iHSYNC1 : std_logic ; signal iHSYNC : std_logic ; signal iHDISP : std_logic ; -- vertical counter signal iYCNT : std_logic_vector(9 downto 0); signal iXCNTS : std_logic_vector(1 downto 0); signal iVSYNC0 : std_logic ; signal iVSYNC1 : std_logic ; signal iVSYNC : std_logic ; signal iVDISP : std_logic ; -- enable graphic data signal iDISP : std_logic ; begin -- sync signal HSYNC <= iHSYNC ; VSYNC <= iVSYNC ; MDISP <= iDISP ; -- generate internal clock process ( nRESET , CLOCK ) begin if ( nRESET = '0' ) then iCNT <= (others => '0') ; elsif rising_edge( CLOCK ) then if ( conv_integer(iCNT) = XPIXEL_MAX ) then iCNT <= (others => '0') ; else iCNT <= iCNT + '1' ; end if ; end if ; end process ; iXCNT <= iCNT ; -- horizontal sync iHSYNC0 <= '0' when ( conv_integer(iXCNT) > HSYNC_0 ) else '1' ; iHSYNC1 <= '1' when ( conv_integer(iXCNT) > HSYNC_1 ) else '0' ; iHSYNC <= iHSYNC0 or iHSYNC1 ; iHDISP <= '1' when ( conv_integer(iXCNT) < XPIXEL ) else '0' ; -- generate vertical counter process ( nRESET , CLOCK ) begin if ( nRESET = '0' ) then iXCNTS <= "00" ; iYCNT <= (others => '0') ; elsif rising_edge( CLOCK ) then iXCNTS <= iXCNTS(0) & iHSYNC ; if ( iXCNTS = "10" ) then if ( conv_integer(iYCNT) = YLINE_MAX ) then iYCNT <= (others => '0') ; else iYCNT <= iYCNT + '1' ; end if ; end if ; end if ; end process ; -- vertical sync iVSYNC0 <= '0' when ( conv_integer(iYCNT) > VSYNC0 ) else '1' ; iVSYNC1 <= '1' when ( conv_integer(iYCNT) > VSYNC1 ) else '0' ; iVSYNC <= iVSYNC0 or iVSYNC1 ; iVDISP <= '1' when ( conv_integer(iYCNT) < YLINE ) else '0' ; -- enable graphic data iDISP <= iVDISP and iHDISP ; end Behavioral;  実際に、VHDLコードをCoolRunnerIIボードに  ダウンロードすると、次のような波形を観測  出来ました。  同期信号だけでは、動作しているかどうか不明なので  Pongの中央にある仕切線を出してみました。  他に、色が変化することを知るには、フランス、イタリアの  国旗を出して確認できます。  同期信号を生成できるようにしたので  アイテム表示を考えます。

アイテム表示

 Pongにおいて、アイテムは以下となります。  これらを1ピクセルごとに描画していては、大変です。  VGAは、640ピクセルx480ラインで、307200ピクセル分の  データを扱います。  RGBの各データに6ビットを割り当てると、上のデータの  18倍になり、膨大なメモリ容量が必要になります。  そこで、8ピクセルx8ラインで、1アイテムを表示します。  8ピクセルx8ラインで、1アイテムを表示すると  水平方向は80カウント、垂直方向は60カウント  でアイテムの位置を座標指定できます。  こうすると水平カウンタ、垂直カウンタの10ビットのうち  上位7ビットを利用すれば、該当アイテムの描画座標を指定  できます。  8ピクセルx8ラインのエリアを、アイテムピクセルと  呼ぶことにします。  3つのアイテムを表現する、アイテムピクセル数を定義します。  壁とボールの動きは、CPLD/FPGAの中で生成できますが  Pongを実現するには、パドルの動作を人間が指定する  ため、入力装置が必要になります。  入力装置には、ボリューム(可変抵抗器)を利用します。  ボリュームを使うと、パドルの動くスピードも反映できます。  ボリュームでは、抵抗値を変化させるので、CPLD/FPGAが  認識できるように、パルスの長さに変化させます。  このような処理は、ワンショットマルチが定番です。  ワンショットマルチは、555を使えば簡単に実現できます。  回路図は、以下です。  CPLD/FPGAからトリガーを与えて、返ってくるパルスの幅を  計測すれば、時間がわかります。時間を、カウント数へと  変換すると、位置を確定できます。  トリガーを与えて、パルス長を計測します。  このトリガーは、VSYNCを利用します。  タイミングチャートで検証すると、うまくいきそうです。  ボールの動きを考えなければなりません。  ボールのサイズは、1アイテムピクセルとします。  人間が目で捉えられる動きでなければ、ゲームになりません。  動かすタイミングは、1画面の描画が終了するごとにして  おくと、動いていると判断できます。  AVRマイコンでPongを作ったとき、VSYNCを利用したので  今回も、VSYNCをボールを動かすトリガーとします。  平面上にあるアイテムを動かすには、定石があります。  点Pの座標を(x,y)として、移動量dx,dyを使い次の式で  トリガーが来るたびに、座標位置を再計算します。 xp = xp + dx yp = yp + dy  移動量をX軸、Y軸方向に分割します。  dx,dyは、正負の値を持つように定義するか  移動方向を別にもつようにします。  dx,dyを正の値として、移動方向を  定義して対応します。 xp = xp + delta_X * direction_X yp = yp + delta_Y * direction_Y  壁は動かないので、パドルもボール同様に  水平方向の幅と方向を与えて、動きをつくります。  壁を赤、パドルを緑、ボールを赤、緑をのぞいた色  で指定し、描画エリアをまとめると次の図になります。  壁、パドル、ボールは、色を持たせるためにORを通して  R、G、B端子に出力します。  ボールとパドルは、衝突判定が必要なので、状況にあわせて  動作を指定します。ボールの動きを中心に判定します。  ボールとパドルが衝突すると、ボールは方向を変えなければ  なりません。各状態で、どう制御するかを考えます。  これらの処理を、アイテムピクセルによるボールと  パドルの座標に対して指定します。  ボールは、壁にも衝突するので、動作をどうするのかを考えます。  壁は、上下左右にあるので、それぞれの場合、どう制御するか指定  します。  衝突時点で、ボールの動きを判定してしまうと、制御が1画面分  だけ遅れて、ボールがパドルあるいは壁を突き抜けてから方向が  変わるように見えます。  この突抜け動作回避のため、ボールの衝突判定は壁やパドルの座標と  1アイテムピクセル分の差を持たせます。  ここまでの内容を、VHDLコードにまとめてみます。 library IEEE; use IEEE.STD_LOGIC_1164.ALL; use IEEE.STD_LOGIC_ARITH.ALL; use IEEE.STD_LOGIC_UNSIGNED.ALL; entity wall_puddle_ball is port ( -- system nRESET : in std_logic; CLOCK : in std_logic; -- 25MHz -- sync VSYNC : in std_logic ; -- counter XCNT : in std_logic_vector(6 downto 0) ; YCNT : in std_logic_vector(6 downto 0) ; -- valid graphic DISP : in std_logic ; -- puddle TRG : out std_logic ; PAL : in std_logic ; -- select ball speed BSEL : in std_logic ; -- RGB data R_DAT : out std_logic ; G_DAT : out std_logic ; B_DAT : out std_logic --; ); end wall_puddle_ball; architecture Behavioral of wall_puddle_ball is -- puddle location signal iPUDDLE_LEFT : std_logic_vector(6 downto 0); signal iPUDDLE_RIGHT : std_logic_vector(6 downto 0); signal iPUDDLE_SFT : std_logic_vector(1 downto 0); signal iPUDDLE_PAL_FINE : std_logic ; -- wall data signal iWALL : std_logic ; -- puddle data signal iPUDDLE : std_logic ; -- ball data signal iBALL_SCNT : std_logic_vector(1 downto 0) ; signal iBALL_UPDATE : std_logic ; signal iBALL : std_logic ; signal iBALL_X : std_logic_vector(6 downto 0); signal iBALL_Y : std_logic_vector(6 downto 0); signal iBALL_DX : std_logic ; -- x axis direction signal iBALL_DY : std_logic ; -- y axis direction begin -- RGB data R_DAT <= (iBALL or iWALL) and DISP ; -- wall G_DAT <= (iBALL or iPUDDLE) and DISP ; -- puddle B_DAT <= iBALL and DISP ; -- ball -- trigger TRG <= VSYNC ; -- draw wall iWALL <= '1' when ( conv_integer(YCNT) = 0 ) else '1' when ( conv_integer(YCNT) = 59 ) else '1' when ( conv_integer(XCNT) = 0 ) else '1' when ( conv_integer(XCNT) = 79 ) else '0' ; -- get puddle location process ( nRESET , CLOCK ) begin if ( nRESET = '0' ) then iPUDDLE_SFT <= "00" ; elsif rising_edge( CLOCK ) then iPUDDLE_SFT <= iPUDDLE_SFT(0) & PAL ; end if ; end process ; iPUDDLE_PAL_FINE <= '1' when ( iPUDDLE_SFT = "01" ) else '0' ; -- judge puddle is left process ( nRESET , CLOCK ) begin if ( nRESET = '0' ) then iPUDDLE_LEFT <= (others => '0') ; elsif rising_edge( CLOCK ) then if ( iPUDDLE_PAL_FINE = '1' ) then iPUDDLE_LEFT <= YCNT ; end if ; end if ; end process ; iPUDDLE_RIGHT <= iPUDDLE_LEFT + conv_std_logic_vector(10,7) ; -- draw puddle iPUDDLE <= '1' when ( (conv_integer(XCNT) >= conv_integer(iPUDDLE_LEFT) ) and (conv_integer(XCNT) <= conv_integer(iPUDDLE_RIGHT)) and (conv_integer(YCNT) = 50) ) else '0' ; -- generate ball speed process ( nRESET , VSYNC ) begin if ( nRESET = '0' ) then iBALL_SCNT <= "00" ; elsif rising_edge( VSYNC ) then iBALL_SCNT <= iBALL_SCNT + '1' ; end if ; end process ; iBALL_UPDATE <= '1' when (BSEL = '0' and iBALL_SCNT(0) = '1' ) else '1' when (BSEL = '1' and iBALL_SCNT(1) = '1' ) else '0' ; -- move ball process ( nRESET , CLOCK ) begin if ( nRESET = '0' ) then iBALL_X <= conv_std_logic_vector(39,7) ; iBALL_Y <= conv_std_logic_vector(29,7) ; iBALL_DX <= '0' ; -- x axis default direction (right) iBALL_DY <= '0' ; -- y axis default direction (down) elsif rising_edge( CLOCK ) then if ( iBALL_UPDATE = '1' ) then -- judge horizontal wall hit -- left side wall hit if ( conv_integer(iBALL_X) = 1 and iBALL_DX = '1' ) then iBALL_X <= conv_std_logic_vector(2,7); iBALL_DX <= '0' ; -- direction is right -- right side wall hit elsif ( conv_integer(iBALL_X) = 78 and iBALL_DX = '0' ) then iBALL_X <= conv_std_logic_vector(77,7); iBALL_DX <= '1' ; -- direction is left else if ( iBALL_DX = '0' ) then iBALL_X <= iBALL_X + '1' ; else iBALL_X <= iBALL_X - '1' ; end if ; end if ; -- judge vertical wall hit -- top end wall hit if ( conv_integer(iBALL_Y) = 1 and iBALL_DY = '1' ) then iBALL_Y <= conv_std_logic_vector(2,7); iBALL_DY <= '0' ; -- direction is down -- bottom end wall hit elsif ( conv_integer(iBALL_X) = 58 and iBALL_DX = '0' ) then iBALL_Y <= conv_std_logic_vector(57,7); iBALL_DY <= '1' ; -- direction is up -- judge ball hit -- crash puddle elsif ( conv_integer(iBALL_X) >= conv_integer(iPUDDLE_LEFT) and conv_integer(iPUDDLE_RIGHT) >= conv_integer(iBALL_X) and conv_integer(iBALL_Y) = 49 and iBALL_DX = '0' ) then iBALL_Y <= conv_std_logic_vector(48,7); iBALL_DY <= '1' ; -- direction is up -- crash puddle elsif ( conv_integer(iBALL_X) >= conv_integer(iPUDDLE_LEFT) and conv_integer(iPUDDLE_RIGHT) >= conv_integer(iBALL_X) and conv_integer(iBALL_Y) = 51 and iBALL_DX = '1' ) then iBALL_Y <= conv_std_logic_vector(57,7); iBALL_DY <= '0' ; -- direction is down else if ( iBALL_DY = '0' ) then iBALL_Y <= iBALL_Y + '1' ; else iBALL_Y <= iBALL_Y - '1' ; end if ; end if ; end if ; end if ; end process ; -- draw ball iBALL <= '1' when ( XCNT = iBALL_X and YCNT = iBALL_Y ) else '0' ; end Behavioral;  この内容をcomponentとし、トップレベルから利用します。

効果音生成

 ゲームに効果音は必須なので、簡単に定義します。  最近のゲームは、効果音生成のためにFM音源、PCM音源を  利用していますが、今回は矩形波の組合せで実現します。  ボールをヒット、ロストした場合で異なる音色にしたり  ゲームスタートとゲームオーバーで音を変えたいので  8ビットのパターン入力で、変化させるようにします。  回路は、単純に、カウンタとセレクターで構成します。  AND回路を、矩形波のデータセレクターに利用し  最後にOR回路で、合成します。  8ビットなるので、矩形波の合成方法は256通りになります。  これだけあれば、実際に耳で聞きながら、場面に合った音を  拾い出せるでしょう。  VHDLコードに変換すると、以下となります。 library IEEE; use IEEE.STD_LOGIC_1164.ALL; use IEEE.STD_LOGIC_ARITH.ALL; use IEEE.STD_LOGIC_UNSIGNED.ALL; entity beep is port ( -- system nRESET : in std_logic; CLOCK : in std_logic; -- 25MHz -- enable BENABLE : in std_logic ; -- selecter SELBEEP : in std_logic_vector(7 downto 0) ; -- beep BEEP : out std_logic --; ); end beep; architecture Behavioral of beep is signal iCOUNT : std_logic_vector(7 downto 0) ; signal iBEEP : std_logic_vector(7 downto 0) ; begin -- update counter process ( nRESET , CLOCK ) begin if ( nRESET = '0' ) then iCOUNT <= (others => '0'); elsif rising_edge( CLOCK ) then iCOUNT <= iCOUNT + '1' ; end if ; end process; -- generate code iBEEP <= iCOUNT and SELBEEP ; -- BEEP <= '0' when (BENABLE = '0') else (iBEEP(7) or iBEEP(6) or iBEEP(5) or iBEEP(4) or iBEEP(3) or iBEEP(2) or iBEEP(1) or iBEEP(0) ); end Behavioral;  人間の可聴帯域は、20Hzから20kHzなので、この周波数帯に  なるように、CLOCKを選択します。  水平同期信号が31kHzなので、2分周で可聴帯域になります。  そこで、CLOCKにはHSYNCを利用します。

スコア表示

 ゲームなので、スコアを表示をしないと面白くないでしょう。  スコアのつけ方を考えます。  2つのスコアを出しておけば、パドル操作の「うまさ」を  体感できるでしょう。  スコア表示の仕様を考えます。  表示フォントを、4x7に限定するのは、CPLD/FPGAに  入れる論理回路の規模を小さくする工夫です。  上のように、スコアを表示する位置を決めます。  この位置を指定すると、X方向、Y方向のカウンタの  上位3ビットで、スコア表示領域になったか否かを  判定できます。  スコアの文字フォントは、アイテムピクセルを組み合わせる  ので、ROMと同じように定義して利用します。 (under construction)

動作検討

 ゲームは、時間を区切った方が面白いので、シーケンサを  利用して、ゲーム開始と終了を指定します。 (under construction)

接続回路


全ソースコード

(under construction)


目次 inserted by FC2 system