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/11 火曜日

CS3素材分離作戦 その後

Action Script, Flashテクニック — Samieru @ 22:19:51

samieruどうも、俺ちゃんです。
トウキョウゲームショウ、行ってきました。ビジネス的には非常に有意義な二日間でしたが、ゲームショウ自体はしんどかったです。疲れますねぇーあれは。でも、なんか打ち合わせしてる時に通りすがりの人が「あれ?NIGOROって書いてある」って言うてたり、どうやらご存知の方もいらっしゃったようです。ありがとうございます。
とりあえず、英語で電話するのはしんどいっすねー。こりゃかなりのリハビリが必要やなと思った今日この頃、皆様如何お過ごしでしょうか。

バウンスショットのネタがバリバリと進んでいる状況ですが、まぁちょいと閑話休題と言うか、俺ちゃんラインの案件が後はクライアントさんの繋ぎこみ完了待ちだけなので今回の案件で実装してみた素材分離作戦の結果報告を兼ねたお話をしようかな、と思います。
いやー、一筋縄では行かなかったですよ。

さて、結構間が開いてるので、素材分離作戦の2とか読み返して頂かないと何の話かさっぱりな気もしますが、要は「Flashって音をmp3に変換したりグラフィクスをJpegに変換したりでコンパイル遅いぜー。ちょっとパラメータ変更してテストしようと思ったらコンパイル5分とかやれやれだぜー」って言う状況をなんとか回避しようって話です。
前回俺ちゃんの出した方法は、「素材は素材用swfで別にして、それのドキュメントクラスに素材呼び出しAPIみたいなんくっつけて、そいつを使ってメインプログラム側に素材読み込むってのはどうだ!?」って事ですね。

とりあえず、先に素材側のドキュメントクラスはこんなんなりました。

//——————————-
package
{

import flash.utils.getDefinitionByName;
import flash.display.MovieClip;
import flash.media.Sound;
import flash.display.BitmapData;
import flash.text.Font;

public class NigoroFactoryClass extends MovieClip implements INigoroFactory
{

private var material_cash_array:Object;

public function NigoroFactoryClass(){

material_cash_array = new Object();

}

public function getMovieClip(mc_name:String):MovieClip{

var cls:Class = getClass(mc_name);
var ret_mc:MovieClip = MovieClip(new cls());

return ret_mc;

}

public function getBitmapFile(mc_name:String):BitmapData{

var cls:Class = getClass(mc_name);
var ret_bmp:BitmapData = BitmapData(new cls(0,0));

return ret_bmp;

}

public function getSoundFile(mc_name:String):Sound{

var cls:Class = getClass(mc_name);
var ret_snd:Sound = Sound(new cls());

return ret_snd;

}

public function getFontFile(mc_name:String):Font{

var cls:Class = getClass(mc_name);
var ret_fnt:Font = Font(new cls());

return ret_fnt;

}

private function getClass(cls_name):Class{

if (material_cash_array[cls_name] == undefined)
material_cash_array[cls_name] = getDefinitionByName(cls_name);

return Class( material_cash_array[cls_name] );
//return Class(getDefinitionByName(cls_name));

}

}
}

//—————————

パッケージは今回関係ないので省いて、INigoroFactoryはただ単にゲッターメソッド(頭にgetが付いてる戻り値でなんか変数とかオブジェクトとかを取得するメソッド)の抽象宣言だけしてるインタフェースで、今の段階だとインタフェースでわける意味はほぼゼロなんですが、まぁ今後の事を考えて、です。

こいつのちょっとだけマトモかも知れない所は、メンバ変数の「material_cash_array」です。
これ何の為にあるかと言うと、getClassメソッドを見てもらえるとわかるんですが、最終的にコイツに呼び出されたクラスをぽんぽん放り込んでいるんですね。っで、二回目以降はコイツから取ってくるようにしてます。
何の為に?
答えは簡単、APIメソッドである「getDefinitionByName」って言う「文字列から指定されたClass(クラス定義オブジェクト、とでも言いますか)を取得する」奴がちょいと重いんです。だからあんまり回数を使いたくない。まーそれだけなんですが、今回コイツがあると無いとでどんだけスピードが変わるかは、そういえば実験してないです。忘れてました。
後はもう、どのゲッターもgetClassで名前が示すClassを取ってきて、それぞれの求められた型にキャストして返す、それだけです。この中で一個だけ注意せんとあかんのがBitmapDataの時で、コイツはコンストラクタに生成するBitmapDataのwidthとheightの指定が必要になるため、「new cls(0,0)」となってます。実際には空のBitmapDataを作る時で無い限り読み込む画像のサイズで勝手に修整される値なので、ここの0,0はコンパイルを通すためには仕方なくそうしているだけで、値は気にしなくて良いです。

ほい、これで基本はOKなんですよ。
これを実装してコンパイルしたswfをLoaderで持ってきて、MovieClipオブジェクトとして保持。前回その問題を言うてましたが、ここでINigoroFactory型とかにキャストしちゃうと、何故か素材が取得出来なくなるので注意が必要です。んで、例えば素材MovieClipオブジェクトの名前が「material」ならば、
var main_mc:MovieClip = material.getMovieClip(”title_movie”);
みたいな感じで貰うと。

素材swf自体が複数存在してもOKです。実際、今回は音素材と画像素材、そしてメインプログラム、とswfを3つに分けて扱っていました。

しかし、これを作っている所で一つの疑問がわきました。「外部素材swfにフォント埋め込んで、それを別のswfが呼び出して使えるってのは著作権的に許されない使い方も簡単に出来るって事よな。」と。
っで、やっぱり問題が出ました。フォントだけはダメだったんです。分けた状態で正常にプログラムは動くんですが取得出来ていません。埋め込みフォントが反映されないんですね。
「埋め込みフォントだけは、最初に呼び出されるswfに存在していないとメソッドで呼び出せない。」
これは注意しましょう。ローダがメインのswfを読んで実行する場合、フォントはローダに置くわけです。まーだから結局このgetFontは使えなかったわけですな。

そして、次の問題。
開発時はコンパイルの関係で素材を分離していたんですが、最終的には「ローダ+本体」の二つにする予定でした。
本体ドッキング時は本体が「NigoroFactoryClass」を継承して、「material = this」になるんすが、最終段階でローダを作って本体と素材を合体させたswfを読み込んだ所、素材が読めなくなったんですよ。ローダを外して本体単体起動だと問題ありません。ローダから始めると全部「そんな名前のクラスないぜー」と例外エラーが出ます。
バラバラのままでローダが本体を読んで、本体が素材を読んだ場合は・・・問題なかったりします。

これは多分「getDefinitionByName」の仕様に起因してるんですが、今んとこ「これか!」ってドキュメントにぶつかってないので決定的な原因は「不明」とさせて下さい。原因を知っている方がいましたら、是非教えていただきたい所です。
んーローダが本体を呼ぶ場合だと、ローダに本体をaddChildしていたのが問題なのかな?

とりあえず解決策としてはローダをメインプログラムに組み込んでしまい、素材だけを分離して読み込ませる事で問題なく動作しています。これだとローダがローディング時のアニメとメインプログラムを含めて容量が3Mぐらいにはなるかもしれませんが、それに対して素材は5倍以上あったりするわけで、ローディングの意味は出来ています。・・・んが、気になりますよねぇ。

こんな感じの現状です。
この件に関してはまた何かわかったら報告したいと思うっす。

さーて次にかかるかぁ。




次のページ »


Copyright (C) Nigoro Allright Reserved. Powered by ASTERIZM