2008/11/18 火曜日

ゲームを作ってみよう 〜その10〜

duplexうっす。
さて、文章を書く際に、いつ読んで貰えるのかわからない物って挨拶が書きづらいですよね。
こんばんわって書いて朝に読まれちゃったら目もあてられない。
そんな時便利なのが「うっす」。いつの間にかNIGOROではうっすを使うことが増えてしまったduplexです。

さてはて、関係無い話を書いても仕方が無いので今回のお題ですが、前回作ってみて何かが違うと思った所はありませんでしたか?
って弾がまっすぐしか飛ばないと言うのは無しよ。

 わからない人は最初の頃に公開したBouceShotの企画書を見てみましょう。
よーく見てください、画面内に弾が2発と制限が指定されてますね。
というわけで今回は弾数の制御をやります。

 弾が画面内に2発という事は、現在の弾数をどこかで管理してやらなくては行けません。
まぁそれは今回やるとして、管理してる場合それ以上の発射要求をユーザーから求められた場合はどこかに弾はもう出せないと言う処理を入れなくてはいかんのです。

 その場合、ぱっと思いつくやり方は何がある考えて見ましょう。

  1. 自機(Shipクラス)で弾を出す処理を飛ばす。
  2. 弾(Myshotクラス)の方で限界を超えてた場合は自動で消える。
  3. 第3の勢力に弾を消滅させる。

等が思いつきます。ほかのやり方とか考えてみると良いかもしれませんが、とりあえず今回思いついたのはこれだけ。
こういう風に手法がいくつも出てくるのは当然なんですが、それじゃ一体どの方法でやればいいのか?
そんな時は利点と欠点をさらさらと出してみて考えます、ワシは。

1の場合

利点

  • 無駄な処理(いらない弾タスクの生成)が無い。
  • 処理の流れがわかり易い
  • Myshotクラスに何も変更を入れる必要が無い。

欠点

  • 弾数管理のタスクとのやり取りが必要になる

2の場合

利点

  • Shipクラスに何も変更を入れる必要が無い。

欠点

  • いらない処理(最初のフレームで弾数をチェックして限界を超えてたら消える)が出る
  • 弾数管理のタスクとのやり取りが必要になる

3の場合

利点

  • 弾数制御の処理がクラス内で行われるので、弾数が可変になっても変更を入れやすい。

欠点

  • そもそも、このタスクだけを見ても何をやってるのかわからない。
  • Myshotクラスを管理する必要が出てくる。

ざっとこんな感じです。
まだまだ利点や欠点はあると思いますが、あまり深く考えても仕方ないので先に進みます。

これを見ると1が利点が多くて作りやすそうです。
ただここで注意したいのが、弾数管理をShip内でやっちゃおうとする事。
これは良くやっちゃうんですが、大抵の場合こういうのは後で「これ変更よろしく」とか来ちゃいます。
てか来ます。
それとプレイヤーキャラクターのプログラムは長くなりがちで、後半てか終盤あたりに変更が入ると泣きを見る事が必死なんです。

そんなわけで、弾を管理する場所は別に作成し、後半で変更が入ったとしてもその別の場所をいじるようにする方が得策ってもんです。
実例でも書かないとわかり辛い所ではありますが、長くなるから却下。
まぁ小さく部品毎に分けた方が「泣かないで済む」と憶えてください。
よく言うオブジェクト指向なんでしょうが、理屈なんてどうでも良いんです、要は自分が楽するためです。

後は、誰が弾管理タスクに弾の増減を通知するかと言う問題があります。
Shipクラスだと弾が発射された事は分かりますが弾が消えた事は分かりません。
Myshotクラスであれば、発射された時と消える時が分かります。
他のクラスは何もわかりません。となればもう考える必要も無く、弾の増減の通知はMyshotクラスで行うのがベストだと思われます。

さらっとおさらいで、ここで決まった事を箇条書きにしてみます。

  1. 弾数管理タスクを作る。
  2. 弾の発射制限はShipクラスで行う。
  3. 弾数管理タスクへの増減の通知はMyshotクラスで行う。

それでは弾数管理タスクを作ります。

  1. import nigoro.lib.duplex.Task.*;
  2. class source.task.Maxshot implements Task2{
  3. private var max_now:Number; //現在の最大弾数
  4. private var now_shot:Number; //現在画面上に出ている弾数
  5. public function Maxshot(max:Number)
  6. {
  7. max_now = max; //このタスク生成時の最大弾数を決める
  8. now_shot = 0; //現在の弾数初期化
  9. }
  10. //タスクシステムに登録した際に呼び出される。
  11. public function init(no:Number, tc:TaskCenter2):Void{
  12. tc.regstData(”maxtask”,”no”,no); //データベースに自分の番号を入れる
  13. }
  14. //タスクシステムからの常時の呼び出しはここ。返り値がtrueだとそのままだが、falseを返すとタスクの削除と
  15. なる
  16. public function run(tc:TaskCenter2):Boolean{
  17. return true;
  18. }
  19. //タスクを削除する際に必ず呼ばれる。なんか処理したい事を入れておけ
  20. public function finalize(tc:TaskCenter2):Void{
  21. tc.elaseData(”maxtask”);
  22. }
  23. //このタスクとやり取りしたい場合これを使え
  24. public function talkTask(mess:Array, tc:TaskCenter2):Array{
  25. switch(mess[0])
  26. {
  27. case 0: //弾が発射された時に呼び出す
  28. now_shot++;
  29. break;
  30. case 1: //弾が消える時に呼び出す
  31. now_shot–;
  32. break;
  33. case 2: //新しく弾を発射出来るかのboolean値を返す
  34. mess[0] = (now_shot < max_now);
  35. break;
  36. }
  37. return mess;
  38. }
  39. //非常に特殊処理、通常は使わない
  40. public function frun(tc:TaskCenter2):Boolean{
  41. return true;
  42. }
  43. }

クラス名はMaxshotとしました、この辺にワシのボギャブラの無さを感じますが、気にしちゃいけない。
よくサミエルさんに「師匠~そのつづり間違ってますよ~」と言われるワシです。
簡単な英単語の組み合わせしか出来ないのです。
それと今回から行番号を付けてみました。
今回からは嫌がらせではありません、別の「大人の事情」です。

今までと同じ様な所は説明しません、あい変わらすワシの作ったタスクシステムを使ったタスクです。

8行目のコンストラクタを見てください。
引数が指定してあり、その引数で最大数を決めるように作ってみました。この場合このクラスを生成する場所で指定する必要があります。

16行目に今まで使った事が無かった命令が出ています。
この命令はTaskCenter2 が管理するMiniDBと言うクラスに「maxshot」と言うカテゴリを作り、その中にさらに「no」と言うタグを付けた配列を作り、そしてその中にタスクシステムが通知した自分の管理番号を入れると言う物です。
そして対になるのが26行目でして、こいつはMiniDBからmaxshotと言うカテゴリとその以下に連なるタグの中身をすべて消去すると言う命令です。
今回はnoと言うタグしかないのでそれだけの消去って事になります。

これは前から出てるのですが49行目にfrunと言うメソッドがあります。
これは以前説明した頃には無かった物でして、以前の説明の後に追加された命令です。
実装されたTask2に定義がしてあるので必ず書かなくてはいけないメソッドなのですが、不要なので何もせずにreturn true;と定義しています。
こいつは他のクラスにも、ワシのタスクシステムを使うものであれば必ず書かれてるので無視してください。

30行の talkTaskメソッドを説明する前に次はShipの改造に入ります。

  1. if(mouse_f){
  2. //弾を発射してよいかMaxshotタスクに問い合わせる
  3. if(tc.acssesTask(tc.checkData(”maxtask”,”no”),[2])[0]){
  4. //弾を発射
  5. tc.setTask(new Myshot(dmc,mc._x,mc._y));
  6. }
  7. }

Ship.as内にあったマウスが押されたら弾を出す部分の修正変更です。
2行めで使われているacssesTaskメソッドは(タスク番号、配列)とやることによって、タスクシステムが管理する番号のタスクの talkTaskに指定の配列を送ると言うものです。また返り値として配列が返ってきます。

次にcheckDataメソッドは(MiniDB内のカテゴリ、その中のタグ)を入れることでデータを取り出せます。
これは(”maxtask”,”no”)となってます、この文字は上で説明したMaxshotクラス内の16行目で設定してますね。つまりMaxshotクラスのタスク番号が入っているわけです。

acssesTaskの第一引数はこれでわかりました、次の[2]って何でしょうか?
これ、1っこだけの配列なんです。別のやり方で書いてみれば

var tmp:Array = new Array(1);
tmp[0] = 2;

のtmpと同じ物になります。
では、この戻り値はどうなるかというとMaxshotクラスの30行からを見てみましょう。
配列の先頭(0番)には2が入ってtalkTaskが呼び出されます。
talkTaskメソッド内では配列の0番をみてswitch文で振り分けを行ってますので、40行目が処理される事になります。

41行目では配列の0番にBoolean値を入れています。
さっきまで数値が入っていたのに、今度はBoolean値を入れるってのも気持ち悪い所がありますが、出来るんだから仕方ない。
とまぁ、その返り値がShipクラス側の3行目に戻ってきてるわけです。分かりにくいかもしれませんが

tc.acssesTask(tc.checkData(”maxtask”,”no”),[2])

この部分が配列名と同等になります。
そして、先ほどのMaxshotクラスから返された配列は0番に目的のデータが入っているので

tc.acssesTask(tc.checkData(”maxtask”,”no”),[2])[0]

と、お尻に配列番号を付けるってわけです。
これでif文がBoolean値を見ている事がわかりました。
後はtrueが返ってくれば弾を発射し、falseであればスルーする分けです。

次にMyshotに、弾数の増減を通知する部分を付け足します。

  1. //タスクシステムに登録した際に呼び出される。
  2. public function init(no:Number, tc:TaskCenter2):Void{
  3. tc.acssesTask(tc.checkData(”maxtask”,”no”),[0]); //Maxshotタスクに、画面上に弾が増えた事を伝える。
  4. }

まずはinitメソッドに一文を追加です。
initはこのタスクが生成された時に呼び出されるのでMaxshotタスクに弾が増えた事を通知します。
そのための処理はShipで説明した方法と同じなので割愛。

次にこのタスクが消える際に、必ずfinalizeメソッドが呼び出されるのでそこに弾が減った事を通知する物を追加します。

  1. //タスクを削除する際に必ず呼ばれる。なんか処理したい事を入れておけ
  2. public function finalize(tc:TaskCenter2):Void{
  3. tc.acssesTask(tc.checkData(”maxtask”,”no”),[1]); //Maxshotタスクに、画面上から弾が減った事を伝える。
  4. mc.removeMovieClip();
  5. }

4行目がそうですね。こいつも何をやっているのかもうわかりますよね?
さてと、最後になりますが、そもそもMaxshotを生成する場所はどこになるか?という問題が出ます。
今現在存在するクラスは

  • bouce
  • Maxshot
  • Myshot
  • Play
  • Ship

の5つ、このうちMaxshotは自分自身なんで除外して、残りの4つの内のどれでMaxshotを生成すれば良いのか。
まずMyshot、こいつが生成するとなると、弾が出るたびに弾管理タスクが生成されます。
どう考えても変ですよね。

次にShip、自機なら良さげですが後々自機は死ぬと言う現象が入ります。
死んだ場合、今の自機タスクは破棄され新しくタスクが生成されます。
と言うことはどうもこいつでも不味そうです。

次はBounce、こいつはゲーム中、タイトル、などの大まかにシーンを管理するためにいます。
一応次々生成されたり破棄されたりするわけではないので、条件的にはMaxshotを生成する事も可能ではありますが、ゲーム中の1タスクを生成するには不適切ではあります。

と言うわけでPlayが残りました。
というかPlayはそもそもゲーム中のタスクを管理するために作ったタスクですので、最初からPlay以外あり得ません。
これが分かっていた人はもうそろそろ自分でゲームを作れそうです。

ではPlayクラスにMaxshotタスクを生成する行を追加しましょうか。

  1. //タスクシステムに登録した際に呼び出される。
  2. public function init(no:Number, tc:TaskCenter2):Void{
  3. tc.setTask(new Maxshot(2)); //最大同時弾数を2に設定して弾管理タスクを生成
  4. tc.setTask(new Ship(mmc)); //自機を生成
  5. }

Shipを生成していた場所の一行上にMaxshotを生成する一文を追加しました。
この順番は少しだけ意味があります。
このタスクシステムは登録された順番で処理されるのですが、Shipは弾を発射する際にMiniDBにmaxshotのnoのデータを参照します。
ですが、そのデータを登録しているのはMaxshotです。
もし、Shipが先に生成された場合、存在しないデータを参照する可能性があります。
そんなわけでShipの生成が後になるわけです。

とは言っても、実の所この場合はどっちが先でも問題は起こりえません。
ただ、こういった依存関係はどんなに減らしても多少は出てくる物なので、いつも頭の隅に置いて処理を組み込まないと重いもしないBugに遭遇してしまうもんです。
日々訓練、これに尽きます。

と言うわけで今回はこれでおしまいです。完成品はこんな感じになります。

バウンスショット第10回完成品

 バウンスショット第10回完成品ソースファイル

次回は弾に角度を付けたいと思います。

関連ページ

  1. 第1回『ゲーム製作の流れ、一から見せます』
  2. 第2回『企画内容を吟味しよう』
  3. 第3回『仕様を詰めておこう』
  4. 第4回『では、仕様書をまとめましょうか』
  5. 第5回『ゲーム制作のためのツールを作る。・・・・いやじゃ!』
  6. 第6回『外部ファイルを読み込ませてみよう』
  7. 第7回『動く物を作ろう』
  8. 第8回『グラフィックデザインを考えるのです』
  9. 第9回『弾が出ます』

ここまで出てきた資料

 




2008/11/7 金曜日

ゲームを作ってみよう 〜その9〜

第9回『弾が出ます』

duplex第7回『動く物を作ろう』で自機が動くとこまでやりました。
でも動くだけ。やっぱりシューティングゲームだったら弾が出なきゃいけないよね。
そんなわけで弾を撃ちましょう。弾を撃つ、男のロマンだねぇ。

しかし前回の記事は長くて疲れませんでしたか?

なーに心配しなくて結構、絶対疲れてるはずだから今回は短くまとめようと思います。
ね、短いって良いよね。短いって最高ぉぃひゃっほぉい!

さて今回に必要な事を考えてみましょう。

1)弾の絵
2)マウスクリック判定の追加。
3)弾のプログラム

当然弾の絵はムービークリップにしちゃいます。
弾の絵は当然として、マウスクリックの判定の追加、これは弾を撃つにはマウスクリックが必要なのはゲームとして当然です。
ですが、そのプログラムは何処に書けば良いのか?
今あるのはBounce・Play・Shipの3つのクラス。
あなたならどのプログラムで判定させますか?

当然ですが、弾の発射位置は自機の座標からとなります。
ということは最低でも自機の座標が分からないと美味しくありません。
となると、BounceやPlayで処理しようと思えばShipが持つムービークリップの座標を何らかの形で取得しなければいけなくなります。
てーことはだ、当たり前かもしれませんがShip内にマウスの判定処理を追加するのが一番楽って事です。

ですが勘違いをしてはいけません。
今回のShip内にマウス処理を追加するのは楽だからやるのであって、しっかりと作ろうと思えばマウスを管理する所を作り、そこで処理させたほうがDebugは楽になる場合もあります。
まぁあれです、ワシは楽なほうを選ぶ!

最後は弾のプログラムです。
とにかく今回は弾がでりゃええやって事で真上に直線的に飛ぶように作ります。
これもまた一つのタスクとして作成しちゃいます。
せっかく作ったタスクシステムはがんがん活用しましょう。

弾のタスクとして作るクラスは、Myshotと言うクラス名にしました。

 

import nigoro.lib.duplex.Task.*;

class source.task.Myshot implements Task2{

private var mc:MovieClip;

public function Myshot(mmc:MovieClip,x:Number,y:Number)

{

mc = mmc.attachMovie(”p_shot”,”p_shot”+mmc.getNextHighestDepth(),mmc.getNextHighestDepth());

mc._x = x;

mc._y = y;

}

//タスクシステムに登録した際に呼び出される。

public function init(no:Number, tc:TaskCenter2):Void{

}

 //タスクシステムからの常時の呼び出しはここ。返り値がtrueだとそのままだが、falseを返すとタスクの削除となる

public function frun(tc:TaskCenter2):Boolean{

return true;

}

 //タスクシステムからの常時の呼び出しはここ。返り値がtrueだとそのままだが、falseを返すとタスクの削除となる

public function run(tc:TaskCenter2):Boolean{

 mc._y -= 8;//弾のY座標を上に移動させる

 //もし弾の座標が0未満になれば画面外へ行ったとみなしタスクを削除する

if(mc._y < 0) return false;

 return true;

}

 //タスクを削除する際に必ず呼ばれる。なんか処理したい事を入れておけ

public function finalize(tc:TaskCenter2):Void{

mc.removeMovieClip();

}

 //このタスクとやり取りしたい場合これを使え

public function talkTask(mess:Array, tc:TaskCenter2):Array{

return mess;

}

}

 弾の絵ですが、そのムービークリップのリンゲージに p_shotという名前を付けてください。後は非常に単純なつくりです。毎フレーム呼ばれるrunメソッド内でY座標を8ピクセルづつ上に動かし、0未満になったら消えるというだけのプログラムです。ただ企画書では壁に跳ね返ったり、角度があったりとこんな単純な処理で終わるものではありません。ですが、初めはマウスボタンを押したら弾が出る、この単純な事を出来る様に集中するべきです。初めからあれやこれやと妄想しちゃうと作れずじまいなんて事になっちゃうでしょ?。結局こういう細かい作業レベルまで分解しないとやりづらいんですよ。

 

そんなわけで次に前回作ったShip.asを改造しちゃいましょう。

 

import nigoro.lib.duplex.Task.*;

import source.task.*;

 

import Mouse.*;

 

class source.task.Ship implements Task2{

private var dmc:MovieClip;

 

private var mc:MovieClip;

private var st:Number;

 

private var mouse_f:Boolean; // マウスが押されたらtrueを入れる

 

public function Ship(mmc:MovieClip)

{

dmc = mmc;

 

mc = mmc.attachMovie(”ship”,”ship”+mmc.getNextHighestDepth(),mmc.getNextHighestDepth());

mc._x = 320;//自機の初期座標X

mc._y = 400;//自機の初期座標Y

 

st = 0;

 

Mouse.addListener(this);

mouse_f = false;

}

 

//タスクシステムに登録した際に呼び出される。

public function init(no:Number, tc:TaskCenter2):Void{

}

 

//タスクシステムからの常時の呼び出しはここ。返り値がtrueだとそのままだが、falseを返すとタスクの削除となる

public function frun(tc:TaskCenter2):Boolean{

return true;

}

 

//タスクシステムからの常時の呼び出しはここ。返り値がtrueだとそのままだが、falseを返すとタスクの削除となる

public function run(tc:TaskCenter2):Boolean{

 

var tmp:Number;

 

switch(st)

{

case 0:

tmp = (_root._xmouse - mc._x) * 0.15;

mc._x += tmp;

mc._rotation = tmp*2;

if(mc._rotation > 75) mc._rotation = 75;

if(mc._rotation < -75) mc._rotation = -75;

if(mc._x < 40) mc._x = 40;

if(mc._x > 600) mc._x = 600;

break;

}

 

if(mouse_f){

tc.setTask(new Myshot(dmc,mc._x,mc._y));

}

 

mouse_f = false;

 

return true;

}

 

//タスクを削除する際に必ず呼ばれる。なんか処理したい事を入れておけ

public function finalize(tc:TaskCenter2):Void

{

mc.removeMovieClip();

Mouse.removeListener(this); // リスナー削除

}

 

//このタスクとやり取りしたい場合これを使え

public function talkTask(mess:Array, tc:TaskCenter2):Array

{

return mess;

}

 

function onMouseDown(){

mouse_f = true;

}

}

 

色を変えた部分が今回追加されて部分となります。

 

マウスの入力が行われたら呼び出して欲しいと思った場合、Mouse.addListener(Object)を使い自分を登録する必要があります。つまり

 

Mouse.addListener(this);

 

これがその命令の部分です。マウスのイベントを扱うときの一連の流れですが

 

1)addListenerを使い自分にマウスイベントを送って欲しいと要求する

2)自分の中にマウスイベントを受け取るメソッドを書く

3)不要になったらremoveListenerを使い、もうマウスイベントは要らないよと通知する

 

となります。この一連の処理に必要なクラスがMouseクラスなんですが、ここがFlashの不思議なところ。Mathなどはimpoertする必要が無いのに、Mouseimportする必要があります。しないとコンパイル時に、Mouseって知らねーよと怒られます。C言語の様にstadio.hIncleudeしてるから別件でMathとかやんなくて良いよってなら話は分かりやすいんですがね。とにかく、同じAPIでもimportを必要とするものと、そうでないものがあるってわけです。

 

そんなわけでimport Mouse.*;があるわけなんです。

 

次にマウスイベントを受け取るメソッドはどうでも良いのか?というとこれまた違いまして、ちゃんとしたメソッド名が決まっています。幾つかあるんですが、今回使いたいのは、マウスボタンが押されたときに通知して欲しいメソッドです。そのメソッド名は onMouseDownと決まっています。そんなわけで今回追加したメソッドが

 

function onMouseDown(){

mouse_f = true;

}

 

こんなのですね。後はこのタスクが消される=マウスイベントも不要になるってことでfinalizeメソッドに

こんなのですね。後はこのタスクが消される=マウスイベントも不要になるってことでfinalizeメソッドに

Mouse.removeListener(this); // リスナー削除

が追加されてます。これで一連のマウスイベントの流れは終わりです。他にもマウスイベントはあるので調べてみるとよりいっそうの理解を得ることが出来るかもしれません。

 

マウスイベントの処理は分かったって事にして、あとは弾を出す処理に入ります。

 

弾を出すのは簡単です、先に作ったMyshotクラスをタスクシステムに登録するだけで事たります。問題はそのタイミング取りです。

 

mouse_fというBoolean変数がどこで何をやっているか眺めてみましょう。

 

まず、コンストラクタ内でfalseを入れて初期化します。

 

次にrun()メソッド内でif文に使われています、これを見るとtrueであればMyshotクラスをタスクシステムに登録するとなっています。ということはfalseで初期化されてるので、マウスボタンを押さない限り何も行われません。

 

その下、同じくrun()メソッド内でまたfalseを入れています。これでは毎フレーム無駄な処理を行ってるようにも見えますが、当然意味があります。

 

最後に onMouseDownメソッド内でtrueが入れられています。 onMouseDownメソッドは先に書いたとおり、マウスのボタンが押された際に呼ばれるメソッドです。

 

ということは、マウスが押されるとmouse_ftrueになる、その後run()メソッドが呼ばれるとif文の中が処理される、最後にfalseが入れられて次の処理からはまたif文内が処理されない。こういう仕組みになってるわけです。

 

しかしmouse_f = false;なんて毎フレーム処理するよりif(mouse_f){}の中に書いたほうが無駄な処理が省けるんじゃないかと思うかもしれません。そうすりゃ、マウスがクリックされた時だけmouse_f = falseの処理をやれば良いのでパフォーマンス上優位です(ものすごく微々たる話ですが)。ただある条件下だとif(mouse_f)自体に飛びたく無いという処理が必要になるかもしれません。実際そういったプログラムは何度も作ってきました、そんなときif文の中に入れておくと後々忘れていてBugの元になったりもします。そんな経験から、まぁ毎フレーム絶対falseを入れるのが安全だなってわけでこんなやり方でやってます。

今回はこれで終わりです、弾はでました?。出来たものは暫く弄ると、何か問題があると気づくことが多々あります。完成品とソースファイルを置いておくので、とりあえず、何か問題が無いか、弄りながら考えるのも良いかもしれません。

バウンスショット第9回完成品
バウンスショット第9回完成品ソースファイル

・・・しかし、短くまとめるって出来ないもんだなこりゃ。

  1. 第1回『ゲーム製作の流れ、一から見せます』
  2. 第2回『企画内容を吟味しよう』
  3. 第3回『仕様を詰めておこう』
  4. 第4回『では、仕様書をまとめましょうか』
  5. 第5回『ゲーム制作のためのツールを作る。・・・・いやじゃ!』
  6. 第6回『外部ファイルを読み込ませてみよう』
  7. 第7回『動く物を作ろう』
  8. 第8回『グラフィックデザインを考えるのです』

ここまで出てきた資料

 




次のページ »


Copyright (C) Nigoro Allright Reserved. Powered by ASTERIZM