Last update 1999/08/07
(C)平山直之
無断転載は禁止、リンクはフリー
誤字脱字の指摘は歓迎
さて、ようやく実践編です。
題材ですが、なんと昔懐かしのタートルグラフィックです。最近の人は知らないかも知れないので軽く説明しておくと、タートルグラフィックスとは、画面上に置かれたカメがオペレータの指定したコマンドによって這いずりまわって軌跡を残すというものです。今回はVAFXとVismの連携が主眼なので、オペレータとの応答の変わりにスクリプトファイルを利用する仕様とします。
ところで、タートルグラフィック などと言いながら、ファイル名ではTortuiseGraphicになっていますね。なぜでしょうか? それは、タートルグラフィックに適当な素材をインターネットで漁ったらいいものが見つかったので使おうと思ったら、どうもその作者の方は陸ガメ好きな方でウミガメと陸ガメを明確に区別していらっしゃるようなので、その意志を尊重しようと考えたからです(笑)。
本当のタートルグラフィックは自分の向きとかを知ってたりして「回転」の要素が含まれてたりしますが、今回はそれが目当てではないので単に指定された座標まで移動するだけのプログラムを作成します。
私は最近、ゲームのようなアプリケーションはすべてVAFXで作っているのですが(ツールっぽいものはMFCもしくはSDKで作ってますけど)、さすがに同じようなコードを何度も書いていると飽きてくるし、スタートアップが面倒だとどうしても新しいことをしようという気力が減退します。
また、美しい設計をサポートするためのシステムがないと、つい手を抜いてしまいがちで、そうやって手を抜いたプログラムはやっぱりどうしても老朽化が激しくなってしまいます。
そこで、それらをある程度サポートするシステムを考案しました。最初はVisualStudioのカスタムAppWizardでやろうかと思ったのですが、結構メンドクセえですし、せっかく(概ね)機種非依存で作ったライブラリの支援システムが環境依存なのでは何やら間が抜けた感じが否めません。かといってそれ専用のプログラムを書くのも面倒です。
というわけで、結局perlのスクリプトに落ち着きました。VAFXの最新パッケージに含まれる
GameGen.pl
ActorGen.pl
がそれです。VisualStudioの機能になぞらえると、それぞれ「AppWizard」「クラスの挿入」に相当します。前者は
vafxgame::IGame
を、後者はvafxgame::IActor
を継承したクラスを生成するプログラムです。perlについて知らない人もいるかもしれませんが、プログラマ必携のツールなのでこの機会に覚えておくとよいでしょう。このスクリプトを使うだけならインストールさえできてれば問題ありません。
これらを使って、VisualStudioによるVAFXアプリケーションの作成を実演してみましょう。
まず、VisualStudioで「新規作成」「プロジェクト」「Win32 Application」を選択し、プロジェクト名を入力します。
こんな感じですね。よければOKを押します。何か聞いてきますが、空でよいのでその旨をAppWizardに伝えて実行を完了します。
つぎはプリコンパイルヘッダの設定です。先ほどのAppWizardの質問で「hello world」あたりにしておけば勝手に設定してくれるのですが、私の場合
- 名前が気に食わない(ファイル一覧で埋もれるから)
- 中身がなんか見づらくてむかつく
のであえて無視して自分で設定しています。それが面倒な人はAppWizardの質問で勝手にやってくれるように答えておくと良いでしょう。
で、私の場合のプリコンパイルヘッダの設定は、
a.cpp
/a.hpp
を作るa.cpp
に「#include "a.hpp"
」とだけ書いてセーブa.hpp
によく使うインクルードファイルをダーッと書いてセーブa.cpp
でのみ「プリコンパイルヘッダの作成」を選び、プロジェクトのデフォルトでは「プリコンパイルの使用」を選ぶ。ファイル名はもちろん「a.hpp
」という感じです。
a.hpp
でインクルードファイルを指定する場合、VAFXPCH.hpp
というプリコンパイルヘッダ用のヘッダファイルを最新パッケージに含めたので、使いたい人は使ってください。それと、perlスクリプトで生成したファイルはVAFXPCH.hpp
を読み込むことになってますので、私と同じようにしたい場合、各ファイルの「#include <VAFXPCH.hpp>
」を「#include "a.hpp"
」に変更する必要があります。プリコンパイルヘッダに下手に手を出すとわからなくなることが多いので、一切使わないか全自動にするのも手です。
さて、次は先ほどのperlスクリプトを使った雛形の作成です。
まず、コマンドプロンプトを起動して、
GameGen.pl
/ActorGen.pl
ファイルのあるディレクトリに移動します。これらはperlインタプリタを除けば単体で動作しますので、perlにパスが通っていれば好きなところに置いて構いません。そののち、コマンドラインから次のように入力します。
C:\PROJECTS\VAFX> perl GameGen.pl TortoiseGraphicもちろん、最後の引数は作成したいアプリケーションの雛形のクラス名です。もしかしたらあなたのシステムではperlはjperlという名前になってるかもしれませんが、その辺はperlのドキュメントの方でなんとかしてください。
このようにすると、カレントディレクトリに
TortoiseGraphic.cpp
TortoiseGraphic.hpp
という2つのファイルが作成されます。
ここまでできると、アプリケーションのビルドが可能になります。今作った2つのファイルをプロジェクトのディレクトリに移動して、プロジェクトに追加します。
そして「プロジェクト」-「設定」-「リンク」の
「オブジェクト/ライブラリ モジュール」の部分に
dxguid.lib ddraw.lib dsound.lib dinput.lib winmm.lib imm32.lib vafx.lib
を追加し、「C/C++」の
「ランタイム タイプ情報(RTTI)を有効にする」にチェックを入れましょう。DebugバージョンとReleaseバージョンの両方で設定を有効にするために、上記の設定を行うために左上の「設定の対象」を「すべての構成」にしておきましょう。
もし、VAFXを使うのは始めて、という場合、「ツール」-「カスタマイズ」-「ディレクトリ」でインクルードパス・ライブラリパスの両方にVAFXを展開したディレクトリを加えておくのを忘れないようにしましょう。
これで、めでたく実行ファイルが生成されるというわけです。
ここまでできたらビルドしてしてみましょう。うまくできましたか?
デバッグモードでコンパイルするとリンク時にひとつwarningがでますが、多分問題ありません(おいおい)。気になる人はVAFXライブラリをデバッグモードでビルドするなどしてください。
うまくビルドが完了したら、実行してみましょう。一瞬透明なウィンドウが出てきたかと思いますが、単に中身をまったく描画してないだけなので心配要りません。その証拠に動かすと中身もついてきます。
次はシーンオブジェクトを追加してみましょう。
シーンオブジェクトとは?
通常、ゲームは起動から終了までひとつのシーンでまかなわれることはありません。多くの場合、
- タイトル画面、
- ゲーム本体、
- エンディング、
- ゲームオーバー画面、
- 名前の入力、
- コンフィグ
などといったシーンを移り変わっていくことになります。
実は構造化プログラミングだとこれが意外に困難です。なぜかというと、これらの状態の遷移が入れ子構造になっていないからです。
せっかくVAFXを使っているのですから、ここはこうした「シーン」もオブジェクトにしてしまいましょう。そうすると特別にコードを書く必要がなくなるので、とても楽チンです。
ただし、今回はタイトル画面などを特に設けないので、シーンは「
GameMain
」一つしか作らないものとします。Actorの追加
VAFXでは、こうした「シーン」や「自機」「敵の弾」などといったゲームオブジェクトを全部ひっくるめてアクタとし、
IActor
から継承することを推奨しています。必ずそうしなければいけないという理由ではないのですが、そうするとVafxGame
コンポーネントが生成・破棄などにまつわる面倒な問題の面倒をみてくれるので、楽です。さて、新しいアクターをプロジェクトに追加するには、先ほどゲームを作るのに
GameGen.pl
を使ったのと同じように、ActorGen.p
lを使います。
C:\PROJECTS\VAFX> perl ActorGen.pl GameMain
でもって、これをプロジェクトのディレクトリに移動してプロジェクトに追加します。エクスプローラからVisualStudioにドラッグで持ってきて、右クリック→プロジェクトに追加、とするのが一番楽でしょうか。
GameMain.hpp
、GameMain.cpp
の両者を加えるのを忘れないようにしましょう。プロジェクトに追加したら、ゲーム本体と接続するために本体の方にコードをちょびっと追加します。まず、
TortoiseGraphic.cpp
の(いじってなければ)4行目くらいに
#include "GameMain.hpp"と挿入します。そして、
void SceneManager::OnInitialize(void) { }と書かれた部分を探して、そのメンバ関数の中に
GetGame()->AddActor(new GameMain(GetGame(),this));と書き加えます。これでアプリケーション起動と同時に
GameMain
がシステムタスクとして登録され、毎フレームごとにOnProcess
が呼び出されるようになるという仕組みです。プリコンパイルヘッダの設定をいじっていたら
GameMain.cpp
のインクルード周りを適宜修正して、ビルドしてみましょう。といっても、GameMain
は何もしないので、うまく動いてもさっきと変わりはありませんけど。
見えないものばかり作っていても面白くもなんともないので、そろそろ見えるオブジェクトを作ってみましょう。今回のプロジェクトで登場する「見える」オブジェクトは、「背景」と「カメ」だけです。
真っ黒な平原
まず背景です。先ほどと同じように
ActorGen.pl
でFieldView.hpp
/FieldView.cpp
ファイルを生成してプロジェクトに追加したら、GameMain.hpp
に
#if !defined(GAMEMAIN_HPP) #define GAMEMAIN_HPP #include <VAFXGame.hpp> #include "FieldView.hpp" /*============================================================================ * * class GameMain * * comment * *==========================================================================*/ class GameMain : public vafxgame::IActor { public: GameMain(vafxgame::IGame* aGame,vafxgame::IActor* aParent); ~GameMain(void); void OnInitialize(void); void OnTerminate(void); void OnProcess(vafx::IDirector&); void OnUpdate(vafx::IDirector&); private: vafxgame::IActor* vParent; int vState; int vFrame; vafximg::Image<24> vScreen; FieldView* vFieldView; }; #endifと書き加え、さらにGameMain.cppに
//**************************************************************** // OnInitialize void GameMain::OnInitialize(void) { TRACE("GameMain::OnInitialize"); vScreen.SetExtent(st2d::Extent(640,480)); vFieldView =new FieldView(GetGame(),this); GetGame()->AddActor(vFieldView); }と挿入しましょう。これで仮想画面の用意ができ、また
GameMain
とFieldView
の親子関係が成立しました。さて次はいよいよ描画です。
ActorGen.pl
によって自動的にオーバーライドされるように生成されるOnUpdate
メンバ関数は、VAFXシステムから画面のレンダリングが必要になるたびに呼ばれるメンバ関数です。画面のレンダリングは非常に時間をくうことが多いので、VAFXは基本的に毎フレームかならずOnUpdate
が呼ばれるとは限らないしくみになっています(毎フレームかならず呼ばれるようにもできますが)。また、OnUpdate
は「先に作ったアクタから順に呼ばれる」ことも保証されてもいるので、画面の描画はそれぞれのアクターで勝手に行ってかまいません。ただ、実際にゲームを作るとなると、優先度などの関係上スプライトマネージャを別にしてオブジェクト≠スプライトとしたり、優先度を制御したくなることが多いでしょう。
ということで、各アクタでは
OnUpdate
は敢えて無視して、変わりにRender
というメンバ関数を実装し、実際の画面の描画は、それを呼び出すオブジェクトがOnUpdate
で仮想画面を用意して行うことを推奨します。具体的には、
GameMain
側はOnUpdate
を次のように実装し、
//**************************************************************** // OnUpdate void GameMain::OnUpdate(vafx::IDirector& dir) { TRACE("GameMain::OnUpdate"); vFieldView->Render(vScreen); GetGame()->GetDisplayManager()->GetDisplay()->Draw( vScreen.GetRect(), vScreen, vScreen.GetRect()); }
FieldView
側では、
//**************************************************************** // Render void FieldView::Render(vafximg::Image<24>& image) { vafximg::ProcessBoxFill( image, image.GetRect(), vafximg::SetPixel24(vafximg::BGRQuad(0,0,0))); }とこんな感じで
Render
関数を実装すればよい、というわけです。クラス宣言のpublic
部にRender
の宣言を挿入しておくのを忘れないようにしましょう。ちなみにカメもあとで同じように実装します。
ここまでで、画面が黒一色で塗りつぶされるようになりました。結構面倒とは思いますが、実際に挿入したコードの量はほとんどない(ほぼ上のリストで水色の部分だけ)ですし、拡張性がとても高いのも想像がつくとおもいます。オブジェクト指向のメリットと楽しさが十分に発揮されるのは、こうして作ったオブジェクトを組み合わせるときなので、その時を想像しながらプログラミングすると楽しくできると思います。
平原とカメ
いよいよカメの登場です。例によって
ActorGen.pl
でTortoise.hpp
/Tortoise.cpp
を生成したら、それをプロジェクトに挿入しましょう。それが済んだら、GameMain.hpp
に先ほどと同じように以下のコードを加えます。
#if !defined(GAMEMAIN_HPP) #define GAMEMAIN_HPP #include <VAFXGame.hpp> #include "FieldView.hpp" #include "Tortoise.hpp" /*============================================================================ * * class GameMain * * comment * *==========================================================================*/ class GameMain : public vafxgame::IActor { public: GameMain(vafxgame::IGame* aGame,vafxgame::IActor* aParent); ~GameMain(void); void OnInitialize(void); void OnTerminate(void); void OnProcess(vafx::IDirector&); void OnUpdate(vafx::IDirector&); private: vafxgame::IActor* vParent; int vState; int vFrame; vafximg::Image<24> vScreen; FieldView* vFieldView; Tortoise* vTortoise; }; #endif
GameMain.cpp
も同じようにします。
//**************************************************************** // OnInitialize void GameMain::OnInitialize(void) { TRACE("GameMain::OnInitialize"); vScreen.SetExtent(st2d::Extent(640,480)); vFieldView =new FieldView(GetGame(),this); GetGame()->AddActor(vFieldView); vTortoise =new Tortoise(GetGame(),this); GetGame()->AddActor(vTortoise); } //**************************************************************** // OnUpdate void GameMain::OnUpdate(vafx::IDirector& dir) { TRACE("GameMain::OnUpdate"); vFieldView->Render(vScreen); vTortoise->Render(vScreen); GetGame()->GetDisplayManager()->GetDisplay()->Draw( vScreen.GetRect(), vScreen, vScreen.GetRect()); }
Tortoise.hpp
/Tortoise.cpp
にも、FieldView.hpp
/FieldView.cpp
と同じ要領でコードを加えましょう。さてここからが背景とカメで異なるところです。背景は今回真っ黒ですが、カメまで真っ黒というわけにはいきませんし、豆腐でも面白くありません。というわけでビットマップの表示です。
ついでにアニメーションもさせてしまいましょう。ただし、VAFXにはGIFアニメーションをそのまま表示するような機能はないので(つけるのも面白いかと思いますが)、GIFアニメーションを素材にする場合はあらかじめビットマップに展開しておく必要があります。
今回用意したのは、このサイトでゲットしたカメの絵です(どうもありがとうございます)。GIFアニメーションなのでビットマップに分解し、さらにそうしておくと後で都合がいいので90度回転しておきます。ファイル名は「
Tortoise1.bmp
」「Tortoise2.bmp
」とします。絵の準備が済んだらコーディングに入りましょう。
まずはビットマップを所持しておく変数を用意します。
vafximg::Image
の配列です。
class Tortoise : public vafxgame::IActor { public: Tortoise(vafxgame::IGame* aGame,vafxgame::IActor* aParent); ~Tortoise(void); void OnInitialize(void); void OnTerminate(void); void OnProcess(vafx::IDirector&); void OnUpdate(vafx::IDirector&); private: vafxgame::IActor* vParent; int vState; int vFrame; vafximg::Image<24> vImage[2]; };
Tortoise
が初期化されるときに、このイメージもファイルから読み込んで初期化します。まず、Tortoise.cpp
の上の方で#include <VAFXWUty.hpp> #define LOAD_IMAGE(x,y) x.InitWith(vafxwuty::file_mapping(y))とインクルード宣言とマクロ定義をしておいて、OnInitializeの中で
LOAD_IMAGE(vImage[0],"Tortoise1.bmp"); LOAD_IMAGE(vImage[1],"Tortoise2.bmp");とします。これで変数がそれぞれファイルの内容で初期化されます。もちろん、マクロを展開して
vImage[0].InitWith(vafxwuty::file_mapping("Tortoise1.bmp"));などとしてもかまいません(インクルードファイルの設定は必要です)。
VAFXWUty.hpp
はコンパイルに時間がかかるようなので、できればプリコンパイルヘッダに入れてしまいましょう。アニメーション
ビットマップの準備ができたので、実際にこれらを使ってアニメーションを作ってみましょう。
コンピュータにおけるアニメーションとはすなわち時間にあわせて表示する物を切り替えるということですから、VAFXでは、毎フレーム送られる描画要求(
Render
メンバ関数が呼ばれること)に対して、その時間に対応するビットマップを表示すればアニメーションになります。このカメの場合2枚のビットマップで構成されているので、オブジェクトの生成時から経過したフレーム数を数えておいて、フレーム数を20で割った値(そのままだとせわしなさすぎるので)が偶数だったら1枚目を、奇数だったら2枚目を表示するという感じでいいでしょう。
というわけで、まずは経過フレーム数をカウントするコードを挿入します。毎フレーム必ず呼ばれるメンバ関数=
OnProcess
を利用するとよいでしょう。コンストラクタか
OnInitialize
あたりでvFrame
変数(ActorGen.pl
が挿入します)を0に初期化しておいて、OnProcess
で
//**************************************************************** // OnProcess void Tortoise::OnProcess(vafx::IDirector&) { TRACE("Tortoise::OnProcess"); vFrame++; }とすれば、めでたく
vFrame
にこのオブジェクトの「年齢」が刻まれるわけです。描画するときはこの変数の値を利用して、
//**************************************************************** // Render void Tortoise::Render(vafximg::Image<24>& image) { int n=(vFrame/20)%2; vafximg::CopyRect( image ,st2d::Point0, vImage[n] ,vImage[n].GetRect(), vafximg::TransparentCopyPixel24( vImage[n].GetPixel(st2d::Point0))); }とでもしておけばよいでしょう。これで左上の色を透明色として描画します。
ここまでできると、カメが左上でアニメーションするようになります。画面はこんな感じです。
カメの動き
次はカメが定点へ移動する機能をつけてみましょう。
一般に、「モノの動き」を実現するコードは、概念の単純さの割にめんどうなことが多いので、私は今この「動き」そのものをオブジェクト化することを試みています。
その際、継承なんぞを使ってしまっては再利用がほとんど利かないと言っても過言ではないので、オブジェクトコンポジションを使って実現しようと考えています。
現在の研究過程がVAFXMot.hppに含まれています。まだ等速度運動(クラス名は
UniformSpeed
になってるけど、これでいいのかな? 英語できる人誰か教えてください)しか用意してませんが、これを利用してみましょう。まず、クラスにモーションオブジェクトをメンバ変数として追加しましょう。
#include <VAFXGame.hpp> #include <VAFXMot.hpp> /*============================================================================ * * class Tortoise * * カメ * *==========================================================================*/ class Tortoise : public vafxgame::IActor { public: Tortoise(vafxgame::IGame* aGame,vafxgame::IActor* aParent); ~Tortoise(void); void OnInitialize(void); void OnTerminate(void); void OnProcess(vafx::IDirector&); void OnUpdate(vafx::IDirector&); void Render(vafximg::Image<24>&); private: vafxgame::IActor* vParent; int vState; int vFrame; vafximg::Image<24> vImage[2]; vafxmot::UniformSpeed vMotion; };試しに目的地を右下に設定してみましょう。その場合、
//**************************************************************** // OnInitialize void Tortoise::OnInitialize(void) { TRACE("Tortoise::OnInitialize"); LOAD_IMAGE(vImage[0],"Tortoise1.bmp"); LOAD_IMAGE(vImage[1],"Tortoise2.bmp"); vMotion=vafxmot::UniformSpeed( st2d::Point(0,0), st2d::Point(640,480), 1); }とします。移動動作させたいときは、単位時間ごとに
//**************************************************************** // OnProcess void Tortoise::OnProcess(vafx::IDirector&) { TRACE("Tortoise::OnProcess"); vFrame++; vMotion.Step(); }とすればよいだけです。また、描画などで現在位置が取得したくなったら、
//**************************************************************** // Render void Tortoise::Render(vafximg::Image<24>& image) { int n=(vFrame/20)%2; vafximg::CopyRect( image ,vMotion.GetCurrent(), vImage[n] ,vImage[n].GetRect(), vafximg::TransparentCopyPixel24( vImage[n].GetPixel(st2d::Point0))); }とすればOKです。
ここまでで、カメが左上から右下に向かって移動するプログラムができました。あとで外部から目的地を指示できるように、Tortoiseクラスにインターフェイスを追加しておきましょう。
class Tortoise : public vafxgame::IActor { public: Tortoise(vafxgame::IGame* aGame,vafxgame::IActor* aParent); ~Tortoise(void); void OnInitialize(void); void OnTerminate(void); void OnProcess(vafx::IDirector&); void OnUpdate(vafx::IDirector&); void Render(vafximg::Image<24>&); void WalkTo(st2d::Point target,vafxgame::IActor* requestor,int speed) { vRequestor =requestor; vMotion =vafxmot::UniformSpeed( vMotion.GetCurrent(), target, speed); } private: vafxgame::IActor* vParent; int vState; int vFrame; vafximg::Image<24> vImage[2]; vafxmot::UniformSpeed vMotion; vafxgame::IActor* vRequestor; };
vRequestor
変数は、指示した相手を覚えておいて、移動が終わったら通知するためのものです。その通知のために、
//**************************************************************** // コンストラクタ Tortoise::Tortoise(vafxgame::IGame* aGame,vafxgame::IActor* aParent) : IActor(aGame) { TRACE("Tortoise::Tortoise"); vParent =aParent; vFrame =0; vRequestor =NULL; } //**************************************************************** // OnProcess void Tortoise::OnProcess(vafx::IDirector&) { TRACE("Tortoise::OnProcess"); vFrame++; if(vMotion.Step() && vRequestor!=NULL){ vafxgame::IActor* r=vRequestor; vRequestor=NULL; r->NotifiedBy(this); } }とコードを追加しておきましょう。
NotifiedBy
はvafxgame::IActor
で定義されている通知用の仮想関数で、相手側でオーバーライドされていなければ無視されます。この場合は、相手が誰であるかは仮定しない(vafxgame::IActor
の派生クラスである必要はありますが)で通知を行うことになります。すなわち、相手がvafxgame::IActor
の派生クラスであれば、カメの移動命令を出していいということです。
Motion
クラスのインターフェイスがなんだかあまりよくない気がしますが、いい手が思い付かなかったためです(何かいい手を思い付いたらぜひ教えてください)。将来的にはこのオブジェクト化したモーションのインターフェイスを統一し、モーションオブジェクトの型を差し替えるだけで動きを変えられるようにできたらいいな、なんて考えています。多分そんなに難しくはないでしょう。
ようやく本題のスクリプトランナーです。
このプロジェクトでは、スクリプトランナーの役目は
- イベントを実行する
- イベントの内容にしたがってオブジェクトに指示を出す
の2点だけですから、別段
vafxgame::IActor
から継承したクラスとしなくても構わないのですが、別に理由がないときは他のオブジェクトと対等にしておいたほうが楽なことが多いので、そのようにします。ということで、
ActorGen.pl
でScriptRunner
というクラスを生成します。生成したら、FieldView
/Tortoise
と同じようにGameMain
クラスに登録コードを入れておきましょう(Render
はしないので不要です)。依存性の問題
ここで問題がひとつ浮上します。「依存性の問題」です。
勘のいい方はお気づきかもしれませんが、このプロジェクトで、
FieldView
/Tortoise
などの「使われる側」のクラスは、「GameMain
」などの「使う側のクラス」に依存しないように作られています。これは、汎用的なクラスを作成した場合は、ほかのプロジェクトへの搬出が容易にできるようにしよう、という配慮です。また、仮にそのようにクラスをほかのプロジェクトに搬出することがないとしても、オブジェクトの相互リンクは一般に、オブジェクトの結合度と複雑さを飛躍的に増大させます。ですから、なるべく避けるべきです。
しかしながら、スクリプトランナーは、定義からもわかるように「イベントの内容にしたがってオブジェクトに指示を出す」という役割を要求されていますから、ほかのオブジェクトに対して無知であるわけにはいきません。
これを迂回する方法がないわけではありません。たとえば、
ScriptRunner
をテンプレートクラスにする、VAFXシステム(あるいはプロジェクト独自)にメッセージキューを導入して疎結合インターフェイスを推進する、vafxutil::closure
を用いる、などです。しかしそれらの方法のどれも「そのシステムなりの」複雑さを必要とします。ある意味、C++を使うのが間違いということになりかねません。ということで、これらに関する研究は将来の課題として、今回は相互参照で行くことにしましょう。というわけで、
GameMain
クラスに以下の関数を追加しておきます。
Tortoise* GetTortoise(void) {return vTortoise;}VismWrap.hpp/VismWrap.cpp
Vismとの接続は、
ScriptRunner
がそれを一手に引き受けることになります。VismDLLのインターフェイスはご存知の通りAPIで提供されますが、自分で作っといて言うのもなんですがこれを使うのはかなり面倒です。というわけで、これを大幅に緩和する
VismWrap.hpp
/VismWrap.cpp
というラッパークラスライブラリを用意しましたので、これを使うことにしましょう。VismWrap.hpp
/VismWrap.cpp
はVismのアーカイヴの展開後、util
ディレクトリに入っています。まず、
VCONTEXT
ハンドルで表わされるVismコンテキストはVismWrap
に含まれるvcontext
クラスで保持されます。vcontext
オブジェクトはコンストラクト時にVICreateContext
を、デストラクト時にVIDestroyContext
をそれぞれ呼び出しますので、簡単に言うと変数を作っときゃOKです。次に、
VISMOBJ
ハンドルで表わされるVismオブジェクトは、vismobj
クラスで保持されます。vismobj
/vcontext
から新しくオブジェクトを生成する場合はvismobj
型で返されます。
vismobj
型のオブジェクトから実際にC++で有用なデータを得るには、基本的にはキャストをします。たとえば数値を得るには、vismobj
型のオブジェクト"x
"がさしているVismオブジェクトが数値であるという前提の下で、
int n=(int)x;などとします。
基本的なスタンスとしては、
vismobj
型はスマートポインタの類であると思っておけばよいでしょう。Vismのガベージコレクタとはまったく無関係に動作しますので、メモリのことはあまり気にしなくて構いません。ガベージコレクションを行いたくなったときに消されては困るオブジェクトにマークしてから行えばよいだけです。これ以上の情報については、
VismWrap.hpp
ファイルを読んで下さい。今回のプログラムも参考になるでしょう。Vismとの結合
実際にVismを
ScriptRunner
に組み込んでみましょう。まず、
vcontext
型のオブジェクトをScriptRunner
のメンバ変数とします。これでvcontext
とScriptRunner
のライフタイムが同一になりますので、コンテキストの生成・削除のことは忘れてよくなります。
VismWrap.hpp
がパスの通ったところにあるとすると、
#if !defined(SCRIPTRUNNER_HPP) #define SCRIPTRUNNER_HPP #include <VAFXGame.hpp> #include <VismWrap.hpp> /*============================================================================ * * class ScriptRunner * * comment * *==========================================================================*/ class ScriptRunner : public vafxgame::IActor { public: ScriptRunner(vafxgame::IGame* aGame,vafxgame::IActor* aParent); ~ScriptRunner(void); void OnInitialize(void); void OnTerminate(void); void OnProcess(vafx::IDirector&); void OnUpdate(vafx::IDirector&); private: vafxgame::IActor* vParent; int vState; int vFrame; vcontext vism; }; #endifとなります。
初めてVismを使うので、一応この時点でビルドしてみましょう。
VismWrap.cpp
をプロジェクトに追加して、Vism.lib
をリンクするのをお忘れなく。ビルドが通ったら、デバッグのためにエラー処理コールバックを組み込んでおきましょう。こんな感じです。
void vism_error_handler(const char* mes,VERROR code) { MessageBox(NULL,mes,"Vism エラー",MB_OK); } //**************************************************************** // OnInitialize void ScriptRunner::OnInitialize(void) { TRACE("ScriptRunner::OnInitialize"); vism.set_error_handler(vism_error_handler); }これでVism側でエラーが出ると
vism_error_handler
が呼ばれてメッセージボックスが表示されるようになります。スクリプトを読む
次はスクリプトの読み込みです。今回はこんなスクリプトを用意しました。まず1つ目がVismとアプリケーションのあいだを取り持つためのコードで、
(define *entry-point* #f) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; run-event ;; 正常終了した場合は#fを、 ;; 脱出された場合は継続を返す (define (run-event event) (call/cc (lambda (x) (set! *entry-point* x) (event) #f)) ) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; continue-event ;; 正常終了した場合は#fを、 ;; 脱出された場合は継続を返す (define (continue-event cont ret) (call/cc (lambda (x) (set! *entry-point* x) (cont ret))) ) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; walk (define (walk x y) (client-walk x y) ;; 一旦脱出 (call/cc *entry-point*) )2つ目が実際のスクリプトです。
(define (event) (walk 3 7) (walk 600 128) (walk 180 440))1つめに関しては、前回おおむね説明したのでそれを読めば分かると思います。
実際にゲームを作る場合に「スクリプト」と呼ばれることになるのは、2つ目のファイルです。イベント=関数という構造になっています。意味は大体分かりますよね。
それぞれ「
tgsys.vsc
」「event.vsc
」というファイル名でプロジェクトと同じディレクトリにおいてあるものとして、両者を読み込みましょう。ScriptRunnerの初期化の時点で、
//**************************************************************** // OnInitialize void ScriptRunner::OnInitialize(void) { TRACE("ScriptRunner::OnInitialize"); vism.set_error_handler(vism_error_handler); vism.load("tgsys.vsc"); vism.load("event.vsc") }とします。これで
vism
が指すコンテキストに2つのschemeファイルが読み込まれます。次に、コールバック関数を登録します。アプリケーション側では
VISMOBJ ScriptWalk(void* param,VCONTEXT c,VISMOBJ a) { ScriptRunner* sr=(ScriptRunner*)param; vismobj args(c,a); sr->Walk(st2d::Point(args.nth(0),args.nth(1))); return VIMakeBoolean(c,false); }といった感じの関数を用意しておいて(
ScriptRunner.cpp
のインクルード設定の次あたりに置いておけばよいでしょう)、これを
//**************************************************************** // OnInitialize void ScriptRunner::OnInitialize(void) { TRACE("ScriptRunner::OnInitialize"); vism.set_error_handler(vism_error_handler); vism.load("tgsys.vsc"); vism.load("event.vsc"); vism.read("client-walk").define( vism.make_callback(ScriptWalk,this)); }と登録します。もちろん、
ScriptRunner
には、
//**************************************************************** // Walk void ScriptRunner::Walk(const st2d::Point& p) { // 暫定 ((GameMain*)vParent)->GetTortoise()->WalkTo(p,this,5); }と
Walk
関数を追加しておきましょう。GameMain
・Tortoise
クラスを参照しているので、インクルードファイルを読み込んでおくのも忘れてはいけません。スクリプトの実行
いよいよスクリプトの実行です。まずはイベント実行用のコードの組み込みです。これについては前回説明しましたね。
ScriptRunner.hpp
は、
class ScriptRunner : public vafxgame::IActor { public: ScriptRunner(vafxgame::IGame* aGame,vafxgame::IActor* aParent); ~ScriptRunner(void); void OnInitialize(void); void OnTerminate(void); void OnProcess(vafx::IDirector&); void OnUpdate(vafx::IDirector&); void NotifiedBy(vafxgame::IActor*); void Walk(const st2d::Point&); void RunEvent(vafxgame::IActor*); protected: void ContinueEvent(vismobj ret); private: vafxgame::IActor* vParent; int vState; int vFrame; vafxgame::IActor* vRequestor; vcontext vism; vismobj vContinuation; };と書いておき、
ScriptRunner.cpp
で
//**************************************************************** // コンストラクタ ScriptRunner::ScriptRunner(vafxgame::IGame* aGame,vafxgame::IActor* aParent) : IActor(aGame) { TRACE("ScriptRunner::ScriptRunner"); vParent =aParent; vRequestor =NULL; } //**************************************************************** // RunEvent void ScriptRunner::RunEvent(vafxgame::IActor* req) { vRequestor=req; vContinuation=vism.read("(run-event event)").eval(); if(!vContinuation && vRequestor!=NULL){ vRequestor->NotifiedBy(this); } } //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // ContinueEvent void ScriptRunner::ContinueEvent(vismobj o) { vContinuation=vism.read("continue-event").eval().apply( vContinuation, o); if(!vContinuation && vRequestor!=NULL){ vRequestor->NotifiedBy(this); } }と実装します。
VismWrap
を用いて置き換えてあるだけで、大まかな意味は前回説明したものと同じです。vRequestor
は、カメのときと同じように「スクリプトの実行が終わったら通知する相手」です。この場合は、イベントの実行が終了したら、イベントの実行を要求した相手に終了通知を出すことになります。これだけだと、カメに移動命令を出してもその終了通知を受け取れないので、スクリプトを再開できませんから、カメの移動終了通知を受け取ってスクリプトを再開するために
//**************************************************************** // NotifiedBy void ScriptRunner::NotifiedBy(vafxgame::IActor* sender) { TRACE("ScriptRunner::OnUpdate"); Tortoise* tortoise=((GameMain*)vParent)->GetTortoise(); if(sender==tortoise){ ContinueEvent(vism.make_boolean(false)); } }と
NotifiedBy
を実装します。イベントを実行するには何かきっかけが必要なので、「ウィンドウのクライアント領域で左マウスボタンが押されたらイベント開始」としましょう。
GameMain
に次の関数を追加します(仮想関数のオーバーライドです)。
class GameMain : public vafxgame::IActor { public: GameMain(vafxgame::IGame* aGame,vafxgame::IActor* aParent); ~GameMain(void); void OnInitialize(void); void OnTerminate(void); void OnProcess(vafx::IDirector&); void OnUpdate(vafx::IDirector&); void OnLButtonDown(vafx::IDirector&,word,const st2d::Point&); Tortoise* GetTortoise(void) {return vTortoise;} private: vafxgame::IActor* vParent; int vState; int vFrame; vafximg::Image<24> vScreen; FieldView* vFieldView; Tortoise* vTortoise; ScriptRunner* vScriptRunner; };実装はこんな感じです。
//**************************************************************** // OnLButtonDown void GameMain::OnLButtonDown(vafx::IDirector&,word,const st2d::Point&) { vScriptRunner->RunEvent(this); }これで完成です。ビルドに成功したら、ウィンドウの上でクリックしてみましょう。スクリプトどおりにカメが動くはずです。
調整
せっかくですから、カメが移動している方向を向くようにしましょう。vMotionのGetAngleを使えば簡単です。
//**************************************************************** // Render void Tortoise::Render(vafximg::Image<24>& image) { int n=(vFrame/20)%2; st2d::Point p(vMotion.GetCurrent()); st2d::Offset o(vImage[n].GetWidth()/2,vImage[1].GetHeight()/2); vafximg::RotateZoomMapRect( image ,p+o ,st2d::Rect(p,vImage[n].GetExtent()), vImage[n] ,st2d::Point0+o ,vImage[n].GetRect(), vafximg::TransparentCopyPixel24( vImage[n].GetPixel(st2d::Point0)), vMotion.GetAngle(),1.0,1.0); }
こんな感じになります。
オブジェクト間の通知がややこしかったと思うので、最後にまとめておきましょう。時間軸をC++言語のフローで管理しないでイベントの連続の形で制御しているためにコード上では切り刻んであってややこしいだけで、基本的には入れ子構造になっています。
- GameMainがマウスの押下を認識。
- GameMainがScriptRunnerにイベントの開始を命令。
- ScriptRunnerは以下を実行。イベントが本当に終了するまで以下を繰り返し。
- Vismでは適宜コールバック関数を呼び出す。ScriptRunnerはそれを受け取ってTortoiseに指示。
- Tortoiseは移動を開始
- Tortoiseが移動を終了すると、ScriptRunnerに通知。ScriptRunnerはイベントが終了していなければループを継続。
- イベントが終了したらScriptRunnerはGameMainに通知。
- GameMainは通知を受け取っても特に何もしない。
C/C++くらい速くてschemeくらい簡単にコンテキストスイッチを扱える言語があれば楽なんですが、当面そうもいかないようです。BeOSくらいならオブジェクトごとにスレッドを持つ、なんて芸当もできるのかも知れませんけど。
(C) 1999 Naoyuki Hirayama. All rights reserved.