通用编程器:游戏引擎中的通用编程技术



这里必须再次提醒你本文介绍些通用游戏编程窍门技巧虽然是通用但是可能并不是非常全面可能存在这样或那样缺陷因此如果你希望它发挥最大效用必须恰当使用它而不是不分场合滥用切记切记个初学者最容易犯就是任意使用些设计模版而不顾它使用范围

在开始构建个游戏引擎时你需要先考虑哪些方面问题呢?这是你必须认真考虑问题答案是首先必须考虑代码可读性尤其是在多人进行开发时更必须高度重视如果你写代码其他人需要花费非常大精力进行阅读那么根本谈不上提高工作效率下面是提高代码可读性些良好建议:

1、建立份简单明了命名规则份良好命名规则可以大幅提高代码可读性规则必须简单明了通常只需要两 3分钟阅读应该可以让其他人掌握例如在代码中直接使用匈牙利命名法这种大家熟知规则使用字母I作为接口类首字母使用C开头作为实现类首字母使用g_开头变量名作为全局变量s_开头作为静态变量名m_开头作为内部变量名使用_开头作为类内部使用名等等通过名字就可以使你大概了解对象使用范围和基本功能

2、不要讨厌写注释个编程者易犯就是不写注释认为它会增加自己工作量但是他没有考虑到相应工作量已经转移到代码阅读者身上可能看代码人会花费比写注释时间两倍或者 3倍时间来阅读代码这是种非常不负责任行为通过段简短注释可以使阅读者迅速了解代码功能从而把时间更多用到功能扩展上下面是些良好建议:尽量对每个变量标明它功能对每声明地方标明它功能对于复杂还应当写清参数和返回值作用注意是在声明头文件中在关键代码处写清它作用尤其是在进行复杂运算时更应如此在每个类声明地方简要介绍它功能

3、减少类继承层次通常对于游戏编程来说每个类继承层次最好不要超过4层过多继承不仅会减少代码可读性同时使类表指针变长代码体积增大减低类执行效率还要注意要减少多重继承不小心它会形成编程者非常讨厌“钻石”形状同时还要注意如果能使用类组合话那么就尽量减少使用类继承当然这是设计窍门技巧问题

4、减少每行代码长度尽量不要在行代码中完成个复杂运算这样做会增加阅读难度同时不符合现代CPU执行由于CPU现在都使用了超长流水线设计它非常适合执行那些每行代码非常短而行数非常多代码例如对个复杂数学运算写成行不如每步骤写
以上建议是我些粗略看法如果你还有什么好看法可以给我指出来同时上面建议并不是绝对例如类继承并不是绝对不能超过4层如果你需要话可以使用更多继承前提是这样带来好处大于代码执行效率损失


接着看看要考虑什么在GameProgrammingGems3个基于对象组合游戏架构文指出了几个值得考虑问题首先是平台相关性和独立性和游戏相关性和独立性问题也就是说应当作到引擎架构和平台和游戏都无关为什么要做到和平台无关性呢?这是你必须在开始架构引擎考虑它可移植性如果在开始你没有注意到这个问题那么旦在游戏完成后需要移植到其他游戏平台上你会发现麻烦大了你需要修改地方实在是太多了所有和平台相关API都需要修改所有使用了平台特定功能模块也需要修改这是个非常耗费精力事情可能需要花费和开发个游戏时间而如果你在开始时候就考虑到这个问题那么非常简单只需要写个相应平台模块替换掉原来模块即可这样精力就可以放在如何充分利用特定平台能力来提高游戏表现力上而不是代码修改上 [Page]
下面简单下如何使引擎作到和平台无关

1、注意操作系统差异现在主流操作系统主要是Windows和Linux两种当然还有Unix和Mac在编程时你必须注意这当你需要包含Windows头文件时你必须将它包含在宏_WIN32中下面是个简单例子:
#def_WIN32
#\"windows.h\"
#end
而你使用Windows平台特定API时也应当如此这样在其他平台上编译时可以保证Windows平台相应代码不会被编译进去对于其他平台也应当如此
2、注意编译器差异现在通用编译器主要有VCBC和gcc几种在进行Windows平台编程时你通常会使用VC或BC而对Linux平台编程时通常使用gcc使用VC编译器你不可能编译出用于Linux平台代码因此在编程时也需要注意你可以使用上面思路方法通过特定宏来将区别编译器分离开个简单例子:
#def_WIN32
#def_MSC_VER
typedefsigned__6464;
#end
#eld_LINUX
typedeflonglong64;
#end
在区别编译器中对64位变量命名是区别它并不是C标准部分而是编译器扩展部分另外个例子是编译器使用内联汇编代码在VC中你可以使用_asm来指明而对于Linux平台编译器你需要使用它专用关键字了
3、注意CPU差异对于区别平台来说它通常会使用区别CPU不过幸好Windows和Linux都支持X86CPU这也是PC游戏主流CPU平台而XBOX使用也是X86CPU除非你需要移植到PS2平台否则这将大大减轻你编程负担在X86平台上提供了个cpuid指令可以非常方便检查CPU特性如是否支持MMXSSESSE23DNow!技术等通过它你可以使用特定CPU特性来加速你代码执行速度
4、注意图形API差异现在图形API主要存在两种主流平台DirectX和OpenGLDirectX只能用于 Windows平台而OpenGL几乎被所有平台所支持因此你需要为区别图形API进行封装将它做成区别模块在需要时候进行切换完成这个工作最好思路方法是使用后面介绍类厂模式
5、注意显卡差异现在显卡有两大主流ATI和NV虽然显卡可以被主流操作系统所支持但是必须注意在区别游戏平台上还是使用区别GPU而在GPU的间也相应有自己功能扩展因此在使用特定扩展功能时必须检查下是否被显卡所支持


6、注意shader语言差异可编程图形语言出现是最重要项发明现在几乎每个游戏都在使用这项技术而正由于它重要性现在出现了多个标准HLSL只能用于DX中而OpenGL由于标准开放性更加混乱个显卡厂商都根据自己产品推出相应扩展指令来实现shader而NV更推出了GC可以同时适用于DirectX和OpenGL这是个非常好想法不过由于这不是个开放标准因此没有得到其他厂商支持在ATI显卡上运行GC代码你会发现比在NV显卡慢了几个数量级由于上面情况你需要根据区别平台相应进行封装思路方法和第4条下面建议值得你去考虑当你使用DirectX平台时应当使用HLSL而对于OpenGL可以封装为两个模块根据显卡区别进行切换也可以使用GC特别为NV显卡封装个模块来对它进行优化这里需要补充如果可以话尽量和OGRE样为区别操作系统进行封装这样方便在区别系统的间进行切换接着看看如何实现游戏无关性通常游戏引擎如果要实现游戏无关性是非常困难这也就是说要求你引擎适合所有游戏类型这太难了考虑个RPG游戏引擎如果用来做个RTS游戏那简直是不可能类似你不可能拿Q3引擎来做RTS游戏但是如果引擎设计非常良好话还是可以实现部分游戏无关性也就是说你可以将引擎部分模块设计成通用模块这样在开发其他类型游戏时可以重用这部分代码这部分代码包括底层显示声音网络输入等部分在设计它们时你必须保证它们具有良好通用性在这些问题的后你应当考虑国际化问题这也是非常重要方面游戏可能在其它国家发行这主要是注意语言方面问题尤其是处理在C标准库中提供了个String容器它提供了对国际化良好支持因此在引擎中你需要从头到尾使用它接下来我们看看本文最重要内容如何组织个引擎架构这是引擎最重要部分为什么重要呢?如果我们把引擎看作间房子那么架构可以看作是房子框架当你完成这个框架后就可以向框架内添砖加瓦盖房子了下面让我们来看看如何构建这个框架通常个大型软件Software工程是按照模块化方式来构建编程的前要进行必要需求分析将软件Software工程根据区别功能划分为几个较大功能模块对比较复杂模块你可能还需要将它分为几个子模块并需要给出各个模块的间逻辑关系当你编写个引擎时也需要进行相应功能分析让我们看看如何来划分引擎功能模块如果按照上面游戏无关性和相关性进行分析话我们可以发现它可以分为游戏相关层和无关层两层游戏相关层由于包含了游戏逻辑性代码也被称为逻辑层逻辑层应该位于引擎最顶层如果你在开发个局域网或在线游戏按照网络C/S开发模式层应该分为两个模块服务器和客户端模块,它包含了和特定游戏相关所有功能如AI游戏角色 游戏事件管理网络管理等等在它下面就是游戏无关层了包括了引擎核心模块GUI模块文件系统管理模块等等其中引擎核心模块是最重要部分逻辑层主要通过它来和底层模块打交道它应该包含场景管理特效管理控制台管理图形处理等等内容在向下就是些底层模块了如图形渲染模块输入设备模块声音模块网络模块物理模块角色模型模块等等所有这些底层模块必须通过核心模块来和逻辑层进行交互因此核心模块是整个引擎枢纽所有模块都通过它来进行交互 [Page]

下面看看应该如何来进行模块设计这里有些通用规则
是你应当遵守:
1、减少模块的间关系复杂度我们知道通常每个模块内部都存在大量对象需要在各个模块的间进行相互如果我们假设每个模块内部对象数量为N那么每两个模块的间关系复杂度为N*N这样复杂度是不可接受为什么呢?首先是它非常不利于管理由于各个
模块都存在大量全局对象并存在相互依存关系并且各自建立时间各不相同这就存在化顺序矛盾考虑这种情况个模块中存在个对象需要另外个模块中对象才能进行当这个对象进行化时而另外对象在的前并没有化就会引发崩溃其次不利于多人进行同时开发由于各个模块存在相互依存关系当复杂度非常高时就会出现模块和模块高度依存也就是说个模块没有完成下个模块就无法完成因此就需要个模块个模块按照它依存关系进行编程而无法同步进行因此在设计模块时件事情是减少模块的间复杂度为此你在设计模块时必须为模块设计个交互接口并约定所有模块的间交互必须通过这个接口来进行这样模块的间关系复杂度就降低为1*1了非常方便管理同时这非常利于多人的间进行开发假如每个人负责个模块开发那么你只需要先完成这个接口类其他人就可以利用这个接口进行其他模块开发而不必等到你完成所有类再进行这样所有模块都是同步进行可以节省大量宝贵开发时间
2、对类抽象接口而不是类实现编程这是DesignPatten书作者对所有软件Software编程者建议它也对游戏编程有很大指导意义对模块中所有被其它模块使用类都要建立个抽象接口其它模块要使用这个抽象接口进行编程这样其它模块就可以在不需要知道类是如何实现情况下进行编程这样做好处是在接口不改变情况下任意对类实现进行改变而不必通知其它人这对多人开发非常有用
3、根据对象区别对类进行分层实际上本条还是对第2条补充分层还是为了更好隐藏底层实现通常个类不仅被其它模块使用还要被自身模块所而且它们需要功能也区别因此我们可以让个类对外部显现个接口而对内部也显现个接口这样做好处和上面 个复杂模块也是多人在进行编程
4、通过让个类对外显现多个接口来减少类数量减少关系复杂度个思路方法是减少类数量因此我们可以把完成区别功能类合并成个类并让它对外表现为多个接口也就是个类实现可以继承多个接口
上面建议只是起到参考作用具体实现时你应该根据情况灵活使用而不是任意乱用下面内容涉及到具体编程窍门技巧
对于引擎中全局对象你可以使用Singleton如果你不了解它是什么可以阅读DesignPatten里面有对它详细介绍具体使用可以通过OGRE引擎获得


模块内对象可以通过类厂来实现COM可以看作是种典型类厂DX就是使用它来进行设计而著名开源引擎CrystleSpace也是做过建立个类似COM物体来实现但是我并不对它很认可首先构建个类似COM类厂非常复杂开销有点大其次COM个优点是可以对实现向下兼容这也是DX使用它重要原因个游戏引擎并不需要OGRE中也实现了个类厂结构这是个比较通用类厂但是使用起来还是需要写段代码我比较欣赏VALVE做法它通过使用个宏就解决了这个问题非常高效使用起来也非常方便这个做法很简单它把每个模块中需要对外暴露接口都连接到个内部维护链表上个接口都和个接口名相连这样外部模块可以通过传入个接口名给CreateInterface就可以获得这个接口指针了非常简单下面看看它具体实现它内部保存链表结构如下: [Page]
InterfaceReg
{
public:
InterfaceReg(InstantiateInterfaceFnfn,constchar*pName);

public:
InstantiateInterfaceFnm_CreateFn;
constchar*m_pName;

InterfaceReg*m_pNext;
InterfaceReg*s_pInterfaceRegs;
};
并定义了两个指针和
#CREATEINTERFACE_PROCNAME\"CreateInterface\"
typedefvoid*(CreateInterfaceFn)(constchar*pName,*pReturnCode);


typedefvoid*(InstantiateInterfaceFn)(void);
DLL_EXPORTvoid*CreateInterface(constchar*pName,*pReturnCode);
下面看看它如何通过宏来建立链表
#EXPOSE_INTERFACE(Name,erfaceName,versionName)void*__Create##Name##_Interface{(erfaceName*)Name;}InterfaceReg__g_Create##erfaceName##_Reg(__Create_##Name##_Interface,versionName);
如果你有个类CPlayer它想对外暴露接口IPlayer那么很简单可以这么做
#PLAYER_VERSION_NAME\"IPlayer001\"
EXPOSE_INTERFACE(CPlayer,IPlayer,PALYER_VERSION_NAME);
如果在其他模块内你需要获得这个接口可以这么做
CreateInterfaceFnfactory=reerpret_cast(GetProcAddress(hDLL,CREATEINTERFACE_PROCNAME));
IPlayerplayer=factory(PLAYER_VERSION_NAME,0);
其中hDLL为模块句柄这里指针factory实际指向模块内部CreateInterface这个
通过比较传入接口名从链表找到指定类指针

解决了类厂问题下面让我们看看如何建立模块对外接口在GameProgrammingGems3个基于对象组合游戏架构文提出了种架构HalfLe2引擎中对这种架构进行了有效扩展你可以让所有对外暴露接口都使用这个架构前提是模块只有个接口对外暴露
IApp
{
public:
//Here’swheretheappsystemsgettolearnabouteachother
virtualboolConnect(CreateInterfaceFnfactory)=0;
virtualvoidDisconnect=0;

//Here’swheresystemscanaccessothererfacesimplementedbythisobject
//ReturnsNULLitdoesn’timplementtherequestederface [Page]
virtualvoid*QueryInterface(constchar*pInterfaceName)=0;

//Init,shutdown
virtualInitReturnVal_tInit=0;
virtualvoidShutdown=0;
};
通过Connect思路方法你可以将两个模块建立个连接关系通过QueryInterface思路方法你可以检索到其他 需要暴露接口这种思路方法很好为所有模块建立个标准对外接口极大减轻了编程复杂性遗憾是在HL2引擎中只有部分模块使用了这个思路方法可能是这个接口引入时间太晚缘故
Tags:  通用plc编程软件 通用编程器

延伸阅读

最新评论

发表评论