커스텀 클래스 직렬화 방법

참고

다음은 고급 기능으로, C++ 코드 편집이 필요합니다.

릴레이 섹션에서 설명했듯이 릴레이 메시지는 Strix 클래스를 가져와서 네트워크를 통해 다른 클라이언트에게 보낼 수 있습니다.

그러나, 이렇게 하려면 클래스에 직렬화가 가능한 속성이 있어야 합니다. 커스텀 클래스에는 Strix가 직렬화하지 못하는 커스텀 타입이 더 있을 수도 있습니다. 이것들을 Strix 클래스에 다시 팩토링하기보다는 이 클래스용으로 커스텀 직렬화와 비직렬화를 지정하는 것이 더 합리적일 수도 있습니다.

아래 가이드에 이 프로세스가 설명되어 있습니다. 다소 길고 boilerplate 코드도 많습니다.

  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 플러그인이 업데이트된다면 충돌하지 않도록 주의해야 합니다.

래퍼 개체 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;

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 메타 클래스를 지정합니다. 이에 따라 Strix는 Unreal NewObject 함수로 커스텀 클래스의 새 인스턴스 만드는 방법을 알 수 있습니다.

// 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(직렬화 가능하도록 기본 설정된 것)를 이용하는 것도 있고, UEObjectAdapter(Unreal 전용 클래스)를 이용하는 것도 있습니다.