yaneuraoGameScript2000で行こう! |
written by やねうらお
1.今回は、簡単なゲームの制作です
これで連載始まって、4回目ですが、もうゲームは作れるようになりましたか?今回は、簡単なゲームを制作して行くことにしましょう。今回からこの講座を読み始められた方も、C言語の入門書等を併用すればなんとか追いつけますので、頑張ってみてくださいね。
それではまず、ygs2kの最新版を用意してください。(右クリックしてファイルに保存してください。使い方は前の号を参考にしてください) 一応、私のホームページからもダウンロード出来ます。
2.関数使ってますか?
みなさん、関数というと何をイメージされるでしょうか? 高1の数学でf(x)とか出てきて、さっぱりわからなかっただとか、そんなの習ったこと無いだとか(笑)、まあ、いろいろあるでしょうが、身構える必要はありません。なぜなら、もうすでに、みなさんは、関数を呼び出しているし、作っているからです。
void main(){ // 1 int n; // 2 LoadBitmap("ash.bmp",0,1); // 3 SetFPS(10); // 4 n = 0; // 5 loop { // 6 ClearSecondary(); // 7 BltRect(0,100,200,n*120,0,120,82); //8 n = n + 1; // 9 if (n==5) n = 0; // 10 halt; // 11 } // 12 } //13 |
これは、前回出てきたプログラムですが、たとえば、//3の、LoadBitmap、これも関数です。//4のSetFPSも、// 7のClearSecondary,// 8のBltRectもすべて関数です。関数を呼び出すには、関数の名前(関数名)に、( ) カッコをつけて、必要ならば、そのなかに関数に渡す数字(引数:ひきすう)などを書けば良かったわけです。究極的にはプログラムに必要なのは、関数呼び出しと、// 5のような変数の操作、そして、// 10のような条件分岐(制御構文)しかありません。
えっ?そんなことは、もうわかってる?確かに、初回からここまで読んでこられた方には簡単すぎたかも知れませんね。
それでは、一番最初の行のvoid main( )という部分は、これは何を意味しているのでしょうか?これこそが、今回、最大のテーマです。私は、最初、void main()はおまじないであって、こう書くものだと言いました。でも、これは正確な表現ではありません。
種を明かしましょう。void main( )という書き出しは、mainという名前の関数をいまから用意しますよ、という合図だったのです。そして、// 1と//13の { } 挟まれた内側にある行は、mainの中身(実体)だったのです。
おおー。なるほど〜。それなら、俺にも関数が作れるかな?と思った方、正解です。あなたにも関数が作れます。
サンプル画像1と2を表示する関数を作ってみましょう。(例によって、右クリックしてファイルに保存して、YGS2000.exeの存在するフォルダにコピーしておいてください)
void main(){ int y; LoadBitmap("p1.jpg",1,0); LoadBitmap("p2.jpg",2,0); y = 0; loop { MyBlt(y); halt; y++; if (y==480) y = 0; } } void MyBlt(int y){ BltFast(1,0,-y); BltFast(2,0,480-y); } |
実行の仕方は忘れていませんよね?YGS2000.exeの存在するフォルダにscriptという名前のフォルダを作り、そのフォルダ内にテキストエディタ(メモ帳でも可)でgamestart.cというファイルを作成(編集・保存)したのちに、YGS2000.exeを実行します。
上のプログラムは、MyBltという関数を作って、mainのなかから、MyBltという関数を呼び出しています。実行すると、2画面スクロールして、下まで達すると、再度、一番上から表示されることがわかると思います。
画像は、『Revolution』(WAFFLE制作)の、ポスター画像より。
3.何のための関数化?
さきほどのMyBltは、何を行なう関数だったのでしょうか?おそらく、コメント無しでは、わからないと思います。これに限ったことではありませんが、プログラムを見て何をやっているのか一つ一つの動作の意味は理解できても、それらが全体として何を意味するのかまで理解するには、かなりの経験が必要なのです。
種を明かしましょう。私は、MyBltを以下のようなイメージで設計しました。
仮想スクリーン(2枚の画像を縦にくっつけたもの)があって、それの座標(0,y)を左上、座標(640,y+480)を右下とする矩形(上図の赤い部分)を、実際の画面に転送したいと思いました。これがMyBltという関数の設計のために私がイメージした図です。引数yとしては、0から480の間の値をとります。
プログラム中には、どの関数がどういう作用を行なうか、どの変数が何のためにあるのかなどは、書かなくともプログラムは動きます。これが、プログラムの理解を難しくしています。他人のプログラムはもちろんのこと、自分のプログラムでさえ、しばらく見ないでいると、何がどうなっているのかさっぱりわかりません。
このへんをどう解決するかが、ソフトウェア工学上の重要課題のような気もしますが、とりあえず、いまみなさんが出来ることは、コメントを残しておくことです。変数を使うならば、これがどういう目的の変数なのか、そして、どういう値をとっているときは、どういう意味があるのかを明確にコメントとして残しておくことです。関数も同じく、どういう作用があって、どういう引数をとるのかそれぞれの意味について詳しく書き残しておくことです。
上図のような、詳しい図を懇切丁寧に残しておく必要は無いと思いますが、第三者が見てわかる言葉で書いておくことは、とても大切なことです。
4.関数化のメリットは?
さきほどのMyBltのような関数を作るメリットはどこにあるんでしょうか?そもそも、さきほどのプログラムならば、
void main(){ int y; LoadBitmap("p1.jpg",1,1); LoadBitmap("p2.jpg",2,1); y = 0; loop { BltFast(1,0,-y); BltFast(2,0,480-y); halt; y++; if (y==480) y = 0; } } |
と、MyBltをmainのなかに無理矢理入れてしまえばいいのではないのでしょうか?確かにこのプログラムは正しく動きます。
そもそも、2つある画像をあらかじめ連結しておいてはどうなんでしょうか?プログラムはもっとすっきりするんではないでしょうか?
ところが、YGS2000では使用できる画像サイズの上限は、640×480という制限があります。(これはDirectDraw側の制限ですが)
そこで、画像を2つに分割する必要がありました。しかし、やはり、頭のなかでは、画像は一つにくっついているものとして考えたかったのです。
そのための仕掛けがMyBltという関数だったのです。この関数を用意することによって、main関数をプログラムしているときは、画像が一つにくっついているものとして考える(MyBltを呼び出す)ことが出来たのです。
このように何かの一連の処理を関数にすると、見通しが立ちやすく、また考えやすくなります。
5.変数のスコープ
関数で一番特徴的なことは、変数が外部から見えないように遮蔽される(隠される)という点です。
void main(){ int n; n = 123; // 1.この時点でnの値は? test(n); // 3.この時点でnの値は? } void test(int n){ n = n + 234; // 2.この時点でnの値は? } |
上のプログラムを実行すると、1.を通過して、test関数が呼び出され、2.を通過して、そのあとtest関数を抜けてmainに戻ってきたあと、3.が実行されます。さて、1.2.3.それぞれの位置で、nの値はいくらになっているでしょうか?
1.の時点では、123が代入されているので、123です。2.の時点では、渡されたnに234を加算するので、357ですね。問題は、3.の時点でのnの値です。単純に考えれば2.と同じ値なので、357ではないかと思うのですが、そうではないのです。ちょっと表示して確認してみましょう。
void main(){ int n; n = 123; test(n); display(n); } void test(int n){ n = n + 234; } void display(int n){ TextLayerOn(0,0,0); sprintf(string[0],"n = %d",n); TextOut(0,string[0]); loop halt; } |
displayという、数字を画面に表示する関数を追加してみました。(この内容については、いまは理解できなくて構いません)
表示された数字は、なんと123です。びっくりしましたか?え?これくらいのことでは驚かない?そう言わずに驚いてください(笑)
これが何を意味するかというと、main関数のなかで使われている変数nと、test関数のなかで使われている変数nとはまったく別の変数であるということです。そして、まったく別の変数なので、test関数のなかの変数nを、いくら操作しても、main関数の変数nには何の影響も及ぼさないということです。このことによるデメリットもありますが、メリットのほうが大きいです。
関数のなかの変数は、外部から遮蔽されている(見えない)ので、外部の変数を潰す(値を書き換える)ことは基本的にありません。逆に、関数を呼び出す側からしてみれば、関数を呼び出すことによって、自分の使っていた変数が潰されることはありません。上の例でmain関数内の変数nがtest関数の呼び出しによって決して破壊されることは無い、ということです。これにより、関数内で、外部で使っている変数と同じ変数名を使ってしまったがために、思いもよならない外部の変数を破壊してしまうという事態が避けられます。
6.必要なものを関数化する
1.キャラクターを表示する
2.キー入力をして、キーに応じて左右に動かす
という2つの関数を作ってみることにしましょう。このように、下請けの関数を順番に作っていき、最後にそれらを統合するボトムアップ式の設計手順と、この逆に、大まかなフレームを先に作って、あとから下請けの関数を作るトップダウン式の設計手順とがありますが、慣れるまでは前者の方式で、下請け関数がきっちり動作することを確認しながら先に進むほうが良いでしょう。まず、歩きのキャラパターンを用意します。(クリックしてファイルに保存してください)
int Mx,My; // メインキャラの座標 void main(){ Mx = 300; My = 400; LoadBitmap("aru.bmp",1,0); SetFPS(30); loop { ClearSecondary(); KeyMove(); DrawChara(); halt; } } void KeyMove(){ KeyInput(); if (IsPressLeftKey()) Mx = Mx - 8; if (IsPressRightKey()) Mx = Mx + 8; } int nCM; // キャラモーションナンバー(0-8) void DrawChara(){ int pat; pat = nCM/3; // 2 BltRect(1,Mx,My,44*pat,0,44,70); nCM++; if (nCM==3*3) nCM = 0; // 1 } |
まず、先頭行の変数Mx,Myが、主人公のキャラクター位置を示す変数です。
KeyMoveが、ユーザーからの入力を受け付けて、矢印キーの左と右が押されていれば、それに応じて、主人公のX座標を−8,+8する関数です。この処理自体は、前回やったのでわかると思います。
DrawCharaは、主人公の座標(Mx,My)に基づいて、そこにキャラパターンを表示する関数です。この関数は、nCMというアニメモーション番号を示す変数(カウンタ)を用意してあります。// 1では、この変数を、0から8までこの関数が呼び出されるごとに1ずつ増やします。// 2では、パターンナンバーを現す変数patに、これを3で割った数字にします。つまり、0,1,2のどれかの数字になります。その数字に基づいて、表示します。それが、その下のBltRectです。(キャラクターは、44×70のサイズのものが横に3つ並べてあるので、その転送元矩形の左上の座標は(44*pat,0)で、サイズは(44,70)となるので、それを4番目から7番目までの引数として指定しています)
3で割っている部分に、首を傾げる方もおられると思いますが、1フレームずつパターンを変えたのでは速すぎてバタバタしているようにしか見えないので、同じパターンを3フレーム間表示するためのものです。
7.フラグをマスターする
次に、スペースキーを押したら、弾を飛ばすという処理について考えてみましょう。弾の画像は何でもいいのですが、とりあえず、これにしましょう。(クリックしてファイルに保存してください)
スペースキーが押されているかどうかは、KeyMove関数のなかで行なうようにするといいでしょう。キー入力に対する応答は、すべてこの関数の中で処理すると考えるわけです。そのように考えることによって、それぞれの関数の役割分担が明確になります。
ivoid KeyMove(){ KeyInput(); if (IsPressLeftKey()) Mx = Mx - 8; if (IsPressRightKey()) Mx = Mx + 8; if (IsPushSpaceKey()) MakeTama(); } |
とりあえず、赤い行を追加したのですが、ここで出てくるMakeTamaというのは、弾の発射関数です。もちろん、まだ作ってませんね(笑) これから作らなくてはなりません。
考え方としては、弾に必要な属性とは何かを考えます。属性のうち変化する要素だけ変数で持たなくてはなりません。そのため、事前に洗い出す作業が必要になります。このへんは、慣れるまでに時間がかかるでしょうから、変数は必要になってから追加しても構いません。いま弾の属性として考えられるのは、まず、弾の位置を示す、座標ですね。それから、場合によっては、飛んで行く方向が必要かも知れません。今回は、前方のみに飛べばいいことにするので、飛んで行く方向は固定ですから変数で持つ必要はありません。あと、飛んで行く速度も必要かも知れません。今回は、飛んで行く速度は16ドットに固定しておくので、これも変数で持つ必要はありません。
ということは、まず弾の座標を用意して、MakeTama関数のなかでは、その弾の座標を、現在の主人公の位置にする必要がある―――というところまではわかりますか?具体的なプログラムは、こうです。
int Tx,Ty; // 弾の座標 void MakeTama(){ // 弾の発射関数 Tx = Mx; Ty = My; } void DrawTama(){ // 弾の描画関数 Blt(2,Tx-22,Ty); } |
弾の描画関数は、現在の弾位置に基づいて、弾を描画するためのものです。-22しているのは、弾のキャラの横幅が大きいからマイキャラと見かけのX座標が合わないため、その分の調整です。意味がわからなければ、この-22という数字を削除するとどう変わるか、そして何故そう変わるのかを考えてみることです。
おっと、もちろん、事前にこの弾のビットマップを読み込んで、かつ、毎フレームごとにこのDrawTamaを呼び出さなくてはなりません。main関数は、次のように修正します。
void main(){ Mx = 300; My = 400; LoadBitmap("aru.bmp",1,0); LoadBitmap("tama.bmp",2,0); // 弾画像の読み込み SetFPS(30); loop { ClearSecondary(); KeyMove(); DrawChara(); DrawTama(); // 毎フレームごとの弾の描画 halt; } } |
これで動きます。実行してみてください。確かにスペースを押すと弾(天使になったパターン)が描画されるようになりましたが、いろいろ想像していたのと違いませんか?
そうです。プログラムは、人間様の思ったように動くのではなく、書かれたプログラムのようにしか動かないのです。
では、気になる点をあげてみますね。
1.弾が前に飛んでいかない。
2.弾と主人公との優先順位がおかしい(主人公のほうが手前にあって欲しい)
3.弾を発射する前、画面の左上に弾が表示されている。
何がいけなかったのでしょうか?1.は、描画毎に、Y座標を−16してやれば、良いでしょう。
2.は、main関数のなかで、DrawCharaとDrawTamaを呼び出す順序に問題があるわけで、ヌキ色有効の転送では、後から呼び出したほうが手前に表示されます。逆に言えば、表示の優先順位の高いキャラ(手前に表示しなくてはならないキャラ)ほど、最後に処理(Blt)するようにします。そこで、2.は、DrawChara();とDrawTama();を入れ替えてやればOKです。
3.は、ちょっと難問なので、とりあえず、1.と2.の修正をしたプログラムが以下のものです。試してみてください。
int Mx,My; // メインキャラの座標 void main(){ Mx = 300; My = 400; LoadBitmap("aru.bmp",1,0); LoadBitmap("tama.bmp",2,0); SetFPS(30); loop { ClearSecondary(); KeyMove(); DrawTama(); DrawChara(); halt; } } void KeyMove(){ KeyInput(); if (IsPressLeftKey()) Mx = Mx - 8; if (IsPressRightKey()) Mx = Mx + 8; if (IsPushSpaceKey()) MakeTama(); } int nCM; // キャラモーションナンバー(0-8) void DrawChara(){ int pat; pat = nCM/3; nCM++; if (nCM==3*3) nCM = 0; BltRect(1,Mx,My,44*pat,0,44,70); } int Tx,Ty; // 弾の座標 void MakeTama(){ Tx = Mx; Ty = My; } void DrawTama(){ Blt(2,Tx-22,Ty); Ty = Ty - 16; } |
さて…
3.弾を発射する前、画面の左上に弾が表示されている。
は、依然として解決されていませんね。どうしましょうか?
これは、プレイヤーがスペースキーを押してMakaTamaを呼び出され、弾を出す前にもmain関数からDrawTamaが呼び出され、玉が表示されてしまうのが原因です。
ということは、現在、弾が有効なのかどうかを示す変数を用意してはどうでしょうか?これをフラグ(flag=旗)と呼びます。フラグが立つとか、降りるとか言います。旗か、あるいは線路の踏み切りのようなものを想像してください。変数は数字しか扱えないので、0ならば、フラグは降りている(=無効),非0ならば、フラグが立っている(=有効)とみなすことが多いです。このように、どの変数を何の役割として使っているかは、コメントが無いと非常にわかりにくいです。このようなフラグのことを論理型(boolean:ブーリアン)の頭文字、bを変数名の頭につけることもあります。こうしておけば、これがフラグであることを変数名を見ただけでわかるようになります。あるいは整数ならばnを変数名の頭につけたりします。(これらは、ハンガリー記法と呼ばれます。ここでは詳しくは述べません) とりあえず、その部分だけ真似させてもらって、ここではフラグの頭文字にbを使うことにします。
DrawTama関数では、弾が有効であるフラグが立っていれば、表示、そうでなければ表示せずに関数から帰れば、3.の処理は実現できそうです。
int Tx,Ty,bT; // 弾の座標と弾が有効かどうかのフラグ void MakeTama(){ Tx = Mx; Ty = My; bT = 1; // 有効に } void DrawTama(){ if (!bT) return ; // bTが0ならばリターンする Blt(2,Tx-22,Ty); Ty = Ty - 16; } |
赤字部分が追加部分です。どうですか?3.の現象はなおりましたか?if文中に、ちょっと見慣れない演算子!が出てきていますが、これは、0ならば1,非0ならば0にするための演算子です。つまりbTが0のときのみ、!bTは1になり、ifの条件式は成立します。もちろん、わかりにくければ、
if (bt==0) return ; |
と書いても構いません。まあ、とにかく、これを実行してみると、もう一つ気になる点があるのです。
4.弾を出すと、前に出していた弾が突然消えてしまう
これを解決するには、MakeTamaのなかで既に弾を発射中ならば何もせずに帰るようにすればどうでしょうか?つまり、こうです。
int Tx,Ty,bT; // 弾の座標 void MakeTama(){ if (bT) return ; // bTが非0ならば帰る Tx = Mx; Ty = My; bT = 1; // 有効に } |
もう、説明は不要ですよね。これでやってみると..?一見うまく発射されているようですが、2回目にスペースを押しても弾が出ませんね(笑) これはまずいです。これを解決するには、弾が画面から消えたら、弾が有効かどうかのフラグbTを降ろしておかなければなりません。具体的には、DrawTamaをこう変更します。
ivoid DrawTama(){ if (!bT) return ; Blt(2,Tx-22,Ty); Ty = Ty - 16; if (Ty<-70) bT = 0; } |
Ty < -70の、-70という数字は、キャラクターの高さが70ドットあるので、y座標が-70より小さければ画面外から消えたことを意味するので-70としてあります。細かいことですが注意してください。
とりあえず、これで、うまく動くようになりましたね。一連の作業でわかっていただけたと思いますが、bTが何を意味する変数なのかは、プログラムを見た第三者には非常にわかりにくいのです。せめて、これがフラグであるという痕跡を残しておくために頭文字をbで始めましたが、だからと言って、これが何のためのフラグであるかは第三者的な視点から見れば、いまひとつ明確ではないのです。
// bTは0であれば弾は発射されていない、非0ならば弾の発射中で、その座標は(Tx,Ty)である
と、変数を使っている近くにでもコメントを書いておくように心がけましょう。
8.配列を使ってみる
さきほどのサンプルですが、弾はひとつでも画面に表示されていると、次の弾が出せませんね。もちろん、そうプログラムしたから、そうなっているわけですが、これを、3連射まで出来るように拡張するにはどうすれば良いのでしょうか?
ひとつの発想としては、弾の属性は、Tx,Ty,bTという3つの変数で現せますから、Tx2,Ty2,bT2そして、Tx3,Ty3,bT3と、3つ分の弾のための変数を用意するというものです。これは、確かにうまく行くでしょうけれど、数が増えてくると管理が大変そうです。もちろんプログラムも大変でしょう。
そこで、配列というものを使ってみることにします。使いかたは、いたって簡単で、宣言するときに
int n[100]; |
というように、[ ]で囲って、そのなかに配列の要素数を書けば、その数だけ要素が確保されます。上の例では、100個の整数型の変数を確保したことになります。この変数にアクセスするには、
n[20] = 123; |
と、[ ]で囲って、その中に、何番目の要素にアクセスするのか、その番号を書きます。この番号を配列のインデックスと言います。このインデックスは、0番から始まり、確保した数までの範囲で指定します。上の例ならば、100個の配列を確保していますので、0番〜99番(100番ではないことに注意!)までが有効なインデックスです。
for ( i = 0 ; i < 100 ; i++) { n[ i ] = 123; } |
上のは、n[0]〜n[99]まですべての配列要素に123という数字を代入する例です。for文は何度か出てきているので、難しくは無いですよね? このように、配列のインデックスに変数も使えるので、すべての配列に対して何か処理を行ないたいときは、このようにfor文とセットにして使います。
とりあえず、弾は3連射するので、配列要素は3つずつ用意して...
int Tx[3],Ty[3],bT[3]; // 弾の座標 void MakeTama(){ int i,j; j = -1; for (i=0;i<3;i++) { // 空き配列の検索 if (!bT[i]) { // 空き番が見つかった j = i; break; } } if (j==-1) return ; // 空き番は無かった Tx[j] = Mx; Ty[j] = My; bT[j] = 1; // 有効に } void DrawTama(){ int i; for (i=0;i<3;i++){ if (bT[i]) { Blt(2,Tx[i]-22,Ty[i]); Ty[i] = Ty[i] - 16; if (Ty[i]<-70) bT[i] = 0; } } } |
ずいぶんとプログラムが変わったように見えますが、MakeTamaではbT[i]を調べて、発射中でない弾を探し、もし見つかれば、それを発射、DrawTameでは、bT[i]を調べて発射中のものに対してのみ描画しています。さきほどのプログラムと比べるとfor文ですべての弾に対する処理が入ってきていますが、それ以外は何も変わっていません。
9.BGをスクロールさせてスピード感を出す
ゲームクリエーターの常識ですが、スピード感というのは主人公の大きさに対するBG(バックグラウンド)のスクロール速度で決まります。つまりは、キャラクターが小さければ、BGがゆっくり流れていても高速スクロールしているように見えます。人間の感覚器官は、うまく(あるいはいい加減に)出来ているんだなーと感心します。
それはともかく、今回、最初にやったRevolutionのポスター画像をBGとしてスクロールさせて、スピード感を出してみましょう。
int Mx,My; // メインキャラの座標 void main(){ Mx = 300; My = 400; LoadBitmap("aru.bmp",1,0); LoadBitmap("tama.bmp",2,0); LoadBitmap("p1.jpg",3,0); LoadBitmap("p2.jpg",4,0); SetFPS(30); loop { ClearSecondary(); KeyMove(); DrawBG(); DrawTama(); DrawChara(); halt; } } int nBGy; void DrawBG(){ BltFast(3,0,-nBGy); BltFast(4,0,480-nBGy); BltFast(3,0,960-nBGy); nBGy = nBGy - 16; if (nBGy < 0) nBGy = nBGy + 960; } void KeyMove(){ KeyInput(); if (IsPressLeftKey()) Mx = Mx - 8; if (IsPressRightKey()) Mx = Mx + 8; if (IsPushSpaceKey()) MakeTama(); } int nCM; // キャラモーションナンバー(0-8) void DrawChara(){ int pat; pat = nCM/3; nCM++; if (nCM==3*3) nCM = 0; BltRect(1,Mx,My,44*pat,0,44,70); } int Tx[3],Ty[3],bT[3]; // 弾の座標 void MakeTama(){ int i,j; j = -1; for (i=0;i<3;i++) { // 空き配列の検索 if (!bT[i]) { // 空き番が見つかった j = i; break; } } if (j==-1) return ; // 空き番は無かった Tx[j] = Mx; Ty[j] = My; bT[j] = 1; // 有効に } void DrawTama(){ int i; for (i=0;i<3;i++){ if (bT[i]) { Blt(2,Tx[i]-22,Ty[i]); Ty[i] = Ty[i] - 16; if (Ty[i]<-70) bT[i] = 0; } } } |
どうですか?なんだかゲームらしくなってきましたね。
10.今回は、ここまで
あ〜残念です。あとは、敵を表示して動かして、前回やった当たり判定をやれば、簡単なゲームになると思ったのに、時間になってしまいました(笑)
だもんで、これは、みなさんへの宿題にしておきましょう。ここまで読んで来られたみなさんには、難しくない..ですよね?
今回は、関数、そして配列と、プログラミングの上でもかなり重要なトピックが出てきました。あとは、これらの組み合わせだけなんですよ、ほんとに。
次回は、今回やったフレームを発展させて、実際に遊べるシューティングに仕上げましょう!