在开始编写DirectX11代码之前,我建议先写一个简单的程序框架。这个框架实现基本的窗口功能,组织清晰可读性强同时又能容易的添加代码。当然本教程的目的仅仅是使用DirectX11的新特性,我们会让架构尽可能的简单。
框架包含四个结构。WinMain函数作为程序的进入点。系统类(SystemClass)囊括了所有WinMain需要调用的函数。在系统类里包括一个输入类,用来处理用户的输入,还有一个图形类,来处理 DirectX相关代码。下面是系统结构图
现在我们知道了程序框架的结构,让我们从 main.cpp文件的WinMain 函数开始吧。
////////////////////////////////////////////////////////////////////////////////
// Filename: main.cpp
////////////////////////////////////////////////////////////////////////////////
#include "systemclass.h"
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR pScmdline, int iCmdshow)
{
SystemClass* System;
bool result;
// Create the system object.
System = new SystemClass;
if(!System)
{
return 0;
}
// Initialize and run the system object.
result = System->Initialize();
if(result)
{
System->Run();
}
// Shutdown and release the system object.
System->Shutdown();
delete System;
System = 0;
return 0;
}
WinMain函数尽可能的简单。我们创建了系统类(SystemClass)然后初始化。如果初始化成功,我们则调用系统类(SystemClass)的Run 函数。Run函数会运行自己的循环处理程序的所有消息直到退出。Run函数执行完后,我们会调用ShutDown()关闭系统类(SystemClass)对象,然后删除该类对象。这样我们让执行过程变得非常简单同时系统类(SystemClass)包含了所有的程序功能。现在让我们来看看系统类(SytemClass)的头文件。
Systemclass.h
Systemclass.h
////////////////////////////////////////////////////////////////////////////////
// Filename: systemclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _SYSTEMCLASS_H_
#define _SYSTEMCLASS_H_
我们在这里声明一个WIN32_LEAN_AND_MEAN。这样做是为了加快编译速度,它不包含某些很少使用的
API函数的头文件,减少Win32头文件的大小。
///////////////////////////////
// PRE-PROCESSING DIRECTIVES //
///////////////////////////////
#define WIN32_LEAN_AND_MEAN
Windows.h包含创建和销毁窗口的函数,还有其他很有用的 Win32函数
//////////////
// INCLUDES //
//////////////
#include
我们要包含其他两个类的头文件,这样我们就能在系统类里定义着俩个类。
///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "inputclass.h"
#include "graphicsclass.h"
定义类非常的简单。我们知道Initialize,shutdown,和Run函数在WinMain中被调用。
一些私有函数会被这三个函数调用。我们会在类里添加一个消息响应函数,用来处理运行时发送给窗口系统的消息。
最后我们还要加入两个私有指针变量m_Input和m_Graphics,分别指向处理输入和渲染的类。
////////////////////////////////////////////////////////////////////////////////
// Class name: SystemClass
////////////////////////////////////////////////////////////////////////////////
class SystemClass
{
public:
SystemClass();
SystemClass(const SystemClass&);
~SystemClass();
bool Initialize();
void Shutdown();
void Run();
LRESULT CALLBACK MessageHandler(HWND, UINT, WPARAM, LPARAM);
private:
bool Frame();
void InitializeWindows(int&, int&);
void ShutdownWindows();
private:
LPCWSTR m_applicationName;
HINSTANCE m_hinstance;
HWND m_hwnd;
InputClass* m_Input;
GraphicsClass* m_Graphics;
};
/////////////////////////
// FUNCTION PROTOTYPES //
/////////////////////////
static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
/////////////
// GLOBALS //
/////////////
static SystemClass* ApplicationHandle = 0;
#endif
WndProc函数和ApplicationHandle和ApplicationHandle 指针也包含在这个类中,这样我们就能把 windows系统消息重定向到我们自己写的消息响应函数里。
下面让我们再来看看系统类的源文件
Systemclass.cpp
////////////////////////////////////////////////////////////////////////////////
// Filename: systemclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "systemclass.h"
在类的构造函数中,我初始化对象的指针为空。这很重要,因为如果这些对象的初始化失败Shutdown函数将会尝试释放掉这些对象。如果对象不是空,就会假设它们已经成功初始化,最后需要被释放。在编程时,把指针和对象全部初始化为空是一个良好的习惯。如果你不初始化有可能在编译 release版的时候失败。
这里我写了一个空的拷贝构造函数和空的类析构函数。在这个类里我不会用到它们。但如果不定义,有些编译器会自动为你加上这两个函数的实现,而有些情况下我希望这两个函数为空。
你可以注意到,我没有在析构函数里对类对象进行释放。我把所有的释放工作都放到了 Shutdown函数里,你会在下面的代码中看到。理由就是我不确定析构函数会被调用。像 windows系统函数ExitThread()执行时不会调用类的析构函数,造成内存泄露。当然你能调用这个函数的安全版本,但在 windows下编程我要非常谨慎。
SystemClass::SystemClass(const SystemClass& other)
{
}
SystemClass::~SystemClass()
{
}
接下来是初始化函数Initialize,这个函数执行程序所有的设置。首先调用 InitializeWindows函数来为程序创建窗口。它同时创建和初始化 input类对象和graphic 类对象,程序将通过这两个类处理用户的输入和渲染图形到显示器。
bool SystemClass::Initialize()
{
int screenWidth, screenHeight;
bool result;
// Initialize the width and height of the screen to zero before sending the variables into the function.
screenWidth = 0;
screenHeight = 0;
// Initialize the windows api.
InitializeWindows(screenWidth, screenHeight);
// Create the input object. This object will be used to handle reading the keyboard input from the user.
m_Input = new InputClass;
if(!m_Input)
{
return false;
}
// Initialize the input object.
m_Input->Initialize();
// Create the graphics object. This object will handle rendering all the graphics for this application.
m_Graphics = new GraphicsClass;
if(!m_Graphics)
{
return false;
}
// Initialize the graphics object.
result = m_Graphics->Initialize(screenWidth, screenHeight, m_hwnd);
if(!result)
{
return false;
}
return true;
}
ShutDown函数执行清理任务。它关闭和释放所有图形类和输入类有关的东西。同时关闭窗口,释放掉相应的句柄。
void SystemClass::Shutdown()
{
// Release the graphics object.
if(m_Graphics)
{
m_Graphics->Shutdown();
delete m_Graphics;
m_Graphics = 0;
}
// Release the input object.
if(m_Input)
{
delete m_Input;
m_Input = 0;
}
// Shutdown the window.
ShutdownWindows();
return;
}
Run函数是程序的循环体同时执行所有的程序进程直到我们决定退出。程序的处理是在Frame函数中完成的,每一次的循环都会调用这个函数。这是一个重要的概念,从现在开始,在完成程序剩余部分的时候必须把这个概念牢记在心。下面是这个函数的伪代码:
while not done
check for windowsmessages
process systemmessages
process applicationloop
check if user want toquit during the frame processing
void SystemClass::Run()
{
MSG msg;
bool done, result;
// Initialize the message structure.
ZeroMemory(&msg, sizeof(MSG));
// Loop until there is a quit message from the window or the user.
done = false;
while(!done)
{
// Handle the windows messages.
if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// If windows signals to end the application then exit out.
if(msg.message == WM_QUIT)
{
done = true;
}
else
{
// Otherwise do the frame processing.
result = Frame();
if(!result)
{
done = true;
}
}
}
return;
}
程序所有的处理都在Frame函数中完成。目前这个函数还比较简单,我们判断输入对象看用户是否按下ESC健退出。如果不是,我们会调用图形对象完成框架的处理,图像对象会完成图像的渲染。随着程序的完善,我们会在这里添加更多的代码。
bool SystemClass::Frame()
{
bool result;
// Check if the user pressed escape and wants to exit the application.
if(m_Input->IsKeyDown(VK_ESCAPE))
{
return false;
}
// Do the frame processing for the graphics object.
result = m_Graphics->Frame();
if(!result)
{
return false;
}
return true;
}
LRESULT CALLBACK SystemClass::MessageHandler(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam)
{
switch(umsg)
{
// Check if a key has been pressed on the keyboard.
case WM_KEYDOWN:
{
// If a key is pressed send it to the input object so it can record that state.
m_Input->KeyDown((unsigned int)wparam);
return 0;
}
// Check if a key has been released on the keyboard.
case WM_KEYUP:
{
// If a key is released then send it to the input object so it can unset the state for that key.
m_Input->KeyUp((unsigned int)wparam);
return 0;
}
// Any other messages send to the default message handler as our application won't make use of them.
default:
{
return DefWindowProc(hwnd, umsg, wparam, lparam);
}
}
}
我们把创建窗口的代码写在InitializeWindows函数里,将来我们会通过这个窗口渲染输出图像。调用该函数会返回屏幕的宽和高,这样我们能在程序中使用这两个参数。我们会用默认的参数创建一个无边框的全黑窗口。通过判断FULL_SCREEN这个全局变量,程序能创建一个普通或者全屏的窗口。如果这个变量的值是TRUE生成的窗口会覆盖整个屏幕。如果这个变量的值是FALSE,程序则会在屏幕中间生成一个800x600的窗口。我把FULL_SCREEN全局变量写在了graphicclass.h的开头,方便你去修改它。以后你就会明白我为什么把全局变量放在graphicclass.h文件里而不是这个类的头文件中。
void SystemClass::InitializeWindows(int& screenWidth, int& screenHeight)
{
WNDCLASSEX wc;
DEVMODE dmScreenSettings;
int posX, posY;
// Get an external pointer to this object.
ApplicationHandle = this;
// Get the instance of this application.
m_hinstance = GetModuleHandle(NULL);
// Give the application a name.
m_applicationName = L"Engine";
// Setup the windows class with default settings.
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = m_hinstance;
wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
wc.hIconSm = wc.hIcon;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = m_applicationName;
wc.cbSize = sizeof(WNDCLASSEX);
// Register the window class.
RegisterClassEx(&wc);
// Determine the resolution of the clients desktop screen.
screenWidth = GetSystemMetrics(SM_CXSCREEN);
screenHeight = GetSystemMetrics(SM_CYSCREEN);
// Setup the screen settings depending on whether it is running in full screen or in windowed mode.
if(FULL_SCREEN)
{
// If full screen set the screen to maximum size of the users desktop and 32bit.
memset(&dmScreenSettings, 0, sizeof(dmScreenSettings));
dmScreenSettings.dmSize = sizeof(dmScreenSettings);
dmScreenSettings.dmPelsWidth = (unsigned long)screenWidth;
dmScreenSettings.dmPelsHeight = (unsigned long)screenHeight;
dmScreenSettings.dmBitsPerPel = 32;
dmScreenSettings.dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT;
// Change the display settings to full screen.
ChangeDisplaySettings(&dmScreenSettings, CDS_FULLSCREEN);
// Set the position of the window to the top left corner.
posX = posY = 0;
}
else
{
// If windowed then set it to 800x600 resolution.
screenWidth = 800;
screenHeight = 600;
// Place the window in the middle of the screen.
posX = (GetSystemMetrics(SM_CXSCREEN) - screenWidth) / 2;
posY = (GetSystemMetrics(SM_CYSCREEN) - screenHeight) / 2;
}
// Create the window with the screen settings and get the handle to it.
m_hwnd = CreateWindowEx(WS_EX_APPWINDOW, m_applicationName, m_applicationName,
WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_POPUP,
posX, posY, screenWidth, screenHeight, NULL, NULL, m_hinstance, NULL);
// Bring the window up on the screen and set it as main focus.
ShowWindow(m_hwnd, SW_SHOW);
SetForegroundWindow(m_hwnd);
SetFocus(m_hwnd);
// Hide the mouse cursor.
ShowCursor(false);
return;
}
void SystemClass::ShutdownWindows()
{
// Show the mouse cursor.
ShowCursor(true);
// Fix the display settings if leaving full screen mode.
if(FULL_SCREEN)
{
ChangeDisplaySettings(NULL, 0);
}
// Remove the window.
DestroyWindow(m_hwnd);
m_hwnd = NULL;
// Remove the application instance.
UnregisterClass(m_applicationName, m_hinstance);
m_hinstance = NULL;
// Release the pointer to this class.
ApplicationHandle = NULL;
return;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT umessage, WPARAM wparam, LPARAM lparam)
{
switch(umessage)
{
// Check if the window is being destroyed.
case WM_DESTROY:
{
PostQuitMessage(0);
return 0;
}
// Check if the window is being closed.
case WM_CLOSE:
{
PostQuitMessage(0);
return 0;
}
// All other messages pass to the message handler in the system class.
default:
{
return ApplicationHandle->MessageHandler(hwnd, umessage, wparam, lparam);
}
}
}
为了保证教程简单,直到我写有关DirectInput(属于高级篇)教程之前,我会使用windows的输入方式。输入类处理用户来自键盘的输入。这个类的输入来自SystemClass::MessageHandler函数。
输入对象会把每个键的状态放入keyboard数组中。当遍历这个数组的时候,函数就能知道某个键是否被按下。下面是头文件。
////////////////////////////////////////////////////////////////////////////////
// Filename: inputclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _INPUTCLASS_H_
#define _INPUTCLASS_H_
////////////////////////////////////////////////////////////////////////////////
// Class name: InputClass
////////////////////////////////////////////////////////////////////////////////
class InputClass
{
public:
InputClass();
InputClass(const InputClass&);
~InputClass();
void Initialize();
void KeyDown(unsigned int);
void KeyUp(unsigned int);
bool IsKeyDown(unsigned int);
private:
bool m_keys[256];
};
#endif
////////////////////////////////////////////////////////////////////////////////
// Filename: inputclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "inputclass.h"
InputClass::InputClass()
{
}
InputClass::InputClass(const InputClass& other)
{
}
InputClass::~InputClass()
{
}
void InputClass::Initialize()
{
int i;
// Initialize all the keys to being released and not pressed.
for(i=0; i<256; i++)
{
m_keys[i] = false;
}
return;
}
void InputClass::KeyDown(unsigned int input)
{
// If a key is pressed then save that state in the key array.
m_keys[input] = true;
return;
}
void InputClass::KeyUp(unsigned int input)
{
// If a key is released then clear that state in the key array.
m_keys[input] = false;
return;
}
bool InputClass::IsKeyDown(unsigned int key)
{
// Return what state the key is in (pressed/not pressed).
return m_keys[key];
}
////////////////////////////////////////////////////////////////////////////////•-
// Filename: graphicsclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _GRAPHICSCLASS_H_
#define _GRAPHICSCLASS_H_
//////////////
// INCLUDES //
//////////////
#include
/////////////
// GLOBALS //
/////////////
const bool FULL_SCREEN = false;
const bool VSYNC_ENABLED = true;
const float SCREEN_DEPTH = 1000.0f;
const float SCREEN_NEAR = 0.1f;
程序运行的时候需要这四个变量
////////////////////////////////////////////////////////////////////////////////
// Class name: GraphicsClass
////////////////////////////////////////////////////////////////////////////////
class GraphicsClass
{
public:
GraphicsClass();
GraphicsClass(const GraphicsClass&);
~GraphicsClass();
bool Initialize(int, int, HWND);
void Shutdown();
bool Frame();
private:
bool Render();
private:
};
#endif
目前我让这个类空着,因为本次的教程仅仅是要完成框架。
////////////////////////////////////////////////////////////////////////////////
// Filename: graphicsclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "graphicsclass.h"
GraphicsClass::GraphicsClass()
{
}
GraphicsClass::GraphicsClass(const GraphicsClass& other)
{
}
GraphicsClass::~GraphicsClass()
{
}
bool GraphicsClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
return true;
}
void GraphicsClass::Shutdown()
{
return;
}
bool GraphicsClass::Frame()
{
return true;
}
bool GraphicsClass::Render()
{
return true;
}
现在我们有了一个框架和一个能显示在屏幕正中间的窗口。以后的教程都会以这个框架为基础,所以理解这个框架的工作方式是非常重要的。在进入下一个教程之前请试着做下面的练习确保代码正确并能运行。如果你不理解这个框架,也能进入到接下来的教程,在以后的教程中你会对整个框架有更深的理解。
练习
把FULL_SCREEN参数改为true,重新编译运行程序。按下ESC键退出程序。m