描画の基本


概要

描画とはキャラクターや背景、UI等を画面上に表示する処理のことで、
ゲームを作る上で絶対に必要な処理です。
描画処理が入っていないゲームは、そもそも画面上に絵を出せないということです。
※これ以降描画するモノについてはオブジェクトという単語を使います。

gmpg_0199

上の絵では「キャラクター」「敵」「背景」のオブジェクトを画面に描画しています。

サンプル

サンプルはここからダウンロードでき、環境については以下の内容となっています。

※今回のサンプルは完全に描画実装の概要のためのサンプルです。
 統合画像を意識したつくりにはしていません。

開発環境
VSのバージョン VisualStudio 2017
DirectXのバージョン DirectX9(June 2010)

描画するオブジェクトの種類

画面に描画するオブジェクトは様々な種類があります。
2Dでは「テクスチャ」、3Dでは「3Dモデル」を主に使っており、
その他、円や矩形、線などがあります。

gmpg_0200

描画に必要な情報

オブジェクトを描画に最低限必要な情報は「何を描画するか」「どこに描画するか」です。
その他の情報として拡縮率や回転角度、描画するオブジェクトの種類毎に
必要な情報(円なら半径等)も存在します。

何を描画するか

何を描画するかというのは「描画するオブジェクトの種類」で書いてある
テクスチャや3Dモデルなどの種類を指します。
描画をする際は、この情報がないと意図したオブジェクトを描画をすることはできません。
また、各種類ごとに描画に必要な情報が異なります。
例えば、テクスチャは使用するテクスチャや、テクスチャの座標、円では半径などです。
そのため、プログラム側では種類ごとで描画しやすいように構造体やクラス、関数を作成します。
これは情報を無理やり1つにまとめることで、結局どのオブジェクトを描画するのかが
分かりづらくなることを回避するために行っています。

どこに描画するか

次に必要な情報は描画位置です。
画面の中心や左隅などを描画したい場所を指定するのですが、
この時に位置情報として使用するのが「座標」です。
座標は2Dでは「X軸とY軸」3Dでは「X軸とY軸とZ軸」で構成されており、
各軸に値して値を設定することで、細かい位置に描画をできるようになります。
※座標については「座標」で解説しています。

共通情報

描画の情報で必須ではありませんが、よく使用される情報があります。
例えば拡縮率回転角度です。
ゲーム上では描画しているオブジェクトを拡大、縮小させたり、
回転させたりすることが多々あるので、これらの情報を設定できるようにしておきます。

その他

描画する内容によって必要な情報が異なります。
例えばテクスチャの場合、どのテクスチャを使用するか、統合画像を使用していたら
描画する絵のテクスチャ座標を指定する必要があります。
他にも、矩形ならば矩形のサイズ、円なら半径、線ならば始点と終点等、
描画するオブジェクトの種類によって独自の情報が必要となります。

実装方法

描画処理実装に最低限欲しい機能としては「描画関数を用意する」「描画情報をまとめる」です。
この二つがないと描画しにくいコードになり、可読性や保守性が低くなります。

描画関数を用意する

描画を行うというのは上で書いている通り「何をどこに描画するのか」なので、
指定した座標に指定した種類のオブジェクトを描画する関数を作成すれば、
プレイヤーでも、敵でもUIでも何でも描画できるようになります。

// テクスチャを描画する関数の例
void DrawTexture(描画するテクスチャの情報, 描画座標(X座標), 描画座標(Y座標));

// テクスチャを描画する関数の具体例
void DrawTexture(int texture_id, float pos_x, float pos_y)
{
	指定されたテクスチャを指定された座標に描画する
}

上のDrawTextureを使用してPlayerを描画させたいと考えた場合、
Playerのテクスチャ番号と描画座標を指定すれば描画できます。
敵の場合も同じで敵のテクスチャ番号と描画座標を指定すれば敵が描画できます。
このように指定した種類を指定した座標に描画できる関数を作っておけば、
渡す情報を変えるだけで様々なオブジェクトを描画できるようになります。

描画情報をまとめる

描画情報をまとめた構造体やクラスを用意した方が
オブジェクトを描画する場合に読みすい、かつ分かりやすいコードになります。
これは、実際にコードを確認したほうが分かりやすいと思いますので、
情報をまとめている場合とまとめていない場合の比較を以下で行います。
結論から書くと、当然ながらまとめている方が良いコードになります。
では、最初はまとめていないコードからです。

// 描画情報をまとめていないコード
// プレイヤーオブジェクト作成
int player_texture_id = -1;
float player_pos_x = 200;
float player_pos_y = 400;

// 敵オブジェクト作成
int enemy_texture_id[] = 
{
	-1,
	-1,
	-1
}; 

float enemy_pos_x[] = 
{
	20,
	400,
	600
};

float enmey_pos_y[] =
{
	400,
	400,
	200
};

// 背景オブジェクト作成
int bg_texture_id = -1;
float bg_pos_x = 0;
float bg_pos_y = 0;

// 背景描画
Draw(bg_texture_id, bg_pos_x, bg_pos_y);
// プレイヤー描画
Draw(player_texture_id, player_pos_x, player_pos_y);
// 敵描画
for (int i = 0; i < 3; i++)
{
	Draw(enemy_texture_id[i], enemy_pos_x[i], enemy_pos_y[i]);
}


まとめてないコードはオブジェクトを描画するために必要な情報一つ一つを
バラバラに変数で宣言しなければいけません。
今回は座標(XとY)と使用するテクスチャIDを必要とするので三ついります。
変数としてバラバラに用意しているので、三つの情報で一つのオブジェクトの
描画ができるということが非常に分かりづらくなっています。
次は情報をまとめたコードです。

// 描画情報をまとめたコード
// 描画情報データ
struct DrawObject
{
	int m_TextureId;	// テクスチャID
	float m_PosX;		// 描画座標X
	float m_PosY;		// 描画座標Y
};

// プレイヤーオブジェクト作成
DrawObject player =
{
	-1, 200, 470
};

// 背景オブジェクト作成
DrawObject bg =
{
	-1, 0, 0
};

// 敵オブジェクト作成
DrawObject enemy[] = 
{
	{ -1, 20, 400 },
	{ -1, 400, 400 },
	{ -1, 600, 200 },
};

// 背景描画
Draw(bg.m_TextureId, bg.m_PosX, bg.m_PosY);
// プレイヤー描画
Draw(player.m_TextureId, player.m_PosX, player.m_PosY);
// 敵描画
for (int i = 0; i < 3; i++)
{
	Draw(enemy[i].m_TextureId, enemy[i].m_PosX, enemy[i].m_PosY);
}

DrawObject構造体によって、描画するために必要な情報がまとめられているので、
「構造体の変数を一つ宣言 = オブジェクトを一つ作成」という式が容易に成り立ちます。
この比較から描画に必要な情報はバラバラにせず、構造体やクラスにまとめた方が
可読性や利便性などが高くなることが分かります。