If you always know the order of the data, and the data sizes, e.g. four 32-bit floats, followed by two 32-bit integers, followed by three Booleans, and you don’t care too much for error detection, i.e. your network protocol will detect and correct the errors, or the data is saved and restored to disk and the possibility of corruption is reasonably low, then a simple bit stream serialization system works just fine. You can take the raw bits or bytes of each piece of data, stick it in to a byte array, and then do whatever you need to with it. The BitStream project I linked to will encapsulate all of that for you very easily, or you can do it manually.
If you are not sure of the order of the data, or the data sizes, or if the data may or may not include certain pieces, i.e. it is context sensitive, sometimes there is a Boolean to indicate some effect in your game and sometimes there isn’t, you can use the more traditional serialization techniques of .NET.
My recommendation is that you do not worry about network optimising or format optimising or coding optimising your serialization system at this time until you have it working just so.
Serialization is just the fancy modern word for sticking data in a container at one end and pulling it out of the container at the other end, but with the added implication that the serialization is more opaque to the programmer, and more forgiving of changing data formats and also different machine architectures. In the bad old days, if I wanted to store a few integers, I would have to worry about whether they were 16-bit, 32-bit or 64-bit integers, whether the machine I was on was using little-endian or big-endian architecture and floats and doubles might be even worse, because IEEE 754 might be a very fine standard for storing floating point numbers in a computer, but slight variations in implementations between different central processing units can cause all sorts of issues.
[System.Serializable]
public struct DataPacketSerialization
{
int health;
int level;
// we are assuming that Vector3 is a simple data structure
// containing intrinsic value types and is therefore can be
// automatically serialized by .NET without any extra work
// on our part
Vector3 pos;
}
public struct DataPacketBadOldDays
{
int health;
int level;
Vector3 pos;
byte[] PackItUp()
{
byte[] data = new byte[20];
data[0] = (byte)((health >> 24) 0xFF);
data[1] = (byte)((health >> 16) 0xFF);
data[2] = (byte)((health >> 8) 0xFF);
data[3] = (byte)(health 0xFF);
data[4] = (byte)((level >> 24) 0xFF);
data[5] = (byte)((level >> 16) 0xFF);
data[6] = (byte)((level >> 8) 0xFF);
data[7] = (byte)(level 0xFF);
// obviously this is C# and this kind of byte manipulation won't work
// we would instead use BitConverter.GetBytes to extract the individual bytes of floats,
// but this code illustrates the nastiness that was "serialization" in C++ in the bad old days
data[8] = (byte)((pos.x >> 24) 0xFF);
data[9] = (byte)((pos.x >> 16) 0xFF);
data[10] = (byte)((pos.x >> 8) 0xFF);
data[11] = (byte)(pos.x 0xFF);
data[12] = (byte)((pos.y >> 24) 0xFF);
data[13] = (byte)((pos.y >> 16) 0xFF);
data[14] = (byte)((pos.y >> 8) 0xFF);
data[15] = (byte)(pos.y 0xFF);
data[16] = (byte)(pos.z >> 24) 0xFF;
data[17] = (byte)(pos.z >> 16) 0xFF;
data[18] = (byte)(pos.z >> 8) 0xFF;
data[19] = (byte)(pos.z 0xFF);
return null;
}
void UnpackIt(byte[] data)
{
health = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | (data[3]);
level = (data[4] << 24) | (data[5] << 16) | (data[6] << 8) | (data[7]);
pos = new Vector3();
pos.x = (float)((data[4] << 24) | (data[5] << 16) | (data[6] << 8) | (data[7]));
pos.y = (float)((data[4] << 24) | (data[5] << 16) | (data[6] << 8) | (data[7]));
pos.z = (float)((data[4] << 24) | (data[5] << 16) | (data[6] << 8) | (data[7]));
}
}
All of this nice-ness and syntactic sugar that .NET adds comes with a price. The overhead of the serialization itself. Whereas the bad old days version takes up 20 bytes of data total, the serialization version will take up some arbitrary number of bytes more memory space simply because it needs to store type information too.
In .NET you can also version your serialization data, so that should your underlying data structure change, such as a new data member is introduced at a later time, but this new data needs to work with older versions of your code, you can say “I am using serialization version 1.7, this is what I expect” and the .NET serialization code will say “Okay, this is what I have, and here are some sane values for the bits I am missing.”
With serialization you can also easily change the format of the serialization, be it XML, which is reasonably readable by humans, especially if you make use of YAXLib to properly format your data, you can save it out as binary, which still includes the type information, error correction, etc all the way down to a bitstream, where the type information is thrown away and you, as the programmer, have to explicitly know what order the data members in, and what type they are, so that you can de-serialize the bitstream. With a pure bitstream you also need to be careful, as previously stated, about endian issues, Intel, Morotola, ARM, etc. Different implementations of floating point numbers (even if they follow the standards), or Booleans, can also cause issues.
You can also marshal your entire data structure by pinning it and then getting the raw underlying bytes in one easy .NET operation, this will give you the raw bytes, which you are then free to throw across a network to an identical machine architecture, and reconstitute at the other end with a similar marshal operation. I use this technique for reading memory out of non-managed applications such as Unity or World of Warcraft, when I want to go poking around in the innards of an application that has no functionality to do what I want. Andorov touched on this, but it is recommended you stay away unless you really have a strong reason to do this as you are relying on how the platform, Mono in this case, arranges its data in memory, though you can use ExplicitLayout on your structs if you need to.
Edit: Eric5h5 has mentioned the BitConverter class, which I also touched on, that can do much of what is needed to extract or inject bytes in to an intrinsic value type so you do not have to go mucking around with the bit-operations I showed in the example code that is more indicative of a C or C++ solution rather than C#.