Bitpacking
Obsolete: please use NetworkReader/Writer instead.
Bitpacking won't be necessary with the new delta compression.
NetworkReader/Writer are faster too.
DOTSNET uses Bitpacking via BitWriter
to serialize, and BitReader
to deserialize data down to the bit.
Usage
For example, this is how we serialize an int
and a float3
position in a message:
using DOTSNET;
public struct TestMessage : NetworkMessage
{
public int classIndex;
public float3 position;
public byte GetID() => 0x31;
public bool Serialize(ref BitWriter writer) =>
writer.WriteInt(classIndex) &&
writer.WriteFloat3(position);
public bool Deserialize(ref BitReader reader) =>
reader.ReadInt(out classIndex) &&
reader.ReadFloat3(out position);
}
Reading and Writing is:
Atomic:
WriteInt
either writes all 4 bytesint
, or none if there is not enough space.ReadInt
either reads all 4 bytesint
, or none if the buffer doesn’t have enough data.Allocation Free: all reads and writes are allocation free for maximum performance.
Fast: the Bitpacker’s internal buffer writes are always word aligned. This makes unaligned writes (
byte
/ushort
/etc.) as fast as aligned writes (uint
,float
, etc.).
Reference Passing
To avoid allocations, BitReader/Writer are value types (structs). When passing a reader/writer through functions, make sure to pass it as reference:
public bool Serialize(ref BitWriter writer) =>
writer.WriteInt(level);
public bool Deserialize(ref BitReader reader) =>
reader.ReadInt(out level);
If you don’t pass it as reference, then it will create a copy of the Writer/Reader, which would not modify the original writer/reader’s Position.
In other words, always pass BitReader/Writer as reference!
Bit Compression
BitWriter/Reader default functions always write the full type unless bitpacking parameters are specified. For example, let’s serialize/deserialize a player’s level which requires 32 bits uint
by default:
writer.WriteUInt(5);
reader.ReadUInt(out uint value);
If we know the value’s minimum and maximum data range, then we can do heavy bandwidth optimizations by packing it into the minimum amount of bits needed for that range. Our player’s level is always in the range of [1..60]
so we can Bitpack it down from 32 bits to 6 bits:
// a player level of '5' with an expected range of [1..60]
// => 60 possible values in that range
// => they all fit into 6 bits
// => so the value is encoded into 6 bits, instead of 32 bits uint!
writer.WriteUInt(5, 1, 60);
reader.ReadUInt(out uint value, 1, 60);
DOTSNET’s Bitpacking automatically does all the complicated bit math internally. All we need to do is specify the data type ranges if we know them.
This also works for float
/double
where we specify min, max, precision
writer.WriteFloat(1.23f, -10f, 10f, 0.1f);
reader.ReadFloat(out float value);
Another interesting example is bool
, which C# stores into 8 bit = 1 byte by default. A bool
is always either 1 or 0, which fits perfectly into just 1 bit. DOTSNET packs bools automatically!
writer.WriteBool(true);
reader.ReadBool(out bool value);
For a deeper understanding about Bitpacking, please read the excellent article by Glenn Fiedler!
Burst
DOTSNET BitReader/Writers are burstable.
Additionally, DOTSNET provides fixed size Readers/Writers for use in IComponentData
:
BitReader128
BitWriter128
Extending BitReader/Writer
You can extend BitReader/Writer with C#’s extension system:
public struct MyStruct
{
public int level;
public int experience;
}
using DOTSNET;
public static class Extensions
{
public static bool WriteMyStruct(ref this BitWriter writer, MyStruct value)
{
return writer.WriteInt(value.level) &&
writer.WriteInt(value.experience);
}
public static bool ReadMyStruct(ref this SBitReader reader, out MyStruct value)
{
value = new MyStruct();
return reader.ReadInt(out value.level) &&
reader.ReadInt(out value.experience);
}
}
Last updated