Direct3D轮回:文字显示及FPS效率统计

组成一个完整游戏的元素大体来说其实只有两种:图形和字体。
前几节涉及的内容大都为图形元素,本节我们来看如何在Direct3D中实现简单的字体绘制~
Direct3D中的文字绘制主要依赖于ID3DXFont接口对象。我们事先构造一个D3DXFONT_DESC结构体,详细描述所要生成字体的信息,而后调用D3DXCreateFontIndirect函数即可获得ID3DXFont接口对象。
那么接下来依然是以人性化接口为目的的二次封装~
/*------------------------------------- 代码清单:D3DFont.h 来自:http://www.cnblogs.com/kenkao -------------------------------------*/ #include "D3DInit.h" #pragma _disibledevent=> 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 #include "mmsystem.h" #pragma _disibledevent=> 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串 };
Direct3D轮回:文字显示及FPS效率统计Direct3D轮回:文字显示及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 #include //---通用全局变量 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); }

如下效果图:
Direct3D轮回:文字显示及FPS效率统计
可能大家会有疑问:只绘制了缪缪几行文字,简单统计一下数据,为什么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的,因此虽然功能强大,但却在效率上不被看好。不过由于游戏大都以图形元素为主要表现形式,而文字大都为辅助作用,因此在一般情况下上述方法应该是够用的。
以上,谢谢 ^ ^
Tags: 

延伸阅读

最新评论

发表评论