Announcement

Collapse
No announcement yet.

Serialize a UObject so it can be sent via an RPC over the network?

Collapse
X
  • Filter
  • Time
  • Show
Clear All
new posts

    Serialize a UObject so it can be sent via an RPC over the network?

    I'm trying to use UCLASS() objects instead of USTRUCTS() in my custom networked movement component. The idea is, I want to build a master component that handles all the primary replication - and then child classes can add custom properties.

    For example, here's a struct that's used so send client input. The issue is, I want to be able to send child-classes of this struct accross the network via RPC, and cast them on the Server side to get the appropriate data. As far as I can tell, this is impossible.
    Code:
    USTRUCT()
    struct ST_NETWORKEDMOVEMENT_API FRepNetworkedPawnInput
    {
    	GENERATED_BODY()
    
    public:
    	UPROPERTY() float ForwardAxis;
    	UPROPERTY() float RightAxis;
    
    	FRepNetworkedPawnInput()
    		: ForwardAxis(0.f)
    		, RightAxis(0.f)
    	{}
    So what I need to do, is switch to using uclasses, providing there is no extra data cost requirement. I really need the flexibility to try and save bandwidth here. Where can I start with this?

    EDIT: If this works with USTRUCTS anyway, that'd be better!
    Last edited by TheJamsh; 03-06-2017, 02:48 PM.

    #2
    Hey TheJamsh. Did you come any further with this? I'm currently trying to implement an system for actions that can be executed. That would require the client as well as the server to be able to create the action, but the action decides where it actually needs to execute.
    I have something, but it is only crashing as I have issues with serialization of the data.

    My workaround here would be that each action has an execution type (ClientOnly, ServerOnly, ServerAndClient) and the action asserts when executed on the wrong instance. But the user is responsible to execute it correctly.
    DebugWidget - Helper for debugging in VR and other means
    WidgetBox - Recycle your widgets the smart way
    NotificationBackbone - Send notifications across your whole project
    SteamWorkhop - Blueprint wrapper with full access
    UnrealPluginBuilder - Package Plugins for multiple engine versions via Drag&Drop

    Comment


      #3
      FArchive is banned from serializing UObjects, you'll reach a check() causing a crash if you serialize the whole object.
      You can seralize its UProperty to a struct and send the struct through RPC, that's pretty much the only way.
      | Finite State Machine | Savior | USQLite | Object-Pool | Sound-Occlusion | Property Transfer | Magic Nodes | MORE |

      Comment


        #4
        The trick I discovered was to provide a function that allows you to serialize the data into a byte array, and send that via RPC across the network, then unpack on the other side. The key is to ensure you pack/unpack in a known way. I actually hijack FArchive do do this exact thing.

        Code:
            /*
            * Packs an arbitrary struct of data for sending across the network via a base RPC
            * The DataType should be a type of USTRUCT(), which has the NetSerialize trait flag applied.
            *
            * param InPackageMap        - The Package Map used for serialization.
            * param InData                - The data struct to serialize. Provided as a TSharedRef.
            * param OutSendBytes        - The bytes which will be sent across the network.
            */
            template<class DataType>
            static bool PackNetworkStruct(UPackageMap* InPackageMap, const TSharedRef<DataType>& InData, TArray<uint8>& OutSendBytes)
            {
                check(InPackageMap);
                OutSendBytes.Reset(0);
                FNetBitWriter TempNetWriter = FNetBitWriter(InPackageMap, 0);
        
                bool bSerializationSuccessful = true;
                const bool bDidWork = InData.Get().NetSerialize(TempNetWriter, TempNetWriter.PackageMap, bSerializationSuccessful);
        
                if (bDidWork && bSerializationSuccessful)
                {
                    OutSendBytes = *TempNetWriter.GetBuffer();
                }
        
                return bDidWork && bSerializationSuccessful;
            }
        Code:
            /*
            * Unpacks an arbitrary struct of data into a new TSharedPtr, which was received from a base RPC.
            * The DataType should be a type of USTRUCT(), which has the NetSerialize trait flag applied.
            *
            * param InPackageMap        - The package map used for serialization.
            * param InReceivedBytes    - The packed data we received from the client.
            */
            template<class DataType>
            static TSharedPtr<DataType> UnpackNetworkStruct(UPackageMap* InPackageMap, const TArray<uint8>& InReceivedBytes)
            {
                check(InPackageMap);
        
                TSharedPtr<DataType> ReturnVal = MakeShareable(new DataType());
                check(ReturnVal.IsValid());
        
                // This is *NOT* the correct way to initialize the bit reader, but not sure how to get the size atm.
                TArray<uint8> DataCopy = InReceivedBytes;
                FNetBitReader TempNetReader = FNetBitReader(InPackageMap, DataCopy.GetData(), sizeof(DataType) * 8);
        
                bool bSerializationSuccessful = true;
                const bool bDidWork = ReturnVal.Get()->NetSerialize(TempNetReader, TempNetReader.PackageMap, bSerializationSuccessful);
        
                if (!bSerializationSuccessful || !bDidWork)
                {
                    // Resulting code should handle the nullptr case
                    ReturnVal.Reset();
                }
        
                return ReturnVal;
            }
        Then you just send the data via a normal Network UFunction. Using TSharedRef / TSharedPtr to statically cast the types if need be:

        Code:
            /* Sends a single move payload to the Server. */
            UFUNCTION(Unreliable, Server, WithValidation)
            void ServerMove(const float TimeStamp, const TArray<uint8>& Payload);
            void ServerMove_Implementation(const float TimeStamp, const TArray<uint8>& Payload);
            bool ServerMove_Validate(const float TimeStamp, const TArray<uint8>& Payload) { return true; }
        The code isn't perfect, I still haven't figured out exactly how to determine some of the properties that FNetBitWriter/FNetBitReader want - but it does work. Been using this for a long time now to make network movement components that share the main bulk of the code (plugin coming soon to marketplace btw).

        You can't (obviously) send UObjects over the network for obvious reasons, but this does allow you to write portable netcode.
        Last edited by TheJamsh; 11-01-2018, 05:31 PM.

        Comment


          #5
          Originally posted by TheJamsh View Post
          The trick I discovered was to provide a function that allows you to serialize the data into a byte array, and send that via RPC across the network, then unpack on the other side. The key is to ensure you pack/unpack in a known way. I actually hijack FArchive do do this exact thing.

          ...
          Thanks you for sharing.

          Thats actually nearly my current state. Scanned NetDriver, NetConnection, ActorChannel, ... for the last hours but that stuff if just to much to get it down in a usefull time frame.
          The difference you do, is you write everything into a struct and serialize that. I was trying to just serialize the object itself (properties of the whole object), which is as BrUnO XaVIeR mentioned not possible.

          For the RPC (had the same issue)
          Code:
          // This is *NOT* the correct way to initialize the bit reader, but not sure how to get the size atm.
          TArray<uint8> DataCopy = InReceivedBytes;
          FNetBitReader TempNetReader = FNetBitReader(InPackageMap, DataCopy.GetData(), sizeof(DataType) * 8);
          You are be able to const_cast<>() the array data.
          Code:
          // This is *NOT* the correct way to initialize the bit reader, but not sure how to get the size atm.
          uint8* DataNonConst= const_cast<uint8*>(InReceivedBytes.GetData());
          FNetBitReader TempNetReader = FNetBitReader(InPackageMap, DataNonConst, sizeof(DataType) * 8);
          I also came across the PackageMap which seems to be needed for names and actors that are referenced via an ID. So I assume that package map must be the same on client as well as server?
          I can't find any documentation about it. Can I just create a package map? If not, where do it get it from?
          Thanks.
          Last edited by Rumbleball; 11-02-2018, 12:20 PM.
          DebugWidget - Helper for debugging in VR and other means
          WidgetBox - Recycle your widgets the smart way
          NotificationBackbone - Send notifications across your whole project
          SteamWorkhop - Blueprint wrapper with full access
          UnrealPluginBuilder - Package Plugins for multiple engine versions via Drag&Drop

          Comment


            #6
            The Package map you can get from the Actors Net Connection (i.e. whoever is sending the data), and yeah it allows for replication of objects via their Network GUID:

            AActor::GetNetConnection()->PackageMap

            Comment


              #7
              Originally posted by TheJamsh View Post
              The Package map you can get from the Actors Net Connection (i.e. whoever is sending the data), and yeah it allows for replication of objects via their Network GUID:

              AActor::GetNetConnection()->PackageMap
              Thanks, getting there. Will post my code when im happy with it.

              Got the data replication to server working, but not in a state where I think it usefull. Still fighting with references to actors.

              Working code:
              Code:
              USTRUCT()
              struct FTestData
              {
                  GENERATED_BODY()
                  UPROPERTY()
                      TArray<float> floatVals;
              
                  UPROPERTY()
                      int32 intVal;
              
                  UPROPERTY()
                      AActor* actorPtr;
              };
              
              // part of Action class
                  virtual void SerializeNetTransfer(FArchive& ar, UPackageMap* packageMap) override
                  {
                      FTestData::StaticStruct()->SerializeBin(ar, &myData);
              
                      FNetworkGUID netGuid;
                      if (ar.IsLoading())
                      {
                          ar << netGuid;
                          myData.actorPtr = Cast<AActor>(packageMap->GetObjectFromNetGUID(netGuid, true));
                      }
                      else
                      {
                          netGuid = packageMap->GetNetGUIDFromObject(myData.actorPtr);
                          ar << netGuid;
                      }
                  }
              Instead of using NetSerialize in the struct itself, the Action says how to serialize as the action knows the data object. This allows to use:
              Code:
                 FTestData::StaticStruct()->SerializeBin(ar, &myData);
              This is good, because it dynamically takes care of the properties which prevents errors. Though, the serialization is missing network improvements, but that is a custom implementation anyway.
              Still, it does not take care of actor references and that I must do manually:
              Code:
                      FNetworkGUID netGuid;
                      if (ar.IsLoading())
                      {
                          ar << netGuid;
                          myData.actorPtr = Cast<AActor>(packageMap->GetObjectFromNetGUID(netGuid, true));
                      }
                      else
                      {
                          netGuid = packageMap->GetNetGUIDFromObject(myData.actorPtr);
                          ar << netGuid;
                      }

              EDIT:
              Never mind, failed when deserializing. Used FBitReader instead of FNetBitReader. Looking good now.
              Last edited by Rumbleball; 11-02-2018, 05:49 PM.
              DebugWidget - Helper for debugging in VR and other means
              WidgetBox - Recycle your widgets the smart way
              NotificationBackbone - Send notifications across your whole project
              SteamWorkhop - Blueprint wrapper with full access
              UnrealPluginBuilder - Package Plugins for multiple engine versions via Drag&Drop

              Comment


                #8
                Yeah that's the one, the Net versions do the GUID serialization automagically. Took a fair bit of digging to get it all working, but it's enabled me to share my netcode really nicely

                Comment


                  #9
                  Thanks again, works beautifully.
                  Here my code:

                  Action.h
                  Code:
                  // Fill out your copyright notice in the Description page of Project Settings.
                  
                  #pragma once
                  
                  #include "CoreMinimal.h"
                  #include "PaintProjectPlayerController.h"
                  #include "Action.generated.h"
                  
                  enum class EActionTarget : uint8
                  {
                      AT_Server = 0,
                      AT_ClientOrServer = 1,
                  };
                  
                  
                  /**
                   * An action is something that the user wants to do.
                   * The child decides what to do and where to execute it (Server/Client) see UAction(const EActionTarget inActionTarget)
                   * Each child can have custom data. The child is responsible for its serialization see UAction::SerializeNetTransfer
                   */
                  UCLASS(Abstract, NotBlueprintable, NotBlueprintType)
                  class THEPAINTPROJECT_API UAction : public UObject
                  {
                      GENERATED_BODY()
                  public:
                  
                      UAction()
                      {
                          world = nullptr;
                          bWasInitCalled = false;
                      }
                  
                      UAction(const EActionTarget inActionTarget) : actionTarget(inActionTarget)
                      {
                          bWasSpecialConstructorCalled = true;
                      }
                      virtual ~UAction() {}
                  
                      // One of the Create functions must be used to create an UAction
                      template<typename T>
                      static T* Create(const UObject* worldContextObject)
                      {
                          return Create<T>(worldContextObject, T::StaticClass());
                      }
                  
                      // One of the Create functions must be used to create an UAction
                      template<typename T = UAction>
                      static T* Create(const UObject* worldContextObject, const TSubclassOf<UAction>& actionClass)
                      {
                          UWorld* world = GEngine->GetWorldFromContextObject(worldContextObject, EGetWorldErrorMode::Assert);
                          T* action = NewObject<T>(world, actionClass.Get());
                          action->Init(world);
                          return action;
                      }
                  
                      #pragma region OverrideInChild
                  public:
                      // Returns the description of the action.
                      virtual FString GetDescription() const
                      {
                          check(0 && "Override in Child");
                          return FString();
                      }
                  
                      /**
                       *    Must be implemented by the child to package up date for NetTransfer.
                       *    If the child has no data, implement it and just return.
                       *    This function is used for read and write.
                       *    param ar                The archive to read/write the data to
                       *    param packageMap        Can be used for FName and UObject serialization (object->ID, ID->Object)
                       */
                      virtual void SerializeNetTransfer(FArchive& ar, UPackageMap* packageMap)
                      {
                          // see UExampleAction for an example.
                          check(0 && "Override in Child")
                      }
                  
                  protected:
                      // Do what must be done!
                      virtual void ExecuteChild()
                      {
                          check(0 && "Override in Child");
                      }
                      #pragma endregion OverrideInChild
                  
                  public:
                      // Do NOT override! But don't fear my child -> see ExecuteChild
                      void Execute();
                  
                      virtual UWorld* GetWorld() const override
                      {
                          return world;
                      }
                  
                  private:
                      // Do Not override!
                      void Init(UWorld* inWorld);
                  
                      UPROPERTY(SkipSerialization)
                      UWorld* world;
                  
                      bool bWasSpecialConstructorCalled = false;
                      EActionTarget actionTarget;
                      uint8 bWasInitCalled : 1;
                  };
                  Action.cpp
                  Code:
                  // Fill out your copyright notice in the Description page of Project Settings.
                  
                  #include "ThePaintProject.h"
                  #include "Action.h"
                  
                  void UAction::Init(UWorld* inWorld)
                  {
                      if(!bWasSpecialConstructorCalled) LOG_FATAL(LogTemp, "The UAction(const bool binMustBeExecutedOnServer) constructor must be called by child constructor. Use your constructor like this: UYourAction() : UAction(EActionTarget)!");
                  
                      world = inWorld;
                      check(world);
                      bWasInitCalled = true;
                  }
                  
                  void UAction::Execute()
                  {
                      if (!bWasInitCalled) LOG_FATAL(LogTemp, "Each action must be created using UAction::Create!");
                  
                      APaintProjectPlayerController* playerController = APaintProjectPlayerController::Get(GetWorld(), 0);
                      if (playerController)
                      {
                          bool bIsServer = playerController->Role == ROLE_Authority;
                          if (!bIsServer && actionTarget == EActionTarget::AT_Server)
                          {
                              UPackageMap* packageMap = playerController->GetNetConnection()->PackageMap;
                              FNetBitWriter writer(packageMap, 16000); // 16000 = 2kb
                              SerializeNetTransfer(writer, packageMap);
                              playerController->Server_ExecuteAction(GetClass(), *writer.GetBuffer(), writer.GetNumBits());
                              return;
                          }
                  
                          ExecuteChild();
                      }
                      else
                      {
                          check(0 && "PlayerController not valid");
                      }
                  }
                  YOURPlayerController.h
                  Code:
                      UFUNCTION(Server, Reliable, WithValidation)
                      void Server_ExecuteAction(UClass* actionClass, const TArray<uint8>& actionData, const uint32& bitCount);
                  YOURPlayerController.cpp
                  Code:
                  bool APaintProjectPlayerController::Server_ExecuteAction_Validate(UClass* actionClass, const TArray<uint8>& actionData, const uint32& bitCount)
                  {
                      return actionClass != nullptr;
                  }
                  
                  void APaintProjectPlayerController::Server_ExecuteAction_Implementation(UClass* actionClass, const TArray<uint8>& actionData, const uint32& bitCount)
                  {
                      check(actionClass);
                      UAction* action = UAction::Create(this, actionClass);
                      // Need to copy data, as BitReader needs NonConstant*. Can't pass array as NonConstant reference either.
                      UPackageMap* packageMap = GetNetConnection()->PackageMap;
                      FNetBitReader bitReader(packageMap, const_cast<uint8*>(actionData.GetData()), (int64)bitCount);
                      action->SerializeNetTransfer(bitReader, packageMap);
                      action->Execute();
                  }
                  ExampleAction.h
                  Code:
                  // Fill out your copyright notice in the Description page of Project Settings.
                  
                  #pragma once
                  
                  #include "CoreMinimal.h"
                  #include "Action.h"
                  #include "ExampleAction.generated.h"
                  
                  USTRUCT(BlueprintType)
                  struct FTestData
                  {
                      GENERATED_BODY()
                      UPROPERTY(BlueprintReadWrite, VisibleAnywhere)
                      TArray<float> floatVals;
                      UPROPERTY(BlueprintReadWrite, VisibleAnywhere)
                      int32 intVal;
                      UPROPERTY(BlueprintReadWrite, VisibleAnywhere)
                      TArray<AActor*> actorPtr;
                  };
                  
                  UCLASS(NotBlueprintable, NotBlueprintType)
                  class THEPAINTPROJECT_API UExampleAction : public UAction
                  {
                      GENERATED_BODY()
                  public:
                      UExampleAction() : UAction(EActionTarget::AT_Server)
                      {}
                      virtual ~UExampleAction() {
                          LOG_WARNING(LogTemp, "ExampleActionwas destructed");
                      }
                  
                      void SetData(const FTestData& testData)
                      {
                          myData = testData;
                      }
                  public:
                      virtual FString GetDescription() const override
                      {
                          return FString(TEXT("Testing the action system"));
                      }
                  
                      virtual void SerializeNetTransfer(FArchive& ar, UPackageMap* packageMap) override
                      {
                          // Serializes the whole struct properly including Object* (Object* must be replicated objects!)
                          FTestData::StaticStruct()->SerializeBin(ar, &myData);
                      }
                  protected:
                      virtual void ExecuteChild() override
                      {
                          if (APaintProjectPlayerController::Get(GetWorld(), 0)->Role == ROLE_Authority)
                          {
                              UKismetSystemLibrary::PrintString(GetWorld(), FString::Printf(TEXT("InstantAction: value: %d"), myData.intVal));
                              for (AActor* actor : myData.actorPtr)
                              {
                                  if (actor)
                                  {
                                      UKismetSystemLibrary::PrintString(GetWorld(), FString::Printf(TEXT("InstantAction: ActorName: %s"), *actor->GetName()));
                                  }
                              }
                              for (float& val : myData.floatVals)
                              {
                                  UKismetSystemLibrary::PrintString(GetWorld(), FString::Printf(TEXT("InstantAction: float: %f"), val));
                              }
                              return;
                          }
                          check(0);
                      }
                  private:
                      UPROPERTY()
                      FTestData myData;
                  };
                  YourFunctionLibrary.h
                  Code:
                      UFUNCTION(BlueprintCallable, Category = "PaintProject Global|Action", meta = (WorldContext = "worldContextObject"))
                      static void Action_Testing(const UObject* worldContextObject, const FTestData& testData)
                      {
                          UExampleAction* action = UExampleAction::Create<UExampleAction>(worldContextObject);
                          action->SetData(testData);
                          action->Execute();
                      }
                  DebugWidget - Helper for debugging in VR and other means
                  WidgetBox - Recycle your widgets the smart way
                  NotificationBackbone - Send notifications across your whole project
                  SteamWorkhop - Blueprint wrapper with full access
                  UnrealPluginBuilder - Package Plugins for multiple engine versions via Drag&Drop

                  Comment


                    #10
                    Hi guys, I also tired to re write every CharacterMovementComponent in all the project that the company have, because we need to add some extra data or remove data that we dont want it.

                    So I take the approach that you mention here, and all works like a charm, EXCEPT that when I go to the network profiler to see how its behave I notice that the move were wayyyy bigger than the legacy ones, it was like the legacy move * 2. So I start digging it and I found that when my Tarray go through NetworkDriver.cpp / RepLayout->SendPropertiesForRPC() go the the dynamic array section and add some extra data.

                    Let met show u images about what exactly Im talking about,

                    Example:

                    https://imgur.com/jlGQJNa



                    When my MoveOld method is called:

                    https://imgur.com/oW0tcjD



                    When the legacy MoveOld method is called:

                    https://imgur.com/FNsbvLE



                    At this point Im out of ideas, I could really use some extra pair of eyes that understand the network serialization better than me.
                    Last edited by MatiasJP; 01-28-2019, 11:34 AM.

                    Comment


                      #11
                      FNetBitWriter calls NetSerialize on its own in case the flags are set up correctly for the structure.

                      I never had looked into MovementComponent. I dunno how the code looks of the legacy OldMove. Sure it is about how much data you serialize into it and how much compression you use. Do you serialize more data?
                      DebugWidget - Helper for debugging in VR and other means
                      WidgetBox - Recycle your widgets the smart way
                      NotificationBackbone - Send notifications across your whole project
                      SteamWorkhop - Blueprint wrapper with full access
                      UnrealPluginBuilder - Package Plugins for multiple engine versions via Drag&Drop

                      Comment


                        #12
                        Originally posted by MatiasJP View Post
                        Hi guys, I also tired to re write every CharacterMovementComponent in all the project that the company have, because we need to add some extra data or remove data that we dont want it.

                        So I take the approach that you mention here, and all works like a charm, EXCEPT that when I go to the network profiler to see how its behave I notice that the move were wayyyy bigger than the legacy ones, it was like the legacy move * 2. So I start digging it and I found that when my Tarray go through NetworkDriver.cpp / RepLayout->SendPropertiesForRPC() go the the dynamic array section and add some extra data.


                        EDIT: After some more testing I can see that an overhead thatis being added is the lenght of that array, and the amount of bits in the payload.
                        I have a custom movement component in my VR plugin that overrides the movement sends and optimizes the payload with custom NetSerialize structs and bit flags. You could reference that? It is much the same thing that epic recently did to not send movement bases if null except more extreme. You could generalize it to serialize entire generic structs instead,
                        Last edited by mordentral; 01-27-2019, 10:02 PM.


                        Consider supporting me on patreon

                        My Open source tools and plugins
                        Advanced Sessions Plugin
                        VR Expansion Plugin

                        Comment


                          #13
                          Thanks for the reply guys, my initial post was lacking of details, sorry for that.

                          mordentral I really like the approach that you take in the movement component of your VRPlugin, but I wanted to make it even more generic.

                          Okay after more research I found out how the buffer expand itself, not just asking the bytes that he need to store the data that we are passing, if get out of space will duplicate the actual buffer lenght:

                          https://imgur.com/xorOF08

                          So that introduce empty memory into my buffer, than means that if I code something like this...

                          Code:
                          UFUNCTION(Server,)
                          Server_SendBytes(const TArray& Bytes);
                          
                          Server_SendBytes(*TempNetWriter.GetBuffer());
                          ...It will ending up sending tons of bytes empty, which scale with the amount of bytes actually stored.

                          So for now I just make this workaround:

                          Code:
                          TArray SanitizedData = *TempNetWriter.GetBuffer();
                          SanitizedData.SetNum(TempNetWriter.GetNumBytes());
                          
                          Server_SendBytes(*TempNetWriter.GetBuffer());
                          I will probably make my inheritence of FNetBitWriter to add a method that clean up my buffer without the need of copy the array.

                          Also I could mention that the overhead of sending a TArray instead of the actual parameters is 2 bytes because it need to add into the payload the uint16 that indicates the lenght of that array.

                          More interesting data discover: the "shifting" operator of the FVector_QuantizeX is not overloaded so if you want to serialize it with the compression you should use:

                          Code:
                          FVector_Quantize10 MyVector = FVector::One;
                          
                          //This will store the compressed value
                          MyVector.NetSerialize(TempNetWriter, nullptr, false);
                          
                          instead of
                          
                          //This will store 12 bytes.
                          TempNetWriter "shifting" MyVector
                          I Hope my suffer with this help someone.
                          Last edited by MatiasJP; 01-28-2019, 08:02 PM.

                          Comment

                          Working...
                          X