カスタムクラスをシリアル化する方法

注釈

以下に説明することは高度な機能です。プラグインのC++ソースコードを編集する必要があります。

リレーメッセージのセクションで示したように、リレーメッセージはStrixクラスを受け取ってネットワーク経由で送信し、他のクライアントで受信することができます。

ただし、これはクラスがシリアル化可能なプロパティを持つことに依存しています。カスタムクラスは、Strixにはシリアル化の方法が分からない多数のカスタム型によって構成されている場合があります。これらのクラスをStrixクラスにリファクタリングするよりも、このクラスのカスタムシリアル化と逆シリアル化を定義する方が合理的かもしれません。

以下では、この手順について説明します。これはかなり長くなりますが、定型的なコードを多数含んでいます。

  1. クラスにクラスIDを指定します。

  2. クラスのラッパーオブジェクト実装を追加します。

  3. カスタムのシリアル化および逆シリアル化の関数を定義します。

  4. オブジェクトアダプターを追加します。

  5. (UCLASSの場合)MetaClass特殊化を追加します。

  6. (UCLASSの場合)MetaClass初期化呼び出しを追加します。

  7. クラスをStrix Relay Argとして使用できるようにします。

クラス定義

// 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;
};

クラス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には、各ラッパークラスのクラスIDが含まれています。これらは、サーバー上でクラスを区別したり、逆シリアル化したりするために使用されます。これらは一意である必要があります。Strixプラグインが更新された場合には、競合が起きていないか注意してください。

ラッパーオブジェクトの実装

// 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;

Strix ComparableWrapperObjectImplタイプは、SDKがオブジェクトをラップするために使用します。ラッパーは、Strixがオブジェクトを比較するために必要な関数を提供します。

注釈

これはオブジェクトをラッピングするので、そのポインター型をラップします。

シリアル化

ラッパーオブジェクトを作成したなら、Strixはそのオブジェクトのシリアル化と逆シリアル化の方法を知る必要があります。これはSerializerImplクラスに適切なメソッドを定義することで行います。

// 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;
    }
};

注釈

書き出しと読み取りの順番は同じでなければなりません。つまり、最初に書き出した値を最初に読み取る必要があります。

シリアル化は、クラスを基本型の値の列として書き出し、クライアントが反対側でそれを読み取るようにします。ほとんどのクラスでは、これは簡単です。

文字列

シリアル化では文字列を読み書きすることもできます。

encoder.WriteString("Will be serialized");

// ---

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

コンテナー

コンテナークラスもシリアル化できます。

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;
    }
};

オブジェクトアダプター

カスタムクラスにアダプターを割り当てる必要もあります。このクラスは、カスタムタイプをStrixオブジェクトとして操作できるようにするために必要なコードを提供します。

// 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)) {}

注釈

アダプターは、ObjectAdapterにラップされた、このクラスの新しいComparableWrapperObjectImplのインスタンスを使用します。

MetaClassT

注釈

これはUCLASSクラスに必要です。

次のMetaClassのコードは、Strixのメタクラスを規定して、カスタムクラスの新しいインスタンスを作成する際にUnrealのNewObject関数を使用することをStrixに知らせています。

// 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());
    }
};
}}}

MetaClassの初期化

注釈

これはUCLASSクラスに必要です。

メタクラスは初期化しなければなりません。これは、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;
}

ObjectAdapterを定義すると、カスタムクラスのインスタンス(この場合はポインター)を簡単にFStrixRelayArgに変換できるようになります。

上に示した例では、ライブラリにアダプター関数を追加しています。こうするとブループリント内でもこの変換が行えます。

注釈

変換関数の中には、ObjectAdapter(デフォルトではStrixによってシリアル化可能なもの)を使用するものと、UEObjectAdapter(Unreal固有のクラス)を使用するものがあります。