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倍以上あったりするわけで、ローディングの意味は出来ています。・・・んが、気になりますよねぇ。

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

さーて次にかかるかぁ。




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