ByteKruncher

by dickvisser

6

Byte Kruncher

Godot plugin that converts gdscript objects to bytes and vice versa, to save bandwidth or disk space.

Installation

Asset Library

See this guide on how to install addons from the asset library.

Manual

  • Download the latest ByteKruncher release
  • Unzip it
  • Copy the addons/byte_kruncher/ folder to your godot project's addons/ folder
  • Open your godot Project Settings > Plugins and enable ByteKruncher

Why?

Why use ByteKruncher? Mostly to save bandwidth in multiplayer games!

You could, for instance, use json or dictionaries to send data from one peer to another.
Imagine you're sending this data:

var player_data = {
    "level": 127,
    "nickname": "MrCoolGuy",
    "is_cool_guy": true
}
var json = JSON.stringify(player_data)
# json = {"level":127,"nickname":"MrCoolGuy","is_cool_guy":true}

That json is 55 characters long, meaning its size is 55 bytes, assuming the ubiquitous utf-8 encoding.
That is quite a lot bytes for so little data.
Using ByteKruncher, the same data is just 13 bytes. Saving us 76% of data!

How?

Whereas with json and dictionaries you encode the data structure along with the values, ByteKruncher only encodes values.

So instead of sending

{"level":127,"nickname":"MrCoolGuy","is_cool_guy":true}

ByteKruncher sends

127MrCoolGuytrue

This way we don't waste any bytes on data we don't have any use for.

But

Why not use Godot's built-in var_to_bytes()?
Because it breaks down quickly when sending data over networks.
It wasn't built for that purpose.
Besides, var_to_bytes() has to be wasteful, because it cannot infer the size of your data.
ByteKruncher requires you to specify your data types manually, so it can be incredibly efficient in its encoding.

Usage

Simple example

Simply create your scripts as usual.
Then you register the data structure with ByteKruncher using Bykr.register().

class_name PlayerData extends Resource

var level: int
var nickname: String
var is_cool_guy: bool

static var bykr := Bykr.register("PlayerData", PlayerData.new, {
	"level": Bykr.u8,
	"nickname": Bykr.string,
	"is_cool_guy": Bykr.boolean,
})

Note: The Bykr.register() call doesn't have to be in the same file, if you prefer to keep it separate.

Now you can convert it to bytes, i.e. for an RPC call.

func sync_player(data: PlayerData) -> void:
    var bytes: PackedByteArray = PlayerData.bykr.to_bytes(data)
    send_player_data.rpc(bytes)

@rpc
send_player_data(bytes: PackedByteArray) -> void:
    var data: PlayerData = PlayerData.bykr.from_bytes(bytes)
    // ...

Nested data structures

ByteKruncher handles nested data, i.e. one of your properties is another custom class that you want to convert to bytes.

player_data.gd

class_name PlayerData extends Resource

var peer_id: int
var level: int
var money: int
var health: float
var nickname: String
var is_cool_guy: bool
var inventory: Inventory

static var bykr: Bykr.Mapper = Bykr.register("PlayerData", PlayerData.new, {
	"peer_id": Bykr.s64,
	"level": Bykr.u8,
	"money": Bykr.u32,
	"health": Bykr.float_,
	"nickname": Bykr.string,
	"is_cool_guy": Bykr.boolean,
	"inventory": Bykr.object("Inventory"),
})

inventory.gd

class_name Inventory extends Resource

var items: Array[Item]
var max_weight: int

static var bykr: Bykr.Mapper = Bykr.register("Inventory", Inventory.new, {
	"items": Bykr.array("Item"),
	"max_weight": Bykr.u16,
})

item.gd

class_name Item extends Resource

enum Type {NONE, SWORD, SHIELD, STAFF, WAND}

var type: Type
var amount: int

static var bykr: Bykr.Mapper = Bykr.register("Item", Item.new, {
	"type": Bykr.u8,
	"amount": Bykr.u16,
})

Custom constructor

You can manually construct an object after ByteKruncher parses the bytes for you.

item.gd

class_name Item extends Resource

enum Type {NONE, SWORD, SHIELD, STAFF, WAND}

var type: Type
var amount: int

static var bykr: Bykr.Mapper = Bykr.register("Item", ItemFactory.create, {
	"type": Bykr.u8,
	"amount": Bykr.u16,
})

item_factory.gd

class_name ItemFactory extends Resource

static func create(type: Item.Type, amount: int) -> Item:
    var item := Item.new()
    item.type = type
    item.amount = amount
    if item.type == Item.Type.SWORD:
        item.set_script(preload("res://sword.gd"))
    return item

As you can see, ByteKruncher passes the arguments in the same order as you registered them.

Advanced usage

Get mapper dynamically

You can also get a mapper without referencing its class.

var data: PlayerData = ...
var mapper: Bykr.Mapper = Bykr.get_mapper("PlayerData")
var bytes: PackedByteArray = mapper.to_bytes(data)

Register custom mapper

You can register custom mappers for types that aren't supported by ByteKruncher.

class_name MyCustomMappers extends Resource
# Just put it anywhere

class Rect2iMapper extends Bykr.Mapper:
    const byte_size: int = 8 + 8 + 8 + 8

	func _read_bytes_at(byte_offset: int, bytes: PackedByteArray) -> ReadResult:
        var position := Vector2i.new(
            bytes.decode_s64(byte_offset + 0),
            bytes.decode_s64(byte_offset + 8),
        )
        var size := Vector2i.new(
            bytes.decode_s64(byte_offset + 16),
            bytes.decode_s64(byte_offset + 24),
        )
        var value := Rect2i.new(position, size)
		return Bykr.ReadResult.new(byte_size, value)
	
	func _append_bytes_to(value: Variant, byte_offset: int, bytes: PackedByteArray) -> void:
        var rect2i := value as Rect2i
		bytes.resize(byte_offset + byte_size)
		bytes.encode_s64(byte_offset + 0, value.position.x)
		bytes.encode_s64(byte_offset + 8, value.position.y)
		bytes.encode_s64(byte_offset + 16, value.size.x)
		bytes.encode_s64(byte_offset + 24, value.size.y)

static func _static_init() -> void:
    Bykr.register_custom("Rect2i", Rect2iMapper.new())

Then you can use it like this.

class_name MyData extends Resource:

var some_rect: Rect2i

static var bykr := Bykr.register("MyData", MyData.new, {
    "some_rect": Bykr.custom("Rect2i"),
})

Note: Mappers should never keep state, because they're re-used.

Supported data types

Data Type Bykr GDScript Bytes Note
Boolean Bykr.boolean bool 1 Will likely support bit flags in future version to efficiently store multiple booleans.
Half Bykr.f16 float 2 A float, but with half the size in bytes.
Float Bykr.f32 float 4 Be aware that a float in gdscript is actually a 64-bit double. But float will usually suffice in terms of precision.
Double Bykr.f64 float 8 A float in gdscript is actually a 64-bit double. So use this if you wish to prefer full precision.
Signed byte Bykr.s8 int 1
Signed short Bykr.s16 int 2
Signed integer Bykr.s32 int 4
Signed long Bykr.s64 int 8
Unsigned byte Bykr.u8 int 1
Unsigned short Bykr.u16 int 2
Unsigned integer Bykr.u32 int 4
Unsigned long Bykr.u64 int 8 Handle with care, as it is technically not representable by gdscript's int.
Vector3 Bykr.vec3 Vector3 12
Vector3i Bykr.vec3i Vector3i 24
String Bykr.string String - Maximum string length is 65535 characters (utf-8). Minimum size of this type will be 2 bytes, to store string length.
Array Bykr.array Array - Maximum array length is 65535 elements. Size depends on contents. Minimum size of this type will be 2 bytes, to store array length.
Object Bykr.object custom class - Size depends on data structure. Minimum size of this type will be 1 byte, to store whether the object is null or not.
Enum Any int enum/int - Enums in gdscript are simply integers, so choose any Bykr int type that fits your highest enum value.

Version

1.1.0

Engine

4.3

Category

Scripts

Download

Version1.1.0
Download Now

Support

If you need help or have questions about this plugin, please contact the author.

Contact Author