从零开始手敲次世代游戏引擎(二十五)

接上一篇,我们首先来看一下基本的文件IO操作。

对于C语言标准库提供了两种文件io的API。一种是C标准库当中提供的f*系列的,带有缓存功能的API,如fopen/fclose;一种是POSIX标准的,不带有缓存功能的API,如open/close。不过,如同我们之前说过,windows并不是POSIX系统,因此windows所提供的POSIX类型的API并不是系统调用,而是win32 API的简单封装。

由于我们目前主要是进行游戏资源文件的加载。这种加载基本上是只读性质的,而且大部分是顺序读取,所以带有缓存的f*系列API就比较适合我们的需要了。而且f*系列的API是属于C标准库的一部分,可移植性较好。

我们当然可以在场景管理模块里面直接使用f*系列API来加载资源。但是这么做有以下几个问题:

  1. 虽然f*系列API是C的标准库的一部分,具有良好的可移植性,但是在一些平台上它并不是系统原生的API,所提供的功能十分有限且优化不足。比如在windows平台上有功能更为强大的win32 API,而在PS4上有可以将多个文件的读写进行统一调度优化的库。如果在资源管理模块当中直接调用f*系列的API,那么如果之后我们希望直接使用平台原生的API,会变得困难;
  2. 各个平台对于文件路径的要求和处理有微妙的区别。比如windows平台有盘符的概念;而Linux平台整个文件系统在一个树状结构当中;而PSV的文件系统在路径前还有媒体标识符;PS4的文件系统虽然类似Linux系统但是是在一个严格的沙箱当中的虚拟路径,与文件实际存放路径不同。这些细节与场景管理本身并没有太多关系,应该放在一个独立的地方进行处理;
  3. f*系列API所提供的是同步阻塞型的文件访问。如果我们在场景管理模块当中直接使用这些API,那么场景管理模块在进行文件操作的时候将会失去响应。场景管理模块是与图形渲染模块,动画模块,游戏逻辑模块等紧密协作的一个模块,因此我们应该将其设计为一个快速响应的模块,不能有这样的阻塞;
  4. 如我们上一篇文章所分析的,场景模块是比较复杂的。为了降低其复杂度,增加其可维护性,我们应该尽量把松耦合的功能从其中剥离出来形成单独的模块。

基于这样的基本设计,我们定义了一个资源加载模块,AssetLoader,专门负责资源文件的加载工作。

namespace My {
    class AssetLoader : public IRuntimeModule {
    public:
        virtual ~AssetLoader() {};

        virtual int Initialize();
        virtual void Finalize();

        virtual void Tick();

        typedef void* AssetFilePtr;

        enum AssetOpenMode {
            MY_OPEN_TEXT   = 0, /// Open In Text Mode
            MY_OPEN_BINARY = 1, /// Open In Binary Mode
        };

        enum AssetSeekBase {
            MY_SEEK_SET = 0, /// SEEK_SET
            MY_SEEK_CUR = 1, /// SEEK_CUR
            MY_SEEK_END = 2  /// SEEK_END
        };

        bool AddSearchPath(const char *path);

        bool RemoveSearchPath(const char *path);

        bool FileExists(const char *filePath);

        AssetFilePtr OpenFile(const char* name, AssetOpenMode mode);

        Buffer SyncOpenAndReadText(const char *filePath);

        size_t SyncRead(const AssetFilePtr& fp, Buffer& buf);

        void CloseFile(AssetFilePtr& fp);

        size_t GetSize(const AssetFilePtr& fp);

        int32_t Seek(AssetFilePtr fp, long offset, AssetSeekBase where);

        inline std::string SyncOpenAndReadTextFileToString(const char* fileN
me)
        {
            std::string result;
            Buffer buffer = SyncOpenAndReadText(fileName);
            char* content = reinterpret_cast<char*>(buffer.m_pData);

            if (content)
            {
                result = std::string(std::move(content));
            }

            return result;
        }
    private:
        std::vector<std::string> m_strSearchPath;
    };
}

目前这个定义当中还只是包括了同步版本的API,因此这个类看起来还只是一个wrapper,似乎没有必要将其定义为一个RunTimeModule。但是为了实现上面所说的让文件操作不要阻塞调用线程,我们有两个选项∶

  1. 使用多线程。我们需要在AssetLoader当中创建工作线程池,将对同步阻塞型f*系列API的调用放到工作线程当中去执行;
  2. 使用异步文件IO API。实质上这也是多线程,只不过线程是由操作系统创建。

选项1的好处是我们可以拥有更为细致的控制权,可以精细安排这些线程的优先级以及执行方式等。缺点是我们需要写更多的代码去维护这个线程池的管理,并且因为操作系统缺乏关于我们要进行的工作的足够的信息,可能无法提供文件读写整体方面的优化;

选项二的好处是代码比较简洁,不用进行线程的管理,并且如果我们一次将多个文件读写请求发送给操作系统,可能会得到操作系统在读取方式方面深度的优化,比如通过妥善安排读取的顺序减少硬盘寻道的时间。然而缺点首先是我们可能比较难以控制文件读取的顺序,而且异步文件IO的API并没有得到标准化,在各个平台上API长得很不一样,甚至不支持。

但是不管我们支持上述两种选项的哪一种,因为是异步操作,就意味着我们的AssetLoader会以一种不同于资源管理模块的节奏进行工作。所以我们将它也定义为一个RuntimeModule。

另外一个需要注意的点就是AssetFilePtr。它被定义成为一个空指针类型。这是因为在不同的平台API当中文件的描述子的定义是不同的。我们用空指针类型来抽象整合这种不同。不过需要注意的是AssetFilePtr并不是资源的唯一识别子。资源的唯一识别子是在游戏当中用来索引资源的唯一标识,它应该具备平台无关性和时序无关性;而AssetFilePtr显然这两条都不符合。

况且,这里面还有一个隐含的问题值得我们思考,那就是游戏资源(Asset)与游戏资源文件(Asset File)是否一定是一对一的关系。如果我们将场景物体考虑为Asset的单位,比如游戏当中的主角人物,那么因为一个人物会包括模型、材质、贴图、动画、声音等许多素材,而这些素材往往是以不同格式的文件进行存放的,那么显然这不是一种一对一的关系。

另一方面,如同我们在上一篇文章所述,为了加快资源的读取速度,我们往往最终会将资源文件进行打包合并,从而多个资源可能会对应同一个资源文件,这也不是一种一对一的关系。

因此,很显然地,我们需要在某个地方进行资源与资源文件之间的映射。我们甚至需要导入或者自定义一种资源描述语法(比如URI),来唯一描述某项资源,以及资源之间的联系。

不过在此之前,我们还有一些基础性工作需要做。因为我们的AssetLoader目前只能完成文件内容的加载(以文本文件模式或者二进制模式),并不能解析文件的内容。我们下一篇将介绍如何为我们的AssetLoader设计各种文件格式的解析器(decoder)。

本文当中涉及到的AssetLoader的实现代码在GitHub的article_25当中。

参考引用

  1. http://man7.org/linux/man-pages/man7/aio.7.html
  2. https://msdn.microsoft.com/en-us/library/aa365683.aspx
  3. https://isocpp.org/wiki/faq/serialization#serialize-text-format

本作品采用知识共享署名 4.0 国际许可协议进行许可。

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

您正在使用您的 WordPress.com 账号评论。 登出 /  更改 )

Google photo

您正在使用您的 Google 账号评论。 登出 /  更改 )

Twitter picture

您正在使用您的 Twitter 账号评论。 登出 /  更改 )

Facebook photo

您正在使用您的 Facebook 账号评论。 登出 /  更改 )

Connecting to %s

%d 博主赞过: