游戏引擎架构选摘之第五章 游戏支持系统

每个游戏都需要一些底层支持,以管理一些例行却关键的任务。例如启动及终止引擎、存取(多个)文件系统、存取各种不同的资产类型(网格、纹理、动画、音频等),以及为游戏团队提供调试工具。

5.1 子系统的启动和终止

游戏引擎是复杂软件,由多个互相合作的子系统结合而成。当引擎启动时,必须一次配置及初始化每个子系统。各子系统间相互依赖关系,隐含地定义了每个子系统所需要的启动次序。例如子系统B依赖子系统A,那么在启动B之前,必须先启动A。各子系统的终止通常采取反向次序,即先终止B,再终止A。

5.1.1 C++的静态初始化次序(是不可用的)

由于多数新式游戏引擎皆采用C++为编程语言,我们应该考虑一下,C++原生的启动和终止语意是否可作为启动及终止引擎子系统之用。C++中,在调用程序进入主函数或者Windows的WinMain())之前,全局及静态对象已被构建。然而,我们完全不可预知这些构造函数的调用次序。在Main或WinMain结束之后,会调用全局及静态对象的析构函数,而这些函数的调用次序也是无法预知的。显而易见,此C++行为并不适合用来初始化及终止游戏引擎的子系统,实际上,对任何含互相依赖全局对象的软件都不适合。

这很令人失望,因为要实现各主要子系统,例如游戏引擎中的子系统,常见的设计模式是为每个子系统定义单例类(singleton class)(通常称作管理器/manager)。若c++能给予我们更多的控制能力,指明全局或静态实例的建构、析构次序,那么我们就可以把单例定义为全局变量,而不必使用动态内存分配。例如,各子系统可写为以下形式:

calss RenderManager

{

  public:

           RenderManager()

           {

              //启动管理器....

           }

           ~RenderManager()

           {

             //终止管理器.....

           }

};

static RenderManager gRenderManager;

 

可惜,由于没法直接控制建构和析构次序,此方法行不通。

5.1.1.1 按需架构

对付此问题,可使出一个C++的花招:函数内声明的静态变量并不会于main()之前建构,而是在第一次调用该函数时才建构。因此,若把全局单例改为静态变量,我们就可以控制全局单例的建构次序。

class RenderManager
{
public:
 static RenderManager& get()
 {
  static RenderManager sSingleton;
  return sSingleton;
 }
 RenderManager()
 {
  //对于需要依赖的管理器,先通过调用它们的get()启动它们
  VideoManager::get();
  TextureManager::get();
  //现在启动渲染管理器
  //.....
 }
 ~RenderManager()
 {
  //终止管理器
};

你会发现,许多的工程教科书都会建议此方法,或以下这种含动态分配单例的变种:

 static RenderManager& get()
 {
  static RenderManager* gpSingleton=NULL;
  if (gpSingleton==NULL)
  {
   gpSingleton=new RenderManager;
  }
  ASSERT(gpSingleton);
  return *gpSingleton;
 }

但是,此方法不可控制析构的次序。例如,在RenderManager析构之前,其依赖的单例可能已被析构。而且,很难预计RenderManager单例的确切析构时间,因为第一次调用RenderManager::get()时,单例就会进行建构,天知道那是什么时候!此外,使用该类的程序员可能不会预期,貌似无伤大雅的get()函数可能会有很高的开销,例如,分配及初始化一个重量级的实例。此方法是难以预计且危险的设计。这促使我们诉诸更直接、更大控制权的方法。

5.1.2行之有效的简单方法

假设我们对于子系统继续采用单例管理器的概念。最简单的“蛮力”方法就是,明确地为各个单例管理类定义启动和终止函数。这些函数取代构造和析构函数,实际上,我们会让构造和析构函数完全不做任何事情。这样的话,就可以在Main()中(或某个管理整个引擎的实例中),按所需的明确次序调用各种启动和终止函数。例如:

class RenderManager
{
public:
 RenderManager()
 {
  //不做任何事情
 }
 ~RenderManager()
 {
  //不做任何事情
 }
 void startUp()
 {
  //启动管理器
 }
 void shutDown()
 {
  //终止管理器
 }
};
class PhysicsManager{
 /*
 同上类似内容
 */
};
class AnimationManager{
 /*
 同上类似内容
 */
};
class MemoryManager{
 /*
 同上类似内容
 */
};
class FileSystemManager{
 /*
 同上类似内容
 */
};
//......
RenderManager gRenderManager;
PhysicsManager gPhysicsManager;
AnimationManager gAnimationManger;
TextureManager gTextureManager;
VideoManager gVideoManager;
FileSystemManager gFileSystemManager;
//.....

int main(int argc,const char*argv)
{
 //以正确次序启动各引擎系统
 gMemoryManager.startUp();
 gFileSystemManager.startUp();
 gVideoManager.startUp();
 gTextureManager.startUp();
 gRenderManager.startUp();
 gAnimationManager.startUp();
 gPhysicManager.startUp();
 //......

 //运行游戏
 gSimulationManage.run();

 //以反向次序终止各引擎系统
 //......
 gPhysicsManager.shutDown();
 gAnimationManager.shutDown();
 gRenderManager.shutDown();
 gTextureManager.shutDown();
 gVideoManager.shutDown();
 gFileSystemManager.shutDown();
 gMemoryManager.shutDown();
 return 0;

}

 

此法还有“更优雅的”实现方式。例如,可以让各管理器把自己登记在一个全局的优先队列中,之后再恰当次序逐一启动所有管理器。此外,也可以通过每个管理器列举其依赖的管理器,定义一个管理器间的依赖图(dependency graph),然后按互相依赖关系计算最优的启动次序。(以上两种建议确实比较有用,不过需要你的逻辑清晰)。根据《游戏引擎架构》作者本人的认为,蛮力方法总是优于其它方法。原因如下:

此方法既简单又容易实现。

此方法明确代码的执行次序(强制自定义次序)

此方法容易调试和维护。若某子系统启动时机不够早或者过早,只需要移动一行代码。

当然,用蛮力方法手动启动及终止子系统,还有各小缺点,就是程序员有可能意外地终止一些子系统,而非按启动的相反次序。只要能够成功启动及终止引擎的各子系统,你的任务就完成了。

书中译注说:要解决按需建构方式的析构次序问题,可以在单例建构时,把自己登记在一个全局堆栈中,在Main()结束之前,逐一 把堆栈弹出并调用其终止函数。此方法假设单例的终止次序可以为启动次序的相反,但理论上不能解决所有情况。

以上内容来自《游戏引擎架构》p185-p189

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值