18.アプリケーションクラス実装

ここまで、2つのコミット間を説明することで、「開発の流れ」を説明してきました。
記事を順を追って読んできた人には、なかなかDx12やDx11のデバイスや頂点バッファ、あるいはシェーダが出てこないことにやきもきしていた人も多いかと思います。
これは、設計の仕方、あるいは作り方の問題かと思いますが、僕は、ゲームのフレームワークに限らず、なにかのライブラリを作るときに、「部品から」作るようにしています。あるいは「道具から」といういい方のほうがあってるかもしれません。
これまで説明してきた、「インターフェイス」「ファクトリー」「計算ライブラリ」「頂点作成関数」などは、部品というよりは「道具」に近い感じです。これらの道具を使って、ではどんなフレームワークにするのか、は今後の話になります。

ここまでは「道具」ですから、1つ1つが比較的独立しています。しかし、これからはフレームワークの設計と密接に絡んでくるものばかりです。
そこで今回からはこれまで追いかけてきた「あるコミットから次のコミットへ」という流れをやめ、前回終了時のコミットから、現時点での最新のコミット「weak_ptrからshared_ptr取得方法の修正」までを、縦割りで説明したいと思います。実質は「Dx11,Dx12フルバージョン、ライブラリ及びテストサンプル追加」というコミットになります。その後のコミットは微調整です。
その中でBaseCrossの全体の設計、そしてDx12、Dx11両デバイスの扱いを解説します。

この記事は、
コミット「Dx11、Dx12両頂点定義とプリミティブメッシュ作成の追加」
から、
コミット「weak_ptrからshared_ptr取得方法の修正」
の間の作業です。
GitHubサイト

https://github.com/WiZFramework/BaseCross

を参照して下さい。
その間にいくつかのコミットがありますが、後ろのコミットを開いていただいてそれと合わせ読んでいただくとわかりやすいと思います。

現時点での最新のコミットの実行結果は以下のようになります。「シンプルバージョンDx12」「シンプルバージョンDx11」「フルバージョンDx12」「フルバージョンDx11」とも同じです。しかし実装方法が違います。

002

ゲームエンジンのサンプルや一般的なDirectX解説ブログなどでは、通常、Dx12デバイス、Dx11デバイスなどの解説がまず真っ先に来ます。
読者が一番知りたい部分かもしれません。しかしこのブログではその方法をとらず、退屈かもしれない「道具」について説明してきました。
ゲームにおけるDx12、Dx11デバイス(OpebGLであっても)は、大変大きな存在です。その仕様や実装方法は一番興味をそそる部分かもしれません。しかし、クロスプラットフォームで使えるように考えていくと、そういうデバイスの違いをどのように吸収していくのか、が大きな目標になります。
そのためプラットフォームを超えて使える「道具」は多ければ多いほど「使いやすく」なります。
また、全体の設計も大きな存在です。これを「プラットフォームごとに」違う設計にするくらいなら、それはすでに「クロス」ではありません。ですから「設計段階」では、Dx12、Dx11デバイスは単なるキャンバスです。そのキャンバスの準備をする、キャンバスにに描画する、描画の後処理をする、などの共通で使える、同名のインターフェイス関数を定義し、実際の中身は、プラットフォームごとに違う処理をするようになります。
そういう意味でも「全体の設計」というのは重要になります。

それではまず、設計の話から始めます。コミット「weak_ptrからshared_ptr取得方法の修正」にはフルバージョンのサンプルも実装されていますが、まずは「シンプルバージョン」の話です。
「SimplSampleTestディレクトリ」の「BaseCrossDx12.sln」(Dx12版)もしくは「BaseCrossDx11.sln」(Dx11版)を開いて以下の文章をよむとわかりやすいでしょう。

BaseCrossの設計
まず最初に「BaseCross全体の設計」を説明します。以下の図を見てください。

003

この図には、BaseCrossには大きくどのようなクラスがあって。クラス同士はどのように絡んでいるかを説明したものです。意外に単純なのがわかります。(単純な割には前置きが長い!)
ここに記述されているクラスの説明を行います。

アプリケーションクラス
上図の「Appクラス」がアプリケーションクラスです。「DxLib」の「App.h/cpp」に記述があります。このクラスは、「デバイスリソース」、「シーンインターフェイス」のポインタと「リソースのマップ」を保持します。
このクラスは「シングルトン」としてインスタンス化されます。static関数「App::CreateApp関数」を呼び出すことでインスタンスが構築されます。「App::CreateApp関数」を2回以上呼び出しても、2回目以降は最初に作成したポインタが返るだけです。
構築された後は、同じくstatic関数「App::GetApp関数」によりAppクラスのポインタを取得できます。このポインタは、unique_ptrで、他にコピーしたり移動したりはできません。構築は通常「WinMain.cpp」で行います。
コンストラクタとデストラクタは直接呼び出すことはできません。もし強制的に破棄したい場合は「App::DeleteApp関数」を呼び出します。「App::DeleteApp関数」を呼び出した後であれば、もう一度「App::CreateApp関数」が意味を持ちますが、あまりやらないと思います。
「App::CreateApp関数」内では、まず、メディア(テクスチャやシェーダやWAVファイルなど)を保存するディレクトリを設定します。「media」という名前のディレクトリです。優先的には、exeファイル(実行ファイル)がある場所の直下です。しかし、そこに「mediaディレクトリ」がなければ、その一つ上の階層の「mediaディレクトリ」の存在を確認します。そこにもなければ例外を送出します。
このように、「mediaディレクトリ」を探す理由は、ゲーム制作中は「デバッグモード」「リリースモード」が変化します。そのため、exeファイルを基準にすると、同じ「mediaディレクトリ」を参照できません。そのため、「デバッグモード」「リリースモード」両方の実行ファイルから同じようにアクセスできるように相対ディレクトリで管理します。

「mediaディレクトリ」の設定が終了したら「App::CreateApp関数」は「デバイスリソース(DeviceResourceクラス)」を構築します。これはDx11とDx12ではクラス名は同じ(DeviceResourceクラス)ですが、内容は全く違います。
また、Appクラスは、リソースの管理を行います。リソースとは「使いまわしできるメッシュ、テクスチャ、オーディオ(ほかにも作れる)」です。リソースは一回読み込むと、ゲームが終了するまでオブジェクトを保持するので、2回目以降のオブジェクトへのアクセスが速くなります。リソースは手動で解放することができます。解放した時は再読み込みには時間がかかります。

シーンクラス
Appクラスを構築する「WinMain.cpp内MainLoop関数」では、App構築後、Appクラスに「シーンを作成」するよう

App::GetApp()->CreateScene<Scene>();

を実行します。これは、テンプレート関数になっていて、Appクラスのインスタンスに、「シーン」を作らせる命令です
「DxLib」内「BaseHelper.h」に「SceneInterfaceクラス」があります。これはAppクラスが保持する「シーンインターフェイスのポインタ」です。
実際にゲームで実装する場合は「SceneInterfaceクラスの派生クラス」として作成します。サンプルでは「Scene」というクラス名です。
この名前は自由につけられます。「GameScene」でもいいし「Game」でも構いません。その場合は、

App::GetApp()->CreateScene<GameScene>();

となります。(もちろん、GameSceneクラスが宣言定義されていなければなりません。)
このクラスは「ゲーム盤」というか「ゲームそのもの」です。

デバイスクラス
Appクラスが保持する「DeviceResourcesクラス」がデバイスクラスです。「Dx12版」も「Dx11版」も同じ名前です。「App::CreateApp関数」によって構築されます。
「DeviceResourcesクラス」の構築は、DirectXデバイスを作成し、スワップチェーン、レンダリングターゲット、デプスステンシルビューなど、描画に必要な準備処理をすべて行います。
この処理は多岐にわたり、またDx12とDx11では全く違うため、後で個別に詳しく説明します、ここでは、「デバイスの初期化」が行われることだけわかれば結構です。
「App::CreateApp関数」は構築が成功すると、「WinMain.cpp内MainLoop関数」に制御が戻ります。そのあとで、上記のシーンの作成が行われます。

メッセージループ
WinMain.cppの説明のところでも触れましたが、Appクラスとシーンの構築が終わると、「メッセージループ」に入ります。ここではWindowsメッセージがあればその処理をして、なければ

	//ウインドウメッセージがなければ更新描画処理
	App::GetApp()->UpdateDraw(1);

を呼び出します。つまり、「WM_QUITE」が発行されるまで、「PeekMessage」が呼ばれ、ウインドウメッセージがなければ上記関数が呼ばれます。これが、ゲームのターンです。
「App::GetApp()->UpdateDraw関数」の中では、先ほど構築した「シーン(SceneInterfaceの派生クラス)」の「OnUpdate仮想関数」や「OnDraw仮想関数」を呼び出します。そして最後にフロントバッファに転送する命令を発行します。
以下は「UpdateDraw関数」の実体です。

void App::UpdateDraw(unsigned int SyncInterval) {
	if (!m_SceneInterface) {
		//シーンがが無効なら
		throw BaseException(
			L"シーンがありません",
			L"if(!m_SceneInterface)",
			L"App::UpdateDraw()"
		);
	}

	// シーン オブジェクトを更新します。
	m_InputDevice.ResetControlerState();
	m_Timer.Tick([&]()
	{
		m_SceneInterface->OnUpdate();
	});
	// 初回更新前にレンダリングは行わない。
	if (GetFrameCount() == 0)
	{
		return;
	}
	m_SceneInterface->OnDraw();
	// バックバッファからフロントバッファに転送
	m_DeviceResources->Present(SyncInterval,0);
}

このように、Appクラスではタイマーも保持していて、各ターン毎に「Tick関数」を呼び出しています。この関数はラムダ式になっていて、ラムダ式の中でシーンの「OnUpdate関数」を呼び出します。
その後、1回目のターンであれば描画は行いません。これは最初のターン時は、各オブジェクトが初期位置に収まってない場合もあるからです。2回目のターン以降であれば、シーンの「OnDraw関数」を呼び出し、シーンに描画させます。
そして最後に「m_DeviceResources->Present(SyncInterval,0)」でバックバッファからフロントバッファに転送します。

ゲーム側の描画処理
じつはこの中には、各ターンごとの画面のクリア処理は含まれません。この処理は、各ゲーム側にゆだねられます。
メインプロジェクト内の、「Scene.cpp」の「Scene::OnDraw関数」を見てみましょう。

void Scene::OnDraw() {
	//描画デバイスの取得
	auto Dev = App::GetApp()->GetDeviceResources();
	Dev->ClearDefultViews(Color4(0, 0, 0, 1.0));
	//デフォルト描画の開始
	Dev->StartDefultDraw();
	m_NormalTextureBox->OnDraw();
	//デフォルト描画の終了
	Dev->EndDefultDraw();
}

このようにAppクラスからデバイスリソースを取得し、描画開始、そして描画の終了を呼び出しています。オブジェクト(立方体)の描画はその中に挟み込んでします(m_NormalTextureBox->OnDraw()呼び出し)。
シンプルバージョンの場合、フレームワークで行うのはデバイスの最低限の準備だけで、実際の描画はゲーム側で行う必要があります。

しかし、「フルバージョン」のソリューションを開いてみましょう。フルバージョンは「FullSampleTest」内にあります。この中の「BaseCrossDx12.sln」もしくは「BaseCrossDx11.sln」を開きます。
メインプロジェクトに「Scene.cpp」はありますが、その中のどこを見ても「Scene::OnDraw関数」は見当たりません。
つまり「フルバージョン」は「ゲーム側は描画処理をしなくても済むように」設計してあります。そのかわり、フルバージョンの「Sceneクラス」は「SceneInterfaceの派生クラス」ではなく「SceneBaseクラス」の派生クラスです。この「SceneBaseクラス」はフルバージョンの場合、フレームワーク側に位置します。
これで「シンプルバージョン」と「フルバージョン」の違いの本質が見えてきたと思います。「フルバージョン」は描画処理ばかりでなく、コンポーネントやゲームステージなど、サービス的な内容も含まれますが、一番大きな違いは「描画処理をゲーム側で行う必要があるかないか」です。
ですから、シンプルバージョンの場合「クロスプラットフォーム」とは言いますが、実際には描画処理はDx11版、Dx12版、別に書かなければいけないのであまり「クロス」の恩恵は、あまり受けません。

さて、この項では、Appクラスとそれをとりまく「デバイスリソース」「シーンクラス」などの話をしました。
「シンプルバージョン」でゲームを作成するには、この先は、「デバイス」の知識が欠かせません。

次項からしばらくは「Dx12デバイス」の話が続きます。興味ある方はお付き合いください。

カテゴリー

ピックアップ記事

  1. 2016092201
    今回は前回のサンプルを少し機能を追加しまして、いろんなオブジェクトを追加しています。FullTuto…
  2. 2016092001
    前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル003をアップしました。内…
  3. eyecatch
    前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル002で懸案となっていまし…
PAGE TOP