1
\$\begingroup\$

I'm creating a class library to read/write .3di files which are binary files describing 3D models for a video game. This review is for the write/serialization process.

I've mapped the different structures in the .3di file (header, textures, vertices, etc.) to the classes shown below. Each class implements the IBinarySerializable interface and is thus responsible for its own serialization logic. I've omitted many parts of the 3D model here for brevity, but they follow the same pattern.

Clients would use the library by building out a Model object and then calling SaveToFile().

interface IBinarySerializable
{
    byte[] Serialize();
}

public class Model
{
    public ModelHeader Header { get; set; }
    public List<Texture> Textures { get; set; }
    public List<Vertex> Vertices { get; set; }

    public void SaveToFile(string filePath)
    {
        using (FileStream fileStream = new FileStream(filePath, FileMode.Create))
        using (BinaryWriter writer = new BinaryWriter(fileStream))
        {
            writer.Write(Header.Serialize());
            writer.Write(Textures.SelectMany(texture => texture.Serialize()).ToArray());
            writer.Write(Vertices.SelectMany(vertex => vertex.Serialize()).ToArray());
        }   
    }
}

public class ModelHeader : IBinarySerializable
{
    public string Name { get; set; }
    public int TextureCount { get; set; }

    public byte[] Serialize()
    {
        using (var buffer = new MemoryStream())
        using (var writer = new BinaryWriter(buffer))
        {
            // Hardcoded values.
            writer.Write(Encoding.ASCII.GetBytes("3DI"));
            writer.Write((byte)8);

            // Header data.
            writer.Write(Encoding.ASCII.GetBytes(Name.PadRight(8, '\0')));
            writer.Write(TextureCount);

            return buffer.ToArray();
        }
    }
}

public class Texture : IBinarySerializable
{
    public string Name { get; set; }
    public int Width { get; set; }
    public int Height { get; set; }

    public byte[] Serialize()
    {
        using (var buffer = new MemoryStream())
        using (var writer = new BinaryWriter(buffer))
        {
            writer.Write(Encoding.ASCII.GetBytes(Name.PadRight(28, '\0')));
            writer.Write(Width);
            writer.Write(Height);

            return buffer.ToArray();
        }
    }
}

public class Vertex : IBinarySerializable
{
    public short X { get; set; }
    public short Y { get; set; }
    public short Z { get; set; }

    public byte[] Serialize()
    {
        using (var buffer = new MemoryStream())
        using (var writer = new BinaryWriter(buffer))
        {
            writer.Write(X);
            writer.Write(Y);
            writer.Write(Z);

            return buffer.ToArray();
        }
    }
}

My Concerns

  • Since the .3di file schema is proprietary, I don't believe I can use the binary serialization that comes with .NET (e.g., BinaryFormatter) since it adds .NET-specific data to the file. Is that correct?
  • Should each class be responsible for its own serialization logic? I've considered centralizing all of that logic to a dedicated class, e.g., ModelSerializer, ModelFileWriter, etc. One benefit of that approach is that I wouldn't have to map the file structures to classes so literally.
  • I repeat this in each Serialize() method:
    using (var buffer = new MemoryStream())
    using (var writer = new BinaryWriter(buffer))
    
    Should I extract those lines to a helper function and just pass in what I want to do with the writer, e.g., BinaryUtils.Serialize(Action<BinaryWriter> writeAction)?
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Please edit your question by inserting the correct code of the Model class. Hint: IBinarySerializable \$\endgroup\$
    – Heslacher
    Commented Feb 7, 2020 at 5:32

1 Answer 1

2
\$\begingroup\$

BinaryFormatter is not adding any extra information. It does not store "here comes a double", this must be stored/handled by the reading algorithm.

But to reproduce a certain byte order, you have to overwrite almost every serializing routine. At the end you gain nothing, comparing to your self-implementation. So it's not the extra-information, it's the order of elements, that might cause trouble when using the given BinaryFormatter.

Each class can be responsible for it's own serialization, if you have only one serialization. If you can always modify your own code, the classes and the serialization never go separate ways to separate libraries, then this might be a good way.

A more sophisticated approach would be a Visitor-pattern. This way you can implement multiple Serializers (Visitors), store in different formats, etc.

One way you have everything concerning a class in the class, the other way you have everything concerning one specific file format in one class. This you have to make up with yourself, it depends on how often you plan to modify or extent anything later on.

Serializing is a streaming process. You create only one Stream ( a File or MemoryStream), don't use one per item, and assemble it later on. You create one Formatter/Writer, and pass this to each item. Each item is storing it's stuff into the output stream. So an item with one property to store, should have one line of code in it's Serializer method.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.