組み込みCでPAC-MANを作っちゃった
投稿日:2018/02/22
前置き
どうも、こちらでは約2か月ぶりということで。
今回は今学期受講したプログラミングC言語(GameBoyAdvanceの組み込みプログラミングという内容)の期末レポートで自由題材で何か製作することになったので、何にしようかと悩んだ果てに...
半年前の(いきなりC言語を触って分からないまま実装していた)演習のリベンジとして『PAC-MAN』を実装しようと決めました!
期間は、期末試験が終わる2/2からレポート期限の2/5までの3日半。実際その期間で完成させました。
タイトルは、【図1】にあるように「PAC-MAN Level is...Never be cleared!」ですね。
絶対クリアできません。仕様上。はい( ;∀;)エッ
ゲーム画面は【図2】のようになっています。もうほぼ、まんまPAC-MANの難易度高いマップだと思います。
後のチャプターで成果物として動画や体験用環境を掲載していますので、そちらもご覧になってください。
どのようなソースコードか
900行弱あるので、すべて説明はできません。そのため個人的に工夫した・重要だった部分を掲載します。
①8x8を1マスとして描写
今回のGBAの画面は240列×160行の点行列からなっているというものでした。
なので、講義でもしつこく取り上げられていたが、ASCIIコードの描写の際は、8x8のドット群を1マスとして1文字を描写することになっていました。つまり、1ドットを0か1かのバイナリ情報にして、1だった場合に指定の色を描写して文字を表現させるというものです。
さらに、バイナリ情報が8列並んだものが8行分あると考えることができるので、
8つの2桁の16進数が要素として持つ配列を利用します。
そうした考え方を利用し、PAC-MAN・モンスター・通路(壁)も8x8ドット群で1体(マス)分描写することにしました。
例えばPAC-MANの描写は以下のようになります。
※ただし、コメントアウト部分で黄色になっているのは描写がどのようになるか分かりやすく示すためのものである。
ASCII文字・モンスターについては上と同様に描写し、通路については8x8のうち上下左右の1行または1列を「壁」とするようにバイナリデータを並べた1マス分としています。
ゲーム画面のマップは、先述した多様な位置にある壁をもつ1マス分を繋いで30x20(240/8と160/8)で構築したものです。
②多様な機能を関数として独立させて自作
可読性の面ではかなり重要になってくると思い、関数の多様化に力をいれた。あまり多すぎてもいけないとは思うのですが、個人的に作っておいた方が独立的に変更、柔軟に呼び出しできるかな、と思うところは作成しました。
例えば、
(1)オブジェクト(PAC-MAN、モンスター)の生成
→createPacman()とcreateEnemy()
構造体の仕組み上PAC-MANとモンスター用のそれぞれ2つの関数を用意してあるが、構造体変数の初期値を代入する処理を実装しました(構造体では変数宣言のみで初期値代入はできないらしい)。
(2)オブジェクトの座標更新・描写
→movePacman()とmoveEnemy()
移動する方向を決定(PAC-MANはキー操作に基づく)し、座標をその方向に応じて更新し、描写をする関数。
モンスターの進路方向に関しては、GBAが持つクロックタイマに基づいたランダム関数で決めるようにしました。
(3)壁やモンスターとの衝突判定
→colorJudge()とcollidedWithEnemy()
PAC-MANとモンスターの壁との衝突に関しては、壁の色である「白」を検出することで実現しました。
PAC-MANとモンスターの衝突に関しては、それぞれの中心点の座標の距離を判定することで実現しました。
また、関数を多く作成することで、main関数の行数を抑えて読みやすく(なってるかな?)できるということも。今回は210行(/900行弱)に収まりました。
【プリプロセッサとmain関数↓】
③構造体の活用
これ重要、構造体です。
講義でもサラッッという感じに薄~く習った感じでしたが、個人的にこれが鍵になると考えてかなり勉強しました。
また、ここで講義でも習っていない、
構造体の値をポインタとして関数の引数に設定して渡す書き方やアロー演算子も導入しました。
今回習った組み込みCでは、クラスという概念が無い感じで、Javaを先に習った者としては構造体がオブジェクト指向に類似したものとして扱うに値すると感じました。
したがって、構造体をJavaでいうコンストラクタと同様に用意し、インスタンス生成、という流れを作ってオブジェクトの更新のたびに呼び出すようにソースを書きました。
その時、構造体を関数に渡す際は構造体としてorポインタとして渡すか、また構造体変数に変更を加える際にドット演算子orアロー演算子を用いるか、という使い分けが必要になってくることも勉強になったんです。
なので、ここで自分メモとしてまとめておきます。
まず構造体は下のソースのようにstruct構文で、関連した複数の変数群を1まとまりにして定義するものです。
ただし変数に初期値を与えてはダメなようです。
構造体やその変数の呼び出し下のようになります。Javaでいうインスタンスの生成に似ています。
変数の呼び出しに関しては基本的にドット演算子を用います。
ここからが構造体の本題、関数に引数として構造体を渡し、構造体変数の値を変更することについてです。
まず構造体を関数に渡す方法は、構造体名で渡すやり方とポインタとして渡すやり方があります。
(1)構造体名で渡す
書き方は下のようになります。
このやり方の場合、関数内で構造体の変数の値を参照・変更する際はドット演算子を使わなくてはなりません。
アロー演算子を用いるとエラーになってしまします。
さらに、構造体名で関数に値を渡すやり方では変数群を
別のアドレスにコピーして渡す、つまり渡された構造体変数はスタックメモリに格納されているので、関数内で構造体変数の値を変更しても、その関数の一連の処理が終わったらメモリから削除され、元の(main関数で設定した)値に戻されてしまいます。
そのため、そうしたことを踏まえたうえでの処理にのみ、このやり方を利用した方がいいでしょう。
では、関数内で構造体の変数の値をグローバル的に変更するにはどうすればよいでしょうか。
先述した2つの方法のうち後者を使います。
(2)ポインタとして渡す
書き方は下のようになります。
先述した構造体名で渡すやり方と違い、こちらでは関数内での構造体の変数の値参照・変更はアロー演算子を使わないといけません。ドットでやっていたところを矢印の形を作る記号2つに置き換えます。
また、こちらの方法では、
構造体の変数を定義したアドレスそのものを関数に渡しているため、直接そのアドレスに格納されている値を参照・変更していることになり、これはグローバル的に値が維持されることになります。
ポインタの扱い方はかなり重要ですね!(´ω`*)
こうした構造体というシステムを利用して、組み込みCをオブジェクト指向っぽくし、PAC-MANを製作することができました。
もっとできたかな~って思うこと
モンスターを生成(createEnemy)・更新する(moveEnemy)にあたり、7つのインスタンスをそれぞれ別々に呼び出して7行も書いちゃうのはどうだったかな~というのがあった。
これについては構造体変数を配列として定義すれば、インデックスで繰り返し処理できるのではないかなと考えました。
それについては、配列のポインタの引数受け渡しという勉強もできたと思うのですが、期限的にちょっと先送りしました。また余裕ができたときにやってみます。
成果物
下の動画は私がツイートしたもので、半年前の大学の演習(C言語習っておらずいきなり作らされていた)で作成したものと今回の成果物を比較したものです。
(半年前の私、PAC-MANのルールを勘違いして、「モンスターを食べたらクリア」だなんてもはやPAC-MANでなくなってる...(´・ω・`)
正しくは、モンスターを回避しながら制限時間内にマップ中にあるエサを全て食べてクリアなんですね。)
1回生の時に作成したパックマンと,今完成したパックマンを比較した動画作りましたw
— へのへのもへじ (@KagenoMoheji) 2018年2月4日
半年の学習の進捗です! pic.twitter.com/tAKWADK9IG
いかがでしょう?
個人的に動画にあるような興奮を覚えています(∩´∀`)∩
下にCコードをコンパイル・リンカして作成した実行(mb)ファイルと、GBAのエミュレータであるVBA(Excelのアレではないよ!)がダウンロードできるように置いておき、操作方法も説明しますので、ぜひ体験してみてください。
ダウンロード&体験はこちらから
①GBAエミュレータであるVBAのダウンロード
http://www.emusite.com/pc/gba.php
上にリンクをクリックして【図3】にあるようなページに飛び、【図3】の赤枠で囲んでいるところからVBAのzipファイルをダウンロードし、「全て展開」とかで解凍してください。
操作方法については③で説明します。
②実行可能(mb)ファイルのダウンロード
PACMAN.zip
上のリンクからダウンロードできます。解凍してmbファイルを取り出してください。
セキュリティ云々とか警告出そうですけど、誓って攻撃の意図はないです、ハイ('ω')
③VBAの操作方法
まず「VisualBoyAdvance.exe」をクリックして起動し、File→Openから②でダウンロードしたmbファイルを導入すると、ゲームが開始されます。
【キー操作】
Option→Joypad→Configure→1…を選択すると、どのGBAのボタンがキーボードのどのキーに対応しているかが分かります。
ただ、なーんか「Xfer」ってでてきて動作しなかったりするんですけど、下に記載する設定例または好みで設定すれば動作したりします。
一応下に設定例を記載しますね。
※GBAにあるB・L・R・Selectは今回使わないので記載していません。好みで設定してください。
GBAのキー | 対応するキー |
---|---|
A | Z |
Up | 上矢印 |
Down | 下矢印 |
Left | 左矢印 |
Right | 右矢印 |
Start | Enter |
また、ワンタッチで反応しなくても、長押ししてれば反応する...かも?
【描画速度】
GBAマシンの処理速度を変更することができます。
私が作成したPAC-MANには除算処理を多く使用していますので、これがなかなか、動作を重くしているんですね。
それにイラついた方は、スロットルを上げてスムーズに動くようにしちゃってください。
Option→Frame Skip→Throttleから、100%以上で設定すれば速くなります。カスタマイズで1000%まで設定できます。
た・だ・し!
処理速度が上がるということは、クロックタイマーも速くなるので、制限時間が減る速度も速くなります。要は4分という制限時間もあっという間に4分経たないうちに過ぎます。
これについてのお問い合わせは受け付けません、ハイ('ω')
【画面サイズ】
初めて起動したサイズでは小さくて見づらいと思います。
Option→Videoから、「x1」~「x4」があるので、例えば「x3」を選択して3倍の画面サイズにすることも可能です。
最後に
なんとも読みにくそうな記事に仕上がってしまいましたが、まぁまぁな成果物を作れたので勝手ながら記事にさせていただきました。
なお、main関数以外のソースについては、大学の後輩などが見てコピーしてしまうなどしては宜しくないかなと考え、省略させていただきました。
今後もプログラミングで形あるモノを、サービスを開発していけるように精進していきたいです。