SPARKCREATIVE Tech Blog

https://www.spark-creative.jp/

ゲームエンジン制作の話 01 ウインドウの生成とDirectX11デバイスの初期化

こんにちは SPARK/SPARKCREATIVE CTO 広本です。

SPARK社ではSPARKGEARというミドルウェアを開発販売しており、多数のゲームに導入いただいています。
また子会社のSPARKCREATIVE社ではゲームの開発運営やデザインリソースの受託などを行っています。

今回、弊社の新人諸君も低レベルなAPIと仲良くなってもらおうという気持ちもあり、今回ゲームエンジン制作という題材でブログに残していこうかと思います。
またゲームエンジンといってもUnityやUE4のような大規模なものではなくちょっとしたゲームが作れるよね程度の規模+広く浅く色々な事に触れてみるといった感じで開発していきます。

ゲームエンジンにした理由

昨今ではUnityやUE4のおかげでゲーム作る時に、ライブラリから作るといった事をする機会がめっきりと減りました。

自分も最後に作ったのはSeleneという名前のもので10年くらい前になるかと思います。
スタジオシエスタさんから出ている「ヴァルシュトレイの狂颷」とか「トラブルウィッチーズ」とか書いたら怒られそうなやつとかはSelene含む自作エンジンで作っていたりしました。

自分が最初に低レイヤーを触ったのは就職して3年目の時で、ザ・コンビニ4というゲームでPlayStation2のライブラリをゼロから作るというものでした。
サウンドは別の人が作っていたのですが、サウンド以外の全機能は黒い本渡されて丸投げ状態だったので、趣味で色々触ってなかったらDMAとかVUとかで死んでいたかもしれません。

そういった経験も踏まえて普段から低レベルがどうなっているかを知っておくのは役に立つ事もあるのと、人気のないジャンル(みんな下回りよりゲームが作りたい)なので敢えてやっておこうかなという感じです。

条件

今回は実用面よりも学習面を重視するので以下のような条件で作ろうかと思います。

  1. Windows10 x64のみ考える(増やせるようにはしておきたい)
  2. 基本的にDirectX11を利用する(変えられるようにはしておきたい)
  3. STLC++11~は使う
  4. 過度な最適化は考えない
  5. 極力外部のライブラリに頼らない
  6. 個々の機能はあまり突き詰めない、広く浅く

準備

こちらは作業用のリポジトリです。
名前ですが前回がSeleneで前々回がLunaだったので、今回はArtemisにしてみます。

まずはウインドウの生成をしてDirectX11を初期化し画面をクリアするところまで用意してあります。
ここからゲーム制作で使いそうな色々な機能を盛り込んでいく形で進めていけたらと思います。

github.com

メイン関数の中身

あまり細かい事を考えなくていいようにシンプルにしてみました。

int32_t WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int32_t)
{
	return Core::Bootup(
		L"Artemis.Sample",	// アプリケーション名
		SizeI(1280, 720),	// スクリーンサイズ
		60,					// フレームレート
		[] {				// メイン関数

			while (Core::Update())
			{
			}

			return true;
		});
}

ウインドウの生成

このへんはテンプレみたいなものです。

	//------------------------------------------------------------
	// ウィンドウクラス生成
	//------------------------------------------------------------
	WNDCLASSW WindowClass;
	WindowClass.style = CS_HREDRAW | CS_VREDRAW;
	WindowClass.lpfnWndProc = WndProc;
	WindowClass.cbClsExtra = 0;
	WindowClass.cbWndExtra = 0;
	WindowClass.hInstance = hInstance;
	WindowClass.hIcon = nullptr;
	WindowClass.hCursor = ::LoadCursor(nullptr, IDC_ARROW);
	WindowClass.hbrBackground = (HBRUSH)::GetStockObject(BLACK_BRUSH);
	WindowClass.lpszMenuName = nullptr;
	WindowClass.lpszClassName = ApplicationName;
	::RegisterClass(&WindowClass);

画面の中央にウインドウを表示したいので、枠やタイトルバーのサイズを考慮してウインドウのサイズを設定し、センタリングして表示します。

	//------------------------------------------------------------
	// ウィンドウ生成
	//------------------------------------------------------------
	int32_t x = 0;
	int32_t y = 0;
	int32_t w = ScreenSize.W;
	int32_t h = ScreenSize.H;
	int32_t Style = WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_EX_ACCEPTFILES;
	RECT Rect = { 0, 0, w, h };
	::AdjustWindowRect(&Rect, Style, FALSE);
	w = Rect.right - Rect.left;
	h = Rect.bottom - Rect.top;
	x = ::GetSystemMetrics(SM_CXSCREEN) / 2 - w / 2;
	y = ::GetSystemMetrics(SM_CYSCREEN) / 2 - h / 2;

	_Global.hWindow = ::CreateWindowEx(
		WS_EX_ACCEPTFILES | WS_EX_APPWINDOW,
		ApplicationName,
		ApplicationName,
		Style,
		x, y, w, h,
		nullptr,
		nullptr,
		hInstance,
		nullptr);

	::ShowWindow(_Global.hWindow, SW_NORMAL);
	::UpdateWindow(_Global.hWindow);

ウインドウメッセージの処理は最低限のものだけ記述しています。

static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg)
	{
	case WM_CREATE:		//====================== ウィンドウ生成
		::timeBeginPeriod(1);
		break;
	case WM_CLOSE:		//====================== ウィンドウを閉じる
		::timeEndPeriod(1);
		break;
	case WM_ACTIVATE:	//====================== ウィンドウのアクティブ化、非アクティブ化の通知
		switch (wParam)
		{
		case WA_INACTIVE:	// 非アクティブ化されます。
			break;
		case WA_ACTIVE:		// マウスクリック以外の方法でアクティブ化されます。
		case WA_CLICKACTIVE:// マウスクリックによってアクティブ化されます。
			break;
		}
		break;
	case WM_DESTROY:	//====================== ウィンドウ破棄
		::PostQuitMessage(0);
		return 0;
	}

	// 自分で処理しないメッセージ
	return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}

以上がウインドウ関連の処理になります。
このへんの処理はC++で何か作る場合のテンプレートみたいなものなので細かい意味とかは気にしなくていいかと思います。

DirectX11の初期化

今回はグラフィックスAPIにはDirectX11を利用します。
OpenGLでもDirectX12でもいいんですがWindows上で実質スタンダードな環境はDirectX11かと思いますのでまずはここから始めます。
そのうちVulkanやDirectX12といったより過酷なAPIへの対応もするかもしれません。

まずはデバイスの生成をします。

	D3D_FEATURE_LEVEL FeatureLevels[] = {
		D3D_FEATURE_LEVEL_11_0,
	};

	hr = ::D3D11CreateDevice(
			0,
			D3D_DRIVER_TYPE_HARDWARE,
			0,
#if defined(ATS_DEBUG)
			D3D11_CREATE_DEVICE_DEBUG,
#else//defined(ATS_DEBUG)
			0,
#endif//defined(ATS_DEBUG)
			FeatureLevels,
			1,
			D3D11_SDK_VERSION,
			&_Global.pDevice,
			nullptr,
			&_Global.pDeviceContext);
	if FAILED(hr) return false;

生成したデバイスからいろいろなインターフェイスを取得して最終的にスワップチェインを作ります。
ほぼテンプレみたいなものでしょうか。
なんならデバイスと一緒にスワップチェインを作る関数もあったりしますのでアダプターなんかがいらない場合はそちらの方が早いです。

	// デバイスからDXGIデバイスを取得
	hr = _Global.pDevice->QueryInterface(__uuidof(IDXGIDevice1), (void**)&_Global.pDXGIDevice);
	if FAILED(hr) return false;

	// DXGIデバイスからアダプターを取得
	hr = _Global.pDXGIDevice->GetAdapter(&_Global.pDXGIAdapter);
	if FAILED(hr) return false;

	// アダプターからファクトリーを取得
	hr = _Global.pDXGIAdapter->GetParent(__uuidof(IDXGIFactory), (void**)&_Global.pDXGIFactory);
	if FAILED(hr) return false;

	// ファクトリーからスワップチェインを生成
	DXGI_SWAP_CHAIN_DESC SwapChainDesc;
	SwapChainDesc.BufferDesc.Width = ScreenSize.W;
	SwapChainDesc.BufferDesc.Height = ScreenSize.H;
	SwapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
	SwapChainDesc.BufferDesc.RefreshRate.Numerator = 60;
	SwapChainDesc.BufferDesc.RefreshRate.Denominator = 1;
	SwapChainDesc.SampleDesc.Count = 1;
	SwapChainDesc.SampleDesc.Quality = 0;
	SwapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
	SwapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
	SwapChainDesc.BufferCount = 2;
	SwapChainDesc.OutputWindow = hWnd;
	SwapChainDesc.Windowed = TRUE;
	SwapChainDesc.Flags = 0;
	hr = _Global.pDXGIFactory->CreateSwapChain(_Global.pDevice, &SwapChainDesc, &_Global.pSwapChain);
	if FAILED(hr) return false;

バックバッファへの描画をする必要があるのでスワップチェインからバックバッファを取得し、
バックバッファからさらにRenderTargetViewを生成しておきます。

	// スワップチェインからバックバッファを取得
	hr = _Global.pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&_Global.pRenderTargetTexutre);
	if FAILED(hr) return false;

	// バックバッファのRenderTargetViewを生成
	D3D11_RENDER_TARGET_VIEW_DESC ViewDesc;
	ZeroMemory(&ViewDesc, sizeof(ViewDesc));
	ViewDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
	ViewDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
	ViewDesc.Texture2D.MipSlice = 0;
	hr = _Global.pDevice->CreateRenderTargetView(_Global.pRenderTargetTexutre, &ViewDesc, &_Global.pRenderTargetView);
	if FAILED(hr) return false;

ちょっと長くなりましたが、デバイスの生成からスワップチェインの生成、バックバッファのRenderTargetViewの生成までの一連のコードになります。
これでバックバッファにアクセスして画面の描画をするための最低限の準備が整いました。

画面(RenderTargetView)をクリアする場合はこのような感じです。

	float ClearColor[] = { 0.2f, 0.2f, 0.2f, 1.0f };
	_Global.pDeviceContext->ClearRenderTargetView(_Global.pRenderTargetView, ClearColor);

最後に

まずは初回という事でウインドウの生成からデバイスの初期化まではざっと作ってみました。
ここからゲームエンジン風に色々と機能の拡張をしていく感じになります。