ゲームプログラミングTips(9):DirectX Graphics(4)(テキスト編)

 

2013年1月27日……前回の講座より、実に約3ヶ月ぶりである。
実際にこの講座を書いたのは前回の講座を実際に書いてからわりとすぐ(5月29日)だが、まあ色々と忙しかったわけで……(言い訳乙)

 

1.DirectX Graphicsとは?(Part4)

 以下略(´Д`)

 

2.DirectX Graphicsのゲームプログラミングにおける活用事例(Part4)

 今回の内容は、今までの内容を踏まえたものとは若干異なる。
 何故なら画像ではなくテキストを扱うからだ、あえていえば基幹部分と若干重なる部分あり。
 何故、基幹編にもテキスト描画機能があったのに別個に実装しているのか? 詳細は以降にて解説する。

 

3.具体例(ヘッダ解説)

 本項では、俺が作ったグラフィック管理クラス(テキスト)を掲載する。

/*************************************************
ファイル名:IDraw2DText.h
作成者  :あびす
役割   :グラフィック管理クラス(2Dテキスト)
*************************************************/
/**
*	@file	IDraw2DText.h
*	@brief	グラフィック(2Dテキスト)の管理を行うクラス関係です。
*/
#ifndef DX9A_GRAPHICS_IDRAW2DTEXT_H
#define DX9A_GRAPHICS_IDRAW2DTEXT_H

namespace nsDX9A{
namespace nsGraphics{
namespace nsIDraw2DText{

//文字テクスチャ
/**
*	@brief	文字テクスチャの情報が格納されている構造体です。
* ユーザーが使用する必要はありません。 */ struct ChTexture{ /** * @brief 文字テクスチャが最後に参照された時間です。 */ DWORD LastRefTime; //最後に参照された時間 /** * @brief 文字テクスチャのサイズです(単位:バイト) */ unsigned int Size; //テクスチャのサイズ /** * @brief 文字テクスチャです(IDirect3DTexture9インターフェイスへのポインタ) */ LPDIRECT3DTEXTURE9 Texture; //テクスチャ /** * @brief 文字テクスチャの縦幅です。 */ unsigned int ChHeight; //文字の縦幅 /** * @brief 文字テクスチャの横幅です。 */ unsigned int ChWidth; //文字の横幅 /** * @brief 文字テクスチャの転送元矩形です。 */ RECT SrcRect; //転送元矩形 }; //グラフィック管理クラス(2Dテキスト)(インターフェース) /** * @brief グラフィック(2Dテキスト)の管理を行うクラスのインターフェースです。 */ class IDraw2DText{ public: /** * @brief 仮想デストラクタです。 */ virtual ~IDraw2DText(){} //仮想デストラクタ /** * @brief 文字列テクスチャの最大サイズを設定します。 * @param StringTextureMaxSize [in]文字列テクスチャの最大サイズを指定します(単位:バイト)
* 自動で拡張される場合があります。
* (例:StringTextureMaxSize = 5 * 1024(5KB)で使用中の文字列テクスチャ(6KB)が存在する場合、
*    StringTextureMaxSize = 6 * 1024(6KB)に自動で拡張されます) */ virtual void SetStringTextureMaxSize(unsigned int StringTextureMaxSize) = 0; //テクスチャの最大サイズを設定 /** * @brief 2Dテキストを設定します。 * @param Text [in]文字列を指定します。 * @param Font [in]文字列のフォントを指定します。 * @param Size [in]文字列のサイズを指定します。 * @param IsBold [in]文字列が太字かを指定します。
* 太字の場合はtrueを、太字でない場合はfalseを指定します。 * @param IsItalic [in]文字列が斜体かを指定します。
* 斜体の場合はtrueを、斜体でない場合はfalseを指定します。 */ virtual void SetText(const string& Text, const string& Font, unsigned int Size, bool IsBold, bool IsItalic) = 0; //2Dテキストを設定 /** * @brief テキストを取得します。 * @return テキストを返します。 */ virtual const string& GetText() const = 0; //テキストを取得 /** * @brief フォントを取得します。 * @return フォントを返します。 */ virtual const string& GetFont() const = 0; //フォントを取得 /** * @brief サイズを取得します。 * @return サイズを返します。 */ virtual unsigned int GetSize() const = 0; //サイズを取得 /** * @brief 太字かを取得します。 * @return 太字かを返します。
* 太字の場合はtrueを、太字でない場合はfalseを返します。 */ virtual bool IsBold() const = 0; //太字かを取得 /** * @brief 斜体かを取得します。 * @return 斜体かを返します。
* 斜体の場合はtrueを、斜体でない場合はfalseを返します。 */ virtual bool IsItalic() const = 0; //斜体かを取得 /** * @brief 文字列の描画を行います。 * @param x [in]文字列の描画先のX座標を指定します。 * @param y [in]文字列の描画先のY座標を指定します。 * @param Color [in]文字列の色をD3DCOLORで指定します。 * @param Degree [in]回転する角度を指定します(単位:度) * @param BlendOp [in]ブレンディング処理をD3DBLENDOPで指定します。 * @param SrcBlend [in]転送元のブレンディングモードをD3DBLENDで指定します。 * @param DestBlend [in]転送先のブレンディングモードをD3DBLENDで指定します。 */ virtual void DrawText(int x, int y, D3DCOLOR Color, float Degree = 0.0f, D3DBLENDOP BlendOp = D3DBLENDOP_ADD, D3DBLEND SrcBlend = D3DBLEND_SRCALPHA, D3DBLEND DestBlend = D3DBLEND_INVSRCALPHA) = 0; //2Dテキストを描画 }; //グラフィック管理クラス(2Dテキスト) /** * @brief グラフィック(2Dテキスト)の管理を行うクラスの基底クラスです。 */ class CDraw2DText : public IDraw2DText{ public: /** * @brief コンストラクタです。 * @param Outer [in]関係する\link nsDX9A::nsGraphics::nsDX9AGraphic::DX9AGraphic DirectX Graphicsラッパークラス\endlinkを指定します。 */ CDraw2DText(DX9AGraphic* Outer); //コンストラクタ(DirectX Graphicsラッパーを指定) /** * @brief デストラクタです。 */ ~CDraw2DText(); //デストラクタ void SetStringTextureMaxSize(unsigned int StringTextureMaxSize); //テクスチャの最大サイズを設定 void SetText(const string& Text, const string& Font, unsigned int Size, bool IsBold, bool IsItalic); //2Dテキストを設定 const string& GetText() const; //テキストを取得 const string& GetFont() const; //フォントを取得 unsigned int GetSize() const; //サイズを取得 bool IsBold() const; //太字かを取得 bool IsItalic() const; //斜体かを取得 void DrawText(int x, int y, D3DCOLOR Color, float Degree = 0.0f, D3DBLENDOP BlendOp = D3DBLENDOP_ADD, D3DBLEND SrcBlend = D3DBLEND_SRCALPHA, D3DBLEND DestBlend = D3DBLEND_INVSRCALPHA); //2Dテキストを描画 private: unsigned int m_StringTextureNowSize; //文字列テクスチャの現在サイズ unsigned int m_StringTextureMaxSize; //文字列テクスチャの最大サイズ string m_Text; //テキスト vector< string > m_KeyText; //テキスト(描画時に利用) unsigned int m_KeyTextHeight; //テキストの縦幅(描画時に利用) string m_Font; //フォント unsigned int m_Size; //サイズ bool m_IsBold; //太字か bool m_IsItalic; //斜体か map< string, ChTexture > m_ChTextures; //文字列テクスチャ DX9AGraphic* m_Outer; //DirectX Graphicsラッパーへのアドレス void CutChTexture(); //文字列テクスチャを削減 CDraw2DText(); //デフォルトコンストラクタ(禁止) CDraw2DText(const CDraw2DText&); //コピーコンストラクタ(禁止) CDraw2DText& operator =(const CDraw2DText&); //代入演算子(禁止) }; } } } #endif

 ヘッダに対する解説では、本クラスの使用方法ならびに実装技法を説明する。

 使用方法は簡単である。

 1.インスタンスを生成する
 2.SetTextメソッド等でテキストの設定を行う
 3.DrawTextメソッドで設定したテキストの描画を行う

具体例

DX9AGraphic* g_DX9AG = new DX9AGraphic();										//DirectX Graphicsラッパークラス
CDraw2DText A(g_DX9AG);															//グラフィック管理クラス(テキスト)
A.SetText("おけぱか", "MS Pゴシック", 20, false, false);						//テキストを設定
A.DrawText(100, 100, D3DCOLOR_ARGB(128, 255, 0, 0));							//テキストを描画

 ……これだけである、特に変なところはない筈だ。

 では、以下にて具体的な説明を行っていく。
 ただし、なるべく説明は簡略に行う。

 ヘッダの説明

  ChTexture構造体について

   基幹編とほぼ同様、クラス使用者が意識する必要のない構造体である。

  なんでこんなクラスが要るの?

   これまでのTipsを読んでいると一目瞭然なのだが、実はテキスト描画自体はDX9AGraphicクラスでも可能である。
   にもかかわらず、CDraw2DTextクラスという新たなクラスでもテキスト描画を可能にしている。それは何故なのか?

   理由を簡潔に述べると、「効率良くメモリを使用するため」である。
   DX9AGraphicクラスは内部で自動解放を行っているが、明示的な解放は行えないため使用メモリが肥大化しがちである。
   それに対し、CDraw2DTextクラスは基幹クラスではない。そのため、解体すれば使用メモリが全て後腐れなく解放される。

   またメモリの消費効率も異なる、DX9AGraphicクラスは「文字列」を保持しているのに対しCDraw2DTextクラスは「文字」である。
   たとえば「あいあいあいあいあい」というテキスト描画を行う際に前者では10文字分、後者では2文字分のメモリを使用している。

   つまり、使い分けが可能である。
   DX9AGraphic…メモリの消費効率などを一切考慮しないテキスト描画(ちょっとしたテキスト描画など)
   CDraw2DText…局所的に大量の描画を行うためにメモリの消費効率などを考慮するべきテキスト描画(ノベルゲームなど)

   上記のような感じである、まあ所詮はテキストのメモリに過ぎないのでそこまで大袈裟に考える必要もないのだが……(;´Д`)

4.具体例(ソース解説)

 次はソース解説である。

#include "stdafx.h"
#include "../index.h"
#include "IDraw2DText.h"

namespace nsDX9A{
namespace nsGraphics{
namespace nsIDraw2DText{

//コンストラクタ(DirectX Graphicsラッパーを指定)
CDraw2DText::CDraw2DText(DX9AGraphic* Outer){
	m_StringTextureNowSize = 0;
	m_StringTextureMaxSize = 0;
	m_KeyTextHeight = 0;
	m_Size = 0;
	m_IsBold = false;
	m_IsItalic = false;
	m_Outer = Outer;
}
//デストラクタ
CDraw2DText::~CDraw2DText(){
	for(map< string, ChTexture >::iterator it=m_ChTextures.begin();it!=m_ChTextures.end();it++){
		SAFE_RELEASE(it->second.Texture);
	}
}

//文字列テクスチャの最大サイズを設定
void CDraw2DText::SetStringTextureMaxSize(unsigned int StringTextureMaxSize){
	m_StringTextureMaxSize = StringTextureMaxSize;
}

//2Dテキストを設定
void CDraw2DText::SetText(const string& Text, const string& Font, unsigned int Size, bool IsBold, bool IsItalic){
	//内部関数用クラス
	class InnerFunction{
	public:
		//文字列を文字群に分解
		static void ResolveText(const string& Text, vector< string >& Chs){
			Chs.clear();
			for(unsigned int i=0;Text[i]!='\0';){
				unsigned int ChByte = ((Text[i]>=static_cast< char >(0x81) && Text[i]<=static_cast< char >(0x9F)) || 
									  (Text[i]>=static_cast< char >(0xE0) && Text[i]<=static_cast< char >(0xFF))) ? 2 : 1;
				Chs.push_back(string(Text.c_str()+i, ChByte));
				i += ChByte;
			}
		}
		//テキスト(描画時に利用)を生成
		static void CreateKeyText(const vector< string >& Chs, const string& Font, unsigned int Size, bool IsBold, bool IsItalic, vector< string >& KeyText){
			KeyText.clear();
			char Suffix[1024] = {0};
			sprintf(Suffix, "_%s_%d_%d", Font.c_str(), Size, (IsBold ? 1 : 0) + (IsItalic ? 2 : 0));
			for(vector< string >::const_iterator it=Chs.begin();it!=Chs.end();it++){
				if((*it)=="\n"){
					KeyText.push_back("\n");
				}
				else{
					KeyText.push_back((*it) + Suffix);
				}
			}
		}
		//GetGlyphOutline用にエンコード
		static void EncodeText(const string& Src, wstring& Dest){
			Dest.clear();
			for(unsigned int i=0;Src[i]!='\0';){
				unsigned int ChByte = ((Src[i]>=static_cast< char >(0x81) && Src[i]<=static_cast< char >(0x9F)) || 
									  (Src[i]>=static_cast< char >(0xE0) && Src[i]<=static_cast< char >(0xFF))) ? 2 : 1;
				Dest.push_back(ChByte==2 ? static_cast< BYTE >(Src[i]) * 256 + static_cast< BYTE >(Src[i+1]) : static_cast< BYTE >(Src[i]));
				i += ChByte;
			}
		}
	};

	unsigned int StringTextureNowSize = 0;
	m_Text = Text;
	m_KeyText.clear();
	m_KeyTextHeight = Size;
	m_Font = Font;
	m_Size = Size;
	m_IsBold = IsBold;
	m_IsItalic = IsItalic;
	//空文字列の場合
	if(Text==""){
		return;
	}
	//空文字列でない場合
	else{
		vector< string > Chs;
		InnerFunction::ResolveText(Text, Chs);				//文字列を文字群に分解
		InnerFunction::CreateKeyText(Chs, m_Font, m_Size, m_IsBold, m_IsItalic, m_KeyText);
															//テキスト(描画時に利用)を生成
		wstring UniChs;
		InnerFunction::EncodeText(Text, UniChs);			//GetGlyphOutline用にエンコード
		//フォントを作成
		LOGFONT LF = {Size, 0, 0, 0, (IsBold ? FW_BOLD : 0), (IsItalic ? TRUE : FALSE), 0, 0,
					  SHIFTJIS_CHARSET, OUT_TT_ONLY_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FIXED_PITCH | FF_MODERN, ""};
		strcpy(LF.lfFaceName, Font.c_str());
		HFONT hFont;
		if((hFont = CreateFontIndirect(&LF))==0){
			DX9AErrOut("Failed : CDraw2DText::SetText() CreateFontIndirect\n");
			return;
		}
		//デバイスコンテキストを取得
		HDC hDC = GetDC(NULL);
		HFONT OldFont = reinterpret_cast< HFONT >(SelectObject(hDC, hFont));
		//テクスチャを生成(存在しない場合のみ)
		for(unsigned int i=0;i< m_KeyText.size();i++){
			UINT UniCh = UniChs[i];
			string KeyCh = m_KeyText.at(i);
			//改行:何もせず
			if(KeyCh=="\n"){
				continue;
			}
			//テクスチャが未作成:作成する
			if(m_ChTextures.find(KeyCh)==m_ChTextures.end()){
				//テクスチャのサイズを取得
				TEXTMETRIC TM;
				GLYPHMETRICS GM;
				UINT TexWidth = 0, TexHeight = 0;
				CONST MAT2 Mat = {{0, 1}, {0, 0}, {0, 0}, {0, 1}};
				GetTextMetrics(hDC, &TM);
				TexHeight = TM.tmHeight;
				GetGlyphOutline(hDC, UniCh, GGO_METRICS, &GM, 0, NULL, &Mat);
				TexWidth = GM.gmCellIncX;
				if(IsItalic){
					TexWidth += Size / 4;
				}
				unsigned int cnt;
				for(cnt=0;pow(2.0f, static_cast< float >(cnt))< TexWidth;cnt++);	TexWidth = static_cast< UINT >(pow(2.0f, static_cast< float >(cnt)));
				for(cnt=0;pow(2.0f, static_cast< float >(cnt))< TexHeight;cnt++);	TexHeight = static_cast< UINT >(pow(2.0f, static_cast< float >(cnt)));
				//文字列テクスチャのパラメータを設定
				ChTexture stTemp;
				stTemp.Size = 4 * TexWidth * TexHeight;
				stTemp.Texture = NULL;
				stTemp.ChHeight = TM.tmHeight;
				stTemp.ChWidth = GM.gmCellIncX;
				stTemp.SrcRect.left = 0;
				stTemp.SrcRect.top = 0;
				stTemp.SrcRect.right = TexWidth;
				stTemp.SrcRect.bottom = TexHeight;
				m_StringTextureNowSize += stTemp.Size;
				if(UniCh==32/* 半角スペース */ || UniCh==33088/* 全角スペース */){
					//半角スペース・全角スペースの場合:テクスチャを確保しない
					stTemp.Size = 0;
				}
				else{
					//半角スペース・全角スペース以外の場合:テクスチャを確保する
					//テクスチャを確保
					if(FAILED(D3DXCreateTexture(GetDX9AGraphicAccessor(m_Outer)->GetD3D9Device(), TexWidth, TexHeight, 1, 0, D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, &(stTemp.Texture)))){
						DX9AErrOut("Failed : CDraw2DText::SetText() D3DXCreateTexture\n");
						SelectObject(hDC, OldFont);
						DeleteObject(hFont);
						ReleaseDC(NULL, hDC);
						return;
					}
					//フォントビットマップ取得→テクスチャに書き込み
					DWORD Alpha, Color;
					D3DLOCKED_RECT LockedRect;
					BYTE* p = new BYTE[stTemp.Size];
					int Bmp_W, Bmp_H, Ofs_X, Ofs_Y;
					if(FAILED(stTemp.Texture->LockRect(0, &LockedRect, NULL, D3DLOCK_DISCARD))){
						DX9AErrOut("Failed : CDraw2DText::SetText() LockRect\n");
						SelectObject(hDC, OldFont);
						DeleteObject(hFont);
						ReleaseDC(NULL, hDC);
						SAFE_DELETE_ARRAY(p);
						return;
					}
					FillMemory(LockedRect.pBits , LockedRect.Pitch * TexHeight, 0);
					GetGlyphOutline(hDC, UniCh, GGO_METRICS, &GM, 0, NULL, &Mat);
					GetGlyphOutline(hDC, UniCh, GGO_GRAY8_BITMAP, &GM, stTemp.Size, p, &Mat);
					Bmp_W = GM.gmBlackBoxX + (4 - (GM.gmBlackBoxX % 4)) % 4;
					Bmp_H = GM.gmBlackBoxY;
					Ofs_X = GM.gmptGlyphOrigin.x;
					Ofs_Y = TM.tmAscent - GM.gmptGlyphOrigin.y;
					for(int Y=0;Y< static_cast< int >(GM.gmBlackBoxY);Y++){
						for(int X=0;X< static_cast< int >(GM.gmBlackBoxX);X++){
							Alpha = (255 * p[Bmp_W * Y + X]) / 64;
							Color = 0x00ffffff | (Alpha << 24);
							memcpy((BYTE*)LockedRect.pBits+LockedRect.Pitch*(Ofs_Y+Y)+4*(Ofs_X+X), &Color, sizeof(Color)); 
						}
					}
					stTemp.Texture->UnlockRect(0);
					SAFE_DELETE_ARRAY(p);
				}
				m_ChTextures.insert(pair< string, ChTexture >(KeyCh, stTemp));
			}
			StringTextureNowSize += m_ChTextures.find(KeyCh)->second.Size;
			if(m_ChTextures.find(KeyCh)->second.ChHeight>m_KeyTextHeight){
				m_KeyTextHeight = m_ChTextures.find(KeyCh)->second.ChHeight;
			}
			m_ChTextures.find(KeyCh)->second.LastRefTime = timeGetTime();
		}
		//後始末
		SelectObject(hDC, OldFont);
		DeleteObject(hFont);
		ReleaseDC(NULL, hDC);
	}
	//生成した文字列テクスチャのサイズ>文字列テクスチャの最大サイズの場合、文字列テクスチャの最大サイズを自動的に拡張する
	if(StringTextureNowSize>m_StringTextureMaxSize){
		m_StringTextureMaxSize = StringTextureNowSize;
	}
	//文字列テクスチャの現在サイズ>文字列テクスチャの最大サイズの場合、テクスチャの仕分けを行う
	if(m_StringTextureNowSize>m_StringTextureMaxSize){
		CutChTexture();
	}
}

//テキストを取得
const string& CDraw2DText::GetText() const{
	return(m_Text);
}
//フォントを取得
const string& CDraw2DText::GetFont() const{
	return(m_Font);
}
//サイズを取得
unsigned int CDraw2DText::GetSize() const{
	return(m_Size);
}
//太字かを取得
bool CDraw2DText::IsBold() const{
	return(m_IsBold);
}
//斜体かを取得
bool CDraw2DText::IsItalic() const{
	return(m_IsItalic);
}

//2Dテキストを描画
void CDraw2DText::DrawText(int x, int y, D3DCOLOR Color, float Degree, D3DBLENDOP BlendOp, D3DBLEND SrcBlend, D3DBLEND DestBlend){
	//空文字列の場合
	if(m_KeyText.empty()){
		return;
	}
	//空文字列でない場合
	else{
		int BaseX = x;
		int BaseY = y;
		for(vector< string >::iterator it=m_KeyText.begin();it!=m_KeyText.end();it++){
			if((*it)=="\n"){
				BaseX = x;
				BaseY += m_KeyTextHeight;
			}
			else{
				if(m_ChTextures.find(*it)->second.Texture){
					RECT SrcRect = {0, 0, m_ChTextures.find(*it)->second.ChWidth, m_ChTextures.find(*it)->second.ChHeight};
					m_Outer->GeneralBlt(m_ChTextures.find(*it)->second.Texture, BaseX, BaseY, Color, &SrcRect, BlendOp, SrcBlend, DestBlend, 1.0f, 1.0f, Degree);
				}
				m_ChTextures.find(*it)->second.LastRefTime = timeGetTime();
				BaseX += m_ChTextures.find(*it)->second.ChWidth;
			}
		}
	}
}

//文字列テクスチャを削減
void CDraw2DText::CutChTexture(){
	DWORD Now = timeGetTime();
	map< string, ChTexture >::iterator it;
	while(m_StringTextureNowSize>m_StringTextureMaxSize){
		it = m_ChTextures.begin();
		for(map< string, ChTexture >::iterator it2=m_ChTextures.begin();it2!=m_ChTextures.end();it2++){
			if(Now-it2->second.LastRefTime>Now-it->second.LastRefTime){
				it = it2;
			}
			if(Now-it2->second.LastRefTime==Now-it->second.LastRefTime){
				if(it2->second.Size>it->second.Size){
					it = it2;
				}
			}
		}
		m_StringTextureNowSize -= it->second.Size;
		SAFE_RELEASE(it->second.Texture);
		m_ChTextures.erase(it);
	}
}

}
}
}

 CDraw2DTextメソッド内がわりとややこしいが、あえて説明はしない(´Д`)
 いくつかの関数を順にググっていけば、何をしているかは読み解ける筈である。

 

 

 

……以上で本項は終了である、お疲れ様でした(´Д`)
次回からようやくDirectSound編である、難筆の予感……?(難易度的な意味で)

 

前へ                                        戻る                                     次へ