1

我花了无数个小时搜索有关此类主题的信息。我正在编写自己的自定义游戏引擎,以便在 C++ 中使用 SDL。我正在尝试创建一个自定义二进制文件来管理我的游戏资源。到目前为止,在存储我放置在文件中的每种“类型”对象时,我还不能让向量发挥得很好。所以我放弃了使用向量的想法,转而使用数组。我在下面有两个示例,其中我同时使用了向量或数组。所以,首先我为文件创建一个标题。这是结构:

    struct Header
{
    const char* name;   // Name of Header file
    float version;      // Resource version number
    int numberOfObjects;
    int headerSize;     // The size of the header

};

然后在创建标头之后,我有另一个结构,它定义了对象如何存储在内存中。这里是:

struct ObjectData{

    int id;
    int size;
    const char* name;
    // std::vector<char> data; // Does not work very well
    // unsigned char* data;    // Also did not 

    // Also does not work, because I do not know the size yet until I have the data.
    // char data[]         

};

这个结构的主要问题是向量不能很好地发挥作用,一个无符号字符指针一直给我带来问题,并且一个字符数据数组(用于十六进制存储)不起作用,因为我的编译器不喜欢变量数组。

最后一个结构是我的资源文件结构。

struct ResourceFile
{
    Header header;

    int objectCount;
    // Again, vectors giving me issues because of how they are constructed internally
    // std::vector<ObjectData> objectList;
    // Below does not work because, again, no variable data types;
    // ObjectData objects[header.numberOfObjects]


};

我的目标是能够将单个结构写入二进制文件。像这样:

    Header header;

    header.name = "Resources.bin";
    header.version = 1.0f;
    header.headerSize = sizeof(header);

    //vector<char> Object1 = ByteReader::LoadFile("D:\\TEST_FOLDER\\test.obj");
    //vector<char> Object2 = ByteReader::LoadFile("D:\\TEST_FOLDER\\test.obj");

    ObjectData cube;
    cube.id = 0;
    cube.name = "Evil Cubie";
    cube.data = ByteReader::LoadFile("D:\\TEST_FOLDER\\test.obj");
    cube.size = sizeof(cube.id) + sizeof(cube.name) + cube.data.size();

    ofstream resourceFile("D:\\TEST_FOLDER\\Resources.bin", ios::out|ios::app|ios::binary);

    resourceFile << header.name << header.version << header.headerSize;;
    resourceFile << cube.id << cube.name << cube.size;
    for each (char ch in cube.data)
    {
        resourceFile << ch;
    }


    resourceFile.close();

    /*
    ObjectData cube2;
    cube.id = 1;
    cube.name = "Ugle Cubie";
    for each (char ch in Object1)
    {
        cube.object.push_back(ch);
    }
    */


    //resourceFile.data.push_back(cube);
    //resourceFile.data.push_back(cube2);

    //resourceFile.header.numberOfObjects = resourceFile.data.size();


    //FILE* dat = fopen(filename, "wb");
    //fwrite(&resourceFile, sizeof(resourceFile), 1, dat);   // <-- write to resource file
    //fclose(dat);

正如您在上面注意到的,我尝试了两种不同的方法。我尝试的第一种方法是使用旧的 fwrite。即使我通过 ofstream 接受的标志告诉计算机这样做,第二种方法甚至都不用二进制编写它。

我的目标是让代码像这样流畅地工作:

ResourceFile resourceFile;

resourceFile.header.name = "Resources.bin";
resourceFile.header.version = 1;
resrouceFile.header.numberOfObjects = 2;
resourceFile.header.headerSize = sizeof(resourceFile.header);

ObjectData cube;
ObjectData cube2;


resourceFile.data.push_back(cube);
resourceFile.data.push_back(cube2);

resourceFile.header.numberOfObjects = resourceFile.data.size();


FILE* dat = fopen(filename, "wb");
fwrite(&resourceFile, sizeof(resourceFile), 1, dat);   // <-- write to resource file
fclose(dat);

还是没有雪茄。任何人都有任何指针(没有双关语)或资源管理器的适当示例?

4

1 回答 1

1

这是我擅长的事情之一,所以给你。围绕这一点有一整所编程学校,但我遵循的基本规则是:

1) 对具有“恒定”布局的事物使用 FIXED-LENGTH 结构。
这些是文件的标志位,指示子记录#的字节等。将尽可能多的文件内容放入这些结构中-它们非常有效,尤其是与良好的 I/O 系统结合使用时.

您可以使用预处理器宏“#pragma pack(1)”将结构与字节边界对齐:

#ifdef WINDOWS
#pragma pack(push)
#endif
#pragma pack(1)

struct FixedSizeHeader {
   uint32 FLAG_BYTES[1];   // All Members are pointers for a reason
   char   NAME[20];
};

#ifdef WINDOWS
#pragma pack(pop)
#endif
#ifdef LINUX
#pragma pack()
#endif

2)创建一个基类,纯接口,名称如“Serializable”。他是您的高级 API,用于将整个文件对象进出原始内存。

class Serializable { // Yes, the name comes from Java. The idea, however, predates it
public:
   // Choose your buffer type- char[], std::string, custom
   virtual bool WriteToBinary(char* buffer) const = 0;
};

注意:要支持静态“加载”,您将需要所有“可序列化”具有额外的静态功能。有几种(非常不同的)方法可以支持这一点,由于 C++ 没有“虚拟静态”,因此单独的语言都不会强制执行。

3) 创建用于管理每种文件类型的聚合类。它们应该与文件类型具有相同的名称。根据文件结构,在您深入到固定结构之前,每个可能依次包含更多“聚合器”类。

这是一个例子:

class GameResourceFile : public Serializable
{
private:
    // Operator= and the copy ctor should point to the same data for files,
    // since that is what you get with FILE*
protected:
    // Actual member variables- allows specialized (derived) file types direct access
    FixedSizeHeader* hdr;     // You don't have to use pointers here
    ContentManager*  innards; // Another aggregator- implements "Serializable"

    GameResourceFile(FixedSizeHeader* hdr, ContentManager* innards)
       : hdr(hdr), innards(innards) {}
    virtual ~GameResourceFile() { delete hdr; delete innards; }
public:
    virtual bool WriteToBinary(char* outBuffer) const 
    {
        // For fixed portions, use this
        memcpy(outBuffer, hdr, sizeof(FixedSizeHeader)); // This is why we 'pack'
        outBuffer += sizeof(FixedSizeHeader);            // Improve safety...
        return innards->WriteToBinary(outBuffer);
    }

    // C++ doesn't enforce this, but you can via convention
    static GameResourceFile* Load(const char* filename)
    {
        // Load file into a buffer- You'll want your own code here
        // Now that's done, we have a buffer
        char* srcContents;
        FixedSizeHeader* hdr = new FixedSizeHeader();
        memcpy(hdr, srcContents, sizeof(FixedSizeHeader));
        srcContents += sizeof(FixedSizeHeader);

        ContentManager* innards = ContentManager::Load( srcContents); // NOT the file
        if(!innards) {
           return 0;
        }
        return new GameResourceFile(hdr, innards);
    }
};

请注意这是如何工作的——每个部分都负责将自己序列化到缓冲区中,直到我们获得可以通过 memcpy() 添加的“原始”结构(您可以使所有组件都成为“可序列化”类)。如果任何部分未能添加,调用将返回“false”并且您可以中止。

我强烈建议使用“引用对象”之类的模式来避免内存管理问题。但是,即使您不这样做,您现在也可以为用户提供一种很好的一站式购物方法来从文件中加载数据对象:

GameResourceFile* resource = GameResourceFile::Load("myfile.game");
if(!resource) { // Houston, we have a problem
   return -1;
}

最好的办法是将此类数据的所有低级操作和检索 API 添加到“GameResourceFile”。然后,任何用于将更改提交到磁盘的低级状态机协调都本地化为 1 个对象。

于 2013-07-30T16:11:26.803 回答