-
[UE5] USTRUCT의 NetSerialize언리얼엔진/그 외 2024. 3. 12. 18:06
언리얼 엔진은 RPC나 프로퍼티 리플리케이션으로 클라이언트의 로컬 프로퍼티를 업데이트 할 수 있다.
프로퍼티를 업데이트 할 때 트래픽과 대역폭을 줄이기 위한 최적화 방법들이 있는데 그 중 구조체에 사용할 수 있는
NetSerialize
가 있다.NetSerialize
는 구조체의 데이터를 Serialize하거나 Deserialize하는 함수다.보통은 언리얼 엔진에서 자동으로 Serialize를 해주므로 필수는 아니지만,
NetSerialize
함수를 통해 조건에 따라 필요한 프로퍼티만 업데이트하는 로직을 구현할 수 있다.예를 들어 Line trace의 결과를 구조체에 담아 보내는 경우를 생각해보면 Line trace 결과에 부딛친 물체가 아무것도 없다면 구조체의 모든 멤버 변수는 비어있는 기본값이 되므로 이런 기본값들을 굳이 보내줄 필요가 없다.
이런 경우에
NetSerialize
를 적절하게 작성하면 부딛친 물체가 없는 경우에는 구조체의 다양한 멤버 변수들의 정보를 보내지 않게 해서 트래픽과 대역폭을 줄일 수 있다.
NetSerialize
를 작성하려면 구조체를USTRUCT
로 선언해야한다.그리고
TStructOpsTypeTraits
라는 구조체를 추가해야한다.이렇게 하면 해당 구조체가 레플리케이트 될 때 언리얼 엔진에서
NetSerialize
를 호출한다.USTRUCT() struct FMyStruct { GENERATED_BODY() bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess); }; template<> struct TStructOpsTypeTraits<FMyStruct> : public TStructOpsTypeTraitsBase2<FMyStruct> { enum { WithNetSerializer = true }; };
TStructOpsTypeTraitsBase2
구조체는 다양한 enum을 통해 옵션을 선택할 수 있다.선택 가능한 옵션들은
UObject/Class.h
에서 볼 수 있다.여기에서는
NetSerialize
를 사용해야하므로WithNetSerializer = true
를 추가해야한다.모든 옵션은 아래와 같다.
// UObject/Class.h /** type traits to cover the custom aspects of a script struct **/ template <class CPPSTRUCT> struct TStructOpsTypeTraitsBase2 { enum { WithZeroConstructor = false, // struct can be constructed as a valid object by filling its memory footprint with zeroes. WithNoInitConstructor = false, // struct has a constructor which takes an EForceInit parameter which will force the constructor to perform initialization, where the default constructor performs 'uninitialization'. WithNoDestructor = false, // struct will not have its destructor called when it is destroyed. WithCopy = !TIsPODType<CPPSTRUCT>::Value, // struct can be copied via its copy assignment operator. WithIdenticalViaEquality = false, // struct can be compared via its operator==. This should be mutually exclusive with WithIdentical. WithIdentical = false, // struct can be compared via an Identical(const T* Other, uint32 PortFlags) function. This should be mutually exclusive with WithIdenticalViaEquality. WithExportTextItem = false, // struct has an ExportTextItem function used to serialize its state into a string. WithImportTextItem = false, // struct has an ImportTextItem function used to deserialize a string into an object of that class. WithAddStructReferencedObjects = false, // struct has an AddStructReferencedObjects function which allows it to add references to the garbage collector. WithSerializer = false, // struct has a Serialize function for serializing its state to an FArchive. WithStructuredSerializer = false, // struct has a Serialize function for serializing its state to an FStructuredArchive. WithPostSerialize = false, // struct has a PostSerialize function which is called after it is serialized WithNetSerializer = false, // struct has a NetSerialize function for serializing its state to an FArchive used for network replication. WithNetDeltaSerializer = false, // struct has a NetDeltaSerialize function for serializing differences in state from a previous NetSerialize operation. WithSerializeFromMismatchedTag = false, // struct has a SerializeFromMismatchedTag function for converting from other property tags. WithStructuredSerializeFromMismatchedTag = false, // struct has an FStructuredArchive-based SerializeFromMismatchedTag function for converting from other property tags. WithPostScriptConstruct = false, // struct has a PostScriptConstruct function which is called after it is constructed in blueprints WithNetSharedSerialization = false, // struct has a NetSerialize function that does not require the package map to serialize its state. WithGetPreloadDependencies = false, // struct has a GetPreloadDependencies function to return all objects that will be Preload()ed when the struct is serialized at load time. WithPureVirtual = false, // struct has PURE_VIRTUAL functions and cannot be constructed when CHECK_PUREVIRTUALS is true WithFindInnerPropertyInstance = false, // struct has a FindInnerPropertyInstance function that can provide an FProperty and data pointer when given a property FName WithCanEditChange = false, // struct has an editor-only CanEditChange function that can conditionally make child properties read-only in the details panel (same idea as UObject::CanEditChange) }; static constexpr EPropertyObjectReferenceType WithSerializerObjectReferences = EPropertyObjectReferenceType::Conservative; // struct's Serialize method(s) may serialize object references of these types - default Conservative means unknown and object reference collector archives should serialize this struct };
NetSerialize
함수에서는FArchive
를 이용해서 값을 Save하거나 Load할 수 있다.NetSerialize
함수가 Serialize와 Deserialize를 하는 함수이므로FArchive
를 통해 두가지 작업을 하도록 구현해야한다.int나 float같은 기본 타입들은
FArchive
의<<
연산자를 통해FArchive
에 값을 저장하거나 불러올 수 있다.하나의 연산자를 두가지 상황에 모두 사용할 수 있다.
NetSerialize
함수에서는 Flag로 사용되는 비트값으로 어떤 값을 다뤄야 할지를 결정한다.데이터를 보내는 입장(Save)에서는
FArchive
에 이 비트값과 데이터를 작성해야하고, 데이터를 받는 입장(Load)에서는 받은 비트값을 보고 어떤 값을FArchive
에서 가져와야하는지 결정한다.NetSerialize
함수를 사용하는 방법을 자세히 알고싶다면FHitResult
구조체에서 구현한NetSerialize
함수를 보면 좋다.FHitResult
는 다양한 타입의 멤버 변수를 가지고있으므로 다양한 타입들의 값을 Save하고 Load 하는 과정과, 크기가 큰 구조체를 어떻게 최적화했는지도 볼 수 있다.
다음은 간단한 예시다.
ProjectileMovementComponent
에는 Homing 기능이 있는데 Homing과 관련된 정보들을 서버에서 클라이언트로 레플리케이트 해야하는 상황이다.Homing과 관련된 여러 정보를 구조체로 묶어서 관리하기로 했다.
USTRUCT() struct FHomingParam { GENERATED_BODY() bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess); // Homing을 하는 발사체면 true UPROPERTY() bool bIsHoming = false; // HomingAcceleration은 각 발사체마다 랜덤값으로 지정받음 UPROPERTY() float HomingAcceleration = 0.f; // HomingTarget이 Pawn의 컴포넌트가 아니고 맵의 특정 위치라면 true UPROPERTY() bool bIsHomingToSceneComp = false; // bIsHomingToSceneComp가 true인 경우 클라이언트에서 해당 위치에 // SceneComponent를 생성해서 목표로 정함. // FVector_NetQuantize는 FVector를 상속받은 구조체로 NetSerialize가 구현되어있음. UPROPERTY() FVector_NetQuantize HomingSceneCompLocation; // Homing 발사체의 목표 컴포넌트. 포인터의 경우에는 Weak포인터를 사용해야함. // bIsHomingToSceneComp가 true인 경우 nullptr // false인 경우 Pawn의 컴포넌트 TWeakObjectPtr<USceneComponent> HomingTarget = nullptr; ~FHomingParam() { if (HomingTarget.IsValid()) { HomingTarget.Reset(); } } }; template<> struct TStructOpsTypeTraits<FHomingParam> : public TStructOpsTypeTraitsBase2<FHomingParam> { enum { WithNetSerializer = true, WithCopy = true }; };
이 경우
bIsHoming
이 false인 경우 모든 구조체의 값을 클라이언트에 보낼 필요가 없다.그리고
bIsHoming
이 true이면서bIsHomingToSceneComp
가 false인 경우에는HomingTarget
을 보내야하고, true인 경우에는HomingSceneCompLocation
를 보내야한다.이런 경우에는
NetSerialize
함수를 아래와 같이 작성할 수 있다.bool FHomingParam::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess) { uint8 Flags = 0; if (Ar.IsSaving()) // Save할 때만 { if (bIsHoming) { Flags |= 1 << 0; if (bIsHomingToSceneComp) { Flags |= 1 << 1; } } } // 여기 아래는 Save, Load 둘 다 // Flags는 2비트 길이 Ar.SerializeBits(&Flags, 2); if (Flags & (1 << 0)) { bIsHoming = true; // bool 값은 Flag를 통해 알 수 있음 Ar << HomingAcceleration; // FArchive를 통해 값 Load 또는 Save if (Flags & (1 << 1)) { bIsHomingToSceneComp = true; // bool 값은 Flag를 통해 알 수 있음 // FVector_NetQuantize의 NetSerialize 함수를 통해 값 Load 또는 Save HomingSceneCompLocation.NetSerialize(Ar, Map, bOutSuccess); } else { Ar << HomingTarget; // FArchive를 통해 값 Load 또는 Save } } return true; }
이렇게 하면 경우에 맞게 필요한 변수들의 값만 보낼 수 있다.
이외에 발사체 관련 코드가 제대로 구현되어있다고 가정했을 때, 위의 코드를 적용해보면 아래와 같이 의도대로 작동한다.
액터의 이동 레플리케이트는 꺼져있는 상태다.
위: 서버, 아래: 클라이언트 Pawn이 목표일때와 맵의 특정 지점이 목표일때 두 경우 모두 서버와 클라이언트는 같은 정보를 가지고있는걸 확인할 수 있다.
특정 정보만을 보내는건지 확인하기 위해서 Pawn을 목표로 Homing할 때에는
HomingTarget
을 보내지 않게 변경했다.중복되는 코드는 생략했다.
bool FHomingParam::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess) { ... if (Flags & (1 << 0)) { ... if (Flags & (1 << 1)) { ... } else { // Ar << HomingTarget; // 확인을 위해 고의로 데이터를 보내지 않게 함. } } return true; }
이렇게 하면 Pawn을 목표로 할 때에는
HomingTarget
을 모르기 때문에 Homing이 작동하지 않을 것이다.하지만 맵의 특정 위치를 목표로 할 때에는 위치 정보를 받기 때문에 이전처럼 제대로 작동할 것이다.
고의로 필요한 정보를 보내지 않은 경우. 위: 서버, 아래: 클라이언트 결과는 예상대로 Pawn을 목표로 했을 때에는 Homing이 작동하지 않고 직진만 한다.
하지만 맵의 특정 위치를 목표로 했을때 위치 정보를 제대로 받은걸 보면 레플리케이션이 발생했음을 알 수 있다.
Homing 발사체의 경우에는 시작 방향, 시작 속도, 목표, 가속도가 일치하면 거의 같은 움직임을 보여줄거라 예상되므로 이동 레플리케이트를 켜는것보단 이렇게 하는게 최적화에 더 도움될 것으로 생각된다.
'언리얼엔진 > 그 외' 카테고리의 다른 글
[UE5] C++로 애니메이션 시퀀스 변경, 커브 추가 (0) 2024.07.03 [UE5] 캐릭터 무브먼트 컴포넌트 작동 순서 정리 (0) 2024.06.13 [UE5] 블렌더로 루트 모션 애니메이션 만드는 방법 (4) 2024.01.25 [UE5] Enhanced Input Binding with Gameplay Tags C++ (0) 2024.01.21 [UE5] 시퀀스 이밸류에이터에 대해 (0) 2023.08.19