Last update 1999/08/07

Scheme処理系の制作 第8回

(C)平山直之
無断転載は禁止、リンクはフリー
誤字脱字の指摘は歓迎


魅惑のタートル(トータス)グラフィック

さて、ようやく実践編です。

題材ですが、なんと昔懐かしのタートルグラフィックです。最近の人は知らないかも知れないので軽く説明しておくと、タートルグラフィックスとは、画面上に置かれたカメがオペレータの指定したコマンドによって這いずりまわって軌跡を残すというものです。今回はVAFXとVismの連携が主眼なので、オペレータとの応答の変わりにスクリプトファイルを利用する仕様とします。

ところで、タートルグラフィック などと言いながら、ファイル名ではTortuiseGraphicになっていますね。なぜでしょうか? それは、タートルグラフィックに適当な素材をインターネットで漁ったらいいものが見つかったので使おうと思ったら、どうもその作者の方は陸ガメ好きな方でウミガメと陸ガメを明確に区別していらっしゃるようなので、その意志を尊重しようと考えたからです(笑)。

本当のタートルグラフィックは自分の向きとかを知ってたりして「回転」の要素が含まれてたりしますが、今回はそれが目当てではないので単に指定された座標まで移動するだけのプログラムを作成します。

ついでにVAFXアプリケーションの簡単な作り方

私は最近、ゲームのようなアプリケーションはすべてVAFXで作っているのですが(ツールっぽいものはMFCもしくはSDKで作ってますけど)、さすがに同じようなコードを何度も書いていると飽きてくるし、スタートアップが面倒だとどうしても新しいことをしようという気力が減退します。

また、美しい設計をサポートするためのシステムがないと、つい手を抜いてしまいがちで、そうやって手を抜いたプログラムはやっぱりどうしても老朽化が激しくなってしまいます。

そこで、それらをある程度サポートするシステムを考案しました。最初はVisualStudioのカスタムAppWizardでやろうかと思ったのですが、結構メンドクセえですし、せっかく(概ね)機種非依存で作ったライブラリの支援システムが環境依存なのでは何やら間が抜けた感じが否めません。かといってそれ専用のプログラムを書くのも面倒です。

というわけで、結局perlのスクリプトに落ち着きました。VAFXの最新パッケージに含まれる

がそれです。VisualStudioの機能になぞらえると、それぞれ「AppWizard」「クラスの挿入」に相当します。前者はvafxgame::IGameを、後者はvafxgame::IActorを継承したクラスを生成するプログラムです。perlについて知らない人もいるかもしれませんが、プログラマ必携のツールなのでこの機会に覚えておくとよいでしょう。このスクリプトを使うだけならインストールさえできてれば問題ありません。

作ってみようTortoiseGraphic

これらを使って、VisualStudioによるVAFXアプリケーションの作成を実演してみましょう。

まず、VisualStudioで「新規作成」「プロジェクト」「Win32 Application」を選択し、プロジェクト名を入力します。

VScheme8-1.gif (10500 バイト)

こんな感じですね。よければOKを押します。何か聞いてきますが、空でよいのでその旨をAppWizardに伝えて実行を完了します。

つぎはプリコンパイルヘッダの設定です。先ほどのAppWizardの質問で「hello world」あたりにしておけば勝手に設定してくれるのですが、私の場合

のであえて無視して自分で設定しています。それが面倒な人はAppWizardの質問で勝手にやってくれるように答えておくと良いでしょう。

で、私の場合のプリコンパイルヘッダの設定は、

という感じです。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のドキュメントの方でなんとかしてください。

このようにすると、カレントディレクトリに

という2つのファイルが作成されます。

ここまでできると、アプリケーションのビルドが可能になります。今作った2つのファイルをプロジェクトのディレクトリに移動して、プロジェクトに追加します。

そして「プロジェクト」-「設定」-「リンク」の

VScheme8-2.gif (10990 バイト)

「オブジェクト/ライブラリ モジュール」の部分に

dxguid.lib ddraw.lib dsound.lib dinput.lib winmm.lib imm32.lib vafx.lib

を追加し、「C/C++」の

VScheme8-3.gif (9746 バイト)

「ランタイム タイプ情報(RTTI)を有効にする」にチェックを入れましょう。DebugバージョンとReleaseバージョンの両方で設定を有効にするために、上記の設定を行うために左上の「設定の対象」を「すべての構成」にしておきましょう。

もし、VAFXを使うのは始めて、という場合、「ツール」-「カスタマイズ」-「ディレクトリ」でインクルードパス・ライブラリパスの両方にVAFXを展開したディレクトリを加えておくのを忘れないようにしましょう。

これで、めでたく実行ファイルが生成されるというわけです。

ここまでできたらビルドしてしてみましょう。うまくできましたか?

デバッグモードでコンパイルするとリンク時にひとつwarningがでますが、多分問題ありません(おいおい)。気になる人はVAFXライブラリをデバッグモードでビルドするなどしてください。

うまくビルドが完了したら、実行してみましょう。一瞬透明なウィンドウが出てきたかと思いますが、単に中身をまったく描画してないだけなので心配要りません。その証拠に動かすと中身もついてきます。

シーンオブジェクトの追加

次はシーンオブジェクトを追加してみましょう。

シーンオブジェクトとは?

通常、ゲームは起動から終了までひとつのシーンでまかなわれることはありません。多くの場合、

などといったシーンを移り変わっていくことになります。

実は構造化プログラミングだとこれが意外に困難です。なぜかというと、これらの状態の遷移が入れ子構造になっていないからです。

せっかくVAFXを使っているのですから、ここはこうした「シーン」もオブジェクトにしてしまいましょう。そうすると特別にコードを書く必要がなくなるので、とても楽チンです。

ただし、今回はタイトル画面などを特に設けないので、シーンは「GameMain」一つしか作らないものとします。

Actorの追加

VAFXでは、こうした「シーン」や「自機」「敵の弾」などといったゲームオブジェクトを全部ひっくるめてアクタとし、IActorから継承することを推奨しています。必ずそうしなければいけないという理由ではないのですが、そうするとVafxGameコンポーネントが生成・破棄などにまつわる面倒な問題の面倒をみてくれるので、楽です。

さて、新しいアクターをプロジェクトに追加するには、先ほどゲームを作るのにGameGen.plを使ったのと同じように、ActorGen.plを使います。

C:\PROJECTS\VAFX> perl ActorGen.pl GameMain

でもって、これをプロジェクトのディレクトリに移動してプロジェクトに追加します。エクスプローラからVisualStudioにドラッグで持ってきて、右クリック→プロジェクトに追加、とするのが一番楽でしょうか。GameMain.hppGameMain.cppの両者を加えるのを忘れないようにしましょう。

プロジェクトに追加したら、ゲーム本体と接続するために本体の方にコードをちょびっと追加します。まず、TortoiseGraphic.cppの(いじってなければ)4行目くらいに

#include "GameMain.hpp"

と挿入します。そして、

void SceneManager::OnInitialize(void)
{
}

と書かれた部分を探して、そのメンバ関数の中に

GetGame()->AddActor(new GameMain(GetGame(),this));

と書き加えます。これでアプリケーション起動と同時にGameMainがシステムタスクとして登録され、毎フレームごとにOnProcessが呼び出されるようになるという仕組みです。

プリコンパイルヘッダの設定をいじっていたらGameMain.cppのインクルード周りを適宜修正して、ビルドしてみましょう。といっても、GameMainは何もしないので、うまく動いてもさっきと変わりはありませんけど。

登場キャラクタは背景とカメ

見えないものばかり作っていても面白くもなんともないので、そろそろ見えるオブジェクトを作ってみましょう。今回のプロジェクトで登場する「見える」オブジェクトは、「背景」と「カメ」だけです。

真っ黒な平原

まず背景です。先ほどと同じようにActorGen.plFieldView.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);
}

と挿入しましょう。これで仮想画面の用意ができ、またGameMainFieldViewの親子関係が成立しました。

さて次はいよいよ描画です。

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.plTortoise.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)));
}

とでもしておけばよいでしょう。これで左上の色を透明色として描画します。

ここまでできると、カメが左上でアニメーションするようになります。画面はこんな感じです。

VScheme8-4.gif (9267 バイト)

カメの動き

次はカメが定点へ移動する機能をつけてみましょう。

一般に、「モノの動き」を実現するコードは、概念の単純さの割にめんどうなことが多いので、私は今この「動き」そのものをオブジェクト化することを試みています。

その際、継承なんぞを使ってしまっては再利用がほとんど利かないと言っても過言ではないので、オブジェクトコンポジションを使って実現しようと考えています。

現在の研究過程が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);
    }
}

とコードを追加しておきましょう。NotifiedByvafxgame::IActorで定義されている通知用の仮想関数で、相手側でオーバーライドされていなければ無視されます。この場合は、相手が誰であるかは仮定しない(vafxgame::IActorの派生クラスである必要はありますが)で通知を行うことになります。すなわち、相手がvafxgame::IActorの派生クラスであれば、カメの移動命令を出していいということです。

Motionクラスのインターフェイスがなんだかあまりよくない気がしますが、いい手が思い付かなかったためです(何かいい手を思い付いたらぜひ教えてください)。

将来的にはこのオブジェクト化したモーションのインターフェイスを統一し、モーションオブジェクトの型を差し替えるだけで動きを変えられるようにできたらいいな、なんて考えています。多分そんなに難しくはないでしょう。

これが影の支配者、スクリプトランナー

ようやく本題のスクリプトランナーです。

このプロジェクトでは、スクリプトランナーの役目は

の2点だけですから、別段vafxgame::IActorから継承したクラスとしなくても構わないのですが、別に理由がないときは他のオブジェクトと対等にしておいたほうが楽なことが多いので、そのようにします。

ということで、ActorGen.plScriptRunnerというクラスを生成します。生成したら、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.cppVismのアーカイヴの展開後、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との結合

実際にVismScriptRunnerに組み込んでみましょう。

まず、vcontext型のオブジェクトをScriptRunnerのメンバ変数とします。これでvcontextScriptRunnerのライフタイムが同一になりますので、コンテキストの生成・削除のことは忘れてよくなります。

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関数を追加しておきましょう。GameMainTortoiseクラスを参照しているので、インクルードファイルを読み込んでおくのも忘れてはいけません。

スクリプトの実行

いよいよスクリプトの実行です。まずはイベント実行用のコードの組み込みです。これについては前回説明しましたね。

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);
}

こんな感じになります。

VScheme8-5.gif (9645 バイト)

まとめ

オブジェクト間の通知がややこしかったと思うので、最後にまとめておきましょう。時間軸をC++言語のフローで管理しないでイベントの連続の形で制御しているためにコード上では切り刻んであってややこしいだけで、基本的には入れ子構造になっています。

C/C++くらい速くてschemeくらい簡単にコンテキストスイッチを扱える言語があれば楽なんですが、当面そうもいかないようです。BeOSくらいならオブジェクトごとにスレッドを持つ、なんて芸当もできるのかも知れませんけど。


(C) 1999 Naoyuki Hirayama. All rights reserved.