组成一个完整游戏的元素大体来说其实只有两种:图形和字体。
前几节涉及的内容大都为图形元素,本节我们来看如何在Direct3D中实现简单的字体绘制~
Direct3D中的文字绘制主要依赖于ID3DXFont接口对象。我们事先构造一个D3DXFONT_DESC结构体,详细描述所要生成字体的信息,而后调用D3DXCreateFontIndirect函数即可获得ID3DXFont接口对象。
那么接下来依然是以人性化接口为目的的二次封装~
/*
-------------------------------------
代码清单:D3DFont.h
来自:
http://www.cnblogs.com/kenkao
-------------------------------------
*/
#include
"
D3DInit.h
"
#pragma
once
class
CD3DFont
{
public
:
CD3DFont(IDirect3DDevice9
*
pDevice, D3DPRESENT_PARAMETERS
*
pD3DPP);
~
CD3DFont(
void
);
public
:
bool
LoadFont(
char
*
fontName, UINT fontSize);
//
加载字体
void
Release();
//
释放字体
public
:
ID3DXFont
*
GetFontHandle(){
return
m_pFont;}
//
获得字体句柄
RECT GetFontArea(){
return
m_FontArea;}
//
获得字体默认有效区域(屏幕区域)
private
:
IDirect3DDevice9
*
m_pDevice;
//
3D设备
ID3DXFont
*
m_pFont;
//
字体对象
RECT m_FontArea;
//
字体默认有效区域
};
/*
-------------------------------------
代码清单:D3DFont.cpp
来自:
http://www.cnblogs.com/kenkao
-------------------------------------
*/
#include
"
StdAfx.h
"
#include
"
D3DFont.h
"
CD3DFont::CD3DFont(IDirect3DDevice9
*
pDevice, D3DPRESENT_PARAMETERS
*
pD3DPP) : m_pDevice(pDevice), m_pFont(NULL)
{
m_FontArea.left
=
0
;
m_FontArea.top
=
0
;
m_FontArea.right
=
pD3DPP
->
BackBufferWidth;
m_FontArea.bottom
=
pD3DPP
->
BackBufferHeight;
}
CD3DFont::
~
CD3DFont(
void
)
{
}
bool
CD3DFont::LoadFont(
char
*
fontName, UINT fontSize)
{
//
生成D3DXFONT_DESC结构体并初始化
D3DXFONT_DESC d3dxFont;
ZeroMemory(
&
d3dxFont,
sizeof
(d3dxFont));
_tcscpy(d3dxFont.FaceName,fontName);
d3dxFont.Width
=
fontSize;
d3dxFont.Height
=
fontSize
*
2
;
d3dxFont.Weight
=
100
;
d3dxFont.Italic
=
false
;
d3dxFont.CharSet
=
DEFAULT_CHARSET;
//
创建字体对象
if
(FAILED(D3DXCreateFontIndirect(m_pDevice,
&
d3dxFont,
&
m_pFont))){
return
false
;
}
return
true
;
}
void
CD3DFont::Release()
{
ReleaseCOM(m_pFont);
}
接下来,为我们先前构造的CD3DSprite添加几个重载函数,赋予其绘制字体的能力:
D3DSprite.h
public
:
void
DrawText(
CD3DFont
*
pFont,
//
字体对象
char
*
szString,
//
文字内容
RECT
&
DesRect,
//
目标区域
DWORD AlignFormat,
//
对齐格式
D3DCOLOR Color);
//
字体颜色
void
DrawText(
CD3DFont
*
pFont,
char
*
szString,
RECT
&
DesRect,
D3DCOLOR Color
=
D3DXCOLOR_WHITE);
void
DrawText(
CD3DFont
*
pFont,
char
*
szString,
D3DXVECTOR2
&
Pos,
//
字体位置
D3DCOLOR Color
=
D3DXCOLOR_WHITE);
D3DSprite.cpp
void
CD3DSprite::DrawText(CD3DFont
*
pFont,
char
*
szString, RECT
&
DesRect, D3DCOLOR Color)
{
DrawText(pFont, szString, DesRect, DT_TOP
|
DT_LEFT, Color);
}
void
CD3DSprite::DrawText(CD3DFont
*
pFont,
char
*
szString, D3DXVECTOR2
&
Pos, D3DCOLOR Color)
{
RECT DesRect;
DesRect.left
=
Pos.x;
DesRect.top
=
Pos.y;
DesRect.right
=
pFont
->
GetFontArea().right;
DesRect.bottom
=
pFont
->
GetFontArea().bottom;
DrawText(pFont, szString, DesRect, Color);
}
void
CD3DSprite::DrawText(CD3DFont
*
pFont,
char
*
szString, RECT
&
DesRect, DWORD AlignFormat, D3DCOLOR Color)
{
pFont
->
GetFontHandle()
->
DrawText(m_pSprite, szString,
-
1
,
&
DesRect, AlignFormat, Color);
}
看到这里,大家可能会觉得奇怪。由第三个重载函数我们不难看出:最终的DrawText调用方依然是ID3DXFont接口对象,那么为什么我们要将字体绘制的功能封装到CD3DSprite对象中呢?
由DirectX说明文档可知,ID3DXFont.DrawText函数的第一个参数是一个宿主ID3DXSprite接口对象,尽管允许其为空,但要想实现更高效率的字体绘制,则我们需要指定一个专属的ID3DXSprite接口对象。
从Xna的SpriteBatch对象封装的手法上,我们可以略微看出端倪,尽管巧合的几率更大些,但各种因素促使我们有理由这么做~
实现字体绘制功能之后,我们再来实现一个可以统计游戏运行效率的计时装置。
FPS 即 Frames Per Second(每秒传输帧数),是我们用于测算游戏运行效率的通用标准。如果游戏在执行密集型计算或大批量渲染时,FPS值依然能达到一个正常标准(通常为60帧/s),则说明我们的游戏运行效率良好。
下面就来看这个游戏计时器的实现及使用方法:
/*
-------------------------------------
代码清单:GameTime.h
来自:
http://www.cnblogs.com/kenkao
-------------------------------------
*/
#include
<
stdio.h
>
#include
"
mmsystem.h
"
#pragma
once
class
CGameTime
{
public
:
CGameTime(
void
);
~
CGameTime(
void
);
public
:
void
Start();
//
游戏计时器启动
void
Tick();
//
游戏计时器心跳(每次Update时调用一次)
void
Stop();
//
游戏计时器停止
public
:
float
GetTotalTicks() {
return
m_totalTicks;}
//
获得系统启动时间
float
GetTotalGameTime() {
return
m_totalGameTime;}
//
获得游戏运作时间
float
GetElapsedGameTime(){
return
m_elapsedGameTime;}
//
获得单帧时间差
public
:
void
CalcFPS();
//
计算FPS(每次Update时调用一次)(需显式调用,默认不执行)
char
*
ShowFPS() {
return
m_strFPS;}
//
显示FPS(字符串)
float
GetFPS() {
return
m_FPS;}
//
获得FPS(float数值)
private
:
float
m_totalTicks;
//
系统启动时间
float
m_totalGameTime;
//
游戏运作时间
float
m_elapsedGameTime;
//
单帧运作时间
float
m_previousTicks;
//
前次时间戳
float
m_startTicks;
//
启动时间戳
DWORD m_FrameCnt;
//
帧总数(计算FPS辅助变量)
float
m_TimeElapsed;
//
消耗时间(计算FPS辅助变量)
float
m_FPS;
//
FPS值
char
m_strFPS[
13
];
//
FPS串
};
GameTime.cpp
/*
-------------------------------------
代码清单:GameTime.cpp
来自:
http://www.cnblogs.com/kenkao
-------------------------------------
*/
#include
"
StdAfx.h
"
#include
"
GameTime.h
"
#pragma
comment(lib, "winmm.lib")
CGameTime::CGameTime(
void
) : m_totalTicks(
0.0f
),
m_totalGameTime(
0.0f
),
m_elapsedGameTime(
0.0f
),
m_previousTicks(
0.0f
),
m_startTicks(
0.0f
),
m_FrameCnt(
0
),
m_TimeElapsed(
0.0f
),
m_FPS(
0.0f
)
{
//
初始FPS串
char
*
strFPS
=
"
FPS:0000000\0
"
;
memcpy(m_strFPS,strFPS,
13
);
}
CGameTime::
~
CGameTime(
void
)
{
}
void
CGameTime::Start()
{
//
计时器启动时先捕捉起始时间戳及前次时间戳
m_startTicks
=
(
float
)timeGetTime();
m_previousTicks
=
m_startTicks;
}
void
CGameTime::Tick()
{
//
每次心跳时更新系统时间戳
m_totalTicks
=
(
float
)timeGetTime();
//
单帧时间差 = 系统时间戳 - 前次时间戳
m_elapsedGameTime
=
m_totalTicks
-
m_previousTicks;
//
游戏运作时间 = 系统时间戳 - 起始时间戳
m_totalGameTime
=
m_totalTicks
-
m_startTicks;
//
更新前次时间戳
m_previousTicks
=
m_totalTicks;
}
void
CGameTime::Stop()
{
//
计时器停止时全部变量归0
m_totalTicks
=
0.0f
;
m_totalGameTime
=
0.0f
;
m_elapsedGameTime
=
0.0f
;
m_previousTicks
=
0.0f
;
m_startTicks
=
0.0f
;
m_FrameCnt
=
0
;
m_TimeElapsed
=
0.0f
;
m_FPS
=
0.0f
;
}
void
CGameTime::CalcFPS()
{
//
帧数递增
m_FrameCnt
++
;
//
累计时间递增
m_TimeElapsed
+=
m_elapsedGameTime;
//
当累计时间超过1秒时
if
(m_TimeElapsed
>=
1000.0f
)
{
//
FPS测算
m_FPS
=
(
float
)m_FrameCnt
*
1000.0f
/
m_TimeElapsed;
sprintf(
&
m_strFPS[
4
],
"
%f
"
, m_FPS);
m_strFPS[
12
]
=
'
\0
'
;
//
处理累计时间及帧数
m_TimeElapsed
-=
1000.0f
;
m_FrameCnt
=
0
;
}
}
很显然,CGameTime的Start和Stop是超越了游戏主循环的,而这部分对CD3DGame而言是透明的,我们需要将CGameTime部分相关代码放置到Win32App的默认代码中:
int
APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int
nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
//
TODO: 在此放置代码。
MSG msg;
HACCEL hAccelTable;
//
初始化全局字符串
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_KEN3DGAME, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
//
执行应用程序初始化:
if
(
!
InitInstance (hInstance, nCmdShow))
{
return
FALSE;
}
hAccelTable
=
LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_KEN3DGAME));
LoadContent();
ZeroMemory(
&
msg,
sizeof
(MSG));
//
声明游戏计时器对象,该对象随入口方法的结束而自动释放(生命周期终止)
CGameTime GameTime;
//
计时器随主循环的启动而启动
GameTime.Start();
while
(msg.message
!=
WM_QUIT) {
if
(PeekMessage(
&
msg, NULL,
0
,
0
, PM_REMOVE)) {
TranslateMessage(
&
msg);
DispatchMessage(
&
msg);
}
//
计时器心跳
GameTime.Tick();
Update(
&
GameTime);
Draw(
&
GameTime);
}
//
计时器随主循环的停止而停止
GameTime.Stop();
UnloadContent();
Dispose();
return
(
int
) msg.wParam;
}
以上为变更之后的Win32入口函数。我们可以看到CD3DGame的Update和Draw函数参数不再是单纯的float值,而是功能完整的游戏计数器指针。
接下来,我们来看CD3DGame主体代码:
D3DGame.cpp
/*
-------------------------------------
代码清单:D3DGame.cpp
来自:
http://www.cnblogs.com/kenkao
-------------------------------------
*/
#include
"
StdAfx.h
"
#include
"
D3DGame.h
"
#include
"
D3DSprite.h
"
#include
"
SpriteBatch.h
"
#include
"
D3DFont.h
"
#include
"
D3DCamera.h
"
#include
"
BaseTerrain.h
"
#include
<
stdio.h
>
#include
<
time.h
>
//
---通用全局变量
HINSTANCE g_hInst;
HWND g_hWnd;
D3DXMATRIX g_matProjection;
D3DPRESENT_PARAMETERS g_D3DPP;
//
---D3D全局变量
IDirect3D9
*
g_pD3D
=
NULL;
IDirect3DDevice9
*
g_pD3DDevice
=
NULL;
CMouseInput
*
g_pMouseInput
=
NULL;
CKeyboardInput
*
g_pKeyboardInput
=
NULL;
CD3DSprite
*
g_pSprite
=
NULL;
CD3DCamera
*
g_pD3DCamera
=
NULL;
CBaseTerrain
*
g_pBaseTerrain
=
NULL;
CSpriteBatch
*
g_pSpriteBatch
=
NULL;
CD3DFont
*
g_pFont
=
NULL;
CD3DFont
*
g_pFont2
=
NULL;
void
Initialize(HINSTANCE hInst, HWND hWnd)
{
g_hInst
=
hInst;
g_hWnd
=
hWnd;
InitD3D(
&
g_pD3D,
&
g_pD3DDevice, g_D3DPP, g_matProjection, hWnd);
g_pMouseInput
=
new
CMouseInput;
g_pMouseInput
->
Initialize(hInst,hWnd);
g_pKeyboardInput
=
new
CKeyboardInput;
g_pKeyboardInput
->
Initialize(hInst,hWnd);
srand(time(
0
));
}
void
LoadContent()
{
g_pD3DCamera
=
new
CD3DCamera;
g_pSprite
=
new
CD3DSprite(g_pD3DDevice);
//
声明并加载两种不同的字体
g_pFont
=
new
CD3DFont(g_pD3DDevice,
&
g_D3DPP);
g_pFont
->
LoadFont(
"
宋体
"
,
8
);
g_pFont2
=
new
CD3DFont(g_pD3DDevice,
&
g_D3DPP);
g_pFont2
->
LoadFont(
"
隶书
"
,
16
);
}
void
Update(CGameTime
*
gameTime)
{
//
统计FPS
gameTime
->
CalcFPS();
g_pMouseInput
->
GetState();
g_pKeyboardInput
->
GetState();
g_pD3DCamera
->
Update();
g_pBoundingFrustum
->
Update(g_pD3DCamera
->
GetViewMatrix()
*
g_matProjection);
}
void
Draw(CGameTime
*
gameTime)
{
g_pD3DDevice
->
SetTransform(D3DTS_VIEW,
&
g_pD3DCamera
->
GetViewMatrix());
g_pD3DDevice
->
Clear(
0
, NULL, D3DCLEAR_TARGET
|
D3DCLEAR_ZBUFFER, D3DCOLOR_RGBA(
100
,
149
,
237
,
255
),
1.0f
,
0
);
if
(SUCCEEDED(g_pD3DDevice
->
BeginScene()))
{
//
开启绘制
g_pSprite
->
Begin(D3DXSPRITE_ALPHABLEND);
//
显示FPS
g_pSprite
->
DrawText(g_pFont, gameTime
->
ShowFPS(), D3DXVECTOR2(
100
,
100
), D3DXCOLOR_WHITE);
//
绘制具体文字
g_pSprite
->
DrawText(g_pFont2,
"
花瓣散落了满地的记忆,拾起来放入口中\r\n细细咀嚼,再也找不回原来的味道…
"
, D3DXVECTOR2(
100
,
200
), D3DCOLOR_ARGB(
128
,
0
,
0
,
255
));
//
结束绘制
g_pSprite
->
End();
g_pD3DDevice
->
EndScene();
}
g_pD3DDevice
->
Present(NULL, NULL, NULL, NULL);
}
void
UnloadContent()
{
ReleaseCOM(g_pFont2);
ReleaseCOM(g_pFont);
ReleaseCOM(g_pSprite);
}
void
Dispose()
{
ReleaseCOM(g_pKeyboardInput);
ReleaseCOM(g_pMouseInput);
ReleaseCOM(g_pD3DDevice);
ReleaseCOM(g_pD3D);
}
如下效果图:
可能大家会有疑问:只绘制了缪缪几行文字,简单统计一下数据,为什么FPS只有60帧左右呢?
DirectX9.0及之后版本在原有基础上做了改进,我们可以通过D3DPRESENT_PARAMETERS.PresentationInterval认为的控制3D设备的刷新行为。
其中,D3DPRESENT_INTERVAL_IMMEDIATE代表即时刷新;D3DPRESENT_INTERVAL_ONE代表屏幕刷新率标准;D3DPRESENT_INTERVAL_DEFAULT为默认方式,与D3DPRESENT_INTERVAL_ONE接近但FPS稍低。
如我们所知,超出显示器刷新频率的刷新标准没有实际意义,而且会空耗D3D设备性能。因此,如果不是效率极端测试,不建议用D3DPRESENT_INTERVAL_IMMEDIATE,只需用D3DPRESENT_INTERVAL_ONE即可。
除去以上三种标准,剩余的还有D3DPRESENT_INTERVAL_TWO、D3DPRESENT_INTERVAL_THREE、D3DPRESENT_INTERVAL_FOUR,它们不太常用,而且使用方式也不像前三种那样单纯,大家感兴趣可自行翻看DX帮助文档,在此不再赘述。
由于ID3DXFont底层是基于Gdi的,因此虽然功能强大,但却在效率上不被看好。不过由于游戏大都以图形元素为主要表现形式,而文字大都为辅助作用,因此在一般情况下上述方法应该是够用的。
以上,谢谢 ^ ^