ゲーム専用のクラスを作ろう
前回はApplication.cppに画面を青で塗り 潰したす処理を書きましたね。本来はこの後、塗り潰した画面にさらにキャラクターなどを書いていきますが、それらは別のソースファイルに作りましょうか。ただでさえごちゃごちゃ書いているApplication.cppにゲームの処理や描画まで書いていると、プログラムの行数が膨大になり大変見にくいことになっていきますからね。
純粋にゲームに関連するプログラムは別の場所に移しましょう。下図のような感じです。
ソースファイルの作成
SourceフォルダにGameフォルダを作成して、GameSystem.cppとGameSystem.hを作成しましょう。
Visual Studioのプロジェクトへの追加もお忘れなく!
プログラムの記述
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
#pragma once //========================================= // GameSystemクラス // ・このゲームの土台となるもの //========================================= class GameSystem { public: // このゲームの初期設定を行う void Initialize(); // このゲーム世界の時間を進める(処理を実行する) void Execute(); // その他、ゲーム用のデータなどをココに書く //========================================= // 今回このクラスも、どこからでもアクセスできるように // シングルトンパターンにしておきます。 // ↓↓↓以下、シングルトンパターンのコード //========================================= private: // 唯一のインスタンス用のポインタ static inline GameSystem* s_instance; // コンストラクタはprivateにする GameSystem() {} public: // インスタンス作成 static void CreateInstance() { DeleteInstance(); s_instance = new GameSystem(); } // インスタンス削除 static void DeleteInstance() { if (s_instance != nullptr) { delete s_instance; s_instance = nullptr; } } // 唯一のインスタンスを取得 static GameSystem& GetInstance() { return *s_instance; } }; // GameSystemの唯一のインスタンスを簡単に取得するためのマクロ #define GAMESYS GameSystem::GetInstance() |
このGameSystemはゲームの中心となる存在です。1つだけ存在させいろいろな場所から参照したいので、こいつもシングルトンにしておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// GameSystemクラスを使えるようにする #include "GameSystem.h" // Direct3Dクラスを使えるようにする #include "Source/DirectX/Direct3D.h" // ゲームの初期設定を行う void GameSystem::Initialize() { } // このゲームの時間を進める(処理を実行する) void GameSystem::Execute() { // 画面を青色で塗りつぶす float color[4] = { 0.2f, 0.2f, 1.0f, 1.0f }; D3D.m_deviceContext->ClearRenderTargetView(D3D.m_backBufferView.Get(), color); // バックバッファの内容を画面に表示 D3D.m_swapChain->Present(1, 0); } |
エラーが出る・・・
#include "Source/DirectX/Direct3D.h" の行でエラーが出るかと思います。ちゃんと正しくファイルのパスを書いているつもりが、ダメなようです。これを修正するために下図のようにプロジェクトの設定をしましょう。
これでApplicationフォルダからのパスを指定できるようになります!
Application.cppでGameSystemクラスを動かすようにする
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
// Application.cpp : アプリケーションのエントリ ポイントを定義します。 // #include "framework.h" #include "Application.h" #include "Source/DirectX/Direct3D.h" #include "Source/Game/GameSystem.h" #define MAX_LOADSTRING 100 // グローバル変数: HINSTANCE hInst; // 現在のインターフェイス WCHAR szTitle[MAX_LOADSTRING]; // タイトル バーのテキスト WCHAR szWindowClass[MAX_LOADSTRING]; // メイン ウィンドウ クラス名 // このコード モジュールに含まれる関数の宣言を転送します: ATOM MyRegisterClass(HINSTANCE hInstance); BOOL InitInstance(HINSTANCE, int); LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM); int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); // TODO: ここにコードを挿入してください。 // グローバル文字列を初期化する LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); LoadStringW(hInstance, IDC_APPLICATION, szWindowClass, MAX_LOADSTRING); MyRegisterClass(hInstance); // アプリケーション初期化の実行: if (!InitInstance (hInstance, nCmdShow)) { return FALSE; } HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_APPLICATION)); MSG msg; // ゲームシステム生成 GameSystem::CreateInstance(); // ゲームシステム初期設定 GAMESYS.Initialize(); // ゲームループ while (1) { if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) { //============================================ // ウィンドウメッセージ処理 //============================================ // 終了メッセージがきた if (msg.message == WM_QUIT) { break; } else { TranslateMessage(&msg); DispatchMessage(&msg); } } //============================================ // ゲームの処理を書く //============================================ /* これはGameSystemのExecute関数へ移動したので、消す // 画面を青色で塗りつぶす float color[4] = { 0.2f, 0.2f, 1.0f, 1.0f }; D3D.m_deviceContext->ClearRenderTargetView(D3D.m_backBufferView.Get(), color); D3D.m_swapChain->Present(1, 0); */ // ゲームシステムの処理を実行する GAMESYS.Execute(); } // ゲームシステム削除 GameSystem::CreateInstance(); // Direct3Dインスタンス削除 Direct3D::DeleteInstance(); return (int) msg.wParam; } ・・・以下省略・・・ |
実行結果はさっきと同じだけど、ゲームのプログラムを分けることができましたね。
framework.hの活用
プロジェクトを作成した時に勝手に出来上がってたframework.hですが、今回からこいつも活用していきましょう。
今後このframework.hには、プログラム全体で使用するような汎用的な機能を記述しましょう。
だからと言って、何でもかんでもここに記述しないようにしましょう。必要なものだけを記述します。例えば確実に全体で使用するであろうC言語やC++のライブラリなどのインクルードとかです。
C++にはSTLという便利で汎用的な機能が提供されています。それを全体で使えるように追記し、各ソースからはこのframework.hをインクルードしましょう。
プログラムの記述
まずはframework.hにSTLが使えるようにインクルードを書きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// header.h : 標準のシステム インクルード ファイルのインクルード ファイル、 // またはプロジェクト専用のインクルード ファイル // #pragma once #include "targetver.h" #define WIN32_LEAN_AND_MEAN // Windows ヘッダーからほとんど使用されていない部分を除外する // Windows ヘッダー ファイル #include <windows.h> // C ランタイム ヘッダー ファイル #include <stdlib.h> #include <malloc.h> #include <memory.h> #include <tchar.h> // C++のSTLを使えるようにする #include <string> #include <vector> #include <list> |
各ソースでframework.hをインクルードする
1 2 3 4 5 6 7 |
#include "framework.h" #include "Direct3D.h" bool Direct3D::Initialize(HWND hWnd, int width, int height) { ・・・以下省略・・・ |
1 2 3 4 5 6 7 8 9 10 11 12 |
#include "framework.h" // GameSystemクラスを使えるようにする #include "GameSystem.h" // Direct3Dクラスを使えるようにする #include "Source/DirectX/Direct3D.h" // ゲームの初期設定を行う void GameSystem::Initialize() { } ・・・以下省略・・・ |
今後作成するcppには、このframework.hをインクルードします。
シェーダーとは?
さてそろそろ描画したいところですが、まだできません・・・。なにかの形を画面に描画するにはシェーダーというプログラムが必ず必要になります。いろんな用語が出てくるので、ちょっとまとめてみます。
ポリゴン
画面に何かを表示するには、基本的には三角形として描きます。四角形を描きたい場合は三角形を二枚で表現できますよね。この三角形のことをポリゴンと呼びます。複雑な地形やキャラクターはこのポリゴン(三角形)が沢山集まってできています。
頂点シェーダー
三角形は点が3つで表現できますよね。この点(頂点と言います)を1つ1つ計算で動かしたりできるのがこの頂点シェーダーです。 移動以外にも、次のピクセルシェーダーへ渡すためのデータを計算したりします。
ピクセルシェーダー
点3つで三角形ができ、それを面と呼びます。その面は何かしらの色で塗る必要がありますよね。その面を塗る場所がピクセルシェーダーです。
入力レイアウト
三角形の点(頂点)1つが、どんな情報を持っているのかを記述したもの。 ちょっとわかりにくいですが、これをコンピューターに教えてあげないといけないんです。
このシェーダーというプログラムを動かすことで、画面に三角形を表示できるようになるのです。
2D描画をするシェーダープログラムを書こう
シェーダー用のソースファイルを下図のように作成してください。これはVisual Studioのプロジェクトの方には追加しなくて良いです。
ちなみにC++言語では書けません。HLSLというシェーダー用の言語で記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// 頂点シェーダーから出力するデータ struct VSOutput { float4 Pos : SV_Position; // 頂点の座標(射影座標系) }; //======================================== // 頂点シェーダー //======================================== VSOutput VS(float4 pos : POSITION) { VSOutput Out; // 頂点座標を、何も加工せずそのまま出力 Out.Pos = pos; return Out; } //======================================== // ピクセルシェーダー //======================================== float4 PS(VSOutput In) : SV_Target0 { // 緑色にする return float4(0, 1, 0, 1); } |
最低限の機能だけを持った頂点シェーダーとピクセルシェーダーを書きました。なんかC言語に似てますね。
書いたシェーダープログラムを読み込み、使用できるようにしよう
あとはDirectXの関数を使用して、このプログラムを読み込みコンパイルします。ここができればやっと描画ができるようになります。
まずはシェーダーをコンパイルできる機能をインクルードし、必要な変数を追加します
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
#pragma once // Direct3Dのライブラリを使用できるようにする #pragma comment(lib, "d3d11.lib") #pragma comment(lib, "dxgi.lib") #pragma comment(lib, "d3dcompiler.lib") // Direct3Dの型・クラス・関数などを呼べるようにする #include <d3d11.h> #include <d3dcompiler.h> // DirectXMath(数学ライブラリ)を使用できるようにする #include <DirectXMath.h> // ComPtrを使用できるようにする #include <wrl/client.h> using Microsoft::WRL::ComPtr; //========================================= // Direct3Dクラス //========================================= class Direct3D { public: // ※変数は今回は全てpublicにしますが、本来はprivateで隠すべき※ // Direct3Dデバイス ComPtr<ID3D11Device> m_device; // Direct3Dデバイスコンテキスト ComPtr<ID3D11DeviceContext> m_deviceContext; // スワップチェイン ComPtr<IDXGISwapChain> m_swapChain; // バックバッファーのRTビュー ComPtr<ID3D11RenderTargetView> m_backBufferView; //-------------------------------------------- // Direct3Dを初期化し、使用できるようにする関数 // hWnd : ウィンドウハンドル // width : 画面の幅 // height : 画面の高さ //-------------------------------------------- bool Initialize(HWND hWnd, int width, int height); // 2D描画用のシェーダー ComPtr<ID3D11VertexShader> m_spriteVS = nullptr; // 頂点シェーダー ComPtr<ID3D11PixelShader> m_spritePS = nullptr; // ピクセルシェーダー ComPtr<ID3D11InputLayout> m_spriteInputLayout = nullptr;// 入力レイアウト //========================================= // 今回このクラスは、どこからでもアクセスできるように // シングルトンパターンにします // ↓↓↓以下、シングルトンパターンのコード //========================================= private: // 唯一のインスタンス用のポインタ static inline Direct3D* s_instance; // コンストラクタはprivateにする Direct3D() {} public: // インスタンス作成 static void CreateInstance() { DeleteInstance(); s_instance = new Direct3D(); } // インスタンス削除 static void DeleteInstance() { if (s_instance != nullptr) { delete s_instance; s_instance = nullptr; } } // 唯一のインスタンスを取得 static Direct3D& GetInstance() { return *s_instance; } }; // Direct3Dの唯一のインスタンスを簡単に取得するためのマクロ #define D3D Direct3D::GetInstance() |
シェーダーを読み込みましょう!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
#include "framework.h" #include "Direct3D.h" bool Direct3D::Initialize(HWND hWnd, int width, int height) { ・・・途中省略・・・ //===================================================== // デバイスコンテキストに描画に関する設定を行っておく //===================================================== auto p = &m_backBufferView; p = nullptr; // バックバッファをRTとしてセット m_deviceContext->OMSetRenderTargets(1, m_backBufferView.GetAddressOf(), nullptr); // ビューポートの設定 D3D11_VIEWPORT vp = { 0.0f, 0.0f, (float)width, (float)height, 0.0f, 1.0f }; m_deviceContext->RSSetViewports(1, &vp); //===================================================== // シェーダーの作成 //===================================================== // 頂点シェーダーを読み込み&コンパイル ComPtr<ID3DBlob> compiledVS; if (FAILED(D3DCompileFromFile(L"Shader/SpriteShader.hlsl", nullptr, nullptr, "VS", "vs_5_0", 0, 0, &compiledVS, nullptr))) { return false; } // ピクセルシェーダーを読み込み&コンパイル ComPtr<ID3DBlob> compiledPS; if (FAILED(D3DCompileFromFile(L"Shader/SpriteShader.hlsl", nullptr, nullptr, "PS", "ps_5_0", 0, 0, &compiledPS, nullptr))) { return false; } // 頂点シェーダー作成 if (FAILED(m_device->CreateVertexShader(compiledVS->GetBufferPointer(), compiledVS->GetBufferSize(), nullptr, &m_spriteVS))) { return false; } // ピクセルシェーダー作成 if (FAILED(m_device->CreatePixelShader(compiledPS->GetBufferPointer(), compiledPS->GetBufferSize(), nullptr, &m_spritePS))) { return false; } // 1頂点の詳細な情報 std::vector<D3D11_INPUT_ELEMENT_DESC> layout = { { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, }; // 頂点インプットレイアウト作成 if (FAILED(m_device->CreateInputLayout(&layout[0], layout.size(), compiledVS->GetBufferPointer(), compiledVS->GetBufferSize(), &m_spriteInputLayout))) { return false; } return true; } |
シェーダープログラムの詳細については、いまのところは省略します。またシェーダーを詳しく解説するコーナーも用意する予定です。
三角形の描画
やっと来ましたね。三角形を描画してみましょう。三角形の描画なんて意味あるの?って思いますが、最終的にこの三角形に画像を張り付ければ2Dゲームが作れるようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
#include "framework.h" // GameSystemクラスを使えるようにする #include "GameSystem.h" // Direct3Dクラスを使えるようにする #include "Source/DirectX/Direct3D.h" // ゲームの初期設定を行う void GameSystem::Initialize() { } // このゲームの時間を進める(処理を実行する) void GameSystem::Execute() { // 画面を青色で塗りつぶす float color[4] = { 0.2f, 0.2f, 1.0f, 1.0f }; D3D.m_deviceContext->ClearRenderTargetView(D3D.m_backBufferView.Get(), color); // 三角形の描画 { // 1頂点の形式(今回は座標だけ) struct VertexType { DirectX::XMFLOAT3 Pos; // 座標 }; // 三角形を作るため、頂点を3つ作る VertexType v[3] = { {{0,0,0}}, {{1,-1,0}}, {{-1,-1,0}}, }; //----------------------------- // 頂点バッファ作成 // ・上記で作った3つの頂点はそのままでは描画に使用できないんす… // ・ビデオメモリ側に「頂点バッファ」という形で作る必要があります! // ・今回は効率無視して、その場で作って、使って、すぐ捨てます。 //----------------------------- // 作成するバッファの仕様を決める // ・今回は頂点バッファにするぞ!って感じの設定 D3D11_BUFFER_DESC vbDesc = {}; vbDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; // デバイスにバインドするときの種類(頂点バッファ、インデックスバッファ、定数バッファなど) vbDesc.ByteWidth = sizeof(v); // 作成するバッファのバイトサイズ vbDesc.MiscFlags = 0; // その他のフラグ vbDesc.StructureByteStride = 0; // 構造化バッファの場合、その構造体のサイズ vbDesc.Usage = D3D11_USAGE_DEFAULT; // 作成するバッファの使用法 vbDesc.CPUAccessFlags = 0; // 上の仕様を渡して頂点バッファを作ってもらう // もちろんデバイスさんにお願いする ComPtr<ID3D11Buffer> vb; D3D11_SUBRESOURCE_DATA initData = { &v[0], sizeof(v), 0}; // 書き込むデータ // 頂点バッファの作成 D3D.m_device->CreateBuffer(&vbDesc, &initData, &vb); //++++++++++++++++++++ ここから描画していきます ++++++++++++++++++++ // ここからは「描画の職人」デバイスコンテキストくんが大活躍 // 頂点バッファを描画で使えるようにセットする UINT stride = sizeof(VertexType); UINT offset = 0; D3D.m_deviceContext->IASetVertexBuffers(0, 1, vb.GetAddressOf(), &stride, &offset); // プロミティブ・トポロジーをセット D3D.m_deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); //----------------------------- // シェーダーをセット //----------------------------- D3D.m_deviceContext->VSSetShader(D3D.m_spriteVS.Get(), 0, 0); D3D.m_deviceContext->PSSetShader(D3D.m_spritePS.Get(), 0, 0); D3D.m_deviceContext->IASetInputLayout(D3D.m_spriteInputLayout.Get()); // こんな感じで、ひたすらデバイスコンテキストに情報を渡す //----------------------------- // 描画実行 //----------------------------- // デバイスコンテキストくん、上記のセットした内容で描画してください、とお願いする D3D.m_deviceContext->Draw(3, 0); } // バックバッファの内容を画面に表示 D3D.m_swapChain->Present(1, 0); } |
実行結果
でましたね!単純な三角形1枚だけですが、ここからどんどんパワーアップしていきましょう。
まとめ
- ゲームのプログラムを書くためのクラスGameSystemを作った。
- 何かを描画するにはシェーダープログラムが必要になる。