uint能开发平面cad软件开发 mfc游戏吗

游戏引擎网络开发者的64做与不做(二A):协议与API
发表于 17:03|
来源IT Hare|
作者Sergey Ignatchenko
摘要:纵观过去10年的游戏领域,单机向网络发展已成为一个非常大的趋势。然而,为游戏添加网络支持的过程中往往存在着大量挑战,这里将为大家揭示游戏引擎网络开发者的64个做与不做。
【编者按】在这个系列之前的文章“”中,Sergey介绍了游戏引擎添加网络支持时在客户端方面的注意点。本文,Sergey则将结合实战,讲述协议与API上的注意点。以下为译文
这篇博文将继续讲述关于为游戏引擎实现网络支持,当然这里同样会分析除下基于浏览器游戏以外的所有类型及平台。
作为系列的第一篇文章,这里将着重讨论不涉及协议的客户端应用程序网络开发。本系列文章包括:
Protocols&and&APIs&(continued)
Server-Side&(Store-Process-and-Forward&Architecture)
Server-Side&(deployment,&optimizations,&and&testing)
Great&TCP-vs-UDP&Debate
Security&(TLS/SSL)
8a. 定制Marshalling:请使用“simple streaming” API
DIY marshalling可以通过多种方式实现。一个简单且高效的方法是提供“simple streaming”compose/parse函数,例如OutputMessage&
compose_uint16(OutputMessage&, uint16_t) /uint16_t parse_uint16(Parser&)
——针对所有需要在网络上传输的数据类型。在这种情况下,OutputMessage 是一个类/结构,封装了一个消息的概念,在添加其他属性后就会增长,而Parser
是通过一个输入消息创建的对象,它有一个指向输入消息的指针和一个针对当下解析发生地的偏移量。
Compose和parse 之间的不对称(Compose是直接针对消息的,而parse需要创建分离的Parser对象)不是完全强制的,但是在实践中却是一个非常好的事情(特别是,其允许在消息中存储解析的内容,允许重复解析,对消息的解析形式不变等等)。通常来说,这个简单的方法同样适用于大规模环境,但是在游戏上却需要更多的努力来保持composer和parser之间的信息一致性。
一个composing可能像下面这样:
uint16_t abc,//initialized with some meaningful values
pose_uint16(abc).compose_uint16(def);对应的parsing的例子是这样:
InputMessage&//initialized with a valid incoming message
Parser parser(msg);
uint16_t abc = parser.parse_uint16();
uint16_t def = parser.parse_uint16();
这种“simple streaming” compose/parse API(以及基于它建立,例如下面讲的IDL,和不同于 compose/parse
API基于明确的大小来处理的功能)的一个优点是使用什么格式并不重要——固定大小或者可变大小(即编码如VLQ和空值终止字符串编码是完全可行的)。另一方面,它的性能无与伦比(即使调用者提前确定消息的大小,它还有利于添加类似void
reserve(OutputMessage&,size_t max_sz);这样的功能)。
8b. 定制Marshalling:提供一些带有IDL-to-code编译器的IDL
对于 compose/parse 一个简单提升是用某种声明的方式来描述消息(某种接口定义语言——IDL)并将它编译成compose_uint16()/parse_uint16()的序列。例子中,这种声明看起来像是一个XML声明。
&struct name=“XYZ“& &field name=“abc“ type=“uint16“ /& &field
name=“def“ type=“uint16“ /& &/struct& &message name=“ZZZ“&
&field name=“abc“ type=“uint16“ /& &field name=“zzz“ type=“XYZ“
/& &/message&
之后则需要提供一个编译器,它读取上面的声明并产生类似下面的东西:
struct idl_struct_XYZ {
void compose(OutputMessage& msg) {
pose_uint16(abc);
pose_uint16(def);
void parse(Parser& parser) {
abc = parser.parse_uint16();
def = parser.parse_uint16();
struct idl_message_ZZZ {
idl_struct_XYZ
void compose(OutputMessage& msg) {
pose_uint16(abc);
pose(msg);
void parse(Parser& parser) {
abc = parser.parse_uint16();
zzz.parse(parser);
实现这样一个编译器是非常简单的(具备一定经验的开发人员最多只需几天就可以完成;顺便说一句,使用Python这样的语言则更加容易——笔者只用了半天)。
需要注意的是,接口定义语言并不要求必须是XML——例如,对于熟悉YACC的程序员,解析同样的例子,用C风格重写IDL不会很困难(再强调一次,整个编译器并不需要耗时数日——也就是说,如果已经使用过YACC/Bison
和Lex/Flex )。struct XYZ {
message struct ZZZ {
struct XYZ;
另一种实现marshalling 的方式是通过RPC调用;在这种情况下,RPC函数原型是一个IDL。然而,应当指出的是阻塞式的RPC调用并不适合互联网应用(这个将在Part
IIb的#12中详细讨论);另一方面,尽管条目#13不使用Unity 3D风格的无返回非阻塞RPC的出发点是好的,笔者仍然喜欢将结构体映射成消息,因为这样能更加清楚地解释正在发生的事情。
8c. 第三方Marshalling:使用平台和语言无关的格式
对于非C类的编程语言,marshalling 的问题并不在于“是否marshal”,而在于“用什么去marshalling”。理论上,任何序列化机制都可以做,但事实上平台和语言无关的序列化或者marshalling
机制(例如JSON)比指定平台和语言的(例如Python pickle)要好的多。
8d. 对于频繁内部交互的游戏使用二进制格式
对于数据格式,有一个强烈但并不是近期的趋势是使用基于文本的格式(例如xml)胜过使用二进制格式(例如VLQ 或 ASN.1 BER)。对于游戏来说,这个论点需要就情况而定。虽然文本格式能够简化调试并且提供更好的交互性,但是它们天生很大(即使在压缩之后通常也是如此),而且需要花费更多的处理时间,这将会在游戏火起来时给你沉重打击(无论是在流量还是服务器的CPU时间上)。笔者的经历是:对于游戏中高要求的交互式处理,使用二进制格式通常更加适合(尽管异常可能取决于特定的例如体积、频率的变化等)。
对于二进制格式,为了简化调试并提高交互性,用一个能够根据IDL分析消息并以文本格式打印的独立程序来实现是十分方便的。甚至更好的方式是用一个目的在于logging/debugging
的库来做这件事。
8e. 对于不频繁的外部交互使用文本格式
不同于内部交互游戏,外部交互例如支付通常是基于文本(XML)的,通常情况运行的不错。对于不频繁的外部交互,针对文本格式的所有参数变得不那么明显(由于罕见的原因),但是调试/互操作性变得更加重要。
8f. 在抛弃之前请考虑下ASN.1
ASN.1是一种需要关注的二进制格式(即:严格来讲,ASN.1也能通过XER生成和解析XML)。它允许通用的marshalling,有自己的IDL,应用于通信领域(ASN.1互联网上最常见的用途是作为X.509证书的基础格式)。而且乍一看,正是二进制marshalling所需要的。再一看,你可能会爱上它,或许也因为复杂的相关性而憎恨它,但是你不尝试的话,永远不知道。
就笔者认为,ASN.1并不值得痴迷(它很笨重,而且类似streaming的API天生在性能上有大幅提高——至少,除非能把ASN.1编译成代码),但也不是在所有游戏中都这样。因此,开发者应该看看ASN.1和可用的函数库(尤其是在一个开源的ASN.1编译器[asn
1 c]),再针对具体的项目,看它是否合适。
使用 asn1c 编译器,性能好的ASN.1更接近于上面描述的streaming解析,尽管笔者对ASN.1是否能够匹配simple streaming抱有疑问(大部分因为执行ASN.1解析需要显著增加更多配置);然而,如果有人做过基准测试,可以回复一下,因为在使用asn1c后差异并不明显。此外,如果大体上性能差异较小(甚至在marshalling中,2倍的性能差异在整体性能中可能都不太明显),其他比如开发时间的考虑就变得更加重要。而且在这里,
ASN.1是否会是一个好的选择将取决于项目具体细节。一个需要注意的问题:当说到开发时间,游戏开发者的时间比网络引擎开发者的时间更重要,因此,需要考虑开发者更喜欢哪类IDL——一种是上面所说的,或ASN.1(顺便说下,如果他们更喜欢定制的简单IDL,那么仍然可以在底层使用ASN.1,提供从IDL到ASN.1的编译器,因为这并不复杂)。
概要:虽然个人真的不太喜欢ASN.1,但它可能会有用(请根据上文自行判定)。
8g. 记住Little-Endian/Big-Endian警告
Big-endian是将高位字节存储在内存的低地址。相反,Little-endian是将低位字节存储在内存的低地址。
当在C/C++上实现compose_*()/parse_*()函数(处理多字节表达式),需要注意的是,相同的整数在不同的平台上表现出不同的字节序列。例如,在“little-endian”系统(尤其是X86),(uint16_t)1234存储表示为0xD2,
0x04,而在“big-endian”系统(如强大的AIX等),同样的(uint16_t)1234表示为0x04,0xD2。这就是为什么如果只写“unit16_t
x=1234;send(socket,&x,2);”,在little-endian和big-endian平台上发送的是不同的数据。
实际上,对于游戏来说,这并不是一个真正的问题。因为需要处理的绝大多数CPU是Little-endian的(X86是Little-endian,ARM可以是Little-endian,也可以是Big-endian,IOS和Android目前是Little-endian)。然而,为了保证正确性,最好记住并选择使用下面一种方法:
逐字节的marshal数据(即:发送 first x&&8, 然后是 x&0xFF——这样无论是Little-endian还是Big-endian,结果都是一样的)。
使用#ifdef BIG_ENDIAN (或者 #ifdef __i386 等),在不同机器上会产生不同的版本。注:严格地说,Big-endian宏不足以运行基于计算的
marshalling;在一些体系结构(尤其SPARC)上,难以读出没有对齐的数据,所以无法运行。然而,ARMv7和CPU的情况更是复杂:虽然技术上,不是所有指令都支持这个偏差,由于marshalling
的代码编译器往往会用错位安全的指令生成代码,所以基于计算的分析可以运行;不过,目前笔者还是不会给ARM使用这个方法。
使用函数,如htons() / ntohs(),注:这些函数生成所谓的“网络字节排序”,这就是Big-endian(就这样发生了)。
最后一个选项通常是文献资料中经常推荐的,但是,在实践应用中的效果并不明显:一方面,由于将所有的marshalling 处理进行封装;第二个选项((#ifdef
BIG_ENDIAN))也是个不错的选择(当在99%的目标机使用Little-endian时,可能会节省一些时间)。另一方面,不可能看到任何能够观察到的性能差异。更重要的是,要记住,确切的实现并没有多大关系。
个人而言,当关注性能的时候,笔者更喜欢下面的方法:有“通用” 的逐字节版本(它可以不顾字节顺序随处运行,而且不依赖于读取未对齐数据的能力),然后为平台特性实现基于计算的专业化版本(例如X86),举个例子:
uint16_t parse_uint16(byte*& ptr) { //assuming little-endian order on the wire
#if defined(__i386) || defined(__x86_64__) || defined(_M_IX86) || defined(_M_X64)
uint16_t ret = *(uint16_t*)
byte low = *ptr++;
return low | ((uint16_t)(*ptr++)) &&8;
通过这种方式,将会获得一个可以工作在任何地方的可信赖版本(“#else”以下),并且有一个基于平台兴趣的高性能版本。
至于其他的编程语言(例如Java):只要底层的CPU仍然是little-endian 或者big-endian的,诸如Java这样的语言不允许观察两者的不同,因此问题也就不存在了。
8h. 记住Buffer Overwrites and Buffer Overreads
当实现解析程序的时候,确保它们不易被异常数据包攻击(例如,异常数据包不能导致缓存溢出)。详细请参考Part VIIb中的#57。另一个需要记住的是不仅仅只有buffer
overwrites 是危险的:buffer overreads (例如,对一个据称是由空终止字符串组成的数据包调用一个strlen(),一旦那些字符很明显不是空终止字符)会导致core
dump(Windows中的0xC0000005 异常),很可能摧毁你的程序。
9. 要有一个单独的网络层与一个定义良好的接口
无论对网络做些什么,它都应当有一个独立的库(在其它游戏引擎内部或相邻)来封装所需的所有网络相关。尽管目前这个库的功能很简单——不久,它可能会演变的很复杂。而且库应该与其它的引擎足够的分离。这就意味着“不要把3D与网络混淆在一起;把它们分离的越远越好”。总之,网络库不应该依赖于图形库,反之亦然。注:对于那些认为没有人能写出一个与网络引擎紧密耦合的图形引擎的人——请看一下Gecko/Mozilla,你会相当惊讶。
警告:网络库的接口需要根据应用的需求做适当的调整(切不可盲目模仿TCP sockets 或者其它正在使用系统级API)。在游戏应用中,任务通常是发送/接收信息(使用或者不使用保证交付),而且库所对应的API应该反映它。举一个很好(虽然不通用)的抽象实例是Unity
3D:他们的网络API提供信息传递或无保证的状态同步,这两者对于实时游戏中的任务来说都是很好的抽象选择。
还有其它是(除了封装系统调用到你的抽象API)属于网络层的吗?做这件事情不止一种方法,但是通常会包括所有的东西,它们会传输网络信息到主线程(看Part
I中的#1),并就地处理。同样的,,marshalling/unmarshalling(看上面的#8)也属于网络层。
毫无疑问,任何系统级的网络调用只会出现在网络层,而且绝对不应该在其他地方使用。整个想法是封装网络层和提供整洁的关注分离,隔离应用程序级别与无关的通信细。
10. 要理解底层到底是怎么回事
当开发网络引擎的时候,使用一些框架(例如TCP sockets)看起来十分有诱惑力(至少乍看如此),它会自动做很多事情,不需要开发者关注。然而,如果想让玩家获得更好的体验,事情就变得棘手了。简而言之:尽管使用框架很省心,但是完全忽视它却并不好。在实践中它意味着只要团队超过2人,通常需要有一个专门的网络开发者——他知道框架底层是怎么回事。
此外,总体项目架构师必须知道至少大部分由互联网带来的局限(例如IP数据包有固有的非保证性,如何保证其准确交付,典型的往返时间等等),并且所有的团队成员必须理解网络是正在传输消息的,而这些消息很可能会被任意的延迟(有保证的消息传输)或者丢失(无保证的消息传输)。
可以总结为如下表格:
有关库及底层机制的一切东西
总体项目架构师
通常的网络局限
所有团队成员
在网络上的消息,以及潜在的延误或潜在的丢失
11.不要假设所有的用户都使用相同版本的App(即提供一个方式去扩展游戏协议)尽管程序会自动升级(包括网络库等),还是要记住那些还没有升级APP的用户。尽管每次应用启动时都会强制升级,仍然有用户在升级的那一刻正在使用互联网,也有一些找到了忽略升级的方法(忽略升级的原因很多,通常是不喜欢更新带来的改变)。处理此问题的两种常用的方法是:提供一种机制,让App开发者将app和一个app版本协议绑定,在服务器上检查它,让使用过期客户端的用户离开,强迫他们去升级。提供一种方式以优雅降级的形式处理协议之间的差异,不提供之前版本协议中没有的功能。走第二条路是很困难的,但是却能给终端用户感到额外舒适(如果做的很细心)。一般来讲,需要在引擎中提供两种机制,使得app开发者能够根据需求作出选择(从长远来看,甚至在是一个app的生命周期中,他们往往两个都需要,)。方法2的一个处理方式是基于这样一个观察,在一个差不多成熟的app中,大多数协议的变更都和在协议中添加新字段有关。这意味着可以在marshalling 层提供一个通用函数,例如end_of_parsing_reached(),这样app开发者就能在消息的末端添加新的字段,并使用下面代码来解析可能已经修改的消息。if( parser.end_of_parsing_reached() )
additional_field = 1;
additional_field = parser.parse_int();如果使用自己的IDL(参见上面#8b),它看起来应该是这样。&struct name=“XYZ“&
&field name=“abc“ type=“uint16“ /&
&field name=“def“ type=“uint16“ /&
&field name=“additional_field“ type=“uint16“ default=“1“ /&
&/struct&当然,在compose() / parse()中会做相应的改变。这个简单的方法,即在消息的末尾添加额外的字段,运行的比较不错,尽管需要游戏开发者弄清楚协议是如何扩展的。当然,不是所有的协议改变都能用这种方式处理,但如果app开发者能够用此方法处理90%以上的协议更新,并将强制更新的数量降低十倍,用户将会十分感激(或许不会——取决于更新带来的负累)。未完待续···显然,Part II变得如此之大以至于必须将它切分。敬请关注——Part IIb,将会讲解protocols and APIs的一些更高级内容。原文链接:(翻译/ 审校/仲浩)
推荐阅读相关主题:
CSDN官方微信
扫描二维码,向CSDN吐槽
微信号:CSDNnews
相关热门文章【Visual C++】游戏开发笔记二十九
一步一步教你用优雅的Direct3D11代码画一个三角形 - 推酷
【Visual C++】游戏开发笔记二十九
一步一步教你用优雅的Direct3D11代码画一个三角形
本系列文章由zhmxy555(毛星云)编写,转载请注明出处。
作者:毛星云&&& 邮箱:
这个demo演示的效果是用Direct3D11在屏幕上渲染一个三角形,当然是通过这个demo进一步巩固和学习Direct3D11,而不是单单为了画一个三角形这么简单。正如之前所说,这个demo是建立在笔记二十八中讲解的D3D BlankWindows Demo之上的。
那么,我们就开门见山,直入正题吧。
一、 载入几何体
我们知道,为了渲染几何图形,我们需要一个顶点缓存,一个描述顶点布局的输入层,以及一系列的着色器,自DirectX10以来,着色器开始作为图形渲染的基础组成部分,在这个demo之中我们会指定顶点着色器与像素着色器,渲染一种简单的纯色表面。后面我们将延伸的讲解如何拓展使用这种效果来在表面映射图形纹理。
下面就开始进行这个demo的书写:
这个demo的核心内容当然是一个叫做TriangleDemo的类,我们为这个类定义几个成员变量,他们分别是ID3D11VertexShader类型的取名为solidColorVS_的变量,一个ID3D11PixelShader类型的唤作solidColorPS的变量。一个ID3D11InputLayout类型的唤作inputLayout_的变量,以及一个ID3D11Buffer类型的叫做vertexBuffer_的变量。
下面就是TriangleDemo.h头文件的源代码,简单的勾勒出了本文主角TriangleDemo类的轮廓:
代码段一& TriangleDemo.h头文件
#include&Dx11DemoBase.h&
class TriangleDemo : public Dx11DemoBase
TriangleDemo( );
virtual ~TriangleDemo( );
bool LoadContent( );
void UnloadContent( );
void Update( float dt );
void Render( );
ID3D11VertexShader* solidColorVS_;
ID3D11PixelShader* solidColorPS_;
ID3D11InputLayout* inputLayout_;
ID3D11Buffer* vertexBuffer_;
顶点我们采用一个简单的三分量式的浮点型结构体,在XNA Math library中一个叫做XMFLOAT3的结构体可以胜任这项殊荣。
接下来,开始丰富我们的TriangleDemo类,我们在代码段二中书写顶点结构体VertexPos和TriangleDemo的类的构造函数以及析构函数
代码段二 TriangleDemo 顶点结构体, 构造函数和析构函数.
#include&TriangleDemo.h&
#include&xnamath.h&
struct VertexPos
TriangleDemo::TriangleDemo( ) : solidColorVS_( 0 ), solidColorPS_( 0 ),
inputLayout_( 0 ), vertexBuffer_( 0 )
TriangleDemo::~TriangleDemo( )
下面继续丰富我们的TriangleDemo类,在代码段三中我们进行UnloadContent函数的书写,顾名思义,UnloadContent是进行unload content工作的,与后面将书写的LoadContent函数相对应。
代码段三 TriangleDemo类的UnloadContent函数的书写
void TriangleDemo::UnloadContent( )
if( solidColorVS_ ) solidColorVS_-&Release( );
if( solidColorPS_ ) solidColorPS_-&Release( );
if( inputLayout_ ) inputLayout_-&Release( );
if( vertexBuffer_ ) vertexBuffer_-&Release( );
solidColorVS_ = 0;
solidColorPS_ = 0;
inputLayout_ = 0;
vertexBuffer_ = 0;
顺理成章的,下一步便是LoadContent函数的书写。这个函数由顶点着色器载入,在文件SolidGreenColor.fx中可以查看。
一旦顶点着色器的源代码编译完成,着色器便创建一个CreateVertexShader函数的调用,我们接着创建顶点格式。由于顶点着色器与顶点格式相关联,所以我们还需要将顶点着色器加载到内存中。
创建完顶点着色器和输入格式后,下一步我们创建像素着色器。下面这段代码实现了LoadContent方法的一半的功能:
代码段四 &LoadContent函数着色器载入代码
bool TriangleDemo::LoadContent( )
ID3DBlob* vsBuffer = 0;
bool compileResult = CompileD3DShader( &SolidGreenColor.fx&,
&VS_Main&, &vs_4_0&, &vsBuffer );
if( compileResult == false )
MessageBox( 0, &载入顶点着色器错误!&, &编译错误&, MB_OK );
HRESULT d3dR
d3dResult = d3dDevice_-&CreateVertexShader( vsBuffer-&GetBufferPointer(
vsBuffer-&GetBufferSize( ), 0, &solidColorVS_ );
if( FAILED( d3dResult ) )
if( vsBuffer )
vsBuffer-&Release( );
D3D11_INPUT_ELEMENT_DESC solidColorLayout[] =
&POSITION&, 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }
unsigned int totalLayoutElements = ARRAYSIZE( solidColorLayout );
d3dResult = d3dDevice_-&CreateInputLayout( solidColorLayout,
totalLayoutElements, vsBuffer-&GetBufferPointer( ),
vsBuffer-&GetBufferSize( ), &inputLayout_ );
vsBuffer-&Release( );
if( FAILED( d3dResult ) )
ID3DBlob* psBuffer = 0;
compileResult = CompileD3DShader( &SolidGreenColor.fx&,
&PS_Main&, &ps_4_0&, &psBuffer );
if( compileResult == false )
MessageBox( 0, &载入像素着色器错误!&, &编译错误&, MB_OK );
d3dResult = d3dDevice_-&CreatePixelShader( psBuffer-&GetBufferPointer( ),
psBuffer-&GetBufferSize( ), 0, &solidColorPS_ );
psBuffer-&Release( );
if( FAILED( d3dResult ) )
//后接函数的下半段
CompileD3DShader相关的代码在代码段五中进行了演绎,这段代码巧妙地被分离于LoadContent之外,这样在加载多个不同的着色效果的时候便可以避免大段大段的冗余代码:
代码段五 &CompileShader 函数的实现方法
bool Dx11DemoBase::CompileD3DShader( char* filePath, char* entry, char*
shaderModel, ID3DBlob** buffer )
DWORD shaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;
#if defined( DEBUG ) || defined( _DEBUG )
shaderFlags |= D3DCOMPILE_DEBUG;
ID3DBlob* errorBuffer = 0;
result = D3DX11CompileFromFile( filePath, 0, 0, entry, shaderModel,
shaderFlags, 0, 0, buffer, &errorBuffer, 0 );
if( FAILED( result ) )
if( errorBuffer != 0 )
OutputDebugStringA( ( char* )errorBuffer-&GetBufferPointer( ) );
errorBuffer-&Release( );
if( errorBuffer != 0 )
errorBuffer-&Release( );
上面我们介绍了上半段LoadContent函数的构成,而下半段LoadContent函数主要实现了顶点缓存的创建。这段代码行文思路很明朗,首先定义一个简单的三角形,沿X轴与Y轴都是0.5f(半个单位的长度)。Z轴依然设为为0.5f,来使此三角形可见。因为若镜头隔表面太近或者太远,表面都不会成功的渲染。
顶点列表存储于一个叫做vertices的数组中,它提供了一个子资源数据,在CreateBuffer函数开始调用进行实际顶点缓存的创建的时候,这些数据可以派上用场。
下面就是上面这段叙述的代码实现,LoadContent函数的下半部分书写风格如下:
代码段六 LoadContent函数的几何图形载入代码
bool TriangleDemo::LoadContent( )
//前接函数的上半段
VertexPos vertices[] =
XMFLOAT3( 0.5f, 0.5f, 0.5f ),
XMFLOAT3( 0.5f, -0.5f, 0.5f ),
XMFLOAT3( -0.5f, -0.5f, 0.5f )
D3D11_BUFFER_DESC vertexD
ZeroMemory( &vertexDesc, sizeof( vertexDesc ) );
vertexDesc.Usage = D3D11_USAGE_DEFAULT;
vertexDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vertexDesc.ByteWidth = sizeof( VertexPos ) * 3;
D3D11_SUBRESOURCE_DATA resourceD
ZeroMemory( &resourceData, sizeof( resourceData ) );
resourceData.pSysMem =
d3dResult = d3dDevice_-&CreateBuffer( &vertexDesc,
&resourceData, &vertexBuffer_ );
if( FAILED( d3dResult ) )
&&&&&&&&&&&&&&&&&&&&
二、渲染几何体
Direct11三角形Demo代码的最后两部分由实现几何渲染功能的代码和着色器本身构成。渲染几何图形的构成代码在TriangleDemo类中的Render函数中进行。函数中有
有一个条件语句,这样可以确保在Direct3D的上下文是有效的。
接下来,我们清除渲染目标,并设定输出程序集(input assembler)。而实际上,因为在这个demo之中的三角形是静态的,我们并不一定非要清除渲染目标,这里只是为了规范我们的代码书写,以免养成不良的开发习惯。在输出程序集阶段的设置由我们已经创建的输出结构(input layout)进行绑定,并提供顶点缓存,设置拓扑三角形的列表。
下面贴出Render函数的书写思路:
代码段七 TriangleDemo类的render函数书写
void TriangleDemo::Render( )
if( d3dContext_ == 0 )
float clearColor[4] = { 0.5, 0.5f, 0.5f, 1.0f };
//设定背景颜色
d3dContext_-&ClearRenderTargetView( backBufferTarget_, clearColor );
unsigned int stride = sizeof( VertexPos );
unsigned int offset = 0;
d3dContext_-&IASetInputLayout( inputLayout_ );
d3dContext_-&IASetVertexBuffers( 0, 1, &vertexBuffer_, &stride, &offset );
d3dContext_-&IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_
TRIANGLELIST );
d3dContext_-&VSSetShader( solidColorVS_, 0, 0 );
d3dContext_-&PSSetShader( solidColorPS_, 0, 0 );
d3dContext_-&Draw( 3, 0 );
swapChain_-&Present( 0, 0 );
最后一部分要介绍的代码是着色器。笼统的来说,顶点着色器基于它得到的内容。详细的来说,顶点着色器的作用是将内部得到的顶点位置传递到输出处,之后,我们须处理这些数据,正确绘制出我们的图形。但对于这个非常基础的demo,仅仅进行顶点位置内容的传递就够了。
如果没有几何图形着色器绑定到输出程序集之上,顶点着色器的输出的数据就是像素着色器的输入的数据。其中,像素着色器的输出就是写到输出缓存之中的颜色值。当交换链中的Present函数调用的时候,这个缓存就会最终显示给用户。
TriangleDemo的顶点着色器和像素着色器的书写方法如下代码段八:
代码段八 Triangledemo着色器的实现代码
float4 VS_Main( float4 pos : POSITION ) : SV_POSITION
float4 PS_Main( float4 pos : SV_POSITION ) : SV_TARGET
return float4( 0.0f, 1.0f, 0.0f, 1.0f );
这样,Triangledemo类就随着一步一步的勾勒,被我们书写完成了。
三、Dx11DemoBase类的书写
接下来,我们将之前讲解的BlankD3DWindows Demo模板中的Dx11DemoBase类进行丰富和修改,即可得到适用于本节demo的Dx11DemoBase类。
代码段九 Dx11DemoBase.h
#ifndef _DEMO_BASE_H_
#define _DEMO_BASE_H_
#include&d3d11.h&
#include&d3dx11.h&
#include&DxErr.h&
class Dx11DemoBase
Dx11DemoBase();
virtual ~Dx11DemoBase();
bool Initialize( HINSTANCE hInstance, HWND hwnd );
void Shutdown( );
bool CompileD3DShader( char* filePath, char* entry,
char* shaderModel, ID3DBlob** buffer );
virtual bool LoadContent( );
virtual void UnloadContent( );
virtual void Update( float dt ) = 0;
virtual void Render( ) = 0;
protected:
HINSTANCE hInstance_;
HWND hwnd_;
D3D_DRIVER_TYPE driverType_;
D3D_FEATURE_LEVEL featureLevel_;
ID3D11Device* d3dDevice_;
ID3D11DeviceContext* d3dContext_;
IDXGISwapChain* swapChain_;
ID3D11RenderTargetView* backBufferTarget_;
代码段十 Dx11DemoBase.cpp
#include&Dx11DemoBase.h&
#include&D3Dcompiler.h&
Dx11DemoBase::Dx11DemoBase( ) : driverType_( D3D_DRIVER_TYPE_NULL ), featureLevel_( D3D_FEATURE_LEVEL_11_0 ),
d3dDevice_( 0 ), d3dContext_( 0 ), swapChain_( 0 ), backBufferTarget_( 0 )
Dx11DemoBase::~Dx11DemoBase( )
Shutdown( );
bool Dx11DemoBase::Initialize( HINSTANCE hInstance, HWND hwnd )
hInstance_ = hI
GetClientRect( hwnd, &dimensions );
unsigned int width = dimensions.right - dimensions.
unsigned int height = dimensions.bottom - dimensions.
D3D_DRIVER_TYPE driverTypes[] =
D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP,
D3D_DRIVER_TYPE_REFERENCE, D3D_DRIVER_TYPE_SOFTWARE
unsigned int totalDriverTypes = ARRAYSIZE( driverTypes );
D3D_FEATURE_LEVEL featureLevels[] =
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0
unsigned int totalFeatureLevels = ARRAYSIZE( featureLevels );
DXGI_SWAP_CHAIN_DESC swapChainD
ZeroMemory( &swapChainDesc, sizeof( swapChainDesc ) );
swapChainDesc.BufferCount = 1;
swapChainDesc.BufferDesc.Width =
swapChainDesc.BufferDesc.Height =
swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.BufferDesc.RefreshRate.Numerator = 60;
swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.OutputWindow =
swapChainDesc.Windowed =
swapChainDesc.SampleDesc.Count = 1;
swapChainDesc.SampleDesc.Quality = 0;
unsigned int creationFlags = 0;
#ifdef _DEBUG
creationFlags |= D3D11_CREATE_DEVICE_DEBUG;
unsigned int driver = 0;
for( driver = 0; driver & totalDriverT ++driver )
result = D3D11CreateDeviceAndSwapChain( 0, driverTypes[driver], 0, creationFlags,
featureLevels, totalFeatureLevels,
D3D11_SDK_VERSION, &swapChainDesc, &swapChain_,
&d3dDevice_, &featureLevel_, &d3dContext_ );
if( SUCCEEDED( result ) )
driverType_ = driverTypes[driver];
if( FAILED( result ) )
DXTRACE_MSG( &创建D3D设备失败!& );
ID3D11Texture2D* backBufferT
result = swapChain_-&GetBuffer( 0, __uuidof( ID3D11Texture2D ), ( LPVOID* )&backBufferTexture );
if( FAILED( result ) )
DXTRACE_MSG( &获取交换链后台缓存失败!& );
result = d3dDevice_-&CreateRenderTargetView( backBufferTexture, 0, &backBufferTarget_ );
if( backBufferTexture )
backBufferTexture-&Release( );
if( FAILED( result ) )
DXTRACE_MSG( &创建渲染目标视图失败!& );
d3dContext_-&OMSetRenderTargets( 1, &backBufferTarget_, 0 );
D3D11_VIEWPORT
viewport.Width = static_cast&float&(width);
viewport.Height = static_cast&float&(height);
viewport.MinDepth = 0.0f;
viewport.MaxDepth = 1.0f;
viewport.TopLeftX = 0.0f;
viewport.TopLeftY = 0.0f;
d3dContext_-&RSSetViewports( 1, &viewport );
return LoadContent( );
bool Dx11DemoBase::CompileD3DShader( char* filePath, char* entry, char* shaderModel, ID3DBlob** buffer )
DWORD shaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;
#if defined( DEBUG ) || defined( _DEBUG )
shaderFlags |= D3DCOMPILE_DEBUG;
ID3DBlob* errorBuffer = 0;
result = D3DX11CompileFromFile( filePath, 0, 0, entry, shaderModel,
shaderFlags, 0, 0, buffer, &errorBuffer, 0 );
if( FAILED( result ) )
if( errorBuffer != 0 )
OutputDebugStringA( ( char* )errorBuffer-&GetBufferPointer( ) );
errorBuffer-&Release( );
if( errorBuffer != 0 )
errorBuffer-&Release( );
bool Dx11DemoBase::LoadContent( )
// 进行相关重载
void Dx11DemoBase::UnloadContent( )
// 进行相关重载
void Dx11DemoBase::Shutdown( )
UnloadContent( );
if( backBufferTarget_ ) backBufferTarget_-&Release( );
if( swapChain_ ) swapChain_-&Release( );
if( d3dContext_ ) d3dContext_-&Release( );
if( d3dDevice_ ) d3dDevice_-&Release( );
backBufferTarget_ = 0;
swapChain_ = 0;
d3dContext_ = 0;
d3dDevice_ = 0;
四、& 赋予程序生命——wWinMain函数的书写
最后一步,依旧是主函数的书写,这个函数大家应该是最熟悉的,任何想顺利运行的C/C++程序中必不可少,而且这里的书写方式和前面demo中的也大同小异,浅墨在这里就不多赘言了,代码如下:
代码段十一 wWinMain函数的书写
#include&Windows.h&
#include&memory&
#include&TriangleDemo.h&
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam );
int WINAPI wWinMain( HINSTANCE hInstance, HINSTANCE prevInstance, LPWSTR cmdLine, int cmdShow )
UNREFERENCED_PARAMETER( prevInstance );
UNREFERENCED_PARAMETER( cmdLine );
WNDCLASSEX wndClass = { 0 };
wndClass.cbSize = sizeof( WNDCLASSEX ) ;
wndClass.style = CS_HREDRAW | CS_VREDRAW;
wndClass.lpfnWndProc = WndP
wndClass.hInstance = hI
wndClass.hCursor = LoadCursor( NULL, IDC_ARROW );
wndClass.hbrBackground = ( HBRUSH )( COLOR_WINDOW + 1 );
wndClass.lpszMenuName = NULL;
wndClass.lpszClassName = &DX11BookWindowClass&;
if( !RegisterClassEx( &wndClass ) )
return -1;
RECT rc = { 0, 0, 640, 480 };
AdjustWindowRect( &rc, WS_OVERLAPPEDWINDOW, FALSE );
HWND hwnd = CreateWindowA( &DX11BookWindowClass&, &Direct3D11三角形demo&, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top,
NULL, NULL, hInstance, NULL );
if( !hwnd )
return -1;
ShowWindow( hwnd, cmdShow );
// Demo 的初始化
bool result = demo.Initialize( hInstance, hwnd );
if( result == false )
return -1;
MSG msg = { 0 };
while( msg.message != WM_QUIT )
if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) )
TranslateMessage( &msg );
DispatchMessage( &msg );
// 更新和绘制
demo.Update( 0.0f );
demo.Render( );
// Demo 关闭
demo.Shutdown( );
return static_cast&int&( msg.wParam );
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
PAINTSTRUCT paintS
switch( message )
case WM_PAINT:
hDC = BeginPaint( hwnd, &paintStruct );
EndPaint( hwnd, &paintStruct );
case WM_DESTROY:
PostQuitMessage( 0 );
return DefWindowProc( hwnd, message, wParam, lParam );
用上面这些代码就可以在屏幕上输出一个绿色的三角形,源码以及工程文件在文章末尾有链接提供打包下载。
最后得到这个DirectX11三角形demo的效果图:
文章最后和大家讲些题外话,这是浅墨回国度假后专栏的第一次更新,回国度假到8月25号。
因为欧洲与国内的五个小时时差问题,回家之后一直在倒时差,白天睡的香,晚上睡不着,刚开始还是比较难受的- -,目前基
本上生物钟正常了,可以早上起来多锻炼身体和看书写代码了。
关于后面接着几篇文章的更新,因为浅墨回国了,更新时间会有所更改,暂时定在每周周一的中午。
感谢一直支持【Visual C++】游戏开发笔记系列专栏的朋友们。
【Visual C++】游戏开发 系列文章才刚刚展开一点而已,因为游戏世界实在是太博大精深了~
但我们不能着急,得慢慢打好基础。做学问最忌好高骛远,不是吗?
浅墨希望看到大家的留言,希望与大家共同交流,希望得到睿智的评论(即使是批评)。
你们的支持是我写下去的动力~
精通游戏开发的路还很长很长,非常希望能和大家一起交流,共同学习,共同进步。
大家看过后觉得值得一看的话,可以顶一下这篇文章,你们的支持是我继续写下去的动力~
如果文章中有什么疏漏的地方,也请大家指正。也希望大家可以多留言来和我探讨相关的问题。
最后,谢谢你们一直的支持~~~
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
已发表评论数()
已收藏到推刊!
请填写推刊名
描述不能大于100个字符!
权限设置: 公开
仅自己可见
正文不准确
标题不准确
排版有问题
没有分页内容
图片无法显示
视频无法显示
与原文不一致

我要回帖

更多关于 平面cad软件开发 mfc 的文章

 

随机推荐