How to Serialize Custom Classes

Note

The following is an advanced feature and requires editing C++ code.

As shown in the Relay section, Relay messages can take Strix classes and send them over the network to be received by other clients.

However, this relies on the class having serializable properties, and custom classes may consist of more custom types that Strix does not know how to serialize. Rather than refactoring these into Strix classes, it may be more reasonable to define a custom serializer and deserializer for this class.

The following how-to describes this process. This is fairly lengthy, with a lot of boilerplate code.

  1. Give the class a Class ID.

  2. Add a wrapper object implementation for the class.

  3. Define custom serialize and deserialize functions.

  4. Add an object adapter.

  5. (If UCLASS) Add a MetaClass specialization.

  6. (If UCLASS) Add the MetaClass initialization call.

  7. Make the class usable as a Strix Relay Arg.

Class Definition

// BearingAndRange.h

// A class representing bearing angle and range.

// This class is a UCLASS as we want it to be usable in Blueprints.
// The STRIXSDK_API can be substituted for your specific API.
UCLASS(BlueprintType)
class STRIXSDK_API UBearingAndRange : public UObject
{
    GENERATED_BODY()
public:

    UPROPERTY(VisibleAnywhere)
    float Bearing;

    UPROPERTY(VisibleAnywhere)
    int Range;
};

Class ID

// UEObjectSerializers.h

// Base UE Classes
const int FVectorClassId =    -30;
const int FRotatorClassId =   -31;
const int FTransformClassId = -32;
const int FStringClassId =    -33;
const int FTextClassId =      -34;
const int FQuatClassId =      -35;

// ...

// Custom Class ID
const int BearingAndRangeClassID = -90;

UEObjectSerializers.h contains class IDs for each wrapper class. These are used to differentiate classes on the server or for deserialization. These should be unique so be careful of any conflicts if the Strix plugin is updated.

Wrapper Object Impl

// UEObjectSerializers.h

typedef ComparableWrapperObjectImpl<FVector,    FVectorClassId>    FVectorObject;
typedef ComparableWrapperObjectImpl<FRotator,   FRotatorClassId>   FRotatorObject;
typedef ComparableWrapperObjectImpl<FString,    FStringClassId>    FStringObject;

// ...

// Custom class wrapper
typedef ComparableWrapperObjectImpl<BearingAndRange*, BearingAndRangeClassID> BearingAndRangeObject;

The Strix ComparableWrapperObjectImpl type is used in the SDK for wrapping objects. The wrapper provides the necessary functions for Strix to compare objects.

Note

As this is an object we are wrapping, we wrap the pointer type.

Serialization

With the wrapper object created, Strix needs to know how to serialize and deserialize the object. This is done with a SerializerImpl class that defines the relevant methods.

// UEObjectSerializers.h

template<>
class strix::net::serialization::serializer::SerializerImpl<FVector, false>
{
public:
    bool Serialize(strix::net::io::Encoder &encoder, const FVector &value)
    {
        bool res = encoder.WriteFloat(value.X);
        res &= encoder.WriteFloat(value.Y);
        res &= encoder.WriteFloat(value.Z);

        return res;
    }

    bool Deserialize(io::Decoder &decoder, FVector &value) {
        bool res = decoder.ReadFloat(value.X);
        res &= decoder.ReadFloat(value.Y);
        res &= decoder.ReadFloat(value.Z);

        return res;
    }
};

// ...

// Custom serialize/deserialize specialization.
template<>
class strix::net::serialization::serializer::SerializerImpl<UBearingAndRange, false>
{
public:

    // The Serialize method takes a const reference to an object of type T
    // and an encoder and serializes the object.
    bool Serialize(strix::net::io::Encoder &encoder, const UBearingAndRange &value)
    {

    // The individual values of the custom class are written out to the
    // encoder. The results are & together to determine success.
    bool res = encoder.WriteFloat(value.Bearing);
    res &= encoder.WriteInt(value.Range);
    return res;
    }

    // The Deserialize method takes a reference to an object of type T
    // and a decoder and deserializes from the decoder into the object.
    bool Deserialize(io::Decoder &decoder, UBearingAndRange &value) {

    // The individual values of the custom class are read from the decoder.
    // The decoders Read methods take a reference to assign the read values
    // to.
    // The results are & together to determine success.
    bool res = decoder.ReadFloat(value.Bearing);
    res &= decoder.ReadInt(value.Range);
    return res;
    }
};

Note

The order of writing and reading must be the same, i.e., the first written value must be read first.

The serialization code relies on writing the class out as primitive values that will be read on the other side by a client. This is straightforward for most classes.

Strings

Strings can also be written and read for serialization:

encoder.WriteString("Will be serialized");

// ---

std::string temp;
decoder.ReadString(temp);

Containers

Container classes can be serialized too:

template <typename T, typename InAllocator>
class strix::net::serialization::serializer::SerializerImpl<MyArray<T, InAllocator>, false>
{
public:
    bool Serialize(strix::net::io::Encoder &encoder, const MyArray<T, InAllocator> &value) {
        int size = value.MyArrayLength();

        // WriteArrayBegin(int len) tells the encoder to expect an array of a length len.
        if (!encoder.WriteArrayBegin(size))
            return false;

        // Loop over each item
        for (size_t i = 0; i < size; ++i)
        {

            // Tell the encoder to expect a new array item.
            if (!encoder.WriteArrayItemBegin(i))
                return false;

            // Serialize the item with its defined serializer.
            // This can also be done manually here.
            if (!SerializerImpl<T>().Serialize(encoder, value[i]))
                return false;

            // Tell the encoder the item has been written.
            if (!encoder.WriteArrayItemEnd(i))
                return false;
        }

        // Tell the encoder the array has been written.
        if (!encoder.WriteArrayEnd())
            return false;

        return true;
    }

    bool Deserialize(strix::net::io::Decoder &decoder, MyArray<T, InAllocator> &value) {
        int len;

        // Read the beginning of the array. This sets len to be the length of the array.
        if (!decoder.ReadArrayBegin(len))
            return false;

        // Allocate enough space in the custom array.
        value.AllocateMyArray(len);

        // Loop each item.
        for (int i = 0; i<len; i++)
        {

            // Deserialize using the item's deserialize method.
            T v;
            if (!SerializerImpl<T>().Deserialize(decoder, v))
                return false;

            // Add the item to the array.
            value.AddToMyArray(v);
        }

        // Check the end of the array has been reached.
        if (!decoder.ReadArrayEnd())
            return false;

        return true;
    }
};

Object Adapters

Custom classes also need to be assigned an Adapter. This class provides the necessary code to allow the custom type to be operated on as a Strix object.

// UEObjectAdapter.h


UEObjectAdapter(FVector& val);
UEObjectAdapter(FRotator& val);
UEObjectAdapter(FTransform& val);

// ...

// Custom Object Adapter
UEObjectAdapter(BearingAndRange* val);
// UEObjectAdapter.cpp

UEObjectAdapter::UEObjectAdapter(FVector& val) : ObjectAdapter(std::make_shared<FVectorObject>(val)) {}
UEObjectAdapter::UEObjectAdapter(FRotator & val) : ObjectAdapter(std::make_shared<FRotatorObject>(val)) {}
UEObjectAdapter::UEObjectAdapter(FTransform & val) : ObjectAdapter(std::make_shared<FTransformObject>(val)) {}

// ...

// Adapter operates on the ComparableWrapperObjectImpl defined previously
UEObjectAdapter::UEObjectAdapter(BearingAndRange& val) : ObjectAdapter(std::make_shared<BearingAndRangeObject>(val)) {}

Note

The adapter uses an instance of the new ComparableWrapperObjectImpl for this class, wrapped in an ObjectAdapter.

MetaClassT

Note

This is required for UCLASS classes.

The following MetaClass code specifies a Strix meta class so that Strix knows to use the Unreal NewObject function to create new instances of the custom class.

// BearingAndRange.h

namespace strix { namespace net { namespace object {

// All methods and arguments should be the same for all your custom types.
// The only difference should be the typename for the template parameters.
template <>
class MetaClassT<UBearingAndRange, false> : public MetaClassBaseT<UBearingAndRange>
{
public:

    static const MetaClassT &Get() {
        static const MetaClassT<UBearingAndRange, false> instance;
        return instance;
    }

    // This is an important function and the reason we have to define this template.
    // Unreal handles object creation, so we have to substitute a standard new
    // method for the Unreal NewObject method for this class.
    void *Create() const override {
        return NewObject<UBearingAndRange>();
    }

private:
    MetaClassT() {
        MetaClass::SetClassId(TypeNameFactory<UBearingAndRange>::GetTypeName(false));
        MetaClass::Register(*this, GetClassId());
    }
};
}}}

Initializing MetaClass

Note

This is required for UCLASS classes.

The meta class must be initialized, which can be done in the existing function in UEObjectSerializers.h.

// UEObjectSerializers.h

class UEObjectsSerializers
{
public:
    static void Init()
    {
        strix::net::object::MetaClassT<strix::net::object::FTransformObject>::Get();
        strix::net::object::MetaClassT<strix::net::object::FQuatObject>::Get();

        // Custom metaclass initialization
        strix::net::object::MetaClassT<UBearingAndRange>::Get();
    }
};

Strix Relay Arg

// StrixBlueprintFunctionLibrary.h

UFUNCTION(BlueprintPure, meta = (DisplayName = "ToStrixRelayArg (BearingAndRange)", CompactNodeTitle = "->", Keywords = "cast convert", BlueprintAutocast), Category = "StrixBPLibrary|Conversions")
static FStrixRelayArg Conv_BearingAndRangeToStrixRelayArg(UBearingAndRange* val);

// StrixBlueprintFunctionLibrary.cpp

FStrixRelayArg UStrixBlueprintFunctionLibrary::Conv_BearingAndRangeToStrixRelayArg(UBearingAndRange* val)
{
    FStrixRelayArg arg(strix::net::object::UEObjectAdapter(val).Get());

    return arg;
}

With the ObjectAdapter defined, any instance (or pointer in this case) of the custom class can be converted into an FStrixRelayArg with ease.

In the case above, an adapter function is added to the library. This allows this conversion to happen inside a Blueprint as well.

Note

Some conversion functions use the ObjectAdapter (those that are by default serializable by Strix), and some the UEObjectAdapter (Unreal specific classes).