14

因此,我在 Gamasutra 阅读了对 John Carmack 的采访,其中他谈到了他所谓的“存在于内存映射文件中的实时 C++ 对象”。以下是一些报价:

JC:是的。实际上,我从中获得了很多好处……上一个 iOS Rage 项目,我们提供了一些新技术,这些技术使用一些聪明的东西来制作活的 C++ 对象,这些对象存在于内存映射文件中,由闪存文件系统支持在这里,这就是我想在 PC 上构建我们所有未来工作的方式。

...

我在这里对自己的行军命令是,我希望在我们的 PC 平台上加载两秒的游戏,这样我们就可以更快地迭代。而现在,即使使用固态驱动器,您在加载时所做的所有事情都被您所支配,因此需要这种不同的纪律才能说“所有内容都将被抽取并用于相对地址”所以你只需说,“映射文件,我所有的资源都在那里,并且在 15 毫秒内完成。”

(完整的采访可以在这里找到)

有人知道卡马克在说什么,以及你将如何设置这样的东西吗?我在网上搜索了一下,但我似乎找不到任何东西。

4

5 回答 5

7

这个想法是,通过内存映射访问该文件,您可以随时将全部或部分程序状态序列化到一个文件中。这将要求您没有通常的指针,因为指针仅在您的进程持续时才有效。相反,您必须从映射开始存储偏移量,以便在重新启动程序并重新映射文​​件时可以继续使用它。这种方案的优点是你没有单独的序列化,这意味着你没有额外的代码,你不需要一次保存所有状态——相反,你的(全部或大部分)程序状态是始终由文件支持。

于 2011-08-23T10:05:45.803 回答
2

您可以直接或通过自定义分配器使用placement new。

查看EASTL以了解(子集)STL 的实现,该 STL 专门适用于自定义分配方案(例如在嵌入式系统或游戏控制台上运行的游戏所需)。

EASTL 的免费子集在这里:

于 2011-08-23T10:07:11.970 回答
2

我们多年来一直使用我们称之为“相对指针”的东西,它是某种智能指针。它本质上是非标准的,但在大多数平台上都很好用。它的结构如下:

template<class T>
class rptr
{
    size_t offset;
public:
    T* operator->() { return reinterpret_cast<T*>(reinterpret_cast<char*>(this)+offset); }
};

这要求所有对象都存储在同一个共享内存中(也可以是文件映射)。它通常还要求我们只在其中存储我们自己的兼容类型,以及编写自己的分配器来管理该内存。

为了始终拥有一致的数据,我们通过 COW mmap 技巧使用快照(在 linux 上的用户空间中工作,不了解其他操作系统)。

随着向 64 位的大迁移,我们有时也只使用固定映射,因为相对指针会产生一些运行时开销。通常使用 48 位地址空间,我们为我们的应用程序选择了一个保留的内存区域,我们总是将此类文件映射到该区域。

于 2011-08-23T10:37:13.317 回答
1

这让我想起了一个文件系统,我在极短的时间内想出了加载 CD 级别的文件(它将加载时间从 10 秒缩短到接近瞬时),并且它也适用于非 CD 媒体。它由三个版本的类组成,用于包装文件 IO 函数,都具有相同的接口:

class IFile
{
public:
  IFile (class FileSystem &owner);
  virtual Seek (...);
  virtual Read (...);
  virtual GetFilePosition ();
};

和一个额外的类:

class FileSystem
{
public:
  BeginStreaming (filename);
  EndStreaming ();
  IFile *CreateFile ();
};

你会编写如下加载代码:

void LoadLevel (levelname)
{
  FileSystem fs;
  fs.BeginStreaming (levelname);
  IFile *file = fs.CreateFile (level_map_name);
  ReadLevelMap (fs, file);
  delete file;
  fs.EndStreaming ();
}

void ReadLevelMap (FileSystem &fs, IFile *file)
{
  read some data from fs
  get names of other files to load (like textures, object definitions, etc...)
  for each texture file
  {
    IFile *texture_file = fs.CreateFile (some other file name)
    CreateTexture (texture_file);
    delete texture_file;
  }
}

然后,您将拥有三种操作模式:调试模式、流文件构建模式和发布模式。

在每种模式下,FileSystem 对象都会创建不同的 IFile 对象。

在调试模式下,IFile 对象只是包装了标准的 IO 函数。

在流文件构建中,IFile 对象还包装了标准 IO,但具有将读取的每个字节写入流文件(所有者 FileSystem 打开流文件)以及写入任何文件指针位置查询的返回值的附加功能(因此,如果需要知道文件大小,则将该信息写入流文件)。这会将各种文件连接成一个大文件,但只有实际读取的数据。

发布模式将创建一个不打开文件或在文件中查找的 IFile,它只是从流文件中读取(由所有者 FileSystem 对象打开)。

这意味着在发布模式下,所有数据都是在一系列连续读取中读取的(操作系统会很好地缓冲它),而不是大量的搜索和读取。这对于寻道时间非常慢的 CD 来说是理想的选择。不用说,这是为基于 CD 的控制台系统开发的。

副作用是数据被剥离了通常会被跳过的不必要的元数据。

它确实有缺点——一个关卡的所有数据都在一个文件中。这些可能会变得非常大,并且数据无法在文件之间共享,如果您有一组纹理,例如,它们在两个或多个级别中是通用的,则数据将在每个流文件中复制。另外,每次加载数据的加载过程都必须相同,不能有条件地跳过或添加元素到关卡。

于 2011-08-23T10:52:26.437 回答
0

正如 Carmack 指出的,许多游戏(和其他应用程序)加载代码是结构化的,就像许多小的读取和分配一样。

您不必这样做,而是将单个fread(或等效的)说一个级别文件放入内存,然后再修复指针。

于 2011-08-23T10:13:09.970 回答