24)状態遷移...。ゲームはありとあらゆる状態遷移の塊です。ゲームをUpdateとDrawで分けるなら、Updateの殆どは状態遷移の管理です。この状態遷移、ど〜も私は苦手意識があるんですよねぇ...。作り方を知らないというのがどうも...。そこで状態遷移についてあれこれ調べて、そもそもにしてゲームの状態遷移には一体何が必要なのだろうか徒然なるままに考えてみることにしました。本当に徒然で適当に思考を巡らせていますので、変なところはご愛嬌です。元祖状態遷移switch〜case...ん〜...C言語で状態遷移と言えばswitch〜case。誰でも最初に学びます。状態遷移って「もし〜になったら〜な状態になってね」という物で、switch〜caseは「もし〜になったら」そのものです。状態遷移にswitch〜caseを使って駄目な事はありません。小さくて固定的な部分なら逆にこの方が見通しが良くなる事もあります。巨大な場合分けに無理やり使うのはダメです。endGame();break;default:break;}上はゲームの初期化からタイトル、ゲーム開始からゲーム中の振る舞いまでを無理やりswitch〜caseで書いたものです。動くには動きますが、何とも見づらい...。上の「Game」の場合分けの最後にbreakが抜けているのに気がつきましたでしょうか?そういうミスもしやすくなります。工夫が欲しいところです。関数ポインタ�@例で、判別している列挙型の名前と対応するメソッド名が殆ど同じなのに注目です。分かりやすい2重化ですね。こんな2重化は避けたい。そこで考えられるのが「関数ポインタ」です。C言語では関数も変数の一種のようなもの(型として識別されるもの)です。変数ならば値を代入出来ます。関数ポインタに代入出来るのは「関数」そのものです。何か動く};}1行目にあるのが関数ポインタを代入出来る変数です。見慣れないと「!!??」となる表記です。変数名は「func」でその前に「*」が付いているのでこれはポインタであることがわかります。(*)(void)」となります。これは「引数が無くて戻り値も無い関数」です。char*型とint型の2つで戻り値がboolである関数を代入出来る変数です。このように、関数ポインタにはその型と厳密に同じ引数と戻り値を持つ関数を代入出来ます。代入するとどいう恩恵があるのか?それは変数を通して代入した関数を呼べてしまう点にあります。上の(*func)()に注目です。これも見た目謎ですが、funcがポインタである事を思い出すとちょっと見えてきます。ポインタ変数の前に「*」を付けるとその実体になります。今実体は関数本体そのもので、それには引数が必要なので、後ろに関数呼び出しと同様に引数を渡します。関数を取っ換え引っ換えできると言うことは、上のように同じ呼び出し方法でも別の関数にぴょんぴょんジャンプ出来る事になります。実際上のWalk関数の中ではキー入力が無ければfuncをIdle関数に変更しています。自分で次の状態を指定したわけです。これがswitch〜caseから大きく進化した部分です。遷移をするために条件判定はしているけれどもその判定部分を個々の関数内部に局所化しています。これ、何となく下のような図式を実現していますよね:これをつなげて行けば、あぁ、なんだか状態遷移がどんどんできていきそうな...そんな淡い感覚がここにはあります。関数ポインタの欠点を考えてみます。歩く人が複数いたらどうでしょう?Walk1、Walk2と関数を作るのは馬鹿げています。できれば「個々の歩く人の中で個別に状態遷移をして欲しい」わけです。条件判定をするには判定元の値との比較が必要ですが、グローバルな領域に歩く人1、歩く人2...の値を保持するのも駄目ですよね。グローバルな関数ポインタには「拡張性と値の隠蔽性」がまるでないんです。するか?個別とか値の隠蔽と言うともう「クラス」しかありません。C言語の世界を卒業して、C++の世界に飛び込んで状態遷移をさらに発展させるのです。〜関数ポインタ発展版〜関数ポインタは何もグローバルな領域にある関数しか代入出来ないという訳ではありません。クラスのメソッドも当たり前ですが関数です。何か動く};}先程の例をクラス化したものです。クラス内メソッドを指定するにはクラス名スコープを付けます。関数呼び出しはかならずthisを通します(こうしないとコンパイルエラーになる)。後は先ほどと全く同じですよね。条件分岐に必要な変数などが外部に漏れない(隠蔽性)という数々の恩恵に授かれています。Humanを使う人は、な〜んにも考えずにHuman::updateメソッドを呼びつづけるだけ。それで内部では勝手に状態がゴロゴロと変わってくれます。ここまでで、次のような状態になったわけです:状態遷移がクラスの中にすっぽりと収まった。これは大きな進歩です。後はクラスを作りこんで行けば局所的な状態遷移がいくらでも書けるのですから。その状態遷移を持ったクラスの複製(オブジェクト)はいくらでも作れます。気弱な人(WeakHuman)がいて、止まる時になぜかびっくりしてしまうとしましょう。{}}Walkメソッド内で入力が無かったら「WeakHuman::Surpriseメソッド」に遷移する。(WeakHuman::*)(void)」であるSurpriseメソッドは型が違うので代入できないんです。引数と戻り値だけでなく、クラス内メソッドの場合はその所属クラスも厳密な型として認識されてしまいます。ある型に別の型を代入する。それが出来る(出来るように見える)物の一つは例えばint型にfloat型の値を代入するなどです。これはコンパイラが暗黙的に型を変換してくれています。一つ、ある型に別の型を入れる仕組みがC++にはあります。それは「親クラスのポインタに小クラスのポインタを代入する」です。WeakHuman;この代入はもちろん合法。WeakHuman::Surprise();とメソッドをnewして代入するという暴挙は...もちろん出来ません(^-^;。この形、な〜んか惜しい感じがしませんか?もしSurpriseがメソッドではなくてクラスだったら...?この代入の形は一応合法です。状態をクラス化すると派生クラスで変更ができる...のか?Humanクラスにある各メソッド。これがメソッドだから派生クラスでの遷移の変更ができない。Class)に格上げされました。共にStateBaseという共通の小さなクラスを継承しています。StateBaseクラスには更新用のupdateメソッドが定義されています。Human親クラスが遷移を取りまとめるのでStateBaseのポインタであるstate変数を持っています。インナークラスは親のstateポインタを直接書き換える事で次の遷移へ移行しています。Humanクラスは実際これで遷移します。んだとこらー!っと怒りたくなります(^-^;。実は、派生クラスのインナークラスを親のインナークラスから派生すると、そのクラスはもう親クラスへのアクセス権を失う「別人」になってしまうんです。そのため、publicで公開されている変数を除いてはアクセスできません。詳しくはC++踏み込み編その13「内部クラスは外側クラスのメンバにアクセスし放題!」を御覧下さい。上のHuman::stateをpublicにすると解消はできます。それはやっちゃだめです。実に惜しい...ちょっと整理しましょう。状態をインナークラスにすると、一つのクラスの中で好きなように状態遷移を実現出来そうです。親の派生クラスでインナークラスを継承すると親クラスの非公開メソッドやメンバにアクセス出来なくなります。「子(インナークラス)が親にアクセスしようとする」から起こっています。何か動く}};上ソースの太文字のように、インナークラスが次の状態遷移オブジェクトを返すようにすると、StateBaseを継承するクラスであれば確実に大丈夫です。親は戻ってくる新しいオブジェクトを受ければ次の状態になれます。WalkEx;}};今度は...大丈夫でした!コンパイルエラーにはなりません。実際上のWeakHumanクラスはWalkとIdleの間にSurprise遷移が挟まります。これで派生クラスで親クラス内遷移の一部分を別ルートにするなどの処理が出来そうな感じがしてきました。遷移挿入の伝播が命取り�Cでは実はハマリがあります。それを図で説明します。下記のような遷移があったとします:遷移に少し変更が欲しくなったとしましょう。Pを挟みこみたいと思います。�Cにならい、派生クラスでインナークラスPを作成したとします。このクラスは仕様通りにDオブジェクトを返します:PからDへは問題ありません。問題は、CからPです。今CはDオブジェクトを返していますので、これをPに変更する必要があります。CExでも作りますかと:これでC→P→DはOK。Bの作成するCオブジェクトをCExオブジェクトにする必要が出てきました。あれあれ?そうなんです。各状態遷移オブジェクトが内部で作成するオブジェクトを固定してしまっているため、一部の挿入に対してそこから上の全クラスの変更を強いられてしまうんです。これは、根本的にダメダメなんです!これは挿入だけじゃなくてどのインナークラスの部分変更でもアウトです...。状態遷移ファクトリ失敗の原因はインナークラス内で他のオブジェクトを直接newしたためでした。Cが次の遷移オブジェクトであるDを「作っちゃった」のが敗因です。Cが作る次のオブジェクトをを外部で指定したらどうでしょう。...それは、ん〜駄目ですね。親が遷移を支配しないようにしている方針に逆行します。次の遷移オブジェクトを作るのはやっぱり内部。なると、「次のオブジェクトを作っているつもり」にするしか無さそうです。CはDを作っているつもりなのに、実はPを作らされていた!っとするわけです。いう芸当をするのは「ファクトリクラス」です。ファクトリクラスというのは、オブジェクトを生成する専門家です。作り方は色々とありますが、引数にオブジェクトを表すIDや名前を指定すると、それに対応したオブジェクトを返してくれます。Cが作るDをPに変更するのが理想なのですが、これは気を付けないといけない事があります。他のクラスがDを作ろうとすると、それが軒並みPになってしまいます。それはきっとまずいです。Cの派生クラスCExを作り、Cの代わりにCExを生成するようファクトリクラスを変更します。CExはもちろん遷移先としてPを指定します。生成部分をファクトリクラスに置き換えた場合の遷移図はこんな感じになります:各状態遷移クラスの中では次の遷移オブジェクトを名前で指定します(これはIDでも構いません)。ファクトリクラスではその名前から生成するクラスを判別してオブジェクトを返します。上の図の青いのがファクトリクラスが返すオブジェクトです。Bが「Cに遷移しますよ」と宣言したのですが、ファクトリクラスはCExを返します。CExは自由に書き換えて良く、次にPへ遷移します。これでPを迂回してDに遷移する遷移の変更ができました。ところで、このファクトリクラス自体はどこにあるのでしょうか?これは...ん〜新たな問題です。親クラスに非公開で持たせると、�C辺りでハマったように、その派生クラスでアクセスできなくなります。言って、インナークラス内に定義するわけにも行きません。何か動く}};まぁ、これでクラス内で好きなように遷移ができますし、派生クラスでちょっと違う遷移にしたい時もファクトリクラスを派生することで実現できます。それにしても...クラスがやけにボリュームが出てきました。StateクラスStateBaseクラスとFactoryクラスは、他のクラスでも使えそうな汎用性を持っています。これがHumanクラスの中にあるのはもったいない。state->update(factory);}};状態遷移があるクラスはこのStateクラスを継承し、インナークラスで具体的なStateBaseとFactoryクラスの派生クラスを作成します。updateメソッドはこれでほぼ固定でしょう。変なことはしないのが吉なのでvirtualにはしていません。factory->create("Walk");}};大分に形になってきました。次に考えるべきは何でしょう...。Humanクラスのupdateを呼ぶと遷移が繰り返されます。このupdateは永遠には続かないかもしれません。いつかは終りを迎えますが、おお、その終りの合図がありません(^-^;。State::updateの戻り値はvoidで、結果を返さないのはマズイですね。true;}};ん〜〜何かが違うなぁ...。Stateクラスの内部にあるStateBaseクラスもupdateメソッドを持っていて、その戻り値があれば遷移を続けるし、戻り値が無ければ...それはもう終わりです。両方のクラスの挙動は極めて良く似ています。0;}};んー、見た目カオスってきました。updateメソッドは最終的には自分自身の振る舞いを更新しますが、先に自分の内部遷移であるstateオブジェクトを評価します。myUpdateメソッドが自分の具体的な振る舞いを記述する部分でvirtualで宣言されています。外から来るouterFactoryからは自分の遷移先をもらいます。一方で内部に持っているfactoryは内部遷移用に作ります。図に書くとこういう感じです:外側のStateは自分のupdateの中で内部に持っている遷移を管理し更新します(updateメソッド)。その後にmyUpdateメソッドの中で自分の具体的な振る舞いを行い、その結果を見て次に移る遷移先を指定します。次も自分自身を呼んで欲しい場合は自分を返します。次の人にバトンタッチする場合はファクトリに頼んで次の人を作ってもらいます。内部遷移の更新が先か自分が先か?上の実装では内部遷移の更新を行ってから自分の更新を行っています。これはいつもそうあるべきでしょうか?例えば、自分がユーザからの入力を管理しているとしましょう。内部遷移ではその入力結果を使うとします。この時、自分の更新が後になると、内部遷移は「1フレーム前の入力結果を利用」する事になってしまいます。これは明らかにまずいです。内部遷移が終わった事を受けて自分の振る舞いも変える事もあります。この場合内部遷移が先に終わらないといけません。あらら、つまり「Stateクラスの更新で内部遷移と自分との順序付けをしてはいけない」んです。0;}};これで仕様によってpreUpdateメソッドとpostUpdateメソッドを使い分けられるようになりました。選択した曲を「再生遷移」で再生するとします。この時、再生する曲を再生遷移に教えないといけません。今、各遷移はStateクラスの派生クラスになっていて、音楽選択遷移の内部で再生遷移オブジェクトが作られます。選択した曲はその時に渡す事になるはずなのですが、これは叶いません。オブジェクトはFactoryオブジェクトを通して生成されるからで、Factoryクラスは「setPlayName」のような具体的なインターフェイスを持っていないからです。それどころか、現在の実装だと各Stateクラスの派生クラスが完全に独立していて、遷移を抜けると何も残せない事になります。「内部遷移間の連携」がまるではかれないわけです。内部遷移間で値を共有するには、2つのアプローチがあるかなと思います。1つは自分の中で具体的な遷移オブジェクトを作り、公開されているコンストラクタやインターフェイスを通して値を渡す方法。一つはFactoryと同様にupdateメソッドの引数で値を代入するオブジェクトを渡し、それを次の状態遷移にも引渡す方法。前者の方法は今の所難しいと言うことで、後者の方法を考えてみます。outerVal);}};Valueというクラスを作って、それを引き渡す形に変更です。このValueをどれだけ多機能にしても、今後作られるであろうすべての変数やオブジェクトに対応できるとはとても思いません。updateの引数に固定的な共有オブジェクトを渡すのは無理があるんです。outerVal);}};これならば、自分の親遷移が適切な引数を与えてくれれば良さそうに思えます。内部遷移にも同じ型を渡すべきでしょうか?これだと先ほどと同様にVAL型はすべての遷移に対応する必要があり、実質何も変わっていません。内部遷移に渡すべきものは別の引数でしょう。その引数は内部でしか使用しないでしょうから、外部の人があまり気にしてはいけません。これはもはや親であるStataクラスで吸収できそうにありません。そもそも親が内部遷移をサポートしなければ!変数の共有化を考えた時にStateクラスに無理が出だしました。内部遷移へ渡す型を指定出来ないのが理由です。ここで考えを再度改めます。そもそも、この内部遷移は自分の内部での状態遷移を表すために設けたものでした。State派生クラス自体が末端状態で、内部遷移を持たない事はもちろんあります。その人にとってみると、内部遷移の機構は無用の押し付けです。内部遷移はすべての状態遷移の必須項目ではないと言うことです。0;}};え〜と、こうなりました(笑)。Stateクラスはupdate内で何かして、次の遷移オブジェクトを返すだけの非常に単純なクラスになってしまいました。今やVALはどんな型にでもなれるので、Factoryクラスをその中に吸収することもできます。0;}};ここまで簡単になると爽快です。なると内部遷移は自前で実装する事になります。PlayMusic(innerVal.getSelectMusic());}};まずSelectMusicクラスの中に内部変数を管理するInnerValクラスを作ります。その引数を共有するState<InnnerVal>内部遷移オブジェクト(innerState)を作ります。自分自身のupdateの中では内部遷移に対してInnerValオブジェクトを渡します。これで「値の共有化」が実現出来ています。内部遷移の結果遷移が終わっていなかったら、自分自身のコピーを返します。内部遷移が終わっていたら(NULLが返ってきていたら)、曲を再生するPlayMusicに再生する曲を渡します。これならば、派生クラスの中で次の遷移や内部遷移などを自由に制御できます。図に書くと次のパーツ範囲を管理できるわけです:左側はStateクラス本体、右側は内部遷移をInnerStateとして定義した場合です。点線部分は他の人の管理範囲です。FSM(有限状態機械)さて、この辺りでFSMについてちょっと考えてみることにします。machine)は「状態」と「動作」からなる「ふるまいモデル」です。普段は状態でぐるぐるしていますが、何らかのきっかけで「遷移」を起こし次の状態になります。次の状態に遷移する動作これらを上の図に当てはめるとこうなります:新しい状態になった時に開始動作が一度だけ実行されます。いわゆる初期化作業です。そのまま入力動作を繰り返し、何らかのきっかけで遷移する時には終了動作を行います。後片付けをするわけです。遷移動作というは次の状態に移るまでの動作の事です。上の図ですと矢印の棒に当たる部分かなと思うのですが、次のフレームで次の遷移に移る場合はこの動作はきっとありません。Machine)」の2タイプがあります。言うと、ムーアマシンは次の状態に移る動作も遷移の一つとして考えます。モデルのアニメーションで「歩く」と「走る」があった時、「歩きから走り初める」や「走りをやめて行歩きに戻る」という遷移をつなぐ部分も記述するのがムーアマシンです。一方でメリーマシンは「歩く」と「走る」の状態だけ記述します。途中動作は各状態遷移の中で記述されます。詳細に記述するならムーアマシン、遷移の骨子で考えるならムーアマシンと言ったところでしょうか。Actionはデストラクタに相当するかなと思います。コンストラクタで初期化作業をするためには、引数に共有変数を渡す必要があります。outerState;}};オブジェクトが作られて最初にupdateが呼ばれた時にはentryActionメソッドが呼ばれ、そのままinputActionに入ります。このメソッドが「自分とは別の」新しい状態オブジェクトを返した時には遷移が移る時なので、exitActionメソッドを呼んで後処理します。これで、FSMになりますね。updateメソッドが仮想関数では無くなった事に注目です。いわゆる「テンプレートメソッドパターン」の典型です。これみよがしに赤い文字で書いた「状態遷移が起こったら」ですが、これ、割と難しい問題です。inputActionが返すのは状態オブジェクトへのポインタですが、内部ではnewが入るはずなので、例え自分のコピーだとしても別のポインタです。状態オブジェクトが自分の分身なのか別人なのかを区別できないんです。これを解決するには、例えば状態オブジェクトに固有IDを振るというのもあるのですが、inputActionの戻り値を状態オブジェクトではなくてフラグにするのが楽で良いかもしれません。次も自分であればfalse、次は新しい人(もしくはNULL)ならばtrueを返すように約束するわけです。outerState;}};inputActionメソッドが変わりました。引数は共有変数オブジェクトと次の遷移オブジェクトへのポインタへのポインタです。ここに次の遷移オブジェクトが返されます。戻り値はTrans列挙型となりました。「StateBaseって何?Stateクラスの中に列挙型を入れちゃいかんの?」と思われたかもしれません。これは列挙型自体はどのステートでも共通です。Stateクラスの中に書くと、これがテンプレート引数によって別々のものと扱われてしまいます。Stateクラスに列挙型を定義すると、State<Value>::Trans_Occurdとテンプレート引数付きで値を返すことになっちゃうわけです。これは面倒くさいんですよ。StateBaseというテンプレート引数の無い親を用意して、そこに列挙型を設ければ、Stateクラスを継承していれば上のように列挙名だけでアクセスできます。これでめでたく状態遷移が一応形になりました。生ポインタをそろそろやめますここまで、新しい遷移オブジェクトを作る時に平気でnewしてきましたが、もちろんこのままだととてつもない数のリークになります。outerState;}};これでリークの問題は解決します。後はnewをする事自体の問題について考えてみます。状態を遷移させる時に新しいオブジェクトをnewします。それはまぁ良いとして、遷移させない時にもnewが発生します。Idle;}曲が鳴っている間はずっとPlayMusicオブジェクトが新規に作成されコピーされ続けられます。あらゆる所で遷移が動いているとき、フレーム内はnewの嵐になってしまうわけです。小さなオブジェクトをnewする時の問題はメモリ断片化です。これがあちこちにばらまかれると、大きなメモリを取りたくても取れなくなってしまいます。確保可能なメモリ領域を探す手間が増えるので、パフォーマンスが落ちていきます。Windowsの場合、OSが非常に賢いため、なるべく断片化が起こらないように調節してくれるそうです。WindowsOSで動かす分には、多量のnewによるメモリ効率についてはそれほど大きな問題にはなりません。newする事のよりきついペナルティは「オブジェクトの生成コスト」です。クラスのオブジェクトを作ると、内部に持っているオブジェクトの生成も同時に行われます。上のようにコピーコンストラクタによってコピーを行うと、代入演算子によるコピーが発生します。言う事は、例えば上のPlayMusicクラスの内部に沢山のメンバ変数があったとすると、毎フレームそのディープコピーが行われてしまうことになります。これは、数がかさばると非現実的になってきます。これを解決するには「内部スマートポインタ」を使います。{}};クラスの中にインナークラス(構造体)を一つ作ります。その中にメンバ変数を全部入れてしまいます。PlayMusicクラス自体はその構造体をコンストラクタで1つ作り、それをスマートポインタに包んでしまいます。すると、PlayMusic自体はスマートポインタを1つだけ持つ大変軽量なクラスに変わります。コピーコンストラクタや代入演算子でディープコピーが発生した時も、起こるのはスマートポインタ間のコピーです。これは取るに足らないものです。逆に言えば、このような状態遷移クラスを使う場合「必ず内部スマートポインタを使わないと死ぬ!」という事です。テストプログラムを作ってみるちょっと息切れしてきましたので、この辺でテストプログラムを作って終わることにします。簡単すぎてもつまらないけど難しすぎても混乱します。ん〜、ちょっと地味ですが、ファイルを読み込んでいる間に「NowLoading」を出すプログラムを作ってみましょうか。アプリが始まると指定のテキストファイルを読み込みます。この時わざと時間をかけるために少しずつゆっくり読み込む事にします。Loading」を出し続けます。全部読み込み終わったらフェードイン。フェードが終わったらそのテキスト内容をちょっとずつ描画します。全部描画し終わったらフェードアウトさせます。いうのってゲームで大変に良く出てきます。これを遷移図で書くとこんな感じになりそうです:この枠一つ一つが状態クラスです。これらはアプリケーションクラスの中で展開する事にしましょう。以下のサンプルの完全なソースはこちらに置いておきます。適当にダウンロードして動かしてみて下さい。Stateクラスもありますよ(^-^)。ファイル読み込み中遷移ファイル読み込み中クラスとしてApplication::Loadingクラスを作ります。インナークラスですよ。Loading」が画面に出ています。これはその名もずばり「NowLoading」クラスを作り管理してもらいましょう。ファイルが読み込まれている間はこのクラスの描画メソッドが呼ばれるようにします。よってNowLoadingオブジェクトをLoadingクラスのメンバとします。Loadingを描画している間は裏でテキストファイルを読み込みます。今回は遷移を感じるために、わざと時間をかけて読み込むようにします。読み込みが終わったら次のApplication::FadeInクラスに遷移します。"");}};最初に呼ばれるのがentryActionメソッドです。初期化メソッドですね。ここでは画面をホワイトアウトしておくためにフェーダーに白色を設定し、読み込むべきファイルをオープンしています。その後呼ばれるinputActionメソッド内ではオープンしたファイルを1フレーム1バイトずつ読んでいます。時間を掛けるためですが、沢山のファイルを分割して読むときも読み込み負荷を低減するために似たような事をします。Loadingを描画するよう指示を出しています。上の場合はこちらから描画して欲しい文字列を指定しています。ファイル読み込みが終了したら(追加読み込みサイズが0)、フェードインする遷移へ移行します。Loading描画をやめるよう指示をしています。フェードイン遷移ロードが完了したらフェードインしてきます。StateBase::Trans_Me_Again;}};inputActionがぐるぐるまわるので、counterをインクリメントさせていき2秒(120フレーム)経ったら次の文字送り遷移に移行します。画面が展開した後に文字送りを行います。StateBase::Trans_Me_Again;}};描画すべきテキストは共有変数(outerVal)にあります。そこから1文字取得するのですが、2バイト文字の場合はさらにもう1バイト取得しています(IsDBCSLeadByte関数)。取得した文字をstd::stringに追加すれば1文字送った事になります。現在までの取得バイト数であるposが60バイトを超えたら次の行を追加しています。文字の描画自体は外の人に頼んでいます。すべての文字を描画し終わったらフェードアウト遷移へ移ります。StateBase::Trans_Me_Again;}};フェーダーに2秒でホワイトアウトするように指示し、あとは時間が来るまでinputActionメソッド内で待機です。先のサンプルプログラムを作って感じたことは「遷移とタスクの使い分け」です。サンプルは上から下に向かって線形に進んでいます。プログラムもその通りに進むのですが、一般にゲームの画面は色々なものがいっぺんに動いています。これを遷移で行うのはちょっと苦しいかなと思いました。実際先のプログラムは文字送りが終わった後、文字が消えてフェードアウトします。改良する事はもちろんできるのですが、線形で単独にイベントが連なる遷移の性質を良く表しています。並列的な処理には遷移よりもタスクが向いています。タスクはタスクで良し悪しがあります。場面を作り込む時に遷移にすべきかタスクにすべきかは慎重に選択する必要があります。遷移を使うべきかどうかは「状態がどれだけ排他的であるか」が重要なのかなと思いました。ゲームではある場面が終わった後に次の場面を裏読みしておく時間が必要になると気があります。この場合一時的に2つの場面が同時に動く状況が生じます。これを排他性の高い遷移で対応させようと思うとなかなかに大変だったりします。一つ、上のサンプルで「おぉどうするべぇ」と思ったのは、更新と描画の使い分けでした。ゲームのタイトルには「背景」「メニュー文字」「カーソル」「3Dモデル」など一つの遷移の中で色々なものを描画する必要があります。この時、通常のゲームプログラムだと次のようなプロセスを経ます:更新してからその状態を元に描画を行う。分かりやすい構図です。ところが、状態遷移を適当に作るとこういう事が起こってしまいます:親の更新中に子の描画処理(実際に描画されてしまう処理)が起き、その後に親の描画処理が起こってしまいます。これはちょっと問題なわけです。するか?「状態遷移クラスの中に更新と描画を分けて『更新』が終わったら『描画』を呼ぶのはどうだろう」と思ったのですが:これは実はまずいです。更新処理の結果「状態が移ってしまっている」事があるためです。更新処理が終わり描画処理でもう一度その状態オブジェクトにアクセスしたいと考えても、すでにそのオブジェクト自体が失われている事があるわけです。遷移クラスに更新と描画メソッドを設けて2度呼ぶというのはどうもうまくありません。あれば、状態の更新時に描画すべきオブジェクトをPushしていくのはどうでしょう?つまりこういう状況を作るわけです:更新処理で子や孫の遷移を更新する際に描画すべきオブジェクトをアプリケーションに登録(Push)してしまいます。赤い丸がその描画オブジェクトだと思って下さい。全更新が終わった後にアプリケーションの描画処理部分でPushされた描画オブジェクトを一気に描画してしまいます。すると、更新順番での描画が保証されます。それだけでなく、描画を登録制にする事でアプリケーションが意図的に描画の順番を変更する事も可能です。これは例えば半透明オブジェクトのZソートや同じテクスチャを用いたメッシュ描画の「属性ソート」などができるわけです。いう事をできるようにするには、描画オブジェクトをしっかりと分離する必要があり、また描画オブジェクトが参照する数値やメッシュなどが更新オブジェクトが無くなっても存在することを保証するひつようがあります。参照先の保証というと「スマートポインタ」です。スマートポインタの威力が最強に発揮されるのはこういう場面かなと思います。いやぁ、実に徒然に遷移について書き殴ってきましたが、ようやく自分の中でも遷移の方法や扱い方について腑に落ちた感があります。別の考えやもっと良い方法はあるかもしれませんが、一つ自分の中で方法論を確立しているというのは安心ですね。よかったよかった(^-^)。