13.Draw系の操作とオーディオ

1304.マルチスレッド、連番アニメ、エフェクトにオーディオ

 このサンプルはFullSample304というディレクトリに含まれます。
 BaseCrossDx11.slnというソリューションを開くとDx11版が起動します。
 すると以下のような、お待ちくださいの画面が現れます。

 

図1304a

 少し経つと、以下の画面になります。

 

図1304b

 ここでBボタンを押すと、ゲーム画面になります。

 

図1304c

 ゲーム画面でBボタンを押すと前の画面に戻ります。
 このサンプルは、いろいろな機能が紹介されています。

マルチスレッドによるリソースの読み込み

 最初のお待ちくださいの画面はリソースを読み込むあいだ、アニメーションをする画面です。
 BaseCross64では、リソース(メッシュやテクスチャやサウンド)を読み込むタイミングを制御できます。
 サンプルではシーンで読み込み、複数のステージで使いまわしする場合にストレスがかからない方法をとってますが、場合によってはステージ単位でこれまでのリソースを開放し、また別ステージで読み込むなどの手法を取りたい場合があります。
 こんな時は、リソースの読み込みと開放を繰り返す必要があるのですが、その間、お待ちくださいなどのメッセージを表示すると親切ですね。
 このような画面お待ちくださいのステージを作るためには、GameStage.h/cppに記述される、WaitStageクラスのようなステージを作成します。以下は宣言部です。
class WaitStage : public Stage {
    //ビューの作成
    void CreateViewLight();
    //スプライトの作成
    void CreateTitleSprite();
    //リソースロード用のスレッド(スタティック関数)
    static void LoadResourceFunc();
    //リソースを読み込んだことを知らせるフラグ(スタティック変数)
    static bool m_Loaded;
    //ミューテックス
    static std::mutex mtx;
public:
    //構築と破棄
    WaitStage() :Stage() {}
    virtual ~WaitStage() {}
    //初期化
    virtual void OnCreate()override;
    //更新
    virtual void OnUpdate()override;
};
 赤い部分のように、
リソースロード用のスレッド関数
リソースが読み込み終わったことを知らせるフラグ
ミューテックス
 をスタティックな変数、関数として用意します。
 そして、WaitStage::OnCreate()は以下のように記述します。
void WaitStage::OnCreate() {
    wstring DataDir;
    //サンプルのためアセットディレクトリを取得
    App::GetApp()->GetAssetsDirectory(DataDir);
    //お待ちくださいのテクスチャのみここで登録
    wstring strTexture = DataDir + L"wait.png";
    App::GetApp()->RegisterTexture(L"WAIT_TX", strTexture);
    strTexture = DataDir + L"movetest.png";
    App::GetApp()->RegisterTexture(L"MOVETEST_TX", strTexture);
    //他のリソースを読み込むスレッドのスタート
    std::thread LoadThread(LoadResourceFunc);
    //終了までは待たない
    LoadThread.detach();

    CreateViewLight();
    //スプライトの作成
    CreateTitleSprite();
}
 このステージはリソースを読み込むステージですが、ここで表示されるお待ちくださいキャラクターのアニメーションのリソースは先に読み込まなければいけません。ですので、冒頭で読み込みます。その後
    //他のリソースを読み込むスレッドのスタート
    std::thread LoadThread(LoadResourceFunc);
    //終了までは待たない
    LoadThread.detach();
 という形で新しいスレッドを開始します。スレッドを作成する方法はいくつかありますが、ここではstd::threadというSTLのクラスを使います。このクラスは、コンストラクタに、新スレッドで動作させる関数(スタティックな関数)を渡すと、新スレッドを作成しその関数を実行してくれます。
 そのスレッドの終了はここでは待たないので、LoadThread.detach();としておきます。
 新スレッドと、メインスレッドの通信は、スタティックな変数であるm_Loadedを通じて行います。
 またミューテックスクラスもスタティッククラスなので実体を記述します  これらはGameStage.cppの231行目付近で以下のように初期化されます。
    bool WaitStage::m_Loaded = false;
    std::mutex WaitStage::mtx;
 新スレッドに割り当てられたスタティックな関数、WaitStage::LoadResourceFunc()は以下の様な記述です。
void WaitStage::LoadResourceFunc() {
    mtx.lock();
    m_Loaded = false;
    mtx.unlock();

    wstring DataDir;
    //サンプルのためアセットディレクトリを取得
    App::GetApp()->GetAssetsDirectory(DataDir);
    //各ゲームは以下のようにデータディレクトリを取得すべき
    //App::GetApp()->GetDataDirectory(DataDir);
    wstring strTexture = DataDir + L"sky.jpg";
    App::GetApp()->RegisterTexture(L"SKY_TX", strTexture);
    strTexture = DataDir + L"trace.png";
    App::GetApp()->RegisterTexture(L"TRACE_TX", strTexture);
    strTexture = DataDir + L"number.png";
    App::GetApp()->RegisterTexture(L"NUMBER_TX", strTexture);
    strTexture = DataDir + L"spark.png";
    App::GetApp()->RegisterTexture(L"SPARK_TX", strTexture);
    strTexture = DataDir + L"fire.png";
    App::GetApp()->RegisterTexture(L"FIRE_TX", strTexture);
    strTexture = DataDir + L"StageMessage.png";
    App::GetApp()->RegisterTexture(L"MESSAGE_TX", strTexture);
    //サウンド
    wstring CursorWav = DataDir + L"cursor.wav";
    App::GetApp()->RegisterWav(L"cursor", CursorWav);
    //BGM
    wstring strMusic = DataDir + L"nanika .wav";
    App::GetApp()->RegisterWav(L"Nanika", strMusic);

    mtx.lock();
    m_Loaded = true;
    mtx.unlock();

}
 赤くなっているところはミューテックスロックをかけています。m_Loadedに値を設定している間アクセスを禁止するためです。
 このように記述しておくと、WaitStage::OnUpdate()では以下のように記述できます。
void WaitStage::OnUpdate() {
    if (m_Loaded) {
        //リソースのロードが終了したらタイトルステージに移行
        PostEvent(0.0f, GetThis<ObjectInterface>(), App::GetApp()->GetScene<Scene>(), L"ToTitleStage");
    }
}
 また、リソースの説明の最後になりますが、メモリ制約などの関係で使わないリソースを開放する場合は
App::GetApp()->UnRegisterResource(L"リソース名")
 で開放することができます。

アニメーション

 WaitStageには2つの、待っている間のアニメーションするクラスが実装されています。点滅する文字と、右下のアニメーションです。点滅する文字はAnimeSpriteクラスで、右下のキャラクターはSerialAnimeSpriteクラスです。
 いずれもCharacter.h/cppに実装があります。

点滅アニメーション

 まず、AnimeSpriteクラスですが、OnCreate()関数は以下の様な記述になります。
void AnimeSprite::OnCreate() {
    float HelfSize = 0.5f;
    //頂点配列
    vector<VertexPositionColorTexture> vertex = {
        { VertexPositionColorTexture(Vec3(-HelfSize, HelfSize, 0),Col4(1.0f,1.0f,1.0f,1.0f), Vec2(0.0f, 0.0f)) },
        { VertexPositionColorTexture(Vec3(HelfSize, HelfSize, 0), Col4(1.0f, 1.0f, 1.0f, 1.0f), Vec2(1.0f, 0.0f)) },
        { VertexPositionColorTexture(Vec3(-HelfSize, -HelfSize, 0), Col4(1.0f, 1.0f, 1.0f, 1.0f), Vec2(0.0f, 1.0f)) },
        { VertexPositionColorTexture(Vec3(HelfSize, -HelfSize, 0), Col4(1.0f, 1.0f, 1.0f, 1.0f), Vec2(1.0f, 1.0f)) },
    };
    //インデックス配列
    vector<uint16_t> indices = { 0, 1, 2, 1, 3, 2 };
    SetAlphaActive(m_Trace);
    auto PtrTransform = GetComponent<Transform>();
    PtrTransform->SetScale(m_StartScale.x, m_StartScale.y, 1.0f);
    PtrTransform->SetRotation(0, 0, 0);
    PtrTransform->SetPosition(m_StartPos.x, m_StartPos.y, 0.0f);
    //頂点とインデックスを指定してスプライト作成
    auto PtrDraw = AddComponent<PCTSpriteDraw>(vertex, indices);
    PtrDraw->SetSamplerState(SamplerState::LinearWrap);
    PtrDraw->SetTextureResource(m_TextureKey);
}
 このようにVertexPositionColorTextureの頂点の配列を作り、PCTSpriteDrawコンポーネントに渡します。
 更新処理は以下のようになります。
void AnimeSprite::OnUpdate() {
    float ElapsedTime = App::GetApp()->GetElapsedTime();
    m_TotalTime += ElapsedTime * 5.0f;
    if (m_TotalTime >= XM_2PI) {
        m_TotalTime = 0;
    }
    auto PtrDraw = GetComponent<PCTSpriteDraw>();
    Col4 col(1.0, 1.0, 1.0, 1.0);
    col.w = sin(m_TotalTime) * 0.5f + 0.5f;
    PtrDraw->SetDiffuse(col);
}
 このように三角関数のサイン(sin)を使って、Diffuseのアルファ値を変化させ、点滅させています

連番アニメーション

 右下のキャラクターのアニメは連番アニメーションです。
 テクスチャ全体では以下の様な画像です。

 

図1304d

 このテクスチャを3×2に分割し、それを順番に表示すると連番アニメーションが出来上がります。
 まずSerialAnimeSprite::OnCreate()関数ですが、これは、AnimeSprite::OnCreate()とほぼ変わりません。
 ただ、一つ注意したいのは、頂点の変更をしたいので、m_BackupVerticesという頂点の配列を作成してそれはとっておくということです。
 重要なのはSerialAnimeSprite::OnUpdate()関数です
void SerialAnimeSprite::OnUpdate() {
    float ElapsedTime = App::GetApp()->GetElapsedTime();
    m_TotalTime += ElapsedTime;
    if (m_TotalTime >= m_AnimeTime) {
        m_PieceIndex++;
        if (m_PieceIndex >= m_PieceXCount * m_PieceYCount) {
            m_PieceIndex = 0;
        }
        m_TotalTime = 0;
    }
    vector<VertexPositionColorTexture> newVertices;
    uint32_t pieceX = m_PieceIndex % m_PieceXCount;
    uint32_t pieceY = m_PieceIndex / m_PieceXCount;
    float pieceWidth = 1.0f / (float)m_PieceXCount;
    float pieceHeight = 1.0f / (float)m_PieceYCount;

    float pieceStartX = (float)pieceX * pieceWidth;
    float pieceEndX = pieceStartX + pieceWidth;

    float pieceStartY = (float)pieceY * pieceHeight;
    float pieceEndY = pieceStartY + pieceHeight;

    for (size_t i = 0; i < m_BackupVertices.size(); i++) {
        Vec2 uv = m_BackupVertices[i].textureCoordinate;
        switch (i) {
        case 0:
            uv.x = pieceStartX;
            uv.y = pieceStartY;
            break;
        case 1:
            uv.x = pieceEndX;
            uv.y = pieceStartY;
            break;
        case 2:
            uv.x = pieceStartX;
            uv.y = pieceEndY;
            break;
        case 3:
            uv.x = pieceEndX;
            uv.y = pieceEndY;
            break;
        }
        auto v = VertexPositionColorTexture(
            m_BackupVertices[i].position,
            m_BackupVertices[i].color,
            uv
        );
        newVertices.push_back(v);
    }
    auto ptrDraw = GetComponent<PCTSpriteDraw>();
    ptrDraw->UpdateVertices(newVertices);
}
 赤くなっているところに注目してください。
 これは、今、表示しようとしているピース(片)の番号から、そのテクスチャ上の位置を計算しているところです。
 連番アニメーションは通常、左上から右下にアニメーションします。すると各片の位置は
    uint32_t pieceX = m_PieceIndex % m_PieceXCount; //Xの位置
    uint32_t pieceY = m_PieceIndex / m_PieceXCount; //Yの位置
 と計算できます。C/C++の文法により、整数型(ここではunsignedですが)の割り算は切り捨て%演算子は剰余(余り)を計算するので、このような形で片の位置出せます。
 また、ここからUV値開始位置、終了位置を計算します。
 そこまで出せたらnewVerticesm_BackupVerticesの内容の中で、UV値だけを変化させます。
 これは先ほど出した開始位置、終了位置から算出します。

 このSerialAnimeSpriteクラスは汎用的にできていて、テクスチャやXY方向の片の数、1辺の表示時間などを設定すれば、別のテクスチャでも連番アニメーションを作成できます。
 またこのアニメーションはゲームステージにも、設置してあります。

エフェクト

 このサンプルではエフェクトを表現しています。
 プレイヤーを動かすとほかのサンプルでも出てくる追いかけるオブジェクトが追いかけてきますが、その際、上図のように炎のようなエフェクトを送出します。プレイヤーがジャンプするとエフェクトが出る頻度が上がります。
 またプレイヤーもジャンプすると赤いエフェクトを送出します。
 エフェクトを表現するには、まずエフェクトの種のような画像を用意します。このサンプルで使用しているのはAssetsディレクトリにあるspark.pngとfire.pngです。
 これらを、Scene.cppなどで、あらかじめリソース登録しておきます。その上で、エフェクトのクラスを作成します。
 クラスはMultiParticleクラスを継承して作成します。
 以下は追いかけるオブジェクトが送出するMultiFireクラスの初期化です。
//初期化
void MultiFire::OnCreate() {
    //加算描画処理をする
    SetAddType(true);
}
 このようにほとんど何もしていません。描画に加算処理するかを設定しています。もう一つのエフェクトのMultiSparkに至ってはほんとに何もしてません。OnCreate()関数を多重定義しているだけです。
 実際のエフェクト送出処理はMultiFire::InsertFire()関数などを作成して記述します。この関数は多重定義するものではないので、自由に関数名を付けて構いません。また、引数にエフェクトの発射位置(エミッターといいうます)を付けておきます。
 そのうえで以下のように記述します。
void MultiFire::InsertFire(const Vec3& Pos) {
    auto ptrParticle = InsertParticle(4);
    ptrParticle->SetEmitterPos(Pos);
    ptrParticle->SetTextureResource(L"FIRE_TX");
    ptrParticle->SetMaxTime(0.5f);
    for (auto& rParticleSprite : ptrParticle->GetParticleSpriteVec()) {
        rParticleSprite.m_LocalPos.x = Util::RandZeroToOne() * 0.1f - 0.05f;
        rParticleSprite.m_LocalPos.y = Util::RandZeroToOne() * 0.1f;
        rParticleSprite.m_LocalPos.z = Util::RandZeroToOne() * 0.1f - 0.05f;
        //各パーティクルの移動速度を指定
        rParticleSprite.m_Velocity = Vec3(
            rParticleSprite.m_LocalPos.x * 5.0f,
            rParticleSprite.m_LocalPos.y * 5.0f,
            rParticleSprite.m_LocalPos.z * 5.0f
        );
        //色の指定
        rParticleSprite.m_Color = Col4(1.0f, 1.0f, 1.0f, 1.0f);
    }
}
 最初の
    auto ptrParticle = InsertParticle(4);
 によって4つのパーティクルスプライトを持つパーティクルが作成されます。親クラスであるMultiParticleというのは複数のパーティクルを持つクラスという意味です。
 各パーティクルは複数のParticleSpriteを持つことができます。
 ここで指定している4つのParticleSpriteという意味です。
 パーティクルが作成されたらそのポインタが返りますので
    ptrParticle->SetEmitterPos(Pos);
    ptrParticle->SetTextureResource(L"FIRE_TX");
    ptrParticle->SetMaxTime(0.5f);
 のように、エミッター、使用するテクスチャ、生存時間を指定します。
 続いて、各パーティクルスプライトの設定に移ります。
    for (auto& rParticleSprite : ptrParticle->GetParticleSpriteVec()) {
        rParticleSprite.m_LocalPos.x = Util::RandZeroToOne() * 0.1f - 0.05f;
        rParticleSprite.m_LocalPos.y = Util::RandZeroToOne() * 0.1f;
        rParticleSprite.m_LocalPos.z = Util::RandZeroToOne() * 0.1f - 0.05f;
        //各パーティクルの移動速度を指定
        rParticleSprite.m_Velocity = Vec3(
            rParticleSprite.m_LocalPos.x * 5.0f,
            rParticleSprite.m_LocalPos.y * 5.0f,
            rParticleSprite.m_LocalPos.z * 5.0f
        );
        //色の指定
        rParticleSprite.m_Color = Col4(1.0f, 1.0f, 1.0f, 1.0f);
    }
 で、各スプライトに対して、初期のローカルポジション(エミッターから見たローカル位置です)、速度、色を設定します。

 このようにパーティクルを発射するという関数を記述したら、このクラスをステージ上に配置します。以下は、GameStage::MultiFire()関数です。
//炎の作成
void GameStage::CreateFire() {
    auto MultiFirePtr = AddGameObject<MultiFire>();
    //共有オブジェクトに炎を登録
    SetSharedGameObject(L"MultiFire", MultiFirePtr);
}
 ここで、SetSharedGameObject()関数を使って共有オブジェクトにしておきます。
 実際に発射するためには、例えばMultiFireは、追いかけるオブジェクトが何かと衝突した時に発射されますが、それはSeekObject::OnCollisionEnter()関数に記述します。
void SeekObject::OnCollisionEnter(shared_ptr<GameObject>& Other) {
    //ファイアの放出
    auto ptriFire = GetStage()->GetSharedGameObject<MultiFire>(L"MultiFire", false);
    if (ptriFire) {
        ptriFire->InsertFire(GetComponent<Transform>()->GetPosition());
    }
}

エフェクトの更新を制御する

 上記のMultiFireのように初期値のままの表現で良ければ更新処理は書かなくていいですが、更新時に変化させたい場合はOnUpdate()関数を多重定義します。以下は、プレイヤーが放出するエフェクトのMultiSparkクラスのOnUpdate()関数です。
void MultiSpark::OnUpdate() {
    for (auto ptrParticle : GetParticleVec()) {
        for (auto& rParticleSprite : ptrParticle->GetParticleSpriteVec()) {
            if (rParticleSprite.m_Active) {
                rParticleSprite.m_Color.z += 0.05f;
                if (rParticleSprite.m_Color.z >= 1.0f) {
                    rParticleSprite.m_Color.z = 1.0f;
                }
            }
        }
    }
    //親クラスのOnUpdate()を呼ぶ
    MultiParticle::OnUpdate();
}
 ここでは、各パーティクルスプライトの色を、動的に変更しています。
 また更新処理が終わったら
    //親クラスのOnUpdate()を呼ぶ
    MultiParticle::OnUpdate();
 と親クラスのOnUpdate()を呼び出すのを忘れないでおきましょう。MultiParticleのOnUpdateでは各パーティクルスプライトの速度や色設定をもとに描画に渡す情報を作り出します。

オーディオ

 このサンプルではオーディオが実装されています。
 BaseCross64ではバックミュージックをミュージック、効果音をサウンドと称します。
 オーディオを実装するには事前にリソース登録します。このサンプルでは、前述した、WaitStage::LoadResourceFunc()スレッド関数で登録しています。
    void WaitStage::LoadResourceFunc() {
        wstring dataDir;
        //サンプルのためアセットディレクトリを取得
        App::GetApp()->GetAssetsDirectory(dataDir);

//中略
        //以下オーディオ
        //サウンド
        wstring CursorWav = dataDir + L"cursor.wav";
        App::GetApp()->RegisterWav(L"cursor", CursorWav);

        //ミュージック
        wstring strMusic = dataDir + L"nanika .wav";
        App::GetApp()->RegisterWav(L"Nanika", strMusic);

    }

ミュージック

 ミュージックの再生は、GameStage::PlayBGM()関数などで以下のように記述します。
    void GameStage::PlayBGM() {
        auto XAPtr = App::GetApp()->GetXAudio2Manager();
        m_BGM = XAPtr->Start(L"Nanika", XAUDIO2_LOOP_INFINITE, 0.1f);
    }
 この際、XAUDIO2_LOOP_INFINITEは繰り返し、0.1fはボリュームです。例えば2回繰り返し再生を終了する場合はXAUDIO2_LOOP_INFINITEの部分を1と設定します。ここには繰り返す回数を設定するので、1回目はカウントしないで指定します。
 ここではGameStageで再生してますが、シーンで再生させることもできます。あるいは、例えばタイトルステージとゲームステージでミュージックが変わる場合も多いと思いますが、そんな時は、それぞれのステージのOnCreate()関数で再生開始します。
 オーディオ関連で気を付けたいのは終了処理です。再生中にゲームを中断した時など、エラーが出ることがあります。
 これを回避するために、再生を開始したオブジェクト(ここではGameStage)のOnDestroy()関数を多重定義し以下のように記述します。
void GameStage::OnDestroy() {
    //BGMのストップ
    auto XAPtr = App::GetApp()->GetXAudio2Manager();
    XAPtr->Stop(m_BGM);
}

サウンド

 このサンプルのサウンド(効果音)は、プレイヤーがジャンプした時にエフェクトと同時に鳴ります。
 サウンドのデータもあらかじめリソース化しておきます。
 このサンプルではL"cursor"という名前でリソース登録があります。その上で、プレイヤーがジャンプするタイミングで
//Aボタン
void Player::OnPushA() {
    auto grav = GetComponent<Gravity>();
    grav->StartJump(Vec3(0,4.0f,0));
    //スパークの放出
    auto PtrSpark = GetStage()->GetSharedGameObject<MultiSpark>(L"MultiSpark", false);
    if (PtrSpark) {
        PtrSpark->InsertSpark(GetComponent<Transform>()->GetPosition());
    }
    //サウンドの再生
    auto ptrXA = App::GetApp()->GetXAudio2Manager();
    ptrXA->Start(L"cursor", 0, 0.5f);
}
 赤くなってる部分のように記述します。繰り返さない(1度だけ再生)ので、繰り返し回数は0、ボリュームは0.5と指定しています。