102.移動する立方体

 ここでは、左右に移動する立方体の紹介です。
このサンプルはFullTutorial002というディレクトリに含まれます。
 BaseCrossDx11.slnというソリューションを開くとDx11版が起動します。

 リビルドして実行すると以下の画面が出てきます。1つの立方体が左右に移動するのがわかると思います。

 

図0102a

 


解説

 このサンプルでは、ゲームステージゲームオブジェクトを配置しています。
 ここで配置しているオブジェクトはプレートと呼ばれるゲーム盤立方体です。
 オブジェクトを配置する前に、リソースを登録します。リソースというのは、テクスチャやオーディオファイル、メッシュなどのメモリに負担かけそうなデータです。
 BaseCrossでは、実際に配置させるゲームオブジェクトとリソースを分けることで、オブジェクト間でのリソースの共有ができるようになってます。
 その記述は、シーンにあります。以下がシーン内のScene::CreateResourses()関数です。
void Scene::CreateResourses() {
    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);
}
 この項からはnamespace basecrossの記述は省略します。何も断らない限りnamespace basecross内にコードがあると思ってください。
 この関数Scene::CreateResourses()では、2つの画像ファイルをテクスチャとしてリソース化しています。
 まず、
    wstring DataDir;
    //サンプルのためアセットディレクトリを取得
    App::GetApp()->GetAssetsDirectory(DataDir);
 でデータディレクトリを取得します。これは、サンプルなのでテクスチャなどのリソースが重複するのを避けるために、サンプル共通のAssetsというディレクトリにテクスチャが入っているのでこう記述していますが、各ゲームを作成する際は、コメントにもあるように
    wstring DataDir;
    //各ゲームは以下のようにデータディレクトリを取得すべき
    App::GetApp()->GetDataDirectory(DataDir);
 と記述すべきです。こう記述するとGetDataDirectory()関数mediaディレクトリを返します。(mediaディレクトリについては後述します)
 App::~という記述はAppクラスのstatic関数呼び出しです。Appクラスというのは、このアプリケーションに1つしか存在しません。(シーンもそうですが)。
 シングルトンというパターンにデザインされていて、App::GetApp()でその唯一のポインタを取得できます。
 このポインタはunique_ptrになっているのでコピーすることができません
 なので、App::GetApp()->GetDataDirectory()のようにAppクラスのスタティックメンバ関数を呼び出す場合は続けて記述します。
 App::GetApp()->GetDataDirectory()のような感じです。
 GetDataDirectory()関数は引数にwstringの参照を取ります。データのディレクトリを取得するわけですが、ソリューションディレクトリの直下にあるmediaディレクトリがデータディレクトリです。
 このmediaディレクトリは、実行ファイルと同じディレクトリ直下にあればその中を参照します。開発中はデバッグモードリリースモードをしょっちゅう切り替えると思うのでソリューションの直下でも参照できるようになってます。
 mediaディレクトリShadersディレクトリにはコンパイルされたシェーダが入ります。(拡張子は.cso)
 Scene::CreateResourses()関数では、このデータディレクトリを取得して、その中のsky.jpgおよびtrace.pngテクスチャリソースとして登録します。
    wstring strTexture = DataDir + L"sky.jpg";
    App::GetApp()->RegisterTexture(L"SKY_TX", strTexture);
 リソースは、このようにキーワードを指定して登録します。上記の登録後は、意識的に登録から削除しない限りL"SKY_TX"というキーワードで参照できるようになります。
 リソースの登録シーンから行わなければいけないわけではありません。ステージで行うこともできます。
 App::GetApp()Appクラスのインスタンスを取得できる場所であれば、実行中はどこでも登録できます。

ゲームステージ

 オブジェクトを配置しているのはGameStage.h/cppになります。

プレートの作成

 まずプレートです。プレートは、いわばゲームの土台で、以下のコードで実装しています。GameStage.cppに記述があります。
//プレートの作成
void GameStage::CreatePlate() {
    //ステージへのゲームオブジェクトの追加
    auto Ptr = AddGameObject<GameObject>();
    auto PtrTrans = Ptr->GetComponent<Transform>();
    Quat Qt;
    Qt.rotationRollPitchYawFromVector(Vec3(XM_PIDIV2, 0, 0));
    PtrTrans->SetScale(200.0f, 200.0f, 1.0f);
    PtrTrans->SetQuaternion(Qt);
    PtrTrans->SetPosition(0.0f, 0.0f, 0.0f);

    //描画コンポーネントの追加
    auto DrawComp = Ptr->AddComponent<PNTStaticDraw>();
    //描画コンポーネントに形状(メッシュ)を設定
    DrawComp->SetMeshResource(L"DEFAULT_SQUARE");
    //自分に影が映りこむようにする
    DrawComp->SetOwnShadowActive(true);

    //描画コンポーネントテクスチャの設定
    DrawComp->SetTextureResource(L"SKY_TX");
}
 ステージに配置されるオブジェクトはGameObjectクラスか、もしくはGameObjectの派生クラスをあらかじめ作成してそれを配置します。
 通常は派生クラスを作成します。プレートについては派生クラスを作成しなくても作れるということのサンプルです。
 ここではまず
    auto Ptr = AddGameObject<GameObject>();
 という記述で、GameObjectクラスのインスタンスを作成し、ステージに登録します。
 AddGameObjec関数可変長のテンプレート関数で、GameStageクラスの親クラスであるStageクラスのメンバ関数です。
    AddGameObject<作成する型>(可変長引数);
 という書式になります。
 この記述により、指定の型のオブジェクトが作成され、そのオブジェクトのOnCreate仮想関数を呼び出し、インスタンスのポインタ(shared_ptr)が返されます。
 続く
    auto PtrTrans = Ptr->GetComponent<Transform>();
 は、AddGameObjectの戻り値(shared_ptr)を使用してTransformコンポーネントを取得します。
 BaseCrossコンポーネント方式を使用しています。コンポーネントというのは、そのオブジェクトが使用する機能をそれぞれのクラスごとに変えることができるものです。
 コンポーネントには大きく分けてUpdate系Draw系があります。Update系にはそのオブジェクトの動きや衝突判定などがあります。Draw系は描画方法です。主にシェーダに関連します。
 コンポーネントはAddComponent関数を使って、使用するコンポーネントを追加します。すでに追加されているコンポーネントはGetComponent関数で取得します。
 しかし、ここのTransformコンポーネントだけは特殊ですべてのゲームオブジェクトが保持しています。ですので追加しなくてもGetComponent関数で取得することができます。
 AddComponent関数およびGetComponent関数はどちらもテンプレート関数で以下のような書式です。
    AddComponent<作成する型>(可変長引数);
    GetComponent<見つける型>(見つからなかったときに例外が発生するかどうか);
 です。GetComponent関数例外が発生するかどうかはbool値で、trueかfalseかを指定します。ただこの引数はデフォルト化されているので何も記述しなければ、true(例外が発生する)になります。
 さてプレートのTransformを取得したら、
    Quat Qt;
    Qt.rotationRollPitchYawFromVector(Vec3(XM_PIDIV2, 0, 0));
    PtrTrans->SetScale(200.0f, 200.0f, 1.0f);
    PtrTrans->SetQuaternion(Qt);
    PtrTrans->SetPosition(0.0f, 0.0f, 0.0f);
 のように、スケーリング、回転、平行移動(位置)を指定します。回転はクオータニオンを使用していますが、上記のように回転ベクトルから作成できます。
 続いて、Draw系コンポーネントである、PNTStaticDrawコンポーネントを追加します。このコンポーネントはTransformのようにあらかじめ追加されてませんのでAddComponent関数を使って追加します。追加したら以下のような設定をします。
    //描画コンポーネントの追加
    auto DrawComp = Ptr->AddComponent<PNTStaticDraw>();
    //描画コンポーネントに形状(メッシュ)を設定
    DrawComp->SetMeshResource(L"DEFAULT_SQUARE");
    //自分に影が映りこむようにする
    DrawComp->SetOwnShadowActive(true);
    //描画コンポーネントテクスチャの設定
    DrawComp->SetTextureResource(L"SKY_TX");
 ここでは、形状(メッシュ)にL"DEFAULT_SQUARE"を使ってます。このDEFAULT_なんたらというキーワードは、あらかじめ登録されているリソースです。リソースはテクスチャリソースの登録はすでに説明しましたが、形状(メッシュ)の登録はまだ説明してません。しかし、ここではあらかじめ登録されている形状を使用します。登録されている形状は以下の通りです。
L"DEFAULT_SQUARE"           3D上の平面
L"DEFAULT_CUBE"             立方体
L"DEFAULT_SPHERE"           球体
L"DEFAULT_CAPSULE"          カプセル
L"DEFAULT_CYLINDER"         シリンダー(土管型)
L"DEFAULT_CONE"             コーン(円錐)
L"DEFAULT_TORUS"            トーラス(ドーナッツ)
L"DEFAULT_TETRAHEDRON"      正四面体
L"DEFAULT_OCTAHEDRON"       正八面体
L"DEFAULT_DODECAHEDRON"     正十二面体
L"DEFAULT_ICOSAHEDRON"      正二十四面体
 これらば頂点と法線とテクスチャUVを持つフォーマットです。
 このほかに、DEFAULT_PC_SQUARE、DEFAULT_PT_SQUARE、DEFAULT_PNTnT_SQUAREなどがあらかじめと登録されています。PCとついているのは頂点とカラーを持つフォーマットPTは頂点とテクスチャUV値を持つフォーマットです。PNTnTは頂点、法線、タンジェント、テクスチャUVを持ちます。タンジェント接ベクトル(接線)と呼ばれるもので、法線マップを適用するときに使います。
 なおメッシュは、自分で作成することもできます。
 これらのデフォルトのメッシュは、別にモデルを作成する必要がないオブジェクトの形状として使用できます。また、メモリ上はそれぞれ1個ずつしかありませんのでメモリ上の圧迫は非常に少なくて済みます(描画上のコストはもちろんかかりますが)。
 プレートはL"DEFAULT_SQUARE"(つまり3D上の平面)を使います。
 続いて、自分に影が映りこむようにする設定を行います。SetOwnShadowActive()関数です。影はシャドウマップ方式を使用しています。影は影を出すほう影を受けるほうを設定します。ここでプレートに設定しているのは影を受ける設定です。
 影を出すほうの設定は、立方体の作成のところで説明します。
 最後に描画コンポーネントへテクスチャの設定を行います。ここではシーンで登録したテクスチャをSetTextureResource()関数で設定します。
 ここまでで、プレートの作成は終わりです。実際にステージ上に配置するためにはこのGameStage::CreatePlate()関数を、 GameStage::OnCreate()関数で呼び出す必要があります。

立方体の作成

 立方体はプレートとは違って、新しいクラスを作成して実装します。プレートのように作成したら最後まで変わらないオブジェクトであればGameObjectクラスのインスタンスとして作成できますが、通常は動的に変化します。その場合はGameObjectの派生クラスを作成します。立方体クラスCharacter.h/cppに記述があります。

 まず、Character.hです。ここでは立方体クラスであるBoxクラスを宣言しています。以下がそのコードです。
class Box : public GameObject {
    Vec3 m_StartPos;
    float m_TotalTime;
public:
    //構築と破棄
    Box(const shared_ptr<Stage>& StagePtr, const Vec3& StartPos);
    virtual ~Box();
    //初期化
    virtual void OnCreate() override;
    //更新
    virtual void OnUpdate() override;
};
 GameObjectの派生クラスを作成するには1つだけ制約があります。それはコンストラクタの第1引数はconst shared_ptr<Stage>& StagePtrにしなければいけない、ということです。
 GameObjectの派生クラスGameObject同様、AddGameObjec()関数により、そのステージに配置されるわけですが、その中で、第1引数であるステージは、該当するステージが自分自身のポインタを渡します。そのようなメカニズムにより、GameObjectの派生クラスコンストラクタやOnCreate関数で、所属ステージにアクセスすることができるようになります。
 GameObjectの派生クラスとして新しいクラスを作成することで、OnCreate()仮想関数OnUpdate()仮想関数を記述することが可能になります。
 続いて、Character.cppです、ここでは宣言された関数の実体を記述します。
//構築と破棄
Box::Box(const shared_ptr<Stage>& StagePtr, const Vec3& StartPos) :
    GameObject(StagePtr),
    m_StartPos(StartPos),
    m_TotalTime(0)
{
}
Box::~Box() {}
//初期化
void Box::OnCreate() {
    auto PtrTrans = GetComponent<Transform>();
    PtrTrans->SetScale(Vec3(1.0f, 1.0f, 1.0f));
    Quat Qt;
    Qt.identity();
    PtrTrans->SetQuaternion(Qt);
    PtrTrans->SetPosition(m_StartPos);

    //影をつける
    auto ShadowPtr = AddComponent<Shadowmap>();
    ShadowPtr->SetMeshResource(L"DEFAULT_CUBE");

    //描画コンポーネント
    auto PtrDraw = AddComponent<PNTStaticDraw>();
    PtrDraw->SetMeshResource(L"DEFAULT_CUBE");
    PtrDraw->SetTextureResource(L"TRACE_TX");
    //透過処理
    SetAlphaActive(true);
}
//更新
void Box::OnUpdate() {
    //前回のターンからの経過時間を求める
    float ElapsedTime = App::GetApp()->GetElapsedTime();
    m_TotalTime += ElapsedTime;
    if (m_TotalTime >= XM_2PI) {
        m_TotalTime = 0;
    }
    auto Pos = GetComponent<Transform>()->GetPosition();
    Pos.x = sin(m_TotalTime);
    GetComponent<Transform>()->SetPosition(Pos);
}
 Character.cppには上記のように4つの関数の実体があります。
 まずコンストラクタですが、メンバイニシャライザで、各メンバ変数の初期化を行っています。
 この各メンバ変数の初期化は、必ず行うようにしましょう。行わない場合思わぬバグになることが多いです。
 デストラクタは空関数となっています。
 OnCreate()仮想関数では、プレートの構築時と同じように、コンポーネントの追加やそのパラメータ設定を行っています。
 ここで注意したいのは影の表示です。プレートでは影の反映を設定しましたが、ここでは影を出す設定をします。
    //影をつける
    auto ShadowPtr = AddComponent<Shadowmap>();
    ShadowPtr->SetMeshResource(L"DEFAULT_CUBE");
 のように、影を付けるためにはShadowmapコンポーネントを追加します。また、影を描画するための形状(メッシュ)も指定します。ここでは描画するのに使用しているL"DEFAULT_CUBE"(同じもの)を指定します。
 なぜ、影の形状を設定するのか疑問に思うかもしれません。このサンプルの場合は、描画も影も同じで問題ないのですが、例えば3D上にあるのに2Dの表現のようなキャラクターを実装する場合があります。この場合、影は丸影を使ったりもします。このように、本体と影を別に設定することができることで、いろんなカスタマイズが可能になります。
 BoxクラスにはBox::OnUpdate()関数が実装されています。この関数は1ターンに一度呼び出される関数で、このオブジェクトの変化を記述します。このサンプルでは、オブジェクトは左右に行ったり来たりするのでその処理を記述します。
 まず
    //前回のターンからの経過時間を求める
    float ElapsedTime = App::GetApp()->GetElapsedTime();
 で、ターン間の時間を取得します。通常は60分の1秒です。その前回のターンからの経過時間を使って、サインカーブをX方向にあてはめます。すると、なめらかな行き来が表現できます。

 このようにして宣言定義したBoxクラスを配置するには、ステージで行います。GameStage.cppの、GameStage::CreateBox()関数を参照してください。
    //ボックスの作成
    void GameStage::CreateBox() {
        AddGameObject<Box>(Vec3(0.0f, 0.5f, 0.0f));
    }
 このようにAddGameObject()関数を使って配置しています。パラメータは最初の位置ですのでVec3(0.0f, 0.5f, 0.0f)のように位置ベクトルを渡します。
 そして最後にGameStage::OnCreate()関数内で、CreateBox()関数を呼び出せば実装できます。

 このサンプルではゲームオブジェクトについて述べました。次項はもう少し複雑になります。