ホーム
チュートリアル
第一回
第二回
第三回
第四回
第五回
第六回
拡張ソース
TrueType表示クラス
(チュートリアル第六回参照)
チュートリアル番外編
第一回
第二回
第三回
第四回
掲示板
メール
(-nospamを外してください)

チュートリアル

第五回 あれこれ動かしてみる

前回、キー入力に応じて3rdPersonビューでプレイヤーが移動するプログラムを作りました。
今回は、Irrlichtにおける「移動」というか、「挙動」というものについてもう少し突っ込んでみたいと思います。

挙動に関するえらい人

挙動担当、ISceneNodeAnimator

前回のプログラムは、プレイヤーを移動させるために、描画ループ中でプレイヤーの座標等を操作していました。
描画ループ中で移動処理を行うということは、非常にお手軽ですが、PCゲームというのは、どのような環境で実行されるかもわからず、そのためプログラムのFPSもまちまちになってしまいます。
そこで、PCゲームにおいては、挙動のコントロールと描画のコントロールは別途行うのが一般的です。
(対象的なのがコンシューマゲーム機やアーケード基盤で、これらは同じハードの下、同じ環境で実行されるのが保障されているので、描画ループと挙動処理を同一ループで行うのが普通です)

Irrlichtには、挙動を管理するためのクラス、ISceneNodeAnimatorというクラスが存在します。
今回は、これを使ってSydney姐さんから弾丸を出してみたり、オプションを出してみたりしてみようと思います。

看板娘登場

まずはSydney姐さんの周りにオプションを出してみましょう。
オプションには、IBillboardSceneNodeを使ってみることにしましょう。
IBillboardSceneNodeは、炎や爆発パターンなど、3D空間上で2Dの絵を表示するのに使われます。
まず、オプションのテクスチャデータを、Irrlichtのサンプルデータから拝借しましょう。
mediaディレクトリのportal1.bmp〜portal7.bmpをコピーしてきてください。
データの準備が整ったら、以下のコードを追加してください。

    SydneyNode->setMD2Animation(EMAT_STAND);                        // アニメーションパターン設定する

    // オプションをBillboardとして作り、Sydneyの子Nodeとする
    IBillboardSceneNode *OptionNode = Scene->addBillboardSceneNode(SydneyNode, core::dimension2d<f32>(20,20),vector3df(0,15,25));
    OptionNode->setMaterialFlag(video::EMF_LIGHTING, false);
    OptionNode->setMaterialTexture(0, Driver->getTexture("portal1.bmp"));
    OptionNode->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR);

    // 進行方向を取得するための空ノードを作る
    ISceneNode *TargetNode = Scene->addEmptySceneNode(SydneyNode);    // 空ノードを作る

まず、Sydneyの子Nodeとして、IBillboardSceneNodeを一つ作ります。
光源をOFFにして、テクスチャを貼り付けます。
そして、表面効果を半透明効果に設定します。

とりあえず実行してみましょう。Sydney姐さんの横に青い光が浮いていれば成功です。

組み込みアニメータを使う

昔ながらのテクスチャアニメーション

続いて、オプションをアニメーションさせてみましょう。いよいよISceneNodeAnimatorを使用します。

まずは以下のコードを追加してください。

    SydneyNode->setMD2Animation(EMAT_STAND);                        // アニメーションパターン設定する

    // オプションをBillboardとして作り、Sydneyの子Nodeとする
    IBillboardSceneNode *OptionNode = Scene->addBillboardSceneNode(SydneyNode, core::dimension2d<f32>(20,20),vector3df(0,15,25));
    OptionNode->setMaterialFlag(video::EMF_LIGHTING, false);
    OptionNode->setMaterialTexture(0, Driver->getTexture("portal1.bmp"));
    OptionNode->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR);

    // オプションをテクスチャ切替アニメーションさせる。
    array<ITexture *> textures;

    // createTextureAnimatorでオプションが明滅するテクスチャアニメータを作り、Nodeに割り当てる
    ISceneNodeAnimator *anim = Scene->createTextureAnimator(textures, 30);
    OptionNode->addAnimator(anim);
    anim->drop();

    // 進行方向を取得するための空ノードを作る
    ISceneNode *TargetNode = Scene->addEmptySceneNode(SydneyNode);    // 空ノードを作る

ISceneManagerには、いくつか定型パターンになるISceneNoneAnimatorを自動で作ってくれるサービスがいくつか存在します。createTextureAnimator()関数もその一つで、ITextureの入った動的配列(arrayクラス)を渡すと、指定された時間でそれらのテクスチャ切替を行ってくれます。
ISceneNodeAnimatorが出来上がったら、それをNodeに割り当てます。
続いて、今作ったISceneAnimatorをdrop()しています。何故?と思う人もいるんじゃないかと思いますが、drop()関数は、オブジェクトの参照カウンタを下げ、0になったら自身を解放する、というサービスです。
ISceneNodeAnimatorを含む、ほとんどのIrrlichtObjectは、IUnknownクラスから継承されていますが、このクラスは自身が作成された直後に参照カウンタを1にします。そして、addAnimator()関数は割り当てられたISceneNodeAnimatorの参照カウンタを内部で+1しています。割り当てた直後に、drop()関数で参照カウンタを下げておくことで、Nodeが解放されるときに、自動的に割り当てられたISceneNodeAnimatorを解放してくれるわけです。
よくわかんない、という人は、
ISceneNodeAnimatorは、作って最初の割り当てが終わったら一回だけdropしとけ
と覚えておいてください。

続いて、アニメーションさせるためのarrayクラスにITextureのポインタを挿入します。以下のコードを追加してください。

    SydneyNode->setMD2Animation(EMAT_STAND);                        // アニメーションパターン設定する

    // オプションをBillboardとして作り、Sydneyの子Nodeとする
    IBillboardSceneNode *OptionNode = Scene->addBillboardSceneNode(SydneyNode, core::dimension2d<f32>(20,20),vector3df(0,15,25));
    OptionNode->setMaterialFlag(video::EMF_LIGHTING, false);
    OptionNode->setMaterialTexture(0, Driver->getTexture("portal1.bmp"));
    OptionNode->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR);

    // TextureAnimatorのために、arrayを準備する
    array<ITexture *> textures;
    for (int i = 1; i < 8; i++)
    {
        char tmp[64];
        sprintf(tmp, "portal%d.bmp", i);
        video::ITexture* t = Driver->getTexture(tmp);
        textures.push_back(t);
    }
    // createTextureAnimatorでオプションが明滅するテクスチャアニメータを作り、Nodeに割り当てる
    ISceneNodeAnimator *anim = Scene->createTextureAnimator(textures, 30);
    OptionNode->addAnimator(anim);
    anim->drop();

    // 進行方向を取得するための空ノードを作る
    ISceneNode *TargetNode = Scene->addEmptySceneNode(SydneyNode);    // 空ノードを作る

これで実行してみましょう。Sydney姐さんの横の光が明滅するはずです。

いつもより余計に回(略)

続いて、Sydney姐さんの周りをオプションが回転するようにしましょう。
一定の座標を中心に回転をするISceneNodeAnimatorは、createFlyCircleAnimator()関数で作ることができます。
以下のコードを追加してください。

    // createTextureAnimatorでオプションが明滅するテクスチャアニメータを作り、Nodeに割り当てる
    ISceneNodeAnimator *anim = Scene->createTextureAnimator(textures, 30);
    OptionNode->addAnimator(anim);
    anim->drop();
    // createFlyCircleAnimatorで特定座標を中心に回転するアニメータを作り、Nodeに割り当てる
    anim = Scene->createFlyCircleAnimator(vector3df(0,15,0),25,0.003f);
    OptionNode->addAnimator(anim);
    anim->drop();

    // 進行方向を取得するための空ノードを作る
    ISceneNode *TargetNode = Scene->addEmptySceneNode(SydneyNode);    // 空ノードを作る

この例のように、Nodeには同時に複数のISceneNodeAnimatorを割り付けることが可能です。

これで実行してみましょう。Sydney姐さんの周囲をオプションが回るようになるはずです。

弾丸を出す前に

続いて、スペースキーを押したらSydney姐さんから弾丸が飛ぶようにしたいと思います。
下準備として、イベントレシーバをスペースキーに対応させましょう。
以下のコードを追加してください。

// 今回のプログラム用のイベントレシーバ
class MyEventReceiver : public IEventReceiver
{
protected:
    int Trigger;
public:
    int State;
    int Roll;
    int Fire;
    MyEventReceiver() : IEventReceiver()
    {
        Fire = Trigger = Roll = State = 0;
    }
    virtual bool OnEvent(SEvent event)
    {
        if (event.EventType == irr::EET_KEY_INPUT_EVENT)    // キー入力であれば
        {
            switch(event.KeyInput.Key)                            // キーの種類が
            {
                case    KEY_UP:                                        // 上矢印か
                case    KEY_DOWN:                                    // 下矢印なら
                    if (event.KeyInput.PressedDown){                    // 押下イベントなら
                        if (event.KeyInput.Key == KEY_DOWN){                // 下矢印なら
                            State = -1;                                            // Stateを-1に
                        } else {                                            // そうでないなら
                            State = 1;                                            // Stateを1に
                        }
                    } else {                                            // そうでないなら(離上イベントなら)
                        State = 0;                                            // Stateを0に
                    }
                    return true;
                case    KEY_LEFT:                                    // 左矢印か
                case    KEY_RIGHT:                                    // 右矢印なら
                    if (event.KeyInput.PressedDown){                    // 押下イベントなら
                        if (event.KeyInput.Key == KEY_LEFT){                // 左矢印なら
                            Roll = -1;                                            // Rollを-1に
                        } else {                                            // そうでないなら
                            Roll = 1;                                            // Rollを1に
                        }
                    } else {                                            // そうでないなら(離上イベントなら)
                        Roll = 0;                                                // Rollを0に
                    }
                    return true;
                case    KEY_SPACE:
                    if (event.KeyInput.PressedDown){                    // 押下イベントなら
                        if (Trigger == 0){
                            Trigger = 1;
                            Fire = 1;
                        }
                    } else {                                            // そうでないなら(離上イベントなら)
                        Trigger = 0;
                    }
                default:
                    return false;
            }
        }
        return false;
    }
};

これで、スペースが押された瞬間にFireメンバが1になるはずです。

今回、変数の初期化をこっそりInit()関数からコンストラクタに切り替えていますので、初期化時にInit()の呼び出しをはずしてください。

    MyEventReceiver Receiver;
    // Receiver.Init();     // この行削除
    // IrrlichtDeviceを確保して、IVideoDriver、ISceneManagerを取得する

弾丸発射

キー入力に対応できたところで、実際に弾丸を出してみましょう。
描画ループ中に以下の処理を記述してください。

        // 前進・後退
        if (Receiver.State){                                        // 前進もしくは後退であったら
            // 移動目標ノードとプレイヤーの現在位置から進行方向ベクトルを得る
            v = TargetNode->getAbsolutePosition() - SydneyNode->getPosition();
            // 進行方向ベクトルに移動速度と進行方向を乗算する
            SydneyNode->setPosition(SydneyNode->getPosition() + v * Receiver.State * 0.2f);
        }
        // 弾丸発射
        if (Receiver.Fire){
            Receiver.Fire = 0;
            vector3df pos = SydneyNode->getPosition();
            pos.Y += 15;
            ISceneNode *BuilletNode = Device->getSceneManager()->addBillboardSceneNode(0, core::dimension2d<f32>(10,10),pos);
            BuilletNode->setMaterialFlag(video::EMF_LIGHTING, false);
            BuilletNode->setMaterialTexture(0, Device->getVideoDriver()->getTexture("portal1.bmp"));
            BuilletNode->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR);
        }
        // カメラ位置の設定

とりあえず実行してみると、スペースを押すたびにSydney姐さんの腰位置あたりに弾丸が発生し、そのまま中に浮いているはずです。

この弾丸は発射すると消えることなく、そのまま宙に浮き続けます。この程度であればそう簡単にはメモリーオーバー等にはならないと思いますが、それでも何千、何万と出し続ければいつかはメモリーオーバーになると思いますから、一定時間後に消えるようにしましょう。以下のコードを追加してください。

            BuilletNode->setMaterialTexture(0, Device->getVideoDriver()->getTexture("portal1.bmp"));
            BuilletNode->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR);

            anim = Device->getSceneManager()->createDeleteAnimator(2000);
            BuilletNode->addAnimator(anim);
            anim->drop();
        }
        // カメラ位置の設定
        CameraNode->setPosition(CameraPosNode->getAbsolutePosition());

createDeleteAnimator()関数は与えられた時間後に自動的にそのノードを消滅させるアニメーターです。
実行してみると、宙に浮いた弾丸は約2000ms(2秒)後に消えるはずです。

では、次にこの弾丸を前に飛ばしてみましょう。以下のコードを追加してください。

        // 弾丸発射
        if (Receiver.Fire){
            Receiver.Fire = 0;
            vector3df pos = SydneyNode->getPosition();
            pos.Y += 15;
            ISceneNode *BuilletNode = Device->getSceneManager()->addBillboardSceneNode(0, core::dimension2d<f32>(10,10),pos);
            BuilletNode->setMaterialFlag(video::EMF_LIGHTING, false);
            BuilletNode->setMaterialTexture(0, Device->getVideoDriver()->getTexture("portal1.bmp"));
            BuilletNode->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR);

            vector3df pos2 = TargetNode->getAbsolutePosition() - SydneyNode->getPosition();
            pos2 *= 400;
            pos2 += pos;
            pos2.Y = pos.Y;
            ISceneNodeAnimator *anim = Device->getSceneManager()->createFlyStraightAnimator(pos,pos2,2000);
            BuilletNode->addAnimator(anim);
            anim->drop();

            anim = Device->getSceneManager()->createDeleteAnimator(2000);
            BuilletNode->addAnimator(anim);
            anim->drop();
        }

createFlyStraightAnimatorは、与えられた視点から終点へ定められた時間でまっすぐに移動するアニメーターです。
実行してみると、弾丸が前に飛ぶようになり、約2秒後に消えるはずです。

自前でアニメータを作ってみる

プレイヤー移動をアニメーター化

これまで、キー入力に対応したSydneyさんの移動は描画ループ中で行っていました。これもアニメータにしてしまいましょう。
アニメータにすることで
・FPSに左右されない移動速度になる。
・移動ロジックを部品化することで、後々の再利用性や、モジュールレベルでの差し替えが簡単になる。

といったメリットが得られます。まあ、よくわからないという人は
挙動に関する事柄はとりあえずアニメータにしとくといろいろとお得

とでも覚えておいてください。
というわけで、これまでの組み込みアニメータではない、オリジナルのアニメータを作りましょう。

まず、描画ループの中の移動に関する処理を全て抜き去ってください。

    // 描画ループ
    while(Device->run())                                            // IrrlichtDeviceが有効な間ループ
    {
        // この間ごっそり削除
        // カメラ位置の設定
        CameraNode->setPosition(CameraPosNode->getAbsolutePosition());
                                                                    // カメラ位置ノードの位置をカメラ位置に

それでは、これまでの仕様を踏襲したオリジナルのアニメータを作ります。
まずは以下のように外枠だけ作ってください。(特に強調をしていません)

// プレイヤー移動に関するオリジナルアニメータ
class MyPlayerAnimator : public ISceneNodeAnimator
{
};

続いて、コンストラクタを作りましょう。プレイヤー移動なのでコンストラクタ中でEventReceiverを受け取って設定しておくことにします。進行方向を得るノードもここで作ることにしてしまうので、初期化中のターゲットを作る処理を取り払っておきましょう。

    // createFlyCircleAnimatorで特定座標を中心に回転するアニメータを作り、Nodeに割り当てる
    anim = Scene->createFlyCircleAnimator(vector3df(0,15,0),25,0.003f);
    OptionNode->addAnimator(anim);
    anim->drop();

    // ↓ここから削除
    // 進行方向を取得するための空ノードを作る
    // ISceneNode *TargetNode = Scene->addEmptySceneNode(SydneyNode);    // 空ノードを作る
    // TargetNode->setPosition(vector3df(1,0,0));                        // プレイヤーの前方に配置
    // ↑ここまで

    // カメラ位置設定用の空ノードを作る
    ISceneNode *CameraPosNode = Scene->addEmptySceneNode(SydneyNode);
// プレイヤー移動に関するオリジナルアニメータ
class MyPlayerAnimator : public ISceneNodeAnimator
{
protected:
    IrrlichtDevice *Device;
    MyEventReceiver *Receiver;
    u32 lastTime;
    int lastState;
    IAnimatedMeshSceneNode *Player;
    ISceneNode *Target;
public:
    MyPlayerAnimator(IrrlichtDevice *device,MyEventReceiver *receiver,IAnimatedMeshSceneNode *node) : ISceneNodeAnimator()
    {
        Device = device;
        Receiver = receiver;
        lastTime = Device->getTimer()->getTime();
        lastState = Receiver->State;                            // プレイヤーの状態変化を初期化
        Player = node;
        // 進行方向を取得するための空ノードを作る
        Target = Device->getSceneManager()->addEmptySceneNode(Player);    // 空ノードを作る
        Target->setPosition(vector3df(1,0,0));                    // プレイヤーの前方に配置
    }
};

いよいよ移動ロジック本体を記述します。移動ロジックはISceneNodeAnimatorのメンバ関数AnimateNode()をオーバーライドすることで実現します。以下のコードを追加してください。基本的にこれまで描画ループ中に書かれていたのと同じロジックです。

// プレイヤー移動に関するオリジナルアニメータ
class MyPlayerAnimator : public ISceneNodeAnimator
{
protected:
    IrrlichtDevice *Device;
    MyEventReceiver *Receiver;
    u32 lastTime;
    int lastState;
    IAnimatedMeshSceneNode *Player;
    ISceneNode *Target;
public:
    MyPlayerAnimator(IrrlichtDevice *device,MyEventReceiver *receiver,IAnimatedMeshSceneNode *node) : ISceneNodeAnimator()
    {
        Device = device;
        Receiver = receiver;
        lastTime = Device->getTimer()->getTime();
        lastState = Receiver->State;                            // プレイヤーの状態変化を初期化
        Player = node;
        // 進行方向を取得するための空ノードを作る
        Target = Device->getSceneManager()->addEmptySceneNode(Player);    // 空ノードを作る
        Target->setPosition(vector3df(1,0,0));                    // プレイヤーの前方に配置
    }
    virtual void animateNode(ISceneNode *node,u32 timeMs)
    {
        u32 now = Device->getTimer()->getTime();
        s32 span = now - lastTime;
        lastTime = now;
        vector3df v;
        // アニメーションパターン設定
        if (lastState != Receiver->State){                            // 「立ち」と「走り」状態に変化があったら
            if (Receiver->State){                                    // 「走り」なら
                Player->setMD2Animation(EMAT_RUN);                    // アニメーションを「走り」に
            } else {                                                // そうでなければ
                Player->setMD2Animation(EMAT_STAND);                // アニメーションを「立ち」に
            }
            lastState = Receiver->State;                                // 状態変化を保存
        }
        // 方向転換
        if (Receiver->Roll){                                            // 方向変化があったなら
            v = Player->getRotation();                                    // プレイヤーの現在方向を得る
            v.Y += 0.08 * span * Receiver->Roll;                            // Y軸角を動かす
            Player->setRotation(v);                                        // プレイヤーの方向をセット
        }
        // 前進・後退
        if (Receiver->State){                                        // 前進もしくは後退であったら
            // 移動目標ノードとプレイヤーの現在位置から進行方向ベクトルを得る
            v = Target->getAbsolutePosition() - Player->getPosition();
            // 進行方向ベクトルに移動速度と進行方向を乗算する
            Player->setPosition(Player->getPosition() + (v * (Receiver->State * span * 0.06f)));
        }
        // 弾丸発射
        if (Receiver->Fire){
            Receiver->Fire = 0;
            vector3df pos = Player->getPosition();
            pos.Y += 15;
            ISceneNode *BuilletNode = Device->getSceneManager()->addBillboardSceneNode(0, core::dimension2d<f32>(10,10),pos);
            vector3df pos2 = Target->getAbsolutePosition() - Player->getPosition();
            pos2 *= 400;
            pos2 += pos;
            pos2.Y = pos.Y;
            BuilletNode->setMaterialFlag(video::EMF_LIGHTING, false);
            BuilletNode->setMaterialTexture(0, Device->getVideoDriver()->getTexture("portal1.bmp"));
            BuilletNode->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR);
            ISceneNodeAnimator *anim = Device->getSceneManager()->createFlyStraightAnimator(pos,pos2,2000);
            BuilletNode->addAnimator(anim);
            anim->drop();
            anim = Device->getSceneManager()->createDeleteAnimator(2000);
            BuilletNode->addAnimator(anim);
            anim->drop();
        }
    }
};

最後に、今作ったアニメータをSydneyさんのNodeに関連づけます。

    SydneyNode->setMD2Animation(EMAT_STAND);                        // アニメーションパターン設定する
    MyPlayerAnimator playeranim(Device,&Receiver,SydneyNode);
    SydneyNode->addAnimator(&playeranim);

    // オプションをBillboardとして作り、Sydneyの子Nodeとする
    IBillboardSceneNode *OptionNode = Scene->addBillboardSceneNode(SydneyNode, core::dimension2d<f32>(20,20),vector3df(0,15,25));

ここまで終わったらビルドして実行してみましょう。以前と違って、FPSがどう変わろうが移動速度は変わらないはずです。(スクリーンショットを取ってみましたが、見た目は全然変わらないので面白くないですねorz)

今回はここまでにしておきましょう、ここまでのソースをここに置いておきます。

トップページへ 第四回へ 第六回へ