[IT168 技术文档] 不知不觉,成为职业游戏程序员已经六年多了。从最初的程序员到技术总监,参与了多款商业游戏的开发,所有的项目都会建立在游戏引擎基础上。游戏引擎是一些常用的底层代码集合,通常包括图形、声音、网络、输入设备、脚本系统,以及对象管理等模块。游戏引擎是建立游戏的基础,可以说引擎的品质在很大程度上决定游戏的成败。好的游戏引擎,它应该具有很高的执行效率,强大的功能和方便易用的接口。考虑到效率和功能强大的因素,目前,绝大部分成熟的游戏引擎,都通常会采用C/C++语言来编写。
引擎最重要是让用户减少开发代码量么?
引擎的作用并非减少代码量,而减少概念,降低复杂度才是封装的意义。为了严谨,灵活,只要不造成概念混淆,让用户多写一些看起来重复,罗嗦的代码并不成问题。不要忘记,用户也是有能力为自己特定功能进行一些组合包装的。
传统的游戏程序开发模式简介
不少游戏都采用C/C++全程开发。为了效率,开发人员使用C++开发底层引擎,引擎人员开发出来的引擎,通常会提供给用户C++头文件,Lib库文件,还有运行的DLL等二进制文件。为了接口一致,具体项目开发程序员们还是会选用c/c++开发项目代码。也有不少引擎,它提供了脚本包装,这样,项目开发中,人们还可以用脚本进行一些配置和简单逻辑的处理。这就是通常的传统游戏程序开发模式。
带来的问题
引擎到游戏,中间缺少一个过渡,项目开发人员不得不面对一大堆的h文件,还可能不得不去接触一些在引擎中的底层概念。进而对项目程序员,提出了更高的要求。C++天生的复杂性使得这个问题更加突出。
寻找解决方案,尝试使用CLR
Vs2005发布后,它的C++编译器内包含了一个CLR语言的编译器,这两种代码可以混合编译!听起来,这是一个多么激动人心的特性阿!让我们看看CLR这门MS新推的程序开发语言是不是适合游戏开发,我们的分析结果是:它很有魅力,我们很值得去尝试。
CLR对我们的项目是很有吸引力。我将在下面作简要说明:
一:它是可以支持所有原生C++代码编译的,这保证如果出现了CLR实在无法解决的问题,我们还有退路。
二:CLR语法和C++很类似,你可以在c++中看到诸如这样的代码
Ref class A
{
Int m_Index;
};
A^ pA = gcnew A;
怎么样?是不是觉得很熟悉?
三:CLR支持垃圾回收机制
像上面出现的A^ pA = gcnew A;用户根本不用考虑pA的释放问题,这很大程度上能降低开发的压力。(然而gc所带来的效率压力,以及对象析构时序的问题也很让人头痛)
四:运行时异常处理
开发C++程序的时候,恐怕所有程序员在诸如写越界,指针错误等上面都或多或少的犯过错,导致程序异常而崩溃吧。这样的错误,一旦出现,很多时候都很不好找到和调试,因为C++的出错现场,很可能根本就不代表出错的原因了。比如数组写越界。在一些如内存缓冲区技术下,根本当时不会发生任何错误,那后面运行结果会怎么样呢?这个恐怕只有上帝才知道。但是CLR在这方面做得就很不错,因为一切数据都是object,而不是原生内存对象,编译器在后台作了很多工作,用户的错误数据读写访问,或者句柄的错误使用,都会在案发现场立即显现。
五:成熟丰富的支持库
在CLR下面写程序,当你using了一些常用的名字空间后,你会发现当你为某个变量或者函数或者类取代有很强功能特征的名字的时候,编译器就开始抱怨了。应为这些名字已经使用过了!你要写得某个功能,大多数在库中已经被人实现了。
这说明什么?CLR的支持库阵容只能用豪华来形容。在它的标准库中,dotnet框架各种运行库都得到了支持,而且还有大量的CLR特有支持。CLR甚至带有大量的模版库。
六:不需要暴露过多的头文件
通常的C++引擎在release的时候,它会需要提供大量的h文件,来让用户知道接口设计和类型原形。缺少这些,哪怕下游用户根本没有使用某些功能,也会可能因为h文件的包含关系,让编译器不知所措,无法工作。而在CLR编译的库文件,这个问题迎刃而解了。在它的释放dll中,可以包含各种函数,类型等的声明。最终用户只需要#using xxx.dll就可以访问xxx.dll中被许可使用的所有接口和功能。如果你愿意,还可以附上用户手册,在Object Explorer中让用户查阅,这听起来简直太美妙了!
当然,天下没有免费的午餐,这个好用的功能,也会带来重复编译的问题。下面会更具体的说明,但相对来说,它的优点更加显而易见。
C++底层游戏引擎的准备
我依然觉得C++是游戏最底层引擎理所当然的开发语言。它更加接近计算机的执行原理,功能更强大,执行效率更高。所以,我们现在最底层的游戏引擎,依然采用C++实现。下面将简单介绍一下我们底层引擎的大致功能模块:
1.图形引擎
我们的图形引擎Victory3D,是对图形绘制和管理的功能集。在这里可以找到对3d设备的操作接口,3d数学库的支持,模型,场景的管理,shader系统的定义和管理等等。这是一个超过8万行C++代码的庞大库,概念很多,头文件包含关系复杂。
2.脚本引擎
gcScript是Victory3D v3的通用脚本语言,我们开发了一套game c的游戏脚本语言,此后多次维护,在引擎v3版本的时候,完全重写了一个完全面向对象,全功能的脚本语言gcScript。该脚本虚拟机执行效率比较高,拥有强大而方便的扩充能力。
3. 物理引擎
现在的游戏,物理系统,是一个很出彩的方向。我们在项目中实际使用的是PhysX免费的PC版,这是一个高效而且稳定的并且非常著名的物理引擎。
期望出现的中层引擎理念
我认为游戏引擎不应该只是一个SDK,而更加应该是一个解决方案。在提供了所有的底层操作能力后,应该向用户提供出来一个整体的项目开发方案,这才能真正的指引项目开发方向。所以我们应该在底层引擎的上面包装出一个中层游戏引擎,它应该能提供以下功能:
1.应用程序框架
游戏首先是执行程序,所以在中层引擎中需要一个让游戏运行起来的环境。程序从入口(比如windows AP环境下开发是WinMain函数)进入,开始进入主循环。通常我们还需要一个游戏主窗口,用来作为程序的显示宿主。这样一个灵活可订制的框架,是游戏程序执行的先决条件。
2.游戏设备和环境框架
游戏需要访问到很多设备,例如显卡、声卡、网卡等。这些设备在引擎程序中,都有相应的接口和对象来表征。那么在中层引擎中,需要把这些必要的设备环境构建起来,并且能够让用户方便的访问和设置。
3.功能扩充框架(dll+脚本扩充)
在应用程序和设备环境构架起来后,理论上游戏就可以运行起来了。但是,游戏是一个复杂的工程,开发人员需要不断的去丰富和完善它,所以需要设计一个方便可扩充的系统,来满足各种新的需求。通常我们可以采用dll+脚本来扩充系统。设计好一个规则后,开发者可以通过增加dll来扩充系统的能力,如果拥有一个优秀的脚本系统的话,还可以以此来扩展脚本功能。
4.UI支持
在电脑游戏中,人机交互的方式,很大部分是通过界面来进行,一个方便完善的UI系统,可以大量节约游戏项目开发周期,降低繁杂度。例如WOW的UI系统就非常强大。结合wow内嵌的lua脚本,甚至普通玩家都可以自己完成一些方便操作的界面。在我们的构架中,这也是一个设计重点。
5.游戏对象的操作管理
游戏不管画面表现如何华丽,其实骨子里面玩得都是数据。如何去组织这些数据才是游戏程序设计的关键点。
目前,主流的开发模式都是采用面向对象的开发模式。合理的对象定义和管理,是开发顺利地保证。在底层引擎中,我们能够操作各种数据,但是当开发者直接面对这些接口的时候,会发现这样的操作实在是麻烦。
如果我们将底层引擎接口进行以下封装呢?比如,可以封装出一个游戏对象类Item,它能自我定位,拥有图形自我绘制接口,3D音效处理接口,基本的网络处理能力SendTo等等。那么后面的游戏开发可以围绕Item来进一步组合设计,把Item变成一个一个概念清晰的具体游戏对象,很方便的就能操作它们了。
我们准备用CLR对C++底层引擎进行一次包装,完成一个中层游戏引擎。察看CLR工程代码,翻阅CLR文档,我们发现Lipman在C++基础上对CLR 注入了很多新东西,下面我来先简单熟悉一下CLR:
CLR我们关心的部分:
一.琳琅满目的关键字
在vs2005集成环境中翻开CLR代码,我们会发现语法加亮了很多以前C++少见的关键字,比如gcnew,ref,value,override,for each,nullptr,#using “xxx.dll”,property,event,delegate等等。这些关键字很多引入了诸如垃圾回收,属性,事件,代理等CLR语言天生支持的特性。其实在C++中,这些新特性大多都有自己的一些实现模式,现在它已经作为语言的直接支持出现了。在很多情况下,它们都能付出很小代价的前提下,解决各种问题。
古怪的上尖括号^:CLR操作的不是地址指针,而是句柄。^就是用来申明句柄类型的关键字,T^ p=gcnew T;这样的语法,就是产生了一个T类型的句柄。句柄这东西简单的也可以就把它考虑成为一类指针。通过句柄,我们可以访问到CLR对象,但是由于GC机制的存在,地址是可变的。为了应付变化的地址,CLR对应的提供了pin_ptr关键字,可以用钉子暂时钉死clr的变量地址。这个在C++/CLR混合编码的时候经常需要用到。
gcnew::托管对象分配,跟C++的new 一样,负责分配内存和构造对象。不同的是,它分配在托管堆上。同时用户不再需要去关心什么时候调用delete来释放。
ref&value:申明一个托管类定义时,我们必须声明或者ref或者value,以告诉编译器,这不是一个C++类。绝大部分托管类都是ref类,value类相对于ref类来说,它适合一些很小型的对象的申明,这样的对象允许在栈中产生,但是它的限制也很多,复杂的构造器就是它所不能承受的。
override:C++申明一个虚拟成员函数的时候,通常如下:
virtual void func();
而在CLR中当重载父类虚方法的时候,编译器会抱怨没有告诉它这个函数要重载还是新写。这时候需要用如下语法来告诉编译器:
virtual void func() override/new;
for each:和明显这是类似stl的for_each迭代。
nullptr:无效的句柄,这个可以理解为c++里面的空指针。任何对nullptr句柄的访问,都会产生CLR异常。
#using “xxx.dll”:导入了clr动态库的符号表,这个对封装非常有用,它可以最大限度的降低不必要的头文件暴露,降低工程耦合度。
property:属性,这是一个原来在大多数C++编译器厂商都有自己规格的支持的特性,但是始终没有被纳入标准C++。
property System::String ^ Text{
System::String ^ get();
void set(System::String ^);
}
这是CLR的一个属性的申明,和以前C++Builder及其类似,它可以为类对象提供一个看起来像操作成员变量一样的语法,但真实的操作确是一个复杂的函数流程。get和set可以隐含技术细节,对外暴露的接口非常简洁明快。但是,我有一个小小的遗憾,CLR里property的set方法没有返回值,在写连等的时候无法编译通过。例如:
var1.Text=var2.Text=”Hello World”;
我想如果set可以申明成 T% set(T^);开发者在set中return *this;这个问题也就可以解决了。(%是CLR的取地址符-!-)
event&delegate:
public delegate void OnSubActionFinished( System::String^ Act , int userdata );
上面的语法可以申明一个代理原形。
event OnSubActionFinished^ SubActionReporter;
这个可以申明一个事件。
此后,我们只要完成一些OnSubActionFinished同型的函数,让事件SubActionReporter+=任意个此类函数,当我们call这个SubActionReporter的时候,所有被+=的函数,都会接受到事件通知。