天才ゲームプログラマ養成ギプス またの名を 略して、「猫には」とお呼びください(笑)
やねうらおの考えでは、ゲームプログラミングに必要なテクニックとは、おおよそ800通りに分類されます。(800だけに嘘800です。すんません:p) そのうちの重要な12パターンを今年一年かけて解説しようと思っています。
内容はなーんも考えていません。12パターンの根拠もありません。毎月一回書いたら12ヶ月で12パターンかなーとか…すんまそん(笑)
プロ・アマ問いません。内容に対する、ご意見・ご感想などお待ちしております。雑誌への掲載も歓迎です。というか原稿書かせてくださいな(笑)
とりあえず、しばらくはゲームプログラミングに必須な実践テクニックをどこよりもおざなりに、誰よりもいい加減に解説しようかなって…(それじゃダメじゃん…)
関連書籍(New '01/02/25)
第14章.コールバックは使いやすいか? 2001/04/15
第11章で書いた通り、Listenerクラスがmediatorパターンを採れば、コールバックする仕組みは完成です。あとは、そのLisenerをsmart_ptrとして渡してやれば、何も案ずることはありません。能動的にイベントを通知してくれるコールバックが実現できます。
ところが、自分でGUIのボタンをエミュレートするようなボタンクラスを作ってみたのですが、これがまた使い辛いのです。ボタンが押されたかどうか判定するために、いちいちListenerクラスを作らなければならないというのが、なんとも鬱陶しいです。
ボタン押されたかどうかを調べるためだけになんで、クラス一つ作らんとあかんねやー、みたいな。C++にmediator的な概念が言語機能として備わっていない(⇒Javaの内部クラスのようなものが書きたい)というのも、その理由に含まれます。
ならば、およそ標準的なボタン機能をすべて実装したListenerクラスを作っておいて、(ボタンクラスを介して)そのListenerクラスに対して、ボタンが押されたかどうかを問いただせばいいのではないか?というのも思いつきます。
これは、“受動的”なイベント通知(=このクラスを使う人間にとっては“能動的”にイベントを取得しなければならない)です。少なくともゲームでは毎フレーム描画処理をしているのが普通であって、だとすれば、後者のほうが使いやすいのです。
となると、毎フレーム、描画するようなタイプのアプリケーション(※1)において、前者のようなイベント通知の仕組みはなんだか大掛かりすぎるとも思えてくるのです。
それとは別に、Listenerクラスによって、アルゴリズム置換(らしきこと)が出来ることに気づかれたでしょうか?次章では、これをもう少しクローズアップしてみます。
※1.「毎フレーム描画処理をするから、CPU稼働率高くて、ノーパソで動かしたときバッテリー食うんじゃ!」と言う意見もございましょう。これについては、メインのスレッドループで、Idle中(何もしていないとき)は::WaitMessageを呼び出すようなフレームワークにすればCPU稼働率は軽減されると考えます。
第15章.クラス置換 2001/04/16
ノベルゲームで、メッセージを表示する画面エフェクト1,2,3,4,5..というのがあるとしましょう。画面エフェクトクラスが、メッセージを表示するクラスに依存すると、画面エフェクトを追加しようとするごとに、メッセージ表示クラスに手を入れないといけないことになって、具合が悪いのです。
画面エフェクトなんてものは、そのゲーム固有のものも含まれるでしょうし、画面エフェクトはメッセージ表示クラスからは独立させたいのです。
となれば、画面エフェクターは、“クラス置換”できるようにしなければなりません。これは、第6章でやりました。(あれ、prototypeパターンと書いてますが、prototypeではないですね^^;) 要するに、factoryのsmart_ptrを渡してやります。
このときfactoryを、画面エフェクター一つにつき、ひとつ用意するか、それとも、エフェクト番号を指定すれば、そのエフェクターのインスタンスを生成するようなfactory(=parameterized factory)を用意するかは、どちらでもいいと思うのですが、前者の場合ならば、
vector < smart_ptr< IEffectorFactory > > m_vEffectFactory;
ってなメンバをメッセージ表示クラスに用意して、IEffectFactoryを設定してやります。後者ならば、
smart_ptr < IEffectorFactory> m_vEffectFactory;
ってな感じで、必要に応じて、メッセージ表示クラスのメンバ
vector < smart_ptr < IEffector > > m_avEffector; に対して
m_avEffector[ nEffectLayer ] . Add ( m_vEffectFactory->CreateInstance( nEffectNo ) );
というようにして使います。( Addは、auto_ptr等に見られる破壊的コピーのセマンティクスだと考えてください)
どちらも、デザインパターンとしての分類は、アルゴリズム置換ということで、おそらくstrategyパターンになります。ただ、その内実は、アルゴリズム置換というより、クラス置換、あるいは、Factory置換という感じですね。
このFactory置換は頻出テクなので覚えておいて損は無いでしょう。
第16章.ノベル系ゲームの作りかた 2001/04/26
なんか、デザインパターン系の話が続いたので、軌道修正して、ここいらで実践的な話を少ししようと思ったのです。
試しに、ビジュアルノベルを作ることを考えてみましょう。
各シーンは、シーン管理します。メーカーロゴ、オープニングをはじめとして、シナリオ表示シーン、ユーザー選択シーン、ロード画面、セーブ画面...。まあ、そこまでは良いでしょう。
シナリオは、シナリオナンバーで管理することにしましょう。シナリオナンバーごとにファイルを用意しましょう。ファイル分割すると、シナリオ管理が少し面倒っぽいですが、そこはPerlで簡単なマクロでも組んで対応するとしましょう。
現在のゲーム上のフラグは、ゲームフラグクラスが保持しているとしましょう。また、画面の設定や、一度見たシナリオをスキップするために、それらはシステムフラグクラスが保持しているとしましょう。
このゲームフラグクラスとシステムフラグは、Tips1024の9の技法によって、ひとつの連続した配列のようにみなせます。
そこで、これらをまとめて、単にフラグと呼びます。
次に、ゲームのフェーズを用意します。要するに、現在のゲーム時間、進行度を表すパラメータです。次に何番のシナリオが発生するのかは、1.このフェーズカウンタ . 2.フラグ によって決まります。
次に発生するシーンを決定するアルゴリズムは、こうです。
int GetEvent( ) { while (true) { int nEvent = 現在のフェーズに合致するイベントを取得( ); フェーズを次のフェーズに( ); if (nEvent != 無し) { return nEvent; } } } |
大雑把ですが、だいたいこんな感じでしょう。フェーズの経過のさせかたは、ゲームにより異なりますが…。
プログラムとしては、以下のようになっています。(これは、『蒼き大地』のプログラムの一部)
DWORD* CEventFlag::GetFlag(int nNo){ if (nNo<=44 || nNo>=70) { return &GetGameFlag()->GetFlag()[1000+nNo]; } return &GetSystemFlag()->GetFlag()[1300+(nNo-45)]; } // 条件を満たすかチェック #define f(v) (*GetFlag(v)) int CEventFlag::IsQualified(int nSceneNo){ switch (nSceneNo) { case 14 : return f(1); case 18 : return f(73); case 19 : return f(21); //中略 } // 条件を満たしたときのフラグ更新 void CEventFlag::DoQualify(int nSceneNo){ switch (nSceneNo) { case 1: f(47)=1; return ; case 2: f(52)=1; return ; case 8: f(73)=1; return ; //中略 |
この条件を満たすか IsQualified をループで呼び出し、現在のフラグ条件を満たすイベントを探します。そして、そのイベントが実行されたあとには、DoQualifyによって、そのイベントに相応するフラグを更新します。この相互作用によって、ゲームが進行して行く、というわけです。
プログラム的に難しいところは無いと思いますが、こういうのを作った経験が無いと、IsQualifiedとDoQualifyを分離するという発想が浮かばないかも知れません。
また、実際には、シナリオ担当の人がExcelで作成されたフラグ表をCSV形式(カンマ区切りのベタテキスト)に変換して、それをPerlで簡単な文字置換を行なうものを作成し、上のcase文を自動生成しています。(別に手打ちでも構いません)
各イベントの発生するフェーズ等も、このCSV形式でデータを吐き出して、発生イベント検索関数では、IsQualifiedとフェーズをチェックしています。IsQualifiedは、フラグがそのイベント発生条件を満たすかだけのチェックにして、フェーズはこのように分離しておいたほうが管理しやすいです。
ところで、yaneSDK2ndの掲示板のほうで、sigeさんから質問があったのですが、
わからない事があるんですが、ゲームに選択肢を付けたいんですが やねうらお先生のサンプル23の様に別のウィンドウで表示させて 例)石を投げる 石を蹴る 選んだ選択肢によってシナリオ(内容)が変って行く様にしたいんですが どうやったら良いのかわからないので教えてください。 |
これです。「選択肢を出して、それのどちらかをユーザーに選択させる」ようなシーンは、サンプル23と同じ方法で作ることが出来ると思います。おそらく、sigeさんがわからないのは、そこから先の部分だと思うのです。
ユーザーの選択した選択肢によってシナリオ分岐を発生させる方法はいくつかありそうです。
1a.まず、選択肢の表示シーンで、ユーザーがどちらを選んだかをどこかのフラグSに書いておきます。
(たとえば1つ目の選択肢が選ばれれば1,2つ目ならば2..)
1b.そして、IsQualifyのcaseブロックで、フラグSも考慮に入れた条件を書きます。
あるいは、
2a.選択肢の表示シーンにも、シナリオ番号を充てます。
2b.そのシナリオ番号に対するDoQualifyに、フラグSの値に応じて、他のフラグに反映する処理を書きます。
まあ、いろいろ考えられそうです。そのゲームに応じて、わかりやすそうなものを選べば良いと思います。
第17章.すべてのゲームにシリアライズ機構を捧ぐ 2001/10/07
ノベルタイプのゲームにせよ、シューティングゲームにせよ、ゲームの途中で、ロード/セーブしたいという欲求は、ユーザーならば誰しもが持っています。
しかし、この途中の状態を保存し、復元するという、いわゆるシリアライズ機構を実装するのは、結構めんどうなのです。
yaneSDK2ndのCSerialize/CArchiveは、それを手助けします。このクラスは、こうやって
class CHoge : public CArchive { vector<bool> aFlag; virtual OnSerialize(CSerialize& s) { s << aFlag; } } |
CArchiveクラスから派生させ、OnSerializeをオーバーライドして、仮想ストリーム(CSerialize)に保存しておきたいデータ等を operator <<で書き出します。
そうすれば、以降、
CSerialize s; CHoge h; s << h; // 格納 s.SetStoring(false); s << h; // 復元 |
と、格納〜復元が出来ます。
面白いのは、SetStoringで復元方向にスイッチングすれば、s << hという同じ表記なのに、それが復元を意味することになる、という点です。これにより、CArchive派生クラスは、OnSerializeを一度オーバーライドするだけで、格納/復元の両方を一気に記述できるというのが、私のちょっとした工夫です。MFCのように、ar.IsStoring( )で判定して、格納/復元の両方のルーチンを書かないといけないのは、面倒なのです。
さて。これで、格納/復元するための仕組みは出来ました。
少しやらしいのは、ポインタをどうやってシリアライズするのか、という部分です。ポインタは、復元過程において元の値に戻したところで、そのオブジェクトはすでに消失して存在しません。元に戻すだけでは不足なのです。オブジェクトを再生成しなければなりません。よって、シリアライズするときに、そのオブジェクトのID的なものを保存しておき、復元するときは、そのオブジェクトのIDから、元のオブジェクトを new するようなfactoryを使うことになります。
このへん、Javaのシリアライズ機構と比較して論じれば、面白いのですが、今回の趣旨とは外れるので、そのへんは別のところで詳しく書くとして、ポインタ使いたいときは、そのポイントしているオブジェクトの復元もなんとかせえ!と簡単に結論だけ書いて、やねうらおは、とっとと逃げてしまうのでありました。^^;
ゲーム全体は、シーン管理(この講座の第7章〜を参照のこと)しているものとします。ということで、シーンの管理クラス的なものが存在するわけですが、そいつが、シリアライズ機構を提供します。
管理クラスは、現在生成(保持)しているサブシーンに対して、そのシーンをシリアライズするように要求します。それぞれのシーンは、CArchive派生クラスとして、シリアライズ機構を提供します。
ところで、サブシーンもオブジェクトなわけで、復元するときはこのオブジェクト自体も用意してやらなければなりません。つまり、管理クラスが、格納するときには、シーンIDも一緒に格納しておき、復元するときは(その格納しておいた)IDから、SceneFactoryを使って、前回生成していたオブジェクトを生成、そのあと、そのサブシーンに対して、復元を要求します。
これで、どんなタイミングでも、途中ロード/セーブを可能にするための汎用的なフレームが整いました。
第18章.UndoとRedoの実装 2001/10/09
テキストエディタ等で、ひとつ前の状態に戻すことをアンドゥ、戻したやつをやっぱりキャンセルすることをリドゥと言います。これを実装するプログラムはどのようになるのでしょうか?ゲームプログラミングにおいても、こういう、アンドゥ・リドゥ機能が欲しい場合もあります。今回は、その実装方法について考えてみます。
普通、プログラマの誰かに聞いたら「Undo、Redoの実装には、(デザインパターンの)Commandパターンを使えるよ」と言われることでしょう。しかし、Commandパターンで本当に綺麗に実装できるのかというと、そうでも無い気がします。
ちょっと、実際にマップエディタを作ることを考えてみましょう。機能は、チップを置く機能と、チップを消す機能と、それをアンドゥ/リドゥする機能しか無いとしましょう。「チップを置く」と「チップを消す」は、コマンドですから、以下のようなCMapEditCommandクラスから派生させて書けば良いのでしょうか?
class CMapEditCommand { public: CMapEditCommand(CMapEdit* pvMapEdit):_pvMapEdit(pvMapEdit){} virtual bool execute(パラメータ) = 0; // そのコマンドを実行 // 実行に成功すればtrue virtual bool undo(パラメータ) = 0; // undoのための関数 // 実行に成功すればtrue protected: CMapEdit* _pvMapEdit; }; |
違います。よく考えてみてください。「チップを置く」という動作をアンドゥするためには、「チップを置く」という行為を行なう前に、どんな状態であったかまで責任を持って戻してやる必要があります。ここでは、すなわち、チップを置く前に存在していたチップについてどこかに書き出していなければなりません。誰がその作業を受け持つんでしょうか?思うに、「チップを置く」というコマンドを実行する、上のCMapEditCommand派生クラスであるCMapEditCommandPutChipとか何とか言うクラスが担わなければなりません。そのデータをどうやって書き出すのか、という部分ですが、これはひとまずあとまわしにします。
まず、上のパラメータとして、何を使うかという部分を決めてしまうことが肝要です。汎用的なクラスなのでvoid*にするべきでしょうか?あるいは、テンプレートで解決できるのでしょうか?私は、そうは思いません。私がお勧めするのは、const CSerialize& です(CSerializeは、第16章で出てきました)。これならば、のちのち、アンドゥ・リドゥデータを保存するのにも困りませんし、どんなデータであろうと(CSerializeがシリアライズできる限りは)渡せます。従来、デザインパターンのCommandパターンとして、この部分の実装が語られるとき、ここをどうやるのか実に不明確で、私は疑問に思っていました。
デザインパターンの慣習に従い、CMapEditCommand派生クラスのことをConcreteCommand(具象化されたCommand)と呼ぶことにしますが、1つの「アクション」は、実行したコマンドのID(このIDから、コマンドfactoryを使って、ConcreteCommandを生成します)と、ConcreteCommand::executeに渡したパラメータとして定義できます。
パラメータをconst CSerialize&で渡すことが決まれば、ユーザーのアクションは、std::vector <smart_ptr<CSerialize> >として格納していけます。(smart_ptr にしているのは、vector::push_backでリサイズが発生したとき、オブジェクトコピーのオーバーヘッドを減らすため)
次に、アンドゥをどうするかですが、「チップを置く」というアクションのアンドゥ操作も実は、「(もとあった)チップを置く」という、アクションなのです。このように、ある操作のアンドゥ操作も、ほとんどの場合アクションなのです。そこで、executeを実行するときに、アンドゥのためのアクションを生成し、そいつを返すための機構を用意しておけば良いのです。そうすれば、undoという関数は不要になります。
つまり、Commandの基底クラスは次のような設計になります。
class CMapEditCommand { public: CMapEditCommand(CMapEdit* pvMapEdit):_pvMapEdit(pvMapEdit){} virtual bool execute(const CSerialize&stream) = 0; // そのコマンドを実行。実行に成功すればtrue smart_ptr<CSerialize> getUndoAction( ) { retunr _vUndoAction; } // execute後、アンドゥのためのアクションを取得できる protected: CMapEdit* _pvMapEdit; smart_ptr<CSerialize> _vUndoAction; }; |
これで、基底クラスは完成しました。エディット中は、さきほど説明したように、std::vector<CSerialize>に、ユーザーのアクションと、アンドゥのためのアクション(↑のgetUndoActionで取得できるもの)を交互に積んでいけば良いわけです。
まあ、リドゥは簡単なので宿題としましょう。(stackでなくvectorを使ったのは、リドゥを実装するためなのですが) 結局、リドゥさえなければ、コンテナに対して欲しい操作は、最後に追加するpush_backと、最後から取り出すpop_backだけで良いのです。リドゥをサポートするために、vectorではなく、vectorを囲ったテンプレートクラスを用意しても良いかも知れません。
第19章.管理クラスは何も管理しない 2001/10/10
なんか、10月8日に10月10日分を更新していたりする、やねうらおのホームページは、インターネット界の週刊ジャンプと呼ばれていますが、遅れて一昨日の分を更新している日記ページの気持ちもわからないではない今日このごろです。(なんのこっちゃ)
ノベルゲームも、シーン管理クラスが次に発生すべきイベントを、イベント発生クラスに尋ねる⇒シーン構築という過程をたどるとなると、マップエディタが次に発生すべきコマンドを、ユーザーに尋ねる⇒ConcreteCommand構築という流れですから、実は、まったく同じ考えかたが適応できるのです。
ここで言うイベントとは、「シナリオ画面を構築し、シナリオナンバー512を再生」だとか、「エンディング画面を構築し、バッドエンディング画面を再生」だとか、そういう感じのものです。
ということで、シーンクラスは、CSceneの派生クラスですが、こいつを初期化するためのパラメータは、smart_ptr<CSerialize>にして、渡してやると良いというのは、第17章の結論からすれば至極当然と言えます。こうしてやることにより、シーン管理クラスは、そのゲーム依存の情報を保持しなくて済みます。すなわち、次に構築すべきシーンは、イベントクラスによって与えられますし、その構築は外部にあるシーンfactoryが担当しますし、その間のパラメータはsmart_ptr<CSerialize>で受け渡ししますから、シーン管理クラスはそのゲーム固有の情報を何も管理しない、ということになります。これがシーン管理の正しい姿です。
私の会社では実作業の分担として、インターフェース(=《業界用語》 ユーザーインターフェースのこと。すなわち画面まわり)のプログラム担当と、メインのプログラム担当を分けていたりします。インターフェース担当は、好きなようにシーンクラスを作ります。アバウトに言えば、OnDraw(CDrawContext*)のような描画関数以外については、規定せず、好きなように作らせます。説明のため、このクラスをCInterfaceと呼びます。
メインプログラム担当は、そのCInterfaceクラスを囲ったシーンクラスを用意します。そのクラスは、CScene派生クラスであり、OnInit(const CSerialize&)という初期化関数をオーバーライドしており、イベント発生クラスから渡された(イベント発生クラスで生成された)パラメータをシーン管理クラスがそのままこいつに投げるという形になります。OnInit関数のなかでは、そのCSerializeのパラメータに基づいて、CInterfaceクラスの設定系の関数に委譲していきます。
このように、インターフェース担当に、CScene派生クラスを直接書かせない理由は、インターフェースのプログラム(h / cpp)をなるべくメインプログラム担当がいじらなくて済むようにするための工夫です。また、このようにすることによって、メインプログラム担当は、インターフェースのプログラム担当の作業を待たずして、ゲームフローおよびフラグ表に基づき、ゲームを作成していくことが出来ます。
第20章.UndoとRedoの実装その2 2001/12/02
掲示板のほうで、しるす様から、
Undo,Redoって一定回数溜まると古い奴から破棄するじゃないですか そう考えるとListかDequeで実装するのがいいのかなぁなんて思ったんですが どうなんですかねぇ。 |
という質問をいただきました。確かにそうです。古いものを破棄するのならば、Dequeで実装するほうが良いです。この場合、Redoするときは、deque<smart_ptr<CSerialize> >::iterator で、現在Redoされたアクションをポイントするようになります。(します)
どうでもいいんですが、イテレータって、C++では普通++,--をオーバーロードしてあるから、次のへ進めるには、++して、戻すには--するではないですか。これ用語が無いんですよっ!用語がっ!!(やねうらおは、ビックリマークをたくさん付けると、ドラゴンボールでかめはめ波を出すときを思い出します。この用語がっ!!のところで、手のひらから出ているかめはめ波を想像しながら読んでください^^;)
nextするだとか、prevするだとかで伝わるような気はするのですが、nextって動詞では無いですし、「〜する」ってのは、どうなんや?と思うわけです。
そう考えると、イテレータを“インクリメント”すると呼ぶほうが、まだマシなような気がして..。でもそれを言うと、重箱の隅を突っつくのが大好きな人から、「イテレータという概念に、インクリメントというのは云々かんぬん…」という突っ込みが必ず入ります。んなこたぁ、わかっとるわい!ボケぇ!!なめとんのかぁ!!!!!!(漫画なら、ここでかめはめ波が出ている)とも言えないシャイボーイ(自分で言っててはずかしないんか>おっさん)な私は、ああ、そうですかー、、ふむふむ、、とか口篭もったこともありましたが、最近はこの手の突っ込みがうざいので、「iteratorを++する」と書いてます。この++は、iterator::opertor ++のことなので、突っ込まれても大丈夫です。(そうかなぁ..) みなさんも、使ってみてはいかが?(とか言ってみる)
あと、もう一つ、別の方からなのですが、concrete commandにconst CSerialize&として渡しているのは何か?という質問もいただきました。(私の説明が悪かったからなのですが)
getUndoActionで返されるCSerializeは、コマンドです。コマンドの定義は、文中に書いていますが、コマンド==「コマンドを実体化するための識別ID(これをパラメータにしてcommand factoryでconcrete commandを実体化する)+そのconcrete commandに渡すためのパラメータ」です。
よってconcrete commandに渡しているのは、上記コマンドの、「コマンドを実体化するための識別ID」が無いものです。こんな説明でわかっていただけるでしょうか...。
とりあえず、これでアンドゥ・リドゥは実装できています。続きはまた今度書きます。
第21章.描画フレームワークについて 2001/12/03
せっかくゲームの制作のためのコーナーなので、今回は、描画フレームワークについて書くことにします。
秒間の描画フレーム数を60とか30に固定するのは、うまくSleepで待てば良いのです。IDirectDrawSurface::Flipなんぞでタイミングを取るのは言語道断です。(リフレッシュレートが60Hzとは限らないため)
しかし、Windowsマシンは遅いものから速いものまで、さまざまなユーザー環境が考えられます。
そういう状況で、遅いマシンで、30FPS(FPS = Frames Per Second : 秒間フレーム数)だとか60FPS出ると仮定してプログラムして良いものでしょうか?
答えはおそらくノーでしょう。やねうらおは、まあ、いまどきのマシンならば、16bppモードにおいて30FPSは出るだろう、というつもりでプログラムしていますが、それでも、あまりいいスタイルとは言いがたい意味もあります。
では、どうするのが良いのでしょうか?
これには、フレームスキップという考えかたを導入することです。こういうと、よく、1フレームスキップするなら、各キャラクターを2倍ずつ動かせばいいように思う人がいるかも知れませんが、そうではありません。そんなことをしてしまうと、本来ならば衝突するはずのキャラクター同士が、通りぬけてしまいます。
処理時間の9割近くは描画関係に時間が食われています。そこで、フレームスキップというのは、本来描画すべき時刻から遅くなっていれば、そのフレームの描画をしないでおく、ということでスピードアップを図ります。
具体的には、60FPSで描画したいのならば、1フレームの描画時間は1/60。ところが前フレーム描画後、2/60秒以上経過しているのならば、そのフレームをプライマリサーフェースへ転送することを省略します。本当はすべての描画をブロック(省略)したほうが実行速度面では良いのですが、そうすると、画面上に描画して接触判定等をしている場合、うまく動かなくなってしまいますので、そこまでする必要は無いと思います。とりあえず、プライマリサーフェースへ転送する部分さえ省略すれば、たいていの場合、それらしく動くはずです。
しかしよく考えると「2/60秒以上経過していれば」という条件では、ある1秒間に、
描画したフレーム数 + スキップしたフレーム数 = 60 |
とはなりません。こうならないと、60FPSを仮定して(60フレーム描画すれば1秒経過していると仮定して)プログラムしているわけで、まずいのです。
このためには、もう少し違う概念を導入しなければなりません。
まず、第1フレームは経過時間s=0秒のところで描画し、第2フレームはs=1/60秒,以下、第nフレームは、s=(n−1)/60秒の段階で描画しなければなりません。いま仮に、第mフレーム目の描画をするとき、経過時間s’が(m−1)/60秒 + 1/60秒より大きいならば、そのフレームは描画をスキップするようにします。
これで、たいていの場合はうまくいきます。ただし、プライマリサーフェースへ転送するのを省略しているだけなので、いつまでたっても描画条件を満たさず、描画がスキップされ続け、画面には何も表示されない、ということにもなりかねません。そこで、経過時間s’が(m−1)/60秒 + 1/4秒より大きいのならば、そのフレームはとりあえず描画するだとか、そういう風に処理して、秒間最低、4FPS(ただしそのときは、上の「描画したフレーム数+スキップしたフレーム数=60」という条件は満たさない)を保証するようにしたほうが良いです。
yaneSDK2nd1.54以降では、このようなFPS維持クラスを、CFPSTimerをとして実装したので、そちらも見てください。
第22章.ゲームにおけるタスク管理について 2001/12/29
講師(やねうらおのことね)多忙につき、各自、自習しておくこと。
自習素材:
http://www.hh.iij4u.or.jp/~peto/Games/games_top.html
これを発展させ、C++を用いたゲームタスクの効果的なコーディングについて考えてみること。特に、親子関係を持ったタスク(例:戦車と砲台)のようなものはゲームで必須になってくるが、その部分をうまく管理する(管理できる)タスクマネージャクラスを設計してみること。
第23章.real smart pointer 2002/03/05
講師(やねうらおのことね)多忙につき、各自、自習しておくこと。(↑のコピペ^^;)
自習素材:
http://www.tomozo.ne.jp/yamazaki/download/doc_design_pattern.htm
デザインパターンのコード集。本を見るより、こういうコードで見せてもらったほうがはるかにわかりやすい。
http://member.nifty.ne.jp/yamazaki/doc_interface.html
C++におけるインターフェースについての紹介記事があります。
ところで、ついに、More Effective C++でも不可能と結論付けられていたポリモーフィックな配列を実現して、かつ、普通の配列と非配列な単一のオブジェクトを同等に扱えて、かつ、範囲外へのアクセスに対して例外を投げるような安全なスマートポインタを開発したんで、ソースを掲載しておきます。>realsmartptr
詳しい内容は春に出す本の原稿として書いているので、ここではこれ以上書けましぇん(笑)
第24章.class export 2002/03/20
講師(やねうらおのことね)多忙につき、各自、自習しておくこと。(またまた↑のコピペ^^;)
DLL側でfactoryを登録して、Main側や、他のDLLから、そのオブジェクトを使えるようにするためのオブジェクトマネージャクラス(CoCreateInstanceと同じようなもの)を作ってみました。ソースを掲載しておきます。>chap24
詳しい内容は春に出す本の原稿として書いているので、ここではこれ以上書けましぇん(笑) < これも↑のコピペ
第24章までの記事内容をベースに、『Windows
プロフェッショナルゲームプログラミング』を書きました。
ここから先は、今回の本に書ききれなかったことや、次の本(出るのかよ!)に書く原稿のベースにしようと思っているアイデアです。
第25章.柔軟性のあるシリアライゼーション 2002/12/08
あるクラスをシリアライズする機構を用意したとして、データメンバが一つ追加されたぐらいで、前のシリアライズデータと互換性がなくなるというのは、少し困り者です。
そこで、データメンバの読み書きを、名前で行なえばどうかと思いました。
CStringMapという、文字列による連想クラス(⇒yaneSDK2nd)を用意して、こいつにデータメンバを放り込み、最終的に、CStringMapをシリアライズしてしまうという方法です。
(以下考察途中=かきかけ。やね本2に書きます(^^;)
第26章.マイクロスレッド 2002/12/08
第22章で紹介したようなタスクベースのゲームプログラムだと、タスクは毎フレーム呼び出されるため、呼び出されるタイプのプログラミングモデルになってしまいます。もっとありていに言えばFSM(有限状態マシン)になる。もう現在の状態がどうとかそんなメンバ変数用意したくないねん!という意見もおありでしょう。
そこで、C++でマイクロスレッドを実現してみます。マイクロスレッドとはPythonのマイクロスレッドです:
http://www-6.ibm.com/jp/developerworks/linux/020809/j_l-pythrd.html
C++で実装する関係上、stacklessではなく事前確保したstack contextの切り替えになるんですが。これにより、シーンクラス等が退屈なFSMになることなくプログラムできます。マイクロスレッドのソースコードは、これ。yaneSDK3rd1.02のなかに含まれているので最新版はそちらからDLしてください。
(課題)マイクロスレッドを用いて、第22章と同じプログラムを書いて、その違いを実感してみること。
(以下考察途中=かきかけ。やね本2に書きます(^^;)
参考文献:「Game Programming Gems 2 」pp.258-264,株式会社ボーンデジタル,ISBN4-939007-33-2,定価12,000+税
第27章.C++的なタスクシステム 2003/06/15
講師(やねうらおのことね)やね本2の追い込み中なので、各自、自習しておくこと。
第22章のタスクシステムをC++で実装した例がyaneSDK3rdのサンプル10である。
これを参考に、自分なりのタスクシステムに発展させよ。
※ 詳しくはやね本2に。
第28章.ゲームのリプレイを実現する 2004/02/03
最近のゲームにおいて、リプレイを記録しなければならないことは多々あるだろう。
毎描画フレームすべてのデータをシリアライズしても良いが、それではあまりにもデータが肥大化しすぎる。
また、フレームスキップしている環境で記録し、フレームスキップしている環境で再現した場合、正しく再現されるかについても子細に検討しておく必要がある。
キー入力のみを記録して、その記録されたキー入力データに基づき再現することを考える。キー入力のみならば、たいした情報量ではないからだ。
ただし、このとき、ゲームには必ず再現性がなければならない。敵の動き等に乱数的な要素を介入させていてはいけない。もし乱数が使いたいならば、再現性のある乱数を用いて、その乱数の種(rand seed)もリプレイデータとして記録しておく必要がある。
次にフレームスキップのことを考えておこう。
1.描画をスキップさせたとしても、敵の移動はスキップしていないことを仮定できる場合を考える。
すなわち、1秒ごとに決められた回数の移動と入力のスキャンは行なわれている。そういう条件ならば、入力のスキャン情報のみをリプレイデータとして記録していくだけで良い。これをお勧めする。
2.フレームスキップ処理をきちんと行なっていないプログラムでリプレイ記録をとるためには、いくつかのテクニックが必要となる。
そのようなゲームでは、1フレームごとにタイマーを参照して、そのタイマーの値に基づいて敵を動かしたりしているはずである。仮に、そういう処理をしているとしよう。ただし、タイマーは1フレームの描画が完了するまでは更新されないものとする。これは、yaneSDK2nd/3rd/4thのFixTimerクラスがそういう仕様になっている。
このFixTimerクラスで得られるタイマー値をリプレイデータとして記録しておく。リプレイ再現時には、このタイマー値と実タイマーとを比較して、実タイマーのほうが先に進んでいれば描画フレームをスキップ、実タイマーのほうが遅れていれば、waitするようにして同期をとる。
これでリプレイ記録時と再現時で同じ速度でゲームが進行することは保証される。ところが、入力のスキャン情報だけを記録しても同じように再現できるとは限らない。これはゲームの仕様による。馬鹿正直に記録し続けると膨大な量になるので、敵の座標等をシリアライズしたものを前フレームとの差分を記録していくのが基本となる。
ニョキニョキ音符ゲー:
http://d.hatena.ne.jp/yaneurao/20040128#p1
でも、リプレイ記録〜再生を実現しているので、参考にすると良いだろう。