学习笔记30——DirectX框架

首先,这一节开始就要接触DX了,希望大家能够把前面讲的游戏程序框架、数学基础和渲染管线相关的内容,能够有一个很好的掌握。然后今天正式开启咱们的旅途!

这里D3D是需要环境配置的,因为我的环境就是按照X_Jun教程搭建的,所以你直接按照他教程中写的环境配置一步步跟着走就行了新建项目 (directx11.tech)

我在这里强调几个点:

第一

如果你要按照我写的教程去学习,这里你暂且不要拿X_Jun的01项目导入,你可以导入下面的一个代码,我在这里第一个给你的项目,是对他的01项目进行了改写,进行了简化,目的就是先别给你那么多附加功能。

第二、

这个一定要重视,一定要把以前配置的东西都给弄干净。主要就是把你SDK删除,然后把以前在VS属性中配置的东西给他删掉,具体的 第三、如果你该过程中出现了一些问题,X_Jun也帮你把坑踩过了。下面链接:DirectX11--教程项目无法编译、运行的解决方法 - X_Jun - 博客园 (cnblogs.com)

一、程序的上层结构介绍

咱们之前在写第一节程序框架的时候说了一点,就是我们会在以后把游戏相关的函数变量啥的给封装成一个类,这也就是咱们今天要做的事情。

但是实际上封装的不是一个类,而是两个类,父子俩。为啥呢?因为咱们是为了有些代码的复用,就是在用DirectX进行写游戏程序的时候,有很多的操作无论在哪一个游戏程序中都是一样写的,所以咱们对于这些哪个游戏程序都一样的东西,封装到一个类中:D3DApp,以后每写一个程序都把这个直接复制拿过去用就行了。这里还会对D3DApp中创建几个纯虚函数,让子类去重写,因为子类函数中有些可能只是内部实现不同,但是框架相同,所以把这部分框架也给拉出来放父类中。

然后我们对于那些每一个游戏特有的一些东西,各不相同的东西,专门为此游戏设计的一些函数等给封装到一个GameApp类,然后这个类需要继承上边的D3DApp。

有了俩类之后,在WinMain程序中,创建出一个对象,然后进行函数的调用程序跑起来就ok了。先来瞥一眼咱们的代码框架!

二、概念介绍

打个预防针,下面的东西会有很多是你从未接触过的用法、概念等等的。

但是你不用怕,其实我在初学的时候,也是看到这样类似的很多东西,完全看不懂。后来是怎么懂的呢?就是你从宏观上先理解程序,程序的架子是啥给他抓住了,有些函数你知道为啥设计这个函数或者设计他是用来干啥的就行了,怎么写的实现,第一遍看程序的时候咱们先不关心这些。

然后就是第二遍,开始尝试着去一点点的啃细节,这时候可能有一些更细的东西,你还是可以先略过的,第二遍只要比第一遍深入一点点就是有进步的。如果这一遍过后还是不理解,你要看看你是不是缺少什么前置知识,去搜一些相关的资料,补充后再看看。或者说你先跳过去,因为有些东西得等到你知道的更多了你才能更好的理解。

就是对于实现你可以不深究,但是他是用来干啥的,为啥要有他你要弄清楚。比如说D3DApp中有一个初始化D3D的程序,可以说是比较复杂,你第一遍看,你就,哦,原来这个叫InitDirect3D的函数是用来初始化D3D程序的,然后你把他当成printf就行了。

啊??把他当成打印的函数,当然不是了,哈哈。因为对于printf来说绝大多数人都是拿他直接用,从不去纠结他里面是怎么实现的,知道他是用来打印,怎么用它打印就行了。甚至有很多人都不知道printf还有返回值,只知道参数作用就够用了,哈哈哈~~~

当然这里不是鼓励大家一直当成printf,当你前两遍的时候可以这么去想,但是再去看的时候就要开始看细节了。这里给大家一篇文章:编程巨星的唯一秘诀_浅墨_毛星云的博客-CSDN博客


Device 和 DeviceContext

咱们正式开始代码部分,这里呢我会先给你讲解框架,了解框架之后对于程序有了一个整体宏观的认知之后,再去给你讲具体函数的实现。 

先补充概念,我这里讲概念也不会给你讲太过于书面话的定义,所以讲出来可能并不是那么精确,但是对于你理解问题是完全ok的。

首先咱们之前讲过GPU,他是处理图形的硬件设备,来加速图像的绘制工作的。而这个GPU怎么应用起来,让他帮我们在游戏程序中加速处理图形绘制。GPU加速是把渲染工作从CPU转运到GPU,然后通过GPU的渲染硬件来渲染。

那么咱们的问题就成了怎么去把渲染工作挪到GPU上?(这个就涉及到CPU和GPU的一些交互,之前也说过,具体内容后面会讲)怎么使用GPU的渲染硬件来渲染?GPU硬件渲染就是一个流水线,所以你要知道怎么使用并控制这个流水线。那就是今天的主题DirectX了,你就是通过DirectX来使用控制这个流水线的。

DirectX咱们之前说过是图形API,那么图形API就相当于一个指挥官,他就是用来对流水线上干的活进行指挥控制的,具体怎么指挥你说了算,你是调用API的人,从而生产出按照你的想法画出来的东西。

但是这里DirectX的X就像一个万能的符号一样(通配符),它可以表示3D,也可以表示2D等等的,也就是说DirectX表示的是Direct 3D 和Direct 2D还有其他各种Direct 。。家族成员,所以Direct是一个大家族。

那么家族中最重要的人是谁?就是Direct3D,就是简称D3D的家伙。从名字上可以看出来,他是用于画3D场景的。而我们写的3D游戏程序核心的工具就是他。

上边说D3D这些东西就是指挥控制流水线的,主要指挥流水线两件事:一个是物资安排,就比如生产盒饭,盒我得找个地放放下,一会一个个放在生产线上,饭我得找个盆盛着,一会往盒里装。另一个是生产方式,就是人员怎么安排,装盒饭的方式是什么等等的。

这也就对应到D3D的两个核心的东西:一个是安排流水线物资的ID3D11Device,他叫设备,他是用来创建各种资源,他干的活就类似:收拾个地方来放饭盒,刷干净一个盆子来盛饭(待会装进饭盒),结合程序来说就是:你的顶点纹理这些数据你要弄到GPU上去,你需要在GPU那边分配一个缓冲区来存放这些资源,这个分配缓冲区就是ID3D11Device的任务之一。

另外一个就是ID3D11DeviceContext,他是设备上下文,用来指挥生产方式的,就涉及到你们饭盒怎么装,装的量是多少等生产的细节,结合程序就是:一些渲染状态的设置,和一些渲染命令等(你听不懂这俩词完全没问题,看懂前面指挥生产方式就行了)。这里他还会干一件事,就是说数据从CPU到了GPU,但是并不代表就上了流水线,就类似:你一个流水线屋子里有很多的饭,但是接下来要给一些老外盛饭,他们饮食习惯不太一样,你就要盛A盆子里面的饭而不是B盆子里的。那么也就是说你要指定从哪一个盆子里盛饭。对应到程序就是:将那些已经在GPU上的资源绑定到流水线上!(比如说你有两组顶点,A组是画一棵树,B组是画一个三角形,这俩都在GPU的缓冲区里面,你要画一个森林场景,你肯定不能用B,你要指定用上A,然后流水线对A进行操作)

终于讲完了这俩最核心的概念。咱们先瞅一眼他们在代码中是怎么用

ComPtr<ID3D11Device> m_pd3dDevice;                    // D3D11设备创建
m_pd3dDevice->CreateRenderTargetView();
m_pd3dDevice->CreateTexture2D();
m_pd3dDevice->CreateDepthStencilView();
dxgiFactory1->CreateSwapChain(m_pd3dDevice.Get());


ComPtr<ID3D11DeviceContext> m_pd3dImmediateContext;   // D3D11设备上下文创建
m_pd3dImmediateContext->OMSetRenderTargets();
m_pd3dImmediateContext->RSSetViewports();

注意上边的函数我省略了很多的参数,我这里只是给你感受一下,不看细节。(参数多到一行写不下,都换行了,所以这里先不拿出来了,吓着你了~~~:)

首先第一行,他就是创建一个ID3D11Device类型的指针,但是ComPtr<ID3D11Device>好吓人,完全看不懂,没事你把这句就类比  int a;a的int类型的一个变量。那么这里m_pd3dDevice就是一个ComPtr<ID3D11Device>类型的对象,也就是我们说的ID3D11Device的指针。待会我会解释为啥这个类型那么奇怪。

然后就是下面的四行都是这个变量的应用,你会发现前三个都是这个指针调用了一些类内的成员函数,都是CreateXXX,最后也是一个CreateXXX,只不过是把他当成参数传进去了。所以说,这个Device主要的作用就是创建一些东西。

类似的看下面三行,先创建一个设备上下文,然后调用的都是XXSetXX,都是设置一些什么东西,设置什么也就是所谓的指挥控制了。


刚刚说到ComPtr<ID3D11Device>,这么一个奇怪的类型,咱们现在就来攻克他。

这里涉及到两个东西要讲,一个是关于ID3D11Device类型,因为你了解了这个类型的特殊性之后,你才能了解为什么会有ComPtr这个东西的出现,从而了解他是怎么用的。

先提前说一下,就是上边的ComPtr的写法是来帮助我们的,但是你可以不用,你不用他就需要做一些其他额外工作。比如下面这么定义一个对象也是可以的。

ID3D11Device* m_pd3dDevice;

那么我们讲解的思路就是先没有ComPtr给你讲清楚,然后把为啥加上ComPtr说明白。

那就开始介绍ID3D11Device类型,首先不光是他,我要联系着其他的类型一起讲。看下面

	ComPtr<ID2D1Factory> m_pd2dFactory;							// D2D工厂
	ComPtr<ID2D1RenderTarget> m_pd2dRenderTarget;				// D2D渲染目标
	ComPtr<IDWriteFactory> m_pdwriteFactory;					// DWrite工厂
	// Direct3D 11
	ComPtr<ID3D11Device> m_pd3dDevice;							// D3D11设备
	ComPtr<ID3D11DeviceContext> m_pd3dImmediateContext;			// D3D11设备上下文
	ComPtr<IDXGISwapChain> m_pSwapChain;						// D3D11交换链
	// Direct3D 11.1
	ComPtr<ID3D11Device1> m_pd3dDevice1;						// D3D11.1设备
	ComPtr<ID3D11DeviceContext1> m_pd3dImmediateContext1;		// D3D11.1设备上下文
	ComPtr<IDXGISwapChain1> m_pSwapChain1;						// D3D11.1交换链
	// 常用资源
	ComPtr<ID3D11Texture2D> m_pDepthStencilBuffer;				// 深度模板缓冲区
	ComPtr<ID3D11RenderTargetView> m_pRenderTargetView;			// 渲染目标视图
	ComPtr<ID3D11DepthStencilView> m_pDepthStencilView;			// 深度模板视图

你会发现长这么丑的东西不只他俩,这里有一大坨。咱们先不看外部的ComPtr,只看里面的,你会发现他们都是IXXX,也就是说他们都是一个I开头的。然后是D,其实这个D就表示Direct,后面再接着就是什么2D,Write,3D,XGI,还记得咱们说过DirectX的X是个通配符可以表示2D,3D等等的,其实这里就是了,最后一个DXGI表示DirectX Graphics Infrastructure这些具体后面再说。

要注意的是他们都有一个共同的I开头,也就是他们都是COM接口。接下来开始解释COM接口的概念。

COM接口

既然上边咱说了定义一个对象原始的模样是下面这样,

ID3D11Device* m_pd3dDevice;

这些所有的I开头的都是COM接口,你可以把他们当作一个C++类来使用,就比如别人封装了一个类,你拿过来使用,你咋用,创建一个对象,然后让对象调用里面的各种函数嘛!

你要使用COM接口,也就是这个类似C++类的东西,那就创建一个他的对象,就叫COM接口对象,咱们创建出来就是m_pd3dDevice。然后就用他调用里面的函数就行了,不就是上边写的么

m_pd3dDevice->CreateRenderTargetView();
m_pd3dDevice->CreateTexture2D();
m_pd3dDevice->CreateDepthStencilView();

按照C++来说,你创建一个指向该类的指针,本来是要用new的,然后最后等你用完了delete一下。而COM接口就不一样了,他有他自己的回收机制,那么怎么创建呢,又如何回收呢?

对于创建:我们必须通过特定的函数或其他的COM接口方法来获取指向COM接口的指针(这句来自龙书11)。注意这里的创建不是

ID3D11Device* m_pd3dDevice;

类似上边一这句的创建,他说的创建主要是赋值,明确的指向谁了,这才算是被创建出来,否则你只是创建出来一个指向空的指针,啥也不能干和没创建没啥区别。(你可能会疑问,我也没写他等于nullptr啊,为啥就是空指针了,类成员会在构造函数中初始化,在那你就可以看到初始化为了nullptr)。光说这个特定函数你可能没有啥感觉,给你贴个代码:

D3D11CreateDevice(
                  nullptr, d3dDriverType, nullptr, createDeviceFlags, featureLevels,                     
                  numFeatureLevels,
			      D3D11_SDK_VERSION, m_pd3dDevice.GetAddressOf(), &featureLevel, 
                  m_pd3dImmediateContext.GetAddressOf()
                  );

这里那么长只是一个函数调用,参数多罢了。我想说的地方是最后一个参数和倒数第三个参数,这里他们 .GetAddressOf() 了一个操作,看英文也知道就是取出他们的地址(对于.GetAddressOf() 后面会详细说)。所以这里的这个函数的作用就是把咱们创建的原本是个空指针的东西,给他指向正确的位置,不再指向空,这样就能正常使用起来了。

对于回收,这里先说一下他的回收机制,就是对于这么一个指针,指向一个COM对象,首先你能给他创建很多的指针指向它(你这里相当于指针的复制,你可能需要调用AddRef),他们都共同指向一个接口实例,这个接口实例内部会记录有几个人指着他。咱们不会去手动释放这个接口实例,咱们是把指针释放了,你释放之后对象会知道少了一个人指他,那这个接口实例到底什么时候释放呢?等指向它的所有的指针都释放了,也就是指针再指向他了,那么他的引用数(也就是几个人指他)就变成了0,他的内存会自动释放掉。

总结来说就是什么呢?如果你要释放掉接口实例的那块内存,你就得把所有指向该接口实例的指针给释放掉。否则的话就会引起内存泄漏,这个bug无疑是最难调的bug之一。那么我们对于释放怎么操作?以为这些I开头的COM接口,都继承了一个叫IUnknown的接口类,这个类里面有一个方法就是专门用来释放的就是Release。我们通常会写一个宏,来对Release进行调用:

#define SAFE_RELEASE(p) { if(p) { (p)->Release(); (p)=NULL; } }

也就是先判断p是否为空,然后再进行释放调用并置空,这一点和C++释放差不多的,之前有一篇关于这里是否判断的文章,大家可以看一下。C++指针delete是否需要判空

但是这么搞会麻烦一点,一个接口实例有仨指针指向它,完了最后你少释放一个,那就内存泄露了,所以人工这么释放有点麻烦,所以我们就可以采用一种新的方式,那就是为啥你的那些类型外面都套了一个ComPtr。

在讲ComPtr之前,先对COM内容补充完:

首先我给你说一个概念叫接口:接口是包含了函数指针数组的内存结构,其中每一个数组元素包含的是一个由组件所实现的函数地址。(来自毛星云的书《Windows游戏编程之从零开始》这本书唯一不好的地方就在于里面讲的大多都是DX9 的东西,而DX9现在已经快要退出舞台了)。

说白了接口就是一个数组,数组里面装有很多的函数地址,我有了函数地址就能去调用相应的函数。那么这里还要说一个概念叫COM组件:COM组件就是很多小的二进制可执行程序的合集,你不是有一个个的COM接口么,接口里面的函数,就是COM组件中的可执行程序。这里就是COM牛逼的一个地方,他是二进制的,就和语言无关了。所以:

组件对象模型(COM)技术使 DirectX 独立于任何编程语言,并具有版本向后兼容的特性。(龙书)

想要了解更多:COM组件开发(一)—— 对象与接口。想当初我学这块的时候,太深的拓展太多的看不懂,太浅的吧,又觉着说的不够,不能把他说的都理解,也废了不少时间。这里你把我上边说的理解了就完全够用。

下面咱们开始智能指针的内容:

智能指针ComPtr

ComPtr智能指针 (directx11.tech)

这里直接给出X_Jun教程中讲的ComPtr的链接。我对于这个东西很大一部分内容都是从这篇文章中学的。

我在这里就大致的说一下:首先这个ComPtr干啥用的,就是你的指针如果是套了ComPtr的,那就是智能指针,怎么个智能?就是当这个指针已经出了他的作用域时,会自动释放掉,这样我们就不必手动释放了,只需要在创建的时候套一个ComPtr。你从他的名字上就能看出来,这玩意是专门为COM来服务的,其他人一般也没有这样的需求。

接下来说一下他是怎么使用的:首先他是一个模板类,类似的你可以类比vector,他也是一个模板类,通常会 vector<int>  v;  这么写。我定义一个容器v,容器里面装int。

那么你怎么理解这个ComPtr<ID3D11Device>   m_pd3dDevice,就是我定义一个智能指针,他叫m_pd3dDevice。它有一些方法和运算符重载:

 (图片取自X_Jun博客,上边有链接)

这里多说一点,他多出的这几个方法是用  .  调用的,也就是  m_pd3dDevice.Get()而不是m_pd3dDevice->Get()。因为咱定义的是ComPtr类的对象,不是ComPtr类的指针,所以就 .xx 来调用这些个方法。

实际上是他在内部维护了一个ID3D11Device的指针,从而把->给重载了,ComPtr对象使用->就相当于内部维护的对象来使用->。这里大家可以转到ComPtr定义看一下。这里确实在类内部创建了一个T类型的指针,并且也重载了->。

 这块对C++做一个补充:就是重载->的时候,实际返回的是一个指针类型。而你现在重载ComPtr<ID3D11Device>类型,返回了某个指针类型,那么我用的时候是不是该这么用:m_pd3dDevice->->CreateTexture2D();  【m_pd3dDevice->】这一部分得到一个指针,然后指针再进行->,但是实际上你用一个就行了,这个点记住就行了。

这里还要说一下using的用法,这里还是直接就搬运链接了:C++11使用using定义别名(替代typedef) (biancheng.net)

至此咱们的COM接口和智能指针就介绍完了。回到开头的问题ComPtr<ID3D11Device>这个类型现在是不是熟悉了,是不是能对他知其然并且知道所以然了?


三、D3DApp类介绍

先贴出来代码,然后一一讲解。讲解过程中可能会涉及到一些基础的东西,我也会稍微讲一下,更多的时候我会直接给你个链接,或者说直接从别的网站上趴下来一些教程贴在这。好下面咱们开始:

D3DApp类代码纵览

#ifndef D3DAPP_H
#define D3DAPP_H

#include <wrl/client.h>
#include <string>
#include <d3d11_1.h>
#include <DirectXMath.h>

class D3DApp
{
public:
	D3DApp(HINSTANCE hInstance);              // 在构造函数的初始化列表应当设置好初始参数
	virtual ~D3DApp();

	//利用函数对外开放数据!
	HINSTANCE AppInst()const;                 // 获取应用实例的句柄
	HWND      MainWnd()const;                 // 获取主窗口句柄
	float     AspectRatio()const;             // 获取屏幕宽高比

	int Run();                                // 运行程序,进行游戏主循环

	                                          // 框架方法。客户派生类需要重载这些方法以实现特定的应用需求
	virtual bool Init();                      // 该父类方法需要初始化窗口和Direct3D部分
	virtual void OnResize();                  // 该父类方法需要在窗口大小变动的时候调用
	virtual void UpdateScene(float dt) = 0;   // 子类需要实现该方法,完成每一帧的更新
	virtual void DrawScene() = 0;             // 子类需要实现该方法,完成每一帧的绘制
	virtual LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
	// 窗口的消息回调函数
protected:
	bool InitMainWindow();      // 窗口初始化
	bool InitDirect3D();        // Direct3D初始化


protected:

	HINSTANCE m_hAppInst;        // 应用实例句柄
	HWND      m_hMainWnd;        // 主窗口句柄
	bool      m_Minimized;       // 应用是否最小化
	bool      m_Maximized;       // 应用是否最大化
	bool      m_Resizing;        // 窗口大小是否变化
	bool	  m_Enable4xMsaa;	 // 是否开启4倍多重采样
	UINT      m_4xMsaaQuality;   // MSAA支持的质量等级




	// 使用模板别名(C++11)简化类型名
	template <class T>
	using ComPtr = Microsoft::WRL::ComPtr<T>;
	// Direct3D 11
	ComPtr<ID3D11Device> m_pd3dDevice;                    // D3D11设备
	ComPtr<ID3D11DeviceContext> m_pd3dImmediateContext;   // D3D11设备上下文
	ComPtr<IDXGISwapChain> m_pSwapChain;                  // D3D11交换链
	// 常用资源
	ComPtr<ID3D11Texture2D> m_pDepthStencilBuffer;        // 深度模板缓冲区
	ComPtr<ID3D11RenderTargetView> m_pRenderTargetView;   // 渲染目标视图
	ComPtr<ID3D11DepthStencilView> m_pDepthStencilView;   // 深度模板视图
	D3D11_VIEWPORT m_ScreenViewport;                      // 视口

	// 派生类应该在构造函数设置好这些自定义的初始参数
	std::wstring m_MainWndCaption;                       // 主窗口标题
	int m_ClientWidth;                                   // 视口宽度
	int m_ClientHeight;                                  // 视口高度
};

#endif // D3DAPP_H

 代码逐模块解析

防卫式声明

下面我会单独贴出来一块块的代码,然后讲解。

#ifndef D3DAPP_H
#define D3DAPP_H

。。。。

#endif // D3DAPP_H

上边这一部分是防卫式声明。

头文件

#include <wrl/client.h>
#include <string>
#include <d3d11_1.h>
#include <DirectXMath.h>

首先第一个头文件,是ComPtr的头文件,加上就行了。

第二个就是字符串相关的,程序里面会有各种名字啥的,你肯定会用的上的。

第三个就是关于D3D11的头文件,因为教程会涉及到D3D11.1的东西所以这里包含的d3d11_1.h 的头文件,这个头文件你转到定义然后利用ctrl + f快捷键进行搜索功能,搜索#include,就会发现这个头文件里面包含了d3d11.h的头文件。

最后一个头文件是数学库,之前咱们说的那些矩阵向量等等的东西,那些怎么在代码中表示就是用的这个库里面的东西。这块多说一句关于硬件加速的SIMD运算,首先这个运算是指的:现代微处理器能用一个指令并行的对多个数据执行数学运算。(本句来自《游戏引擎架构》)也就是说硬件牛逼了,然后原来一次算一个现在一次算多个比如4个,特别适合于4D向量或者4x4的矩阵的运算,这也是我们游戏中需要的东西。咱们的这个数学库就采用了这一硬件加速。了解更多:

奇妙之旅:SIMD加速矩阵运算__luna的博客-CSDN博客_simd 矩阵乘法

其实咱们的第一个项目并没有用任何的数学相关的东西,为啥要把它包含进来呢?因为它里面有assert.h头文件,这个assert关键字 我们是用了的。

如果你对这个assert不熟悉,那么趁着这个机会去看看。 断言(assert)的用法 | 菜鸟教程 (runoob.com)

注意这个玩意是只用于Debug模式下的。

class结构

class D3DApp
{
public:
	//对外开放的函数
protected:
	//不对外开放的函数
protected:
    //数据
};

这是class的整体结构,首先把要实现的一些函数开放出去来使用不必说了吧,这里要说的一点是:为啥这里用了protected,因为儿子要访问这些数据和函数。(尤其是数据,对于函数可以写成private,因为这块的函数是被父类public中函数服务的,儿子最终要的是public中的函数。)如果不熟悉这些,下面:

C++中public、protected、private的区别​​​​​​

最开始说我是搬运工,现在真的已经精通搬运了,哈哈哈哈:)~~~

方法之间的结构

//-------------------------------public方法------------------------------------------
    D3DApp(HINSTANCE hInstance);              // 在构造函数的初始化列表应当设置好初始参数
	virtual ~D3DApp();

	//利用函数对外开放数据!
	HINSTANCE AppInst()const;                 // 获取应用实例的句柄
	HWND      MainWnd()const;                 // 获取主窗口句柄
	float     AspectRatio()const;             // 获取屏幕宽高比

	int Run();                                // 运行程序,进行游戏主循环
	
    // 框架方法。客户派生类需要重载这些方法以实现特定的应用需求
	virtual bool Init();                      // 该父类方法需要初始化窗口和Direct3D部分
	virtual void OnResize();                  // 该父类方法需要在窗口大小变动的时候调用
	virtual void UpdateScene(float dt) = 0;   // 子类需要实现该方法,完成每一帧的更新
	virtual void DrawScene() = 0;             // 子类需要实现该方法,完成每一帧的绘制
	virtual LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
//-------------------------------protected方法------------------------------------------
	bool InitMainWindow();      // 窗口初始化
	bool InitDirect3D();        // Direct3D初始化

这里先不讲构造函数,因为构造函数里面就是初始化数据为主,你想要理解有啥类里面有啥数据,你得理解这个类要干啥,那么咱们先讲明白这个类要干啥,那也就是先介绍类里面的核心的函数。

先说思路,我现在要把游戏相关的东西给封装到一个class中,那么也知识给写到了类中,原本该创建窗口的还是创建窗口,还是和咱们之前讲的架构一样的。

由于有些东西进行了封装,所以我这里重新帮你捋捋程序的结构:你先尝试着回顾一下之前讲过的游戏的框架是怎么样的,如果你忘记了,一定要先去复习。这里加了一些额外的东西,因为咱们接下来的程序是需要使用DirectX来渲染的,需要DirectX程序相关的初始化和使用操作。

先看初始化:Init会进行窗口的初始化和Direct3D 的初始化,初始化窗口的时候需要指定窗口处理函数,初始化Direct3D的时候会调用一个OnResize。

然后就run起来了:Run调用,里面主要还是UpdateScene和DrawScene函数了。

讲实现之前先看一下d3dApp.cpp文件,除了各种实现之外,还加了一个头文件,如下

#include <sstream>

 c++标准库sstream的用法_svdalv的博客-CSDN博客_sstream

关于为啥加他,上边链接看看用法!

下面对函数一一讲解:

初始化程序

bool D3DApp::Init()
{
	if (!InitMainWindow())
		return false;

	if (!InitDirect3D())
		return false;

	return true;
}

就是对InitMainWindow和InitDirect3D进行调用,这里的返回值是来自上边俩函数的初始化返回值。总之来说就是成功返回true,失败返回false。

顺序的话就是:创建窗口,再进行初始化D3D,因为这玩意是为窗口进行渲染的,所以你得把窗口的大小尺寸啥的在初始化窗口时给指定清楚了,然后让Direct3D对照着你创建出来的窗口的属性进行D3D的初始化。其实说白了就是D3D初始化过程会用到窗口的宽高和句柄等属性。(句柄你不懂,别着急,你知道他是窗口的属性就行了,后面会介绍。)

对于这个Init函数,他是一个虚函数,因为子类可能需要对他进行重写,也就是说子类的初始化可能不只父类初始化的这些,可能有他自己额外需要初始化的内容。那么这时候就要写成虚函数了。但是为啥不写成纯虚函数,因为写成纯虚函数就不是在父类定义了,就直接父类定义一个纯虚函数,用来规范派生类的行为而已。

对于这块小总结一下:

1、如果子类中完全一样的东西,父类直接写成普通成员函数就行(这里的Run)。

2、如果子类中有一部分是完全一样的,还可能有不一样的,父类把完全一样的部分写成虚函数(这里的 Init 和 OnResize )

3、如果子类中必然有这个东西,但是找不出确定性的公共的东西,那么父类写成纯虚函数。(这里的UpdateScene 和 DrawScene)。

初始化窗口

bool D3DApp::InitMainWindow()
{
   //----------------------------注册窗口-----------------------------
	WNDCLASS wc;
	wc.style = CS_HREDRAW | CS_VREDRAW;
	wc.lpfnWndProc = MainWndProc;
	wc.cbClsExtra = 0;
	wc.cbWndExtra = 0;
	wc.hInstance = m_hAppInst;
	wc.hIcon = LoadIcon(0, IDI_APPLICATION);
	wc.hCursor = LoadCursor(0, IDC_ARROW);
	wc.hbrBackground = (HBRUSH)GetStockObject(NULL_BRUSH);
	wc.lpszMenuName = 0;
	wc.lpszClassName = L"D3DWndClassName";

	if (!RegisterClass(&wc))
	{
		MessageBox(0, L"RegisterClass Failed.", 0, 0);
		return false;
	}
   //----------------------------创建窗口-----------------------------
	// Compute window rectangle dimensions based on client area dimensions requested.
	RECT R = { 0, 0, m_ClientWidth, m_ClientHeight };
	AdjustWindowRect(&R, WS_OVERLAPPEDWINDOW, false);
	int width = R.right - R.left;
	int height = R.bottom - R.top;

	m_hMainWnd = CreateWindow(L"D3DWndClassName", m_MainWndCaption.c_str(),
		WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, width, height, 0, 0, m_hAppInst, 0);
	if (!m_hMainWnd)
	{
		MessageBox(0, L"CreateWindow Failed.", 0, 0);
		return false;
	}
   //----------------------------展示窗口-----------------------------
	ShowWindow(m_hMainWnd, SW_SHOW);
	UpdateWindow(m_hMainWnd);//send a message WM_PAINT to window. 

	return true;
}

这里和之前的代码几乎一样(改动在于窗口大小是按照客户区指定的),我说几个点:

1、在WinMain创建的时候会有一个参数,hInstance,因为WinMain代表着一个程序的运行,所以说这个句柄就是该程序的句柄。

关于句柄是什么?下面引用一个知乎的回答,讲的挺通俗的!

--------------------------------------------------------------------------------------------------------------------------

句柄就是个数字,一般和当前系统下的整数的位数一样,比如32bit系统下就是4个字节。

这个数字是一个对象的唯一标示,和对象一一对应。

这个对象可以是一个块内存,一个资源,或者一个服务的context(如 socket,thread)等等。

这个数字的来源可以有很多中,只要能保证和它代表的对象保持唯一对应就可以,比如可以用内存地址,也可以用句柄表的序号,或者干脆用一个自增ID,再或者用以上的值去异或一个常数。

传统上操作系统内核和系统服务API都是 C 语言接口的,但是其内部设计理念上又是OO的,所以有对象概念却没有对应的语言语法支持。

句柄的作用就是在 C 语言环境下代替 C++ 的对象指针来用的。

创建句柄就是构造,销毁句柄就是析构,用句柄调用函数相当于传入this指针。

如果有系统API是 C++ 接口的,那么就没有句柄了,而是某个接口指针,IXXXPtr之类的,比如Windows的com ptr。



作者:姚冬
链接:https://www.zhihu.com/question/27656256/answer/37556901
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

--------------------------------------------------------------------------------------------------------------------------

这个程序的句柄和创建窗口有什么关系,这个句柄会在构造函数的时候被拷贝到成员变量m_hAppInst中去。然后等到创建窗口的时候直接填进去就行了。

2、对于下面的代码:

	RECT R = { 0, 0, m_ClientWidth, m_ClientHeight };
	AdjustWindowRect(&R, WS_OVERLAPPEDWINDOW, false);
	int width = R.right - R.left;
	int height = R.bottom - R.top;

AdjustWindowRect这个函数,我不讲,给你链接自己看:AdjustWindowRect function (winuser.h) - Win32 apps | Microsoft Docs

 因为里面的英文很简单,如果下定决心要走游戏这条路,英文能力必须要培养起来。

3、对于创建窗口的返回值现在要存到成员变量中m_hMainWnd。

剩余的东西再说

C++ win32窗口创建详解 - osc_57h7mkgj的个人空间 - OSCHINA - 中文开源技术交流社区

(78条消息) 创建一个Win32窗口_douzhq的博客-CSDN博客_win32窗口

Creating a Window - Win32 apps | Microsoft Docs

消息处理函数

初始化窗口的时候需要指定消息处理函数,这个东西咱们在类里面有写:

virtual LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

你可能会认为,之前我们在写程序框架的时候直接把函数名,也就是函数地址赋值给相应的成员就行了。但是这里不一样,因为对于创建窗口时填写的消息处理函数有一个要求就是他不能是成员函数。所以这里就要在d3dApp.cpp中多加上以下代码:

namespace
{
	D3DApp* g_pd3dApp = nullptr;
}

LRESULT CALLBACK
MainWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	return g_pd3dApp->MsgProc(hwnd, msg, wParam, lParam);
}

然后再把MainWndProc赋值到初始化窗口时相应的位置上。

解释一下:就是他不能用成员函数,那么我就写一个全局的函数,然后全局的函数里面我去调用成员的就行了,但是调用我需要有一个成员或者指针我才能调用啊,那么我可以创建一个指针,然后把他放在匿名空间中,让这个指针只在当前文件中可以被访问。他和用static修饰是差不多的,具体你可以看下面,顺带static也帮你复习复习:

C++匿名命名空间 - youxin - 博客园 (cnblogs.com)

C/C++ 中 static 的用法全局变量与局部变量 | 菜鸟教程 (runoob.com)

D3D初始化

下面开始对于初始化D3D进行介绍:

这里还是不展开介绍了。

OnResize函数

对于OnResize介绍:

这里也不展开介绍。

UpdateScene 和 DrawScene

对于UpdateScene 和 DrawScene的介绍:这俩函数在基类中是纯虚函数,所以在父类中不需要写实现,具体实现什么完全由子类进行书写。

这里说一下对于UpdateScene的参数,其实这个参数是后期项目要用到的一个参数,现在根本没有用,所以你了解一下情况,写的时候抄上就行了,咱们后面再去说他。 

关于虚函数的知识补充,可以看下面的链接:

C++ 虚函数和纯虚函数的区别 | 菜鸟教程 (runoob.com)

构造函数

现在说一下构造函数,对于参数,就是把程序的句柄传进来,然后实现就是各种东西的初始化,这里涉及到很多成员我们还没有说。

D3DApp::D3DApp(HINSTANCE hInstance)
	: m_hAppInst(hInstance),
	m_MainWndCaption(L"DirectX11 Initialization"),
	m_ClientWidth(1280),
	m_ClientHeight(720),
	m_hMainWnd(nullptr),
	m_AppPaused(false),
	m_Minimized(false),
	m_Maximized(false),
	m_Resizing(false),
	m_Enable4xMsaa(true),
	m_4xMsaaQuality(0),
	m_pd3dDevice(nullptr),
	m_pd3dImmediateContext(nullptr),
	m_pSwapChain(nullptr),
	m_pDepthStencilBuffer(nullptr),
	m_pRenderTargetView(nullptr),
	m_pDepthStencilView(nullptr)
{
	ZeroMemory(&m_ScreenViewport, sizeof(D3D11_VIEWPORT));
	g_pd3dApp = this;
}

 这里初始化用了C++构造函数的特殊语法。C++ 类构造函数初始化列表 | 菜鸟教程 (runoob.com)

 析构函数

析构函数写成了虚函数,也就是虚析构!

C++虚析构函数详解 (biancheng.net)

具体的实现代码,暂时不讲。

三个开放数据的函数

    HINSTANCE AppInst()const;                 // 获取应用实例的句柄
	HWND      MainWnd()const;                 // 获取主窗口句柄
	float     AspectRatio()const;             // 获取屏幕宽高比

部分成员介绍:

    HINSTANCE m_hAppInst;        // 应用实例句柄
	HWND      m_hMainWnd;        // 主窗口句柄

    // 使用模板别名(C++11)简化类型名
	template <class T>
	using ComPtr = Microsoft::WRL::ComPtr<T>;
	// Direct3D 11
	ComPtr<ID3D11Device> m_pd3dDevice;                    // D3D11设备
	ComPtr<ID3D11DeviceContext> m_pd3dImmediateContext;   // D3D11设备上下文	

    std::wstring m_MainWndCaption;                       // 主窗口标题
	int m_ClientWidth;                                   // 视口宽度
	int m_ClientHeight;                                  // 视口高度

1、前面的俩,就是两个句柄,他是不同东西的句柄,所以这里不一样的类型。H开头表示handle,也就是句柄

2、中间俩,我们也已经介绍过了,就是设备和设备上下文

3、先说最后俩,表示客户区域大小,这里说一下窗口的客户区:下图中用蓝色圈住的部分就是客户区 。

 4、对于最后一个是一个字符串,窗口的标题,这里由于编码问题,你需要使用wstring类型,具体请看下面的链接。

C++标准里 string和wstring_天地蜉蝣的博客-CSDN博客_c++ wstring


四、GameApp类介绍

GameApp类代码纵览

#ifndef GAMEAPP_H
#define GAMEAPP_H
#include "d3dApp.h"
class GameApp : public D3DApp
{
public:
	GameApp(HINSTANCE hInstance);
	~GameApp();

	bool Init();
	void OnResize();
	void UpdateScene(float dt);
	void DrawScene();
};

#endif

上一主题讲的内容D3DApp类是我们通用的一个抽象类(就是里面有至少一个纯虚函数)。C++ 接口(抽象类) | 菜鸟教程 (runoob.com)

而这里我们要说的GameApp是我们为每一个游戏专门设计一个类,他要继承之前说的抽象类,然后再去根据本游戏的具体需要实现具体的内容,主要就是往DrawScene 和 UpdateScene填内容。

这里的四个函数,其实都是从父类中重写的函数,所以他们主要的功能在之前的讲解中已经说了,这里就不重复说了。到现在你要对这些函数之间的关系了如执掌,对子类和父类函数之间继承重写啥的了如执掌!

好接下来咱们来看代码的具体细节。

GameApp类代码解析

这个代码就比较简单啦,直接重写父类的四个虚函数,还有构造虚构。

1、构造函数说一下:你可能会有点奇怪构造函数的长相,

GameApp::GameApp(HINSTANCE hInstance)
	: D3DApp(hInstance)
{
}

为什么要在参数的小括号后面加上  D3DApp(hInstance)?

他这里的用法就是子类直接调用父类的构造函数,他是和之前说过的构造函数的初始化列表是类似的。

2、对于初始化,由于目前的第一个程序啥也没有,所以这里只调用父类的初始化就够了,并不需要在此基础上添加东西。如果你的程序复杂起来,需要在运行初期初始化一些数据,那么就往这面放。

bool GameApp::Init()
{
	if (!D3DApp::Init())
		return false;

	return true;
}

3、OnResize也是只用父类的就够了。这个也是和Init类似的道理,等程序以后复杂了,你想要在窗口大小变化的时候,加一些新的操作,那么你就可以往这里面加入代码。

void GameApp::OnResize()
{
	D3DApp::OnResize();
}

4、UpdateScene需要子类重写,还是因为咱们的程序啥也没实现,所以这里就没有写任何代码。同理的,以后程序在两帧之间想要更新什么内容的话,就写在这!

void GameApp::UpdateScene(float dt)
{
}

5、DrawScene

void GameApp::DrawScene()
{
	assert(m_pd3dImmediateContext);
	assert(m_pSwapChain);

	static float blue[4] = { 1.0f, 0.0f, 0.0f, 1.0f };	// RGBA = (0,0,255,255)
	m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), blue);
	m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(),         
                            D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

	m_pSwapChain->Present(0, 0);
}

由于这里涉及很多陌生的概念,还是暂时不讲。


五、主函数介绍

#include "GameApp.h"


int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE prevInstance,
	_In_ LPSTR cmdLine, _In_ int showCmd)
{
	GameApp theApp(hInstance);

	if (!theApp.Init())
		return 0;

	return theApp.Run();
}

如果你对前面讲的内容都消化好了,那么这里主函数怎么去应用那些函数,你自己就应该能猜出来怎么写主函数。

这里就是先创建一个游戏类对象,然后调用初始化函数初始化程序,接着进入游戏的主循环就行了。 


六、代码汇总

上边图中就是我们本节课的成果,对于里面的代码我并没有全部做讲解,因为有一部分我打算留在后一节。

另外代码我里面对内容进行了一些标注,比如:首先你现在看到了文章末尾,我希望你目前对于上边的所有讲述都能够理解,如果不理解的内容请你多看几遍教程,或者是自己上网搜一些相关的内容补充一下基础,一定尽可能弄懂,还有实在不懂的,可以先跳过,或者说是评论区留言。

当你对上边内容理解差不多之后,我希望你去下载一份代码,然后整体上对代码熟悉熟悉(最好在学习本节课之前就下载本章的代码),因为上边咱们讲解都是一块一块的讲的。看完之后,你要做的任务就是去实践,你看懂了不代表真的懂了,只有你去写一遍,你才知道哪里有坑,哪里没有理解好。所以自己去重新创建一个项目,然后把那些我标记你不用掌握的部分直接复制到相应的位置,然后就去从头开始默写,你去回想程序的架子,先把函数框架写出来,然后再去一个个的填写内容,一般第一遍你都默写不下来,这时候你再去看看示例程序中怎么写的,然后抄,边默写边抄,这个过程中用注释标记下自己不理解的部分,以后可以用来复习。

看代码的时候,一些好用的工具(VS):

CTRL + F查找功能,当你看到一个变量一个函数,想要看看他在哪调用了的时候(尤其是代码多之后且面对的是新程序的时候),你就可以去这么搜索一下,按回车切换下一个。 这里还可以限定搜索的区域:

你还可以用下面的这两个功能,选中变量或者函数,然后右键,一个是转到定义(这个操作也可以按住CTRL键然后鼠标点击一下函数名字就转到定义了),一个是查找所有引用(和搜索差不多)

                                勿在浮沙筑高台

参考资料:https://zhuanlan.zhihu.com/p/452013032

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值