delete 메모리 해제를 안해도 되는 편리함

자동NULL, 메모리릭 방지 등 버그를 줄이기 위해서 사용

언리얼 오프젝트가 되어있다면, 그것은 GC가 관리를 한다.
해지 명령을 내려도 바로 해지되지 않고 GC 시스템에 의해 회수된다 (자세한 건 GC 파트에서)


https://docs.unrealengine.com/en-US/Programming/UnrealArchitecture/SmartPointerLibrary/index.html

 

Unreal Smart Pointer Library

Custom implementation of shared pointers, including weak pointers and non-nullable shared references.

docs.unrealengine.com


Unreal Smart Pointer


The 
Unreal Smart Pointer Library is a custom implementation of C++11 smart pointers designed to ease the burden of memory allocation and tracking.

This implementation includes the industry standard Shared Pointers, Weak Pointers, and Unique Pointers.
It also adds Shared References which act like non-nullable Shared Pointers. 

These classes cannot be used with the UObject system 
because Unreal Objects use a separate memory-tracking system that is better-tuned for game code.



언리얼 스마트 포인터 라이브러리는 메모리 할당과 트래킹의 부담을 줄이기 위해 디자인된,
C++ 11 커스텀 스마트 포인터이다

구현에는 공유포인터, 약포인터, 유니트포인터가 있다
최근 추가된 공유레퍼런스는 null이 값에만 사용할 수 있는 공유포인터랑 같은 역활을 한다.

이런 클래스들은 언리얼 시스템과 같이 사용할 수가 없는데, 언리얼 오브젝트는 별도의 메모리트래킹 시스템으로
 더 좋게 게임코드 사용할 수 있기 떄문이다.


Smart Pointer Types

 

1. Shared Pointers       (TSharedPtr)
침범형(non-intrusive), 레퍼런스가 카운팅되는 편리한 유형의 스마트 포인터

A Shared Pointer owns the object it references,
indefinitely preventing deletion of that object, and ultimately handling its deletion
when no Shared Pointer or Shared Reference (see below) references it.

A Shared Pointer can be empty, meaning it doesn't reference any object. 
Any non-null Shared Pointer can produce a Shared Reference to the object it references.

공유포인터는 객체를 참조하며,
삭제를 방지하다가, 더이상 공유포인터 또는 공유레퍼런스가 없을떄(참조카운트가 0)일떄 삭제된다

공유포인터가 비었다는 건, 참조하는 객체가 없을 수 있다. (값이 없을 수 도 있음)
null이 아닌 공유포인터는 참조하는 객체에 대한 공유레퍼런스를 생성 있다. 

2. Shared References    (TSharedRef)
null 가능하지 않은 공유 포인터

 

A Shared Reference acts like a Shared Pointer, in the sense that it owns the object it references.
T
hey differ with regard to null objects; Shared References must always reference a non-null object.

Because Shared Pointers don't have that restriction, a Shared Reference can always be converted to a Shared Pointer, and that Shared Pointer is guaranteed to reference a valid object.

Use Shared References when you want a guarantee that the referenced object is non-null,
or if you want to indicate shared object ownership.


공유레퍼런스는 참조하는 객체를 소유(?참조?)한다는 점에서 공유포인터랑 비슷하다.

그러나 공유참조는, 항상 null이 아닌 객체를 참조해야 한다는 점이 다르다. (항상 값이 있어야 함)

왜냐하면 공유포인터는 이러한 제한이 없으므로, 공유레퍼런스는 항상 공유포인터로 변활 수 있으므로, 
공유포인터는 유요한 객체를 참조하도록 보장된다.(값이 들어있다, 간단히 참조라서 값이 있어야 함)

공유레퍼런스는 null이 아닌 것을 보장하거나, 공유객체 소유권(?)을 나타낼 떄 사용한다,

 

※ 공유포인터는 null일 수도 있음, null이 아니면 공유레퍼런스 생성가능
※ 공유레퍼런스는 항상 null이 아닌 객체를 참조해야함, 항상 값이 있으므로, 공유포인터로 변경가능
※ 공유 레퍼런스는 항상 유효하기 때문에 공유 포인터와는 달리 IsValid() 메서드조차 없음

 

3. Weak Pointers         (TWeakPtr)
오브젝트로의 약 레퍼런스 (weak reference)를 보관하는 포인터

 

Weak Pointers are similar to Shared Pointers,
but do not own the object they reference, and therefore do not affect its lifecycle.


This property can be very useful, as it breaks reference cycles,
but it also means that a Weak Pointer can become null at any time, without warning.

For this reason, a Weak Pointer can produce a Shared Pointer to the object it references,
ensuring programmers safe access to the object on a temporary basis.

 

약포인터는 공유포인터랑 유사하다
그러나 참조한 객체 소유하지 않으므로, 라이프사이클(액터 수명주기?)에 영향을 주지 않는다.

약포인터는 레퍼런스 사이클이 없어졋을때,  경고없이 NULL이 될 수 있어서 유용 할 수 있다.
(예기치 못하게 비워질 수도, 레퍼런스 사이클을 깨는 데 사용)

이런 이유는 약포인터는 객체에 대한 공유포인터를 생성하고, 개체에 안전하게 접근 할수 있게 한다.
(NULL이 아닌건 없음)


4. Unique Pointers        (TUniquePtr)
고유 소유권을 가지는 포인터

A Unique Pointer solely and explicitly owns the object it references.

Since there can only be one Unique Pointer to a given resource, Unique Pointers can transfer ownership, but cannot share it.  Any attempts to copy a Unique Pointer will result in a compile error.

When a Unique Pointer is goes out of scope, it will automatically delete the object it references.

 

고유 포인터는 객체를 명시적으로 소유한다

주어진 리소스에 하나의 고유포인터고 소유권이 존재하는데, 고유 포인터는 소유권을 이전 할 수 있지만,
공유를 할 수없다. 고유포인터를 복사할려고 하면, 컴파일 에러가 발생할 것이다
 
고유포인터는 객체에 소유권을 벗어나거나 없어지면, 자동으로 소멸한다.

 


Benefits of Smart Pointers

 

Prevents memory leaks (메모리릭 방지)
Smart Pointers (other than Weak Pointers) automatically delete objects when there are no more shared references.
(스마트포인터는 공유레퍼런스가 없을떄 자동으로 소멸한다)

Weak referencing (약레퍼런싱)
Weak Pointers break reference cycles and prevent dangling pointers.
(약포인터로 언제 오브젝트가 소멸되었는지를 안전하게 검사할 수 있다.)

Optional Thread safety (스레드 안정성)
The Unreal Smart Pointer Library includes thread-safe code that manages reference counting across multiple threads. Thread safety can be traded out for better performance if it isn’t needed.
(여러 스레드에서 안전하게 접근할 수 있는 "thread safe" 버전이 포함되어 있다.)
(스데르 안정성이 필요 없는 경우에 성능향상을 위해서 교환할 수 있다)

Runtime safety (런타임 안정성)
Shared References are never null and can always be dereferenced.

Confers intent (명확한 의도)
You can easily tell an object owner from an observer.
(오브젝트 owner  observer 를 쉽게 구분할 수 있다.)

Memory (메모리)
Smart Pointers are only twice the size of a C++ pointer in 64-bit (plus a shared 16-byte reference controller).
The exception to this is Unique Pointers, which are the same size as C++ pointers.
[64 비트의 C++ 포인터 크기에 비해 두 배밖에 안된다 (추가로 공유 16 바이트 레퍼런스 컨트롤러)]



Helper Classes and Functions

스마트 포인터를 더욱 쉽고 직관적으로 사용할 수 있도록, 클래스와 함수 형태의 헬퍼가 다수 라이브러리에 제공

Helper Description

Classes

TSharedFromThis

"this" 에서 TSharedRef 를 구하려면 여기에서 클래스를 파생시키면 된다

Functions

MakeShared 
and 
MakeShareable

C++ 포인터로 부터 공유포인터를 만든다

MakeShared 
새로운 객체 인스턴스와 레퍼런스 컨트롤러를 단일 메모리 블록에 할당한다. 
그러나  객체의 public 생성자가 필요하다

MakeShareable 
비효율적이지만, 생성자가 private 경우에도 작동하며, 생성하지 않은 객체의 소유권을 가져올 수 있다. 그리고 객체를 삭제할떄, 사용자행동을 할수 있다

StaticCastSharedRef and 
StaticCastSharedPtr

정적인 형변환 유틸리티 함수로, 보통 파생 유형으로 내림변환(downcast)하는 데 사용

ConstCastSharedRef
and
ConstCastSharedPtr

'const' 스마트 포인터를 'mutable' 스마트 포인터로 변환

 


Smart Pointer Implementation Details


언리얼 스마트 포인터 라이브러리에 구현되어 있는 여러 유형의 스마트 포인터는
모두 퍼포먼스, 메모리 등의 측면에서 일반적인 특징을 공유한다

퍼포먼스(Speed)

항상 스마트 포인터를 사용할 떄는 퍼포먼스를 고려해야 한다.
일반적으로 꽤 빠릅니다만, 아무 데나 쓰라는 용도는 아니다.
특정 하이 레벨 시스템이나 툴 프로그래밍에는 좋지만, 로우 레벨 엔진/렌더링 패스에는 그다지 적합하지 않다.

스마트 포인터의 일반적인 퍼포먼스 장점:

  • 모든 연산이 고정비(constant-time) 입니다.

  •  스마트 포인터 레퍼런스 해제가 C++ 포인터만큼 빠릅니다.

  • 스마트 포인터를 복사한다고 절대 메모리가 할당되지 않습니다.

  • Thread Safe 버전은 교착상태에 빠지지 않습니다(lock-free).


스마트 포인터의 퍼포먼스 단점:

  • 스마트 포인터는 C++ 포인터 보다 생성과 복사에 부하가 걸린다

  • 레퍼런스 카운트를 유지하면서 작업을 추가해야 한다

  • 스마트 포인터는 C++ 포인터보다 메모리 사용한다.

  • 레퍼런스 컨터롤러는 2번째 힙할당이 있다.  
    MakeShareable 대신 MakeShared 사용하면 2번쨰 할당을 피할 수 있으며, 성능을 향상 시킬수 있다

Intrusive Accessors


Shared pointers are non-intrusive, which means the object does not know whether or not a Smart Pointer owns it. This is usually acceptable, but there may be cases in which you want to access the object as a Shared Reference or Shared Pointer.

To do this, derive the object’s class from TSharedFromThis, using the object’s class as the template parameter. 

TSharedFromThis provides two functions, AsShared and SharedThis, that can convert the object to a Shared Reference (and from there, to a Shared Pointer).

This can be useful with class factories that always return Shared References, or when you need to pass your object to a system that expects a Shared Reference or Shared Pointer. 

AsShared will return your class as the type originally passed as the template argument to TSharedFromThis, which may be a parent type to the calling object, while SharedThis will derive the type directly from this and return a Smart Pointer referencing an object of that type.

The following example code demonstrates both functions:

 

class FRegistryObject;
class FMyBaseClass: public TSharedFromThis<FMyBaseClass>
{
    virtual void RegisterAsBaseClass(FRegistryObject* RegistryObject)
    {
        // Access a shared reference to 'this'.
        // We are directly inherited from <TSharedFromThis> , so AsShared() and SharedThis(this) return the same type.
        TSharedRef<FMyBaseClass> ThisAsSharedRef = AsShared();
        // RegistryObject expects a TSharedRef<FMyBaseClass>, or a TSharedPtr<FMyBaseClass>. TSharedRef can implicitly be converted to a TSharedPtr.
        RegistryObject->Register(ThisAsSharedRef);
    }
};
class FMyDerivedClass : public FMyBaseClass
{
    virtual void Register(FRegistryObject* RegistryObject) override
    {
        // We are not directly inherited from TSharedFromThis<>, so AsShared() and SharedThis(this) return different types.
        // AsShared() will return the type originally specified in TSharedFromThis<> - TSharedRef<FMyBaseClass> in this example.
        // SharedThis(this) will return a TSharedRef with the type of 'this' - TSharedRef<FMyDerivedClass> in this example.
        // The SharedThis() function is only available in the same scope as the 'this' pointer.
        TSharedRef<FMyDerivedClass> AsSharedRef = SharedThis(this);
        // RegistryObject will accept a TSharedRef<FMyDerivedClass> because FMyDerivedClass is a type of FMyBaseClass.
        RegistryObject->Register(ThisAsSharedRef);
    }
};
class FRegistryObject
{
    // This function will accept a TSharedRef or TSharedPtr to FMyBaseClass or any of its children.
    void Register(TSharedRef<FMyBaseClass>);
};

Casting

 

You can cast Shared Pointers (and Shared References) through several support functions included in the Unreal Smart Pointer Library. Up-casting is implicit, as with C++ pointers.

You can const cast with the ConstCastSharedPtr function, and static cast (often to downcast to derived class pointers) with StaticCastSharedPtr.

Dynamic casting is not supported, as there is no run-type type information (RTTI); static casting should be used instead, as in the following code:

 

언리얼 스마트 포인터 라이브러리에 포함 된 여러 지원 기능을 통해 공유 포인터 (+ 공유 참조)를 사용할 수 있습니다.
올림변환(up-casting)은 C++ 포인터와 마찬가지로 묵시적(지정하지 않아도 알아서 해줌)이다.

const cast 할 수 있는 ConstCastSharedPtr 기능과 static cast(다운캐스트) 할 수 있는 StaticCastSharedPtr

Dynamic 형변환은 지원되지 않되고,  (no RTTI)가 안됨. 대신 위의 static 형변환을 사용해야 한다


Thread Safety

 

보통의 공유 포인터는 싱글 스레드에서만 안전하게 접근할 수 있습니다.
멀티 스레드에서 접근하도록 해야 한다면, Thread Safe 버전 포인터 클래스를 사용하야 한다

  • TSharedPtr<T, ESPMode::ThreadSafe>

  • TSharedRef<T, ESPMode::ThreadSafe>

  • TWeakPtr<T, ESPMode::ThreadSafe>

  • TSharedFromThis<T, ESPMode::ThreadSafe>

 

이 버전은 레퍼런스 카운팅을 개별적으로(atomic) 하기 때문에 약간 느리지만,
일반 C++ 포인터와 작동하는 방식은 거의 같다:

  • 읽기와 복사는 항상 스레드 안전합니다.

  • 쓰기/리셋의 안전을 위해서는 반드시 동기화시켜야 합니다.

※ 포인터가 둘 이상의 스레드에서 접근될 일이 절대 없는 게 확실한 경우, Thread Safe 버전을 사용하지 마시오.


Tips and Limitations

 

  • TSharedRef or TSharedPtr 를 사용해서, 파라미터를 함수를 넘길떄는 피하고. 되도록 피해라,
    레퍼런스 해제 및 레퍼런스 카운팅을 통해 오버헤드 가 발생 할 수 있기 떄문.
    그래서 전달할떄는 참조된 객체를 const &로 전달 하는게 좋다

  • C++ 포인터와는 달리 공유 포인터는 memcpy 가 불가능하므로, 공유 포인터 배열을 사용할 때는 이 점을 고려하시기 바랍니다

  • 스마트 포인터를 함수 파라미터로 전달할 때는 TWeakPtr 가 아닌, TSharedRef 나 TSharedPtr 를 사용

  • 공유 포인터를 불완전한 유형으로 전달 선언 할 수 있다

  • 공유 포인터는 Unreal 객체 ( UObject및 파생 클래스)와 호환되지 안된다. 
    엔진은 관리를 위해 별도의 메모리 관리 시스템 ( 객체 처리 문서 참조 )이 UObject있으며,
    두 시스템은 서로 겹치지 않는다.

'Unreal > Concept' 카테고리의 다른 글

언리얼 메타휴먼 애니메이터  (0) 2023.08.29
unreal - code & blueprint  (0) 2020.12.08
FArchive 아카이브, << 연산자  (1) 2019.05.31
델리게이트 + 종류 및 함수 + 시그니처 + 바인딩  (0) 2019.05.30
UWorld  (0) 2019.05.30

 

유니크포인터 이동시멘틱 : MoveTemp

공유포인터 생성 :   TShard<class> Name = MakeShareable()

 


마지막으로 정리한 내용은 언리얼 엔진에서 일반 C++ 객체를 다룰 때 편리하게,
사용할 수 있는 스마트포인터 라이브러리에 관한 내용입니다.

 

 이번에 설명할 개념 예시

로또에 당첨되 돈이 넘쳐나게된 당신은 드디어 자신만의 집을 짓게 됩니다. 
일반 C++ 클래스인 집을 아래와 같이 선언하겠습니다. 이전 강좌에서 언급했지만,
언리얼 엔진 코드에서는 일반 C++ 클래스를 선언할 때 F 접두사를 씁니다.

집에 대한 클래스 선언과 집 크기를 멤버 변수로 하나 만들었습니다. 

//ABGameInstance.h
class FHouse
{
public:
    int32 Size = 10;
};

 

그런데 한 집에서 오래 살다보니 보다 넓은 집을 짓고 싶어집니다.
그럴러면 건물을 허물고 다시 지어야 겠지요?

집을 짓고 허무는 과정을 일반 C++ 코드로 표현하면 다음과 같습니다. 

 

//ABGameInstance.cpp
void UABGameInstance::Init()
{
    Super::Init();   

    // 1단계
    AB_LOG(Warning, TEXT("****** 1단계 ******"));
    FHouse* NewHouseAddress = new FHouse(); //동적할당
    
    AB_LOG(Warning, TEXT("집을 새로 지었습니다. 내집크기 : %d"), NewHouseAddress->Size);

    if (NewHouseAddress)
    {
        delete NewHouseAddress; //삭제
        AB_LOG(Warning, TEXT("내가 직접 집을 철거했습니다. 집크기 : %d"), NewHouseAddress->Size);
        NewHouseAddress = nullptr; 
    }
}

 

집 오브젝트가 소멸되었는지의 판단은 집 크기 값을 참조했을 때,
아래 
로그와 같이 음수의 쓰레기 값이 들어있는지로 판단하겠습니다. 아래와 같이 말이지요.

로그를 통해 객체의 소멸을 확인한 결과 

 

그런데 (코딩을 하다보면) 새로운 집을 지을 때 깜빡하고 이전 집을 철거안하지 않은 채로 새로운 곳에 집을 짓는 경우가 종종 발생합니다.  (해제 및 삭제를 안하는 경우)

이렇게 철거가 안된 집들이 계속 늘어나게 되면 언젠가는 새로운 집을 지을 수 있는 공간이 부족하게 되 사회적인 문제로 발전하게 됩니다. 이러한 실수를 보완하기 위해서 스마트 포인터개념이 등장했습니다. 스마트포인터는 내가 직접 철거하는 대신, "나 떠납니다"라고 신고만하면 알아서 집을 철거해주는 편리한 대행 서비스입니다.  

 


스마트 포인터

스마트 포인터의 사용법은 일반 포인터와 거의 동일합니다.
하지만 일반 포인터와 스마트 포인터가 지양하는 컨셉은 좀 다릅니다.
일반 포인터가 객체에 접근가능한 주소를 보관하는 개념이라면, 스마트 포인터는 객체의 소유권을 증명하는 개념입니다.

원래 C++언어에서는 집주소만 보관할 수 있어서 집을 없애려면 그 주소(포인터)에 내가 직접 가서 철거를 했어야 했습니다. 하지만 스마트 포인터가 등장하면서 집의 소유권이 없으면 그 집은 자동으로 철거되버립니다.(자동으로 삭제 delete를 해줄 필요가 없다)

이제는 집주인이 깜빡해도 집주인이 떠난 집은 깔끔하게 정리되면서 토지의 재활용성이 높아지게 되었습니다.
이 편리한 서비스가 개발자들 사이에서 점점 보편화되다보니 C++ 11 버젼에 이르러서는,

공식적인 표준으로 인정받게 됩니다. 

C++에서 대표적인 스마트 포인터 서비스는 용도에 따라 삼총사가 있습니다.
언리얼에서는 이를 T로 시작하는 라이브러리로 제공합니다. 이번 강좌에서는 이들에 대해 알아보겠습니다.


스마트 포인터 종류

 

1. 유니크포인터(TUniquePtr)  :  집의 유일한 소유권을 보장하는 서비스입니다. 공동 소유는 불가합니다.

2. 공유포인터(TSharedPtr) :  집에 대해 공동 소유권을 주장할 수 있는 서비스입니다. (참조 횟수 카운트 가능)

3. 약포인터(TWeakPtr) : 집에 대한 정보는 가지고 있으나, 소유권은 없는 서비스입니다.
굳이 예를 들자면 부동산 중개업이라고 보면 됩니다.  


1. 유니크포인터(TUniquePtr)

 

스마트 포인터를 사용하게 되면 포인터처럼 사용하지만 집주소가 아닌 집의 소유를 증명할 수 있는 집문서의 개념으로 변화됩니다.  스마트포인터는 집의 소유를 증명하는 집문서가 모두 사라지면 자동으로 집은 철거되는 개념이라고 설명드렸습니다.  

여러분들이 집을 지을 때, 단독 명의로 집을 짓고 싶다면 유니크 포인터 서비스를 이용하면 됩니다. 
단독 명의로 선언하면 이 집을 혼자서 독차지할 수 있지만 다른 사람들과 나누어 가질 수는 없습니다. 

하지만 혼자 소유한다고 해서 매매가 불가능한 것은 아닙니다.
이미 만들어진 집은 그대로 두고 주인만 바꿔치기할 수 있습니다.  (std::move) ,MoveTemp

이러한 개념을 C++에서는 무브시멘틱(Move Semantic) 이라고 하는데,
언리얼 엔진에서는 MoveTemp라는 이름으로 API를 제공합니다.  

아래는 위에서 언급한 모든 기능을 구현한 코드입니다.
1단계와 2단계의 코드를 잘 비교해보시기 바랍니다.

void UABGameInstance::Init()
{
    Super::Init();   

    // 1단계
    AB_LOG(Warning, TEXT("****** 1단계 ******"));
    FHouse* NewHouseAddress = new FHouse();
    AB_LOG(Warning, TEXT("집을 새로 지었습니다. 내집크기 : %d"), NewHouseAddress->Size);
    if (NewHouseAddress)
    {
        delete NewHouseAddress;
        AB_LOG(Warning, TEXT("내가 직접 집을 철거했습니다. 집크기 : %d"), NewHouseAddress->Size);
        NewHouseAddress = nullptr;
    }

 
    // 2단계
    AB_LOG(Warning, TEXT("****** 2단계 ******"));

    NewHouseAddress = new FHouse();
    NewHouseAddress->Size = 100;
    AB_LOG(Warning, TEXT("집을 다시 지었습니다. 집크기 : %d"), NewHouseAddress->Size);
    {
        TUniquePtr<FHouse> MyHouseDeed = TUniquePtr<FHouse>(NewHouseAddress);

        if (MyHouseDeed.IsValid())
        {
            AB_LOG(Warning, TEXT("이 집은 제 단독소유 주택입니다. 내집크기 : %d"), MyHouseDeed->Size);
        }

        //TUniquePtr<FHouse> FriendsHouseDeed = MyHouseDeed;  // 컴파일 에러! 단독 소유만 가능
        TUniquePtr<FHouse> FriendsHouseDeed = MoveTemp(MyHouseDeed); // 집은 그대로 두고 집주인만 변경

        if (!MyHouseDeed.IsValid())
        {
            AB_LOG(Warning, TEXT("친구에게 집을 팔았습니다. 친구집크기 : %d"), FriendsHouseDeed->Size);
        }
    }

    AB_LOG(Warning, TEXT("집문서가 사라져서 집은 자동으로 철거되었습니다. 집크기 : %d"), NewHouseAddress->Size);
}

 

 

//Movetemp
template <typename T>
FORCEINLINE typename TRemoveReference<T>::Type&& MoveTemp(T&& Obj)
{
	typedef typename TRemoveReference<T>::Type CastType;

	// Validate that we're not being passed an rvalue or a const object - the former is redundant, the latter is almost certainly a mistake
	static_assert(TIsLValueReferenceType<T>::Value, "MoveTemp called on an rvalue");
	static_assert(!TAreTypesEqual<CastType&, const CastType&>::Value, "MoveTemp called on a const object");

	return (CastType&&)Obj;
}

 

MoveTemp 사용 후 변화


2. 공유포인터(TSharedPtr)

 

여러분이 제작한 객체를 여러 군데에서 사용하고자 할 경우에는,
공동명의로 집을 소유하고 싶으면 공유 포인터 서비스를 이용하면 됩니다.  

공유포인터는 내부적으로 레퍼런스 카운팅 기법을 사용합니다.  MakeShareable()
누군가 객체를 참조할 때마다 레퍼런스 카운트가 하나씩 올라가고, 해지할 때 하나씩 내려갑니다.
이렇게 관리하다가 
레퍼런스 카운트가 0이되면 스마트포인터는 자동으로 해당 객체를 소멸시킵니다. 

아래는 공유포인터를 사용해 구현한 예시입니다. 이제 집문서를 복제해서 아무한테나 마구 뿌릴 수 있게 되었습니다. 하지만 세상에 공짜는 없습니다. 이제 여러분은 집을 누구에게나 공유할 수 있게 되었지만, 집을 철거할 때에는 뿌린 집문서를 모두 거두어서 소멸시켜야 합니다.

////선인 및 초기화

// Create an empty shared pointer
TSharedPtr<FMyObjectType> EmptyPointer;
// Create a shared pointer to a new object
TSharedPtr<FMyObjectType> NewPointer(new FMyObjectType());
// Create a Shared Pointer from a Shared Reference
TSharedRef<FMyObjectType> NewReference(new FMyObjectType());
TSharedPtr<FMyObjectType> PointerFromReference = NewReference;
// Create a Thread-safe Shared Pointer
TSharedPtr<FMyObjectType, ESPMode::ThreadSafe> NewThreadsafePointer = MakeShared<FMyObjectType, ESPMode::ThreadSafe>(MyArgs);

 

//Reset 기능
PointerOne.Reset();
PointerTwo = nullptr;
// Both PointerOne and PointerTwo now reference nullptr.



 AB_LOG(Warning, TEXT("****** 3단계 ******"));

    //포인터1
    NewHouseAddress = new FHouse();
    NewHouseAddress->Size = 150.0f;
    
    AB_LOG(Warning, TEXT("집을 또 다시 지었습니다. 집크기 : %d"), NewHouseAddress->Size);
    {
        //참조포인터1 생성 (포인터1을 이용해서)
        TSharedPtr<FHouse> MyHouseDeed = MakeShareable(NewHouseAddress); // 만들어진 집을 차후에 등록

        if (MyHouseDeed.IsValid())
        {
            AB_LOG(Warning, TEXT("공동 소유 가능한 집이 되었습니다. 내집크기 : %d"), MyHouseDeed->Size);

            if (MyHouseDeed.IsUnique())
            {
                AB_LOG(Warning, TEXT("현재는 혼자 소유하고 있습니다. 내집크기 : %d"), MyHouseDeed->Size);
            }
        }

        //참조포인터2 생성(참조 포인터1을 이용)
        TSharedPtr<FHouse> FriendsHouseDeed = MyHouseDeed; 
        

        if (!FriendsHouseDeed.IsUnique())
        {
            AB_LOG(Warning, TEXT("친구와 집을 나눠가지게 되었습니다. 친구집크기 : %d"), FriendsHouseDeed->Size);
        }

            MyHouseDeed.Reset(); // 내가 집 소유권을 포기함

        if (FriendsHouseDeed.IsUnique())
        {
            AB_LOG(Warning, TEXT("이제 친구만 집을 소유하고 있습니다. 친구집크기 : %d"), FriendsHouseDeed->Size);
        }

        AB_LOG(Warning, TEXT("집은 아직 그대로 있습니다. 집크기 : %d"), NewHouseAddress->Size);
    }
  

    AB_LOG(Warning, TEXT("집은 자동 철거되었습니다. 집크기 : %d"), NewHouseAddress->Size);

 

아래는 공유 포인터를 사용한 결과입니다. 모든 공유 포인터가 소멸되어야 객체가 소멸되는 것을 확인할 수 있습니다.

 

실행결과

 


공유 포인터 문제점

 

공유포인터는 가장 많이 사용하는 스마트포인터라고 할 수 있습니다.
하지만 뿌린 집문서가 모두 사라져야 집이 소멸되는 특징이 있기 때문에, 예상치 못하는 상황이 발생할 수 있습니다.

대표적인 문제가 공유 포인터의 순환 참조(Circular Reference) 문제입니다.  
이 문제를 직접 확인하고 테스트하기 위해, 집 클래스를 확장해 다른 집 문서를 보관하는 기능을 추가합시다.

 

class FHouse
{
public:
    TSharedPtr<FHouse> OthersDeed; //추가
    int32 Size = 10;
};

 

아래 코드처럼 나와 친구가 각각 집을 짓고, 서로 집을 공동 소유하는 문서를 보관하게 만들어봅시다. 

둘은 서로의 집에 대해 소유권을 별도로 가지고 있기 때문에, 양쪽의 집문서가 모두 소멸되도 집이 철거되지 않고 그대로 남아있는 문제가 발생합니다

    NewHouseAddress = new FHouse();
    NewHouseAddress->Size = 200.0f;
    
    AB_LOG(Warning, TEXT("집을 한번 더 다시 지었습니다. 첫번째집크기 : %d"), NewHouseAddress->Size);   

    FHouse* NewHouseAddress2 = new FHouse();
    NewHouseAddress2->Size = 250.0f;

    AB_LOG(Warning, TEXT("친구도 집을 직접 지었습니다. 두번째집크기 : %d"), NewHouseAddress2->Size);
    {
        TSharedPtr<FHouse> MyHouseDeed = MakeShareable(NewHouseAddress);
        AB_LOG(Warning, TEXT("내 집은 내가 소유합니다. 내집크기 : %d"), MyHouseDeed->Size);

        TSharedPtr<FHouse> FriendsHouseDeed = MakeShareable(NewHouseAddress2);
        AB_LOG(Warning, TEXT("친구 집은 친구가 소유합니다. 친구집크기 : %d"), FriendsHouseDeed->Size);

        MyHouseDeed->OthersDeed = FriendsHouseDeed;
        AB_LOG(Warning, TEXT("친구 집을 공동 소유하고 문서를 내 집에 보관합니다. 친구집크기 : %d"), MyHouseDeed->OthersDeed->Size);

        FriendsHouseDeed->OthersDeed = MyHouseDeed;
        AB_LOG(Warning, TEXT("친구도 내 집을 공동 소유하고 문서를 자기 집에 보관합니다. 내집크기 : %d"), FriendsHouseDeed->OthersDeed->Size);
    }   

    AB_LOG(Warning, TEXT("집문서가 사라져도 내가 지은 집이 자동 철거되지 않습니다. 첫번째집크기 : %d"), NewHouseAddress->Size);
    AB_LOG(Warning, TEXT("친구가 지은 집도 자동 철거되지 않습니다. 두번째집크기 : %d"), NewHouseAddress2->Size);

    NewHouseAddress->OthersDeed.Reset();
    AB_LOG(Warning, TEXT("친구가 지은 집을 수동으로 철거했습니다. 집주소가 남아있어서 다행입니다. 두번째집크기 : %d"), NewHouseAddress2->Size);
    AB_LOG(Warning, TEXT("이제서야 내가 지은 집도 자동 철거됩니다. 첫번째집크기 : %d"), NewHouseAddress->Size);

 

아래는 위의 코드를 실행한 결과화면입니다.

실행 화면

 


이전에 저장해 둔 포인터 변수가 있어서 다행히 직접 삭제를 진행해 메모리 릭을 막았습니다.

하지만 이와 같이 공유포인터에만 모든 것을 의존하면 문제가 발생할 수 있음을 확인할 수 있습니다.


3. 약포인터(TWeakPtr)

 

공유 포인터에 의한 문제를 해결하기 위해 등장한 것이 약포인터입니다.  
약포인터는 부동산 중개업자로 비유할 수 있습니다. 집에 대한 소유권은 없지만 집에 대해서는 잘 알고 있죠.

위의 문제를 해결하기 위해 한 쪽에는 약포인터로 선언해 필요할 때에만 중개업자를 통해서 이용하도록(?) 구성해봅시다. 그러면 양쪽이 모두 서로의 집을 사용할 수 있으면서,

양쪽 집문서가 소멸되면 자동으로 집도 철거되게 만들 수 있습니다. 

먼저 약포인터 멤버 변수를 추가합시다. 

class FHouse
{
public:
    TSharedPtr<FHouse> OthersDeed;
    TWeakPtr<FHouse> AccessHouse;
    int32 Size = 10;
};

 

한쪽은 공유포인트를 쓰고, 다른 한쪽은 약포인터를 사용해서 상호 참조하게 만들어줍시다.
공유포인터를 사용하는 쪽은 편하게 쓸 수 있는 반면,

약포인터를 쓰는 곳은 약포인터가 제공하는 Pin()함수를 사용해서 항상 집문서가 제대로 되었는지 체크해주어야 하는 불편함이 있습니다. 

하지만 이렇게 약포인터를 사용하면 위의 문제 없이 안전하게 사용할 수 있습니다. 

 

 // 5단계

    AB_LOG(Warning, TEXT("****** 5단계 ******"));
    NewHouseAddress = new FHouse();
    NewHouseAddress->Size = 300.0f;
    AB_LOG(Warning, TEXT("이제 마지막으로 집을 짓습니다. 첫번째집크기 : %d"), NewHouseAddress->Size);   

    NewHouseAddress2 = new FHouse();
    NewHouseAddress2->Size = 350.0f;
    AB_LOG(Warning, TEXT("친구도 집을 다시 지었습니다. 두번째집크기 : %d"), NewHouseAddress2->Size);   

    {
        TSharedPtr<FHouse> MyHouseDeed = MakeShareable(NewHouseAddress);
        AB_LOG(Warning, TEXT("내 집은 내가 소유합니다. 내집크기 : %d"), MyHouseDeed->Size);

        TSharedPtr<FHouse> FriendsHouseDeed = MakeShareable(NewHouseAddress2);
        AB_LOG(Warning, TEXT("친구 집은 친구가 소유합니다. 친구집크기 : %d"), FriendsHouseDeed->Size);   

        MyHouseDeed->OthersDeed = FriendsHouseDeed;
        AB_LOG(Warning, TEXT("친구 집을 공동 소유하고 문서를 내 집에 보관합니다. 친구집크기 : %d"), MyHouseDeed->OthersDeed->Size);

        FriendsHouseDeed->AccessHouse = MyHouseDeed;
        AB_LOG(Warning, TEXT("친구가 내 집 정보를 열람합니다. 내집크기 : %d"), FriendsHouseDeed->AccessHouse.Pin()->Size);
    }   

    AB_LOG(Warning, TEXT("내가 지은 집은 자동 철거됩니다. 첫번째집크기 : %d"), NewHouseAddress->Size);

    AB_LOG(Warning, TEXT("친구가 지은 집도 자동 철거됩니다. 두번째집크기 : %d"), NewHouseAddress2->Size);

아래는 이를 실행한 로그 결과입니다.

 

약포인터 사용 결과 

 


 

언리얼 오브젝트의 메모리 관리

 

위에서 언급한 스마트 포인터 라이브러리는 일반 C++ 객체를 위한 라이브러리이고,
언리얼 오브젝트에는 사용할 수 없습니다.  

언리얼 오브젝트는 언리얼 엔진 가상머신의 가비지 컬렉션(Garbage Collection, 줄여서 GC) 시스템에 의해 자동으로 관리되기 때문입니다.

하나의 언리얼 오브젝트가 GC 시스템에 의해 자동 관리되기 위해서는
선언에 반드시 UPROPERY 매크로가 들어가야 합니다.
그러면 언리얼 엔진에 의해서 필요가 없어질 때 자동으로 회수됩니다. 

GC 시스템이 언리얼 오브젝트를 관리하는 방식은
스마트 포인터 삼총사 중 두 번째에 설명한 공유 포인터와 거의 유사합니다.
다만 다른 점은 중앙의 GC 시스템에 의해서 모든 관리되기 때문에,
메모리에서 해지되는 타이밍을 정확히 예측할 수 없다는 점입니다.   

따라서 언리얼 오브젝트의 포인터를 소멸할 때에는 BeginConditionalDestroy()라는 함수를 호출해주고,
시스템이 해지해줄 때까지 기다리는 수 밖에 없습니다.  
(액터의 경우에는 월드의 씬 정보를 업데이트 해야하기 때문에, 
먼저 DestroyActor 함수도 추가로 호출해야 합니다. ) 

가비지 컬렉션은 프로젝트 세팅에서 지정된 시간마다 언리얼 오브젝트를 감지해서 제거해줍니다.
이는 프로젝트 세팅의 Garbage Collection 탭에서 확인할 수 있습니다.
빠른 테스트를 위해서 기본 값 60초를 변경해 10초마다 거둬들이도록 조정했습니다. 

 

가비지 컬렉션의 자원 회수 타이머 설정 

 

이제 언리얼 오브젝트가 잘 회수되는지 테스트 코드로 살펴봅시다.  

테스트를 위해 ABGameInstance의 선언에 있는WebConnectionNew 멤버 변수의 UPROPERTY를 주석처리했습니다.

UPROPERTY매크로를 선언하면 게임 인스턴스가 제거될 때까지,
게임인스턴스는 언리얼 오브젝트의 인스턴스를 계속 유지하기 때문입니다. 

앞서서 언리얼 오브젝트는 해지 명령을 내려도 바로 해지되지 않고 GC 시스템에 의해 회수된다고 설명드렸습니다.
이를 테스트하기 위해 1초마다 주기적으로 콜백함수를 호출하는 타이머핸들과 타이머콜백함수를 함께 선언했습니다

아래는 새롭게 수정된 ABGameInstance의 선언입니다.

//GameInstance.h
UCLASS()
class ABC_API UABGameInstance : public UGameInstance
{
    GENERATED_BODY()    

public:
    UABCGameInstance();
    virtual void Init() override; 

    UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "WebService")
    class UWebConnection* WebConnection;

    //UPROPERTY()
    class UWebConnection* WebConnectionNew;

    UPROPERTY()
    FStreamableManager AssetLoader; 

    UFUNCTION()
    void RequestTokenComplete(const FString& Token); 

    FTimerHandle ObjectCheckTimer;

    UFUNCTION()
    void CheckUObjectAlive();
};

 

이제 WebConnection 언리얼 오브젝트를 생성해 WebConnectionNew 변수에 할당하고,
ConditionalBeginDestroy() 함수를 실행해 제거 명령을 내립시다.
삭제 명령을 내리자마자 타이머를 돌려서 GC 시스템에 의해 삭제될 때까지 로그를 찍도록 타이머를 설정합시다. 

//ABGameInstance.cpp
// 6단계

    AB_LOG(Warning, TEXT("****** 6단계 ******"));
    WebConnectionNew = NewObject<UWebConnection>();
    WebConnectionNew->Host = TEXT("127.0.0.1");
    //WebConnectionNew->AddToRoot();

    WebConnectionNew->ConditionalBeginDestroy()
    //GetWorld()->ForceGarbageCollection(true);
    GetWorld()->GetTimerManager().SetTimer(ObjectCheckTimer, this, &UABCGameInstance::CheckUObjectAlive, 1.0f, true);
}

void UABGameInstance::CheckUObjectAlive()
{
    if (!WebConnectionNew)
    {
        AB_LOG(Warning, TEXT("언리얼오브젝트가 널입니다."));
        return;
    } 

    if (!WebConnectionNew->IsValidLowLevel())
    {
        AB_LOG(Warning, TEXT("언리얼오브젝트가 유효하지 않습니다."));
        return;
    }
     AB_LOG(Warning, TEXT("언리얼오브젝트가 아직 살아있습니다. : %s"), *WebConnectionNew->Host);
}

 

GC 시스템의 동작을 확인한 결과 화면

 

참고로 주석처리한 월드의 ForceGarbageCollection(true) 함수를 실행해 GC 시스템에게 바로 자원 회수를 명령할 수 있습니다.

이는 GC 시스템에 의해 많은 수의 언리얼 오브젝트를 한번에 회수할 때,
일시적인 랙이 생기는 딸꾹질(Hiccup) 현상을 미연에 방지해 줄 수 있는 효과가 있습니다.

 반대로 언리얼 오브젝트의 삭제를 명령했음에도 불구하고,
계속 언리얼 오브젝트를 메모리에 유지시키고 싶을 떄에는 주석처리한 AddToRoot() 함수를 사용하면
자원회수를 원천적으로 봉쇄하는 것도 가능합니다


언리얼 오브젝트의 약포인터

 

언리얼 오브젝트 메모리 관리는 공유 포인터와 동일한 방식으로 동작하기 때문에,
위에서 언급한 공유 포인터의 순환 참조의 문제에서 자유롭지 않습니다.

그래서 언리얼 C++은 언리얼 오브젝트를 약하게 참조하는
TWeakObjectPtr이라는 별도의 라이브러리를 제공하고 있습니다.  

특정 언리얼 오브젝트를 참조할 때,
반드시 소유권이 필요하지 않는 경우에는 약 참조를 걸어주는 것을 추천합니다. 

예를 들어 UI의 리스트박스에서 언리얼 오브젝트의 목록을 보여주고 싶을 때
TWeakObjectPtr을 사용해 언리얼 오브젝트를 약참조(Weak Referencing)하는 것이 바람직합니다.  

일반 참조를 걸게되면 UI가 띄워져 있는 동안에는 UI에서 보여지는 모든 언리얼 오브젝트의 레퍼런스 카운팅이 올라가게되므로, 언리얼 오브젝트를 삭제해도 GC시스템에서 회수가 일어나지 않습니다.

 

 

 

https://api.unrealengine.com/INT/API/Runtime/Core/Serialization/FArchive/index.html

 

데이터를 전송하는 모든 매체는 아카이브 클래스 FArchive를 상속받아서 구현
언리얼은 입출력을 << 으로만 사용 (C++과 다른 점)

게임이 동작할 플랫폼에 맞도록 디스크나 메모리 및 다양한 매체등으로 객체를 전송해야 할텐데요, 
언리얼 엔진답게 이 부분에서 멀티플랫폼에서 동작하는 매체의 규약을 직접 만들었습니다.

이 클래스가 FArchive입니다. 

언리얼 엔진에서  파일, 메모리 등등 
데이터를 전송하는 모든 매체는 아카이브 클래스 FArchive를 상속받아서 구현합니다. 

이를 활용하기 위해 지난 강좌에서 제작한 WebConnecton 언리얼 오브젝트를 파일에 저장하고,
다시 빼오는 예제를 한번 만들어봅시다. 

WebConnection 언리얼 오브젝트는 Host와 URI의 멤버 변수를 가지고 있으므로,
이 정보를 파일에 저장했다가, 새로운 언리얼 오브젝트를 생성하고 이 데이터를 꺼내서 저장해봅시다.

아래는 이를 구현한 코드입니다.

 

//WebConnection.h 
UCLASS()
class WEBSERVICEK_API UWebConnection : public UObject
{
public:
	UPROPERTY()
	class UWebConnection* WebConnectionNew;
}

 

//ABGameInstance.cpp

void UABCGameInstance::Init()
{
	Super::Init();
	AB_LOG_CALLONLY(Warning);
    
    
    WebConnectionNew = NewObject<UWebConnection>(this);
	WebConnectionNew->Host = TEXT("127.0.0.7");
	WebConnectionNew->URI = TEXT("/");
	
    //경로설정
	FString FullPath = FString::Printf(TEXT("%s%s"), *FPaths::GameSavedDir(), TEXT("WebConnection.txt")); //경로에 파일을 만듬
	
    //롸이터 생성
	FArchive* ArWriter = IFileManager::Get().CreateFileWriter(*FullPath); //경로에 파일은 만든다
	
	if (ArWriter) //작성하고 종료
	{
		*ArWriter << WebConnectionNew->Host;
		*ArWriter << WebConnectionNew->URI;
		ArWriter->Close();
		delete ArWriter;
		ArWriter = NULL;
	}

	//리더 생성
	TSharedPtr<FArchive> FileReader = MakeShareable(IFileManager::Get().CreateFileReader(*FullPath)); //경로에 있는
	{
		FString Host;
		FString URI;
		*FileReader.Get() << Host;
		*FileReader.Get() << URI;
		FileReader->Close();
		AB_LOG(Warning, TEXT("WebConnection : Host %s , URI %s"), *Host, *URI);
	}
 }   
    

저장할 때에는 C++ 객체의 소멸을 사용했고,
불러들일 때에는 언리얼 엔진이 제공하는 공유포인터 라이브러리를 사용했습니다.

둘 간의 차이를 한번 비교해보시기 바랍니다. ( 공유포인터에 대한 내용은 다음 강좌에서 다루겠습니다. )

언리얼 에디터에서 세이브폴더라 함은, 프로젝트에 있는 Saved 폴더가 됩니다. 
그래서 
플레이를 눌러 실행한 후 탐색기에서 Saved 폴더를 확인하면 
WebConnection.txt 라는 파일이 생성되어 있는 것을 볼 수 있습니다. 

실행후 생성된 파일

추가로 에디터에서 로그를 살펴보면 파일로부터, 읽어들인 데이터 로그가 잘 찍혀있는 것을 확인할 수 있습니다. 

 

 


여기서 한가지 이상한 점이 있는데 일반 C++에서는 입출력을 << 와 >> 연산자를 구분해 사용했는데,  
언리얼에서는 특이하게 << 연산자만 사용한다는 점입니다.

언리얼에서는 데이터 입출력시에 항상 왼쪽 시프트 연산자만 사용하고,
아카이브의 처리시 현재 상태에 따라서 사용자가 분기해서 처리하도록 설계되어 있습니다. 
이후에 이 부분은 다시 살펴보도록 합시다. 

이번에는 구현에는 연산자에 인스턴스만 통채로 넘기게 만들고, 
클래스 선언에서 전송에 필요한 데이터를 지정하도록 변경해봅시다.

//ABGameInstance.cpp

void UABCGameInstance::Init()
{
	Super::Init();
	AB_LOG_CALLONLY(Warning);    
    
    WebConnection->TokenCompleteDelegate.AddDynamic(this, &UABGameInstance::RequestTokenComplete);
    WebConnection->RequestToken(TEXT("destiny")); 

    WebConnectionNew = NewObject<UWebConnection>(this);
    WebConnectionNew->Host = TEXT("127.0.0.7");
    WebConnectionNew->URI = TEXT("/"); 

    FString FullPath = FString::Printf(TEXT("%s%s"), *FPaths::GameSavedDir(), TEXT("WebConnection.txt"));

    FArchive* ArWriter = IFileManager::Get().CreateFileWriter(*FullPath);

    if (ArWriter)
    {
        *ArWriter << *WebConnectionNew; //추가
        ArWriter->Close();
        delete ArWriter;
        ArWriter = NULL;
    }

 
    TSharedPtr<FArchive> FileReader = MakeShareable(IFileManager::Get().CreateFileReader(*FullPath));

    if (FileReader.IsValid())
    {
        UWebConnection* WebConnectionFromFile = NewObject<UWebConnection>(this);  //추가
        *FileReader.Get() << *WebConnectionFromFile;                              //추가 
        FileReader->Close();
        AB_LOG(Warning, TEXT("WebConnection From File : Host %s , URI %s"), *WebConnectionFromFile->Host, *WebConnectionFromFile->URI); //추가
    }
}

 

현재 연산자 재정의가 안되어서 컴파일 오류가 나옴
WebConnection의 왼쪽 시프트 << 오퍼레이터를 전역함수 오버로딩 구문을 사용해 다음과 같이 선언해줍시다.

//WebConnection.h
class WEBSERVICE_API UWebConnection : public UObject
{
public:
	friend FArchive& operator<<(FArchive& Ar, UWebConnection& WC)
   {
        if (Ar.IsLoading())
        {
           UE_LOG(LogTemp, Warning, TEXT("Archive is Loading State"));
        }
        else if(Ar.IsSaving())
        {
            UE_LOG(LogTemp, Warning, TEXT("Archive is Saving State"));
        }
        else
        {
            return Ar;
        }
        return Ar << WC.Host << WC.URI;
   }
}   

이 선언에서 주의깊게 볼 부분은 아카이브의 함수 IsLoading과 IsSaving입니다.

이 함수를 사용하면 현재 아카이브의 상태가 저장 상태인지, 로딩 상태인지를 파악할 수 있습니다. 

'Unreal > Concept' 카테고리의 다른 글

unreal - code & blueprint  (0) 2020.12.08
언리얼 스마트포인터  (0) 2019.06.03
델리게이트 + 종류 및 함수 + 시그니처 + 바인딩  (0) 2019.05.30
UWorld  (0) 2019.05.30
World - 작성 중  (0) 2019.05.29

직렬화=시리얼라이제이션을 하기 위해선, 패키징을 사용한다?
직렬화=시리얼라이제이션을 하기 위해선, 패키징은 한 가지 방법? 


직렬화 로직

 

사전적인 의미로 시리얼라이제이션은 잡지와 같은 출판물의 연재를 의미합니다.
뭔가 계속 이어서 할 수 있는 상황을 만들기 때문에, 직렬화라고 변역이 되었다고 생각하는데요, 

컴퓨터 공학에서 시리얼라이제이션은 현재 오브젝트의 상태를 보관하고,
이를 다른 컴퓨터 환경에서 불러와 동일한 상황으로 만들어주는 기법을 의미합니다. 

예를 들어 한 오브젝트에 을 다른 공간으로 완벽하게 옮기기 위해서, 분해한후 ,
분해한 데이터를 전송해서 다른 공간에서 완벽하게 복원해줍니다.
이러한 방식이 시리얼라이제이션이다라고 이해하시면 될 것 같습니다

C++에서는 객체를 옮겨주는 시리얼라이제이션 기능을 지원하지는 않습니다. 
다만 아래와 같이 시프트(Shift) 연산자를 통해 스트림에 데이터를 전송하는 기능을 제공합니다.

아래 예제와 같이 
데이터를 스트림에 보낼 때는 왼쪽 시프트(Bitwise Left Shift) '<<' 연산자를,
스트림에서 데이터를 빼올 때에는 오른쪽 시프트(Bitwise Right Shift) '>>' 연산자를 사용합니다. 

#include <iostream>
using namespace std;

int main ()
{
  int i;
  cout << "Hello, World! \n";
  cout << "Enter your number : ";
  cin >> i;
  cout << "You entered " << i;
  return 0;
}

 

하지만 시리얼라이제이션의 개념이 성립하기 위해서는 단일 데이터를 주고받는 것이 아니라
텔레포터처럼 객체를 안전하게 보내고 받을 수 있어야 합니다. 

그래서 C++에서는 연산자 오버로딩friend 키워드를 사용해 객체 데이터를 주고 받는 방법을 많이 사용합니다. 

//연산자 오버로딩을 사용해 객체를 전달한 예제

#include <iostream>
using namespace std; 

class Date
{
public:
    Date(int m, int d, int y)
    {
       mo = m; da = d; yr = y;
    }
    friend ostream& operator<<(ostream& os, const Date& dt); 

private:
    int mo, da, yr;
}; 

ostream& operator<<(ostream& os, const Date& dt)
{
    os << dt.mo << '/' << dt.da << '/' << dt.yr;
    return os;
} 

int main()
{
    Date dt(5, 6, 92);
    cout << dt;
} 

FArchive

데이터를 전송하는 모든 매체는 아카이브 클래스 FArchive를 상속받아서 구현

 

언리얼 엔진은 표준 C++ 규약을 사용하므로 위와 같이 사용하는데에는 아무런 문제가 없습니다. 
하지만 예제에서 사용한 cout, cin의 콘솔 입출력이 아닌

게임이 동작할 플랫폼에 맞도록 디스크나 메모리 및 다양한 매체등으로 객체를 전송해야 할텐데요,
언리얼 엔진답게 이 부분에서 멀티플랫폼에서 동작하는 매체의 규약을 직접 만들었습니다.

이 클래스가 FArchive입니다. 

언리얼 엔진에서  파일, 메모리 등등
데이터를 전송하는 모든 매체는 아카이브 클래스 FArchive를 상속받아서 구현합니다. 

 

이를 활용하기 위해 지난 강좌에서 제작한 WebConnecton 언리얼 오브젝트를 파일에 저장하고,
다시 빼오는 예제를 한번 만들어봅시다. 

WebConnection 언리얼 오브젝트는 Host와 URI의 멤버 변수를 가지고 있으므로,
이 정보를 파일에 저장했다가, 새로운 언리얼 오브젝트를 생성하고 이 데이터를 꺼내서 저장해봅시다.

아래는 이를 구현한 코드입니다.

//WebConnection.h 

UCLASS()
class WEBSERVICEK_API UWebConnection : public UObject
{
public:
	UPROPERTY()
	class UWebConnection* WebConnectionNew;
}

 

//ABGameInstance.cpp

void UABCGameInstance::Init()
{
	Super::Init();
	AB_LOG_CALLONLY(Warning);
    
    
    WebConnectionNew = NewObject<UWebConnection>(this);
	WebConnectionNew->Host = TEXT("127.0.0.7");
	WebConnectionNew->URI = TEXT("/");
	
    //경로설정
	FString FullPath = FString::Printf(TEXT("%s%s"), *FPaths::GameSavedDir(), TEXT("WebConnection.txt")); //경로에 파일을 만듬
	
    //롸이터 생성
	FArchive* ArWriter = IFileManager::Get().CreateFileWriter(*FullPath); //경로에 파일은 만든다
	
	if (ArWriter) //작성하고 종료
	{
		*ArWriter << WebConnectionNew->Host;
		*ArWriter << WebConnectionNew->URI;
		ArWriter->Close();
		delete ArWriter;
		ArWriter = NULL;
	}

	//리더 생성
	TSharedPtr<FArchive> FileReader = MakeShareable(IFileManager::Get().CreateFileReader(*FullPath)); //경로에 있는
	{
		FString Host;
		FString URI;
		*FileReader.Get() << Host;
		*FileReader.Get() << URI;
		FileReader->Close();
		AB_LOG(Warning, TEXT("WebConnection : Host %s , URI %s"), *Host, *URI);
	}
 }   
    

 

저장할 때에는 C++ 객체의 소멸을 사용했고,
불러들일 때에는 언리얼 엔진이 제공하는 공유포인터 라이브러리를 사용했습니다.

둘 간의 차이를 한번 비교해보시기 바랍니다. ( 공유포인터에 대한 내용은 다음 강좌에서 다루겠습니다. )

언리얼 에디터에서 세이브폴더라 함은, 프로젝트에 있는 Saved 폴더가 됩니다.
그래서 
플레이를 눌러 실행한 후 탐색기에서 Saved 폴더를 확인하면
WebConnection.txt 라는 파일이 생성되어 있는 것을 볼 수 있습니다. 

 

실행후 생성된 파일

 

추가로 에디터에서 로그를 살펴보면 파일로부터, 읽어들인 데이터 로그가 잘 찍혀있는 것을 확인할 수 있습니다. 

 

 


여기서 한가지 이상한 점이 있는데 일반 C++에서는 입출력을 << 와 >> 연산자를 구분해 사용했는데,  
언리얼에서는 특이하게 << 연산자만 사용한다는 점입니다.

언리얼에서는 데이터 입출력시에 항상 왼쪽 시프트 연산자만 사용하고,
아카이브의 처리시 현재 상태에 따라서 사용자가 분기해서 처리하도록 설계되어 있습니다.
이후에 이 부분은 다시 살펴보도록 합시다. 

지금까지 구현한 기능은 시리얼라이제이션이라고 하기에는 애매합니다.
객체 자체를 전달하기 보다는 객체가 가지고 있는 멤버 변수들의 값을 우리가 직접 파악해서 전달했기 때문에
이 방식대로 하면 모든 객체별로 직접 전송할 값을 일일히 지정해주어야 합니다.

이번에는 구현에는 연산자에 인스턴스만 통채로 넘기게 만들고, 
클래스 선언에서 전송에 필요한 데이터를 지정하도록 변경해봅시다.


//ABGameInstance.cpp

void UABCGameInstance::Init()
{
	Super::Init();
	AB_LOG_CALLONLY(Warning);
    
    
    WebConnection->TokenCompleteDelegate.AddDynamic(this, &UABGameInstance::RequestTokenComplete);

    WebConnection->RequestToken(TEXT("destiny"));

 

    WebConnectionNew = NewObject<UWebConnection>(this);

    WebConnectionNew->Host = TEXT("127.0.0.7");

    WebConnectionNew->URI = TEXT("/");

 

    FString FullPath = FString::Printf(TEXT("%s%s"), *FPaths::GameSavedDir(), TEXT("WebConnection.txt"));

    FArchive* ArWriter = IFileManager::Get().CreateFileWriter(*FullPath);

    if (ArWriter)
    {
        *ArWriter << *WebConnectionNew; //추가
        ArWriter->Close();
        delete ArWriter;
        ArWriter = NULL;
    }

 
    TSharedPtr<FArchive> FileReader = MakeShareable(IFileManager::Get().CreateFileReader(*FullPath));

    if (FileReader.IsValid())
    {
        UWebConnection* WebConnectionFromFile = NewObject<UWebConnection>(this);  //추가
        *FileReader.Get() << *WebConnectionFromFile;                              //추가 
        FileReader->Close();
        AB_LOG(Warning, TEXT("WebConnection From File : Host %s , URI %s"), *WebConnectionFromFile->Host, *WebConnectionFromFile->URI); //추가
    }
}

 

현재 연산자 재정의가 안되어서 컴파일 오류가 나옴
WebConnection의 왼쪽 시프트 << 오퍼레이터를 전역함수 오버로딩 구문을 사용해 다음과 같이 선언해줍시다.

//WebConnection.h
class WEBSERVICE_API UWebConnection : public UObject
{
public:
	friend FArchive& operator<<(FArchive& Ar, UWebConnection& WC)
   {
        if (Ar.IsLoading())
        {
           UE_LOG(LogTemp, Warning, TEXT("Archive is Loading State"));
        }
        else if(Ar.IsSaving())
        {
            UE_LOG(LogTemp, Warning, TEXT("Archive is Saving State"));
        }
        else
        {
            return Ar;
        }
        return Ar << WC.Host << WC.URI;
   }
}   

이 선언에서 주의깊게 볼 부분은 아카이브의 함수 IsLoading과 IsSaving입니다.

이 함수를 사용하면 현재 아카이브의 상태가 저장 상태인지, 로딩 상태인지를 파악할 수 있습니다.
개인적으로 이렇게 왼쪽 시프트 오퍼레이터로 통일 시키게 되면,
시리얼라이제이션에 관련된 로직을 한 군데로 모을 수 있어서 보다 편리한 것 같습니다.

컴파일한 후 플레이 버튼을 눌러서 잘 동작하는지 확인해봅시다. 

이전 방식보다 간결해진 것 같긴한데, 그럼에도 불구하고 뭔가가 복잡해보입니다.  
언리얼 오브젝트마다 전역 함수 연산자 오버로딩을 선언하고 일일히 지정하는 방법이 그리 좋아보이지 않지요?
맞습니다. 이렇게 불편하게 설계할 언리얼 엔진이 아닙니다


직렬화 =  시리얼라이제이션


언리얼 오브젝트에 한해 언리얼 엔진은 시리얼라이제이션 기능을 제공합니다.

이를 위해 패키징이라는 클래스를 제공해줍니다.
패키징 클래스는 저장할 언리얼 오브젝트가 잘 저장되도록 포장해주는 역할을 하는 클래스인데,
언리얼 오브젝트 하나만 저장하지 않고,
언리얼 오브젝트에 속한 계층 구조에 있는 모든 오브젝트를 저장할 수 있습니다. 

예를 들어 지난 강좌에서 설명한 복잡한 계층 구조를 가진 월드도 결국 패키징을 통해서
관련된 모든 정보가 저장됩니다. 저장하는 파일의 확장자는 umap이고요.

우리가 콘텐츠 브라우저에서 보는 애셋들은 모두 패키징을 통해 저장된 애셋이라고 보면 됩니다.

월드의 패키징 예시

 

그러면 이제 패키징을 사용해 WebConnection 언리얼 오브젝트를 통짜로 저장해봅시다. 

아래 예제에서 패키징의 이름의 루트 폴더를 /Temp로 지정했는데,

이 패키지 이름을 변환한 실제 폴더의 위치는 현재 프로젝트의 Saved 폴더가 됩니다. 

SavePackage 함수를 사용해 저장한 후에는
바로 LoadPackage 함수를 사용해 언리얼 오브젝트를 불러들였습니다.

 

//ABGameInstance.cpp
 
 void UABGameInstance::Init()
{
    Super::Init();
    AB_LOG_CALLONLY(Warning);
 
    WebConnection->TokenCompleteDelegate.AddDynamic(this, &UABGameInstance::RequestTokenComplete);
    WebConnection->RequestToken(TEXT("destiny"));
 

    FString PackageName = TEXT("/Temp/SavedWebConnection"); //패키징경로 및 이름 지정

    UPackage* NewPackage = CreatePackage(nullptr, *PackageName);  //패키징 생성
    WebConnectionNew = NewObject<UWebConnection>(NewPackage);

    FString PackageFileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());  //경로+이름, uassdt 확장자가 합쳐진 경로+이름+확장자가 나옴

    WebConnectionNew->Host = TEXT("127.0.0.7");
    WebConnectionNew->URI = TEXT("/");

    if (UPackage::SavePackage(NewPackage, WebConnectionNew, RF_Standalone, *PackageFileName))//(경로_패키지이름, BASE ,플래그, 파일이름) 패키징 저장한다
    {
        UPackage* SavedPackage = LoadPackage(NULL, *PackageFileName, LOAD_None);    //패키징파일이름( 세이브된 패키징)을 불러온다

        TArray<UObject *> ObjectsInPackage;											//패키징을 닮을 배열생성
        GetObjectsWithOuter(SavedPackage, ObjectsInPackage, false);					//세이브된패키징을 배열에 넣는다

        for (const auto& EachObject : ObjectsInPackage)								//auto문으로 배열을 순회한다, WebConnection + 패키징메타 데이트 이 2개가 들어있다.()
        {
            UWebConnection* WebConnectionFromFile = Cast<UWebConnection>(EachObject);  //배열을 UWebConnection 캐스팅으로 한다
            if (WebConnectionFromFile)
            {
                 AB_LOG(Warning, TEXT("WebConnection From File : Host %s , URI %s"), *WebConnectionFromFile->Host, *WebConnectionFromFile->URI);  //HOST ,URI 출력
            }
        }
    }
}

 

 

////SavePackage
bool UPackage::SavePackage(UPackage* InOuter, UObject* Base, EObjectFlags TopLevelFlags, const TCHAR* Filename,
	FOutputDevice* Error, FLinkerLoad* Conform, bool bForceByteSwapping, bool bWarnOfLongFilename, uint32 SaveFlags,
	const class ITargetPlatform* TargetPlatform, const FDateTime&  FinalTimeStamp, bool bSlowTask)
{
	const FSavePackageResultStruct Result = Save(InOuter, Base, TopLevelFlags, Filename, Error, Conform, bForceByteSwapping,
		bWarnOfLongFilename, SaveFlags, TargetPlatform, FinalTimeStamp, bSlowTask);
	return Result == ESavePackageResult::Success;
}


////LoadPackage
UPackage* LoadPackage(UPackage* InOuter, const TCHAR* InLongPackageName, uint32 LoadFlags, FArchive* InReaderOverride, FUObjectSerializeContext* InLoadContext)
{
	COOK_STAT(LoadPackageStats::NumPackagesLoaded++);
	COOK_STAT(FScopedDurationTimer LoadTimer(LoadPackageStats::LoadPackageTimeSec));
	// Change to 1 if you want more detailed stats for loading packages, but at the cost of adding dynamic stats.
#if	STATS && 0
	static FString Package = TEXT( "Package" );
	const FString LongName = Package / InLongPackageName;
	const TStatId StatId = FDynamicStats::CreateStatId<FStatGroup_STATGROUP_UObjects>( LongName );
	FScopeCycleCounter CycleCounter( StatId );
#endif // STATS

	// since we are faking the object name, this is basically a duplicate of LLM_SCOPED_TAG_WITH_OBJECT_IN_SET
	FString FakePackageName = FString(TEXT("Package ")) + InLongPackageName;
	LLM_SCOPED_TAG_WITH_STAT_NAME_IN_SET(FLowLevelMemTracker::Get().IsTagSetActive(ELLMTagSet::Assets) ? FDynamicStats::CreateMemoryStatId<FStatGroup_STATGROUP_LLMAssets>(FName(*FakePackageName)).GetName() : NAME_None, ELLMTagSet::Assets, ELLMTracker::Default);
	return LoadPackageInternal(InOuter, InLongPackageName, LoadFlags, /*ImportLinker =*/ nullptr, InReaderOverride, InLoadContext);
}

 

 

컴파일을 하고 플레이를 누르면 아래와 같이 Saved 폴더에 uasset 파일이 생성된 것을 확인할 수 있습니다. 

결과 화면

 

참고로 위와 같이 코드로 직접 패키징을 구현할 수도 있지만,
에디터 Factory 클래스를 생성해 에디터에서,
사용자가 직접 WebConnection 애셋을 생성하도록 만드는 것도 가능합니다.

https://kyoun.tistory.com/143 참고

 

딜리게이트

 

이번 시간에는 언리얼 C++의 특징인 딜리게이트(Delegate)에 대해 알아보겠습니다.

딜리게이트(delegate)라는 단어를 사전에서 검색해보면 집단의 의사를 대표하는 대표자라고 나옵니다.
하지만 컴퓨터 프로그래밍에서 딜리게이트는 함수를 안전하게 호출할 수 있는 기능을 의미합니다. 

얼핏 들어보면 둘 간의 개념이 잘 매칭이 안되는데요, 잠깐 딜리게이트에 대해 알아보겠습니다. 

사실 C++ 언어에서는 딜리게이트라는 개념이 존재하지 않습니다.
딜리게이트는 C++ 의 다음 세대 언어인 C#에서 ( 정확히는 CLI 플랫폼 기반 ) 선보인 개념인데,

콜백 함수를 등록하기 위해 C 혹은 C++에서 사용한 함수 포인터가 문법이 복잡하고, 위험한 방식이었다면,
딜리게이트는 간편한 문법과 안전성을 갖춰 콜백 함수를 호출하기 위해 고안되었습니다. 

딜리게이트의 간편함과 안정성 외에도 이전 방식과 다른 큰 특징은 사전적 의미대로 집단의 의사를 대표합니다.
딜리게이트가 하나의 함수 뿐만 아니라 동일한 리턴값과 인자 타입을 가지는 여러 개를 관리할 수 있다는 것 입니다. 

딜리게이트의 이러한 특징은 C# 언어의 주요 패턴 중 하나인 발행/구독(Publish/Subscribe) 패턴의 구현으로 이어집니다.

C#의 발행/구독 패턴은 우리가 일상 생활에서 흔히 접하는 신문을 구독하는 절차와 유사한 방식입니다. 
구독자가 신문사에게 신문을 받아보겠다는 구독 의사를 밝히고 등록하면, 신문이 발행될 때마다
동일한 시간에 구독한 모든 구독자의 집으로 신문을 배달해주듯이, 특정 이벤트가 발생하면
딜리게이트에 등록된 모든 함수를 한꺼번에 호출할 수 있습니다.  

예를 들어 어떤 게임에서 보스와 보스가 스폰한 미니언(Minion)이 있어서 보스가 죽으면,
이 미니언(Minion)도 함께 죽게 만들고 싶다고 가정합시다. 

보스가 죽는 이벤트를 딜리게이트로 정의하고 미니언을 스폰할 때마다 각 미니언들이 이를 구독하게 설정해두면,
보스가 죽을 때, 명령 하나로 모든 미니언들에게 보스가 죽었다고 알려줄 수 있습니다. 


 딜리게이트 기능의 특징은 다음과 같이 요약할 수 있습니다.

- 함수 포인터직접 접근이 아닌 대리자를 통한 함수 호출 방식 
- 호출할 함수나 이를 포함하는 객체가 없어져도, 대리자가 체크해 안전하게 처리할 수 있음. 
- 동일한 형을 가진 함수 여러 개를 대리자가 묶어서 관리하고, 필요할 때 동시에 모두 호출하는 것이 가능함.

※ 딜리게이트 기능은 C++언어에서는 제공하지 않습니다만, 언리얼 C++은 자체적으로 프레임웍을 제작해 이 기능을 지원하고 있습니다.


 

언리얼에서 딜리게이트 기능을 사용하려면 먼저 매크로를 사용해 딜리게이트를 선언해야 합니다.
딜리게이트는 모든 함수 유형을 대변할 수 없고,
우리가 지정한 함수의 리턴값과 인자 타입가지는 함수 대표할 수 있습니다. 

그러면 기존 코드를 업그레이드해 직접 딜리게이트를 사용해봅시다. 

이번에 구현할 예제는 지난번에 제작한 두 언리얼 오브젝트인,
게임 인스턴스와 WebConnect를 활용해 볼텐데요, 게임 인스턴스가 WebConnect에게 아이디를 넘겨주면서,
네트웍상에서 사용자 아이디의 임시 토큰 데이터를 가져오도록 지시하고,
WebConnect는 서버에 질의한 결과를 딜리게이트를 통해 게임인스턴스에 전달하도록 구현해봅시다.

 

//UWebConnection.h

DECLARE_DELEGATE_OneParam(FTokenCompleteSignature, const FString&);  //인자 1개 델리게이트
DECLARE_DYNAMIC_DELEGATE(FTestDynamic);                              //다이나믹 델리게이트
DECLARE_MULTICAST_DELEGATE(FTestMutiDelegate);                       //멀티캐스트 델리게이트

UCLASS()
class WEBSERVICEK_API UWebConnection : public UObject
{
public:
....
	UFUNCTION()
	void RequestToken(const FString& UserID);

	UFUNCTION()
	void TestFunc();

	FTokenCompleteSignature TokenCompleteDelegate;
	//FTestMutiDelegate TestMuti;
	//FTestDynamic TestDynamic;

};
DECLARE_LOG_CATEGORY_EXTERN(WebConnection, Log, All);

 

UWebConnection::UWebConnection()
{

	TestMuti.AddLambda([this]()->void {TestFunc(); });
}


void UWebConnection::TestFunc()
{
	UE_LOG(WebConnection, Warning, TEXT("TestMuti"));
}


void UWebConnection::RequestToken(const FString& UserID)

{
	UE_LOG(WebConnection, Warning, TEXT("Request Token Call!"));
	TokenCompleteDelegate.ExecuteIfBound(TEXT("0LCJydGkiOiI2a3NjVE9pTUNESVZWM05qVTIyUnlTIn0.VJyMOicM"));
}

 

이제 게임 인스턴스에서는 WebConnect에 선언한 딜리게이트 타입과 동일하게 const FString& 인자 하나를 소유한 멤버 함수를 하나 만들고 WebConnection의 RequestToken 함수를 호출하기 전에 먼저 등록해줍시다. 
그러면 WebConnect의 작업이 끝나면 자동으로 WebConnect에 있는 딜리게이트에 의해 새로운 함수가 호출됩니다.  

아래는 이를 구현한 코드입니다.

 

//ABCGameInstance.h
class ABC_API UABCGameInstance : public UGameInstance
{
	GENERATED_BODY()
	
public:

	UFUNCTION()
	void RequestTokenComplete(const FString& Token);
};

//ABCGameInstance.cpp
void UABCGameInstance::RequestTokenComplete(const FString& Token)
{
	AB_LOG(Warning, TEXT("Token : %s"), *Token);
}

void UABGameInstance::Init()
{
    Super::Init();
    AB_LOG_CALLONLY(Warning);


    WebConnection->TokenCompleteDelegate.BindUObject(this, &UABGameInstance::RequestTokenComplete);
    WebConnection->RequestToken(TEXT("destiny"));
    //WebConnection->TestMuti.Broadcast();
}

 


델리게이트 바인딩

 

딜리게이트를 바인딩할 시 주의할 점은 딜리게이트에 등록할 함수의 종류에 따라 호출하는 함수가 달라진다는 점입니다. 

언리얼 딜리게이트 시스템에 등록 가능한 함수는 다음과 같습니다.

  • 전역 C++ 함수 : BindStatic API를 사용해 등록
  • 전역 C++ 람다 함수 : BindLambda API를 사용해 등록
  • C++클래스 멤버 함수 : BindRaw  API를 사용해 등록
  • 공유포인터 클래스의 멤버 함수 (쓰레드 미지원) : BindSP API를 사용해 등록 
  • 공유포인터 클래스의 멤버 함수 (쓰레드 지원) : BindThreadSafeSP API를 사용해 등록
  • UFUNCTION 멤버 함수 : BindUFunction API를 사용해 등록
  • 언리얼 오브젝트의 멤버함수 : BindUObject API를 사용해 등록

위 API 목록 중에서 우리가 바인딩하려는 RequestTokenComplete 함수는
언리얼 오브젝트 ABGameInstance의 멤버 함수이므로, CreateUObject를 사용해 바인딩하여야 함을 알 수 있습니다. 


Input 바인딩

 

언리얼 엔진의 입력 처리도 위에서 진행한 방식과 유사하게 딜리게이트를 통해서 동작합니다. 
언리얼 시스템은 프로젝트의 설정에 입력한 입력 설정마다 딜리게이트를 가지는데,
이 각각의 딜리게이트에 우리가 지정한 언리얼 오브젝트의 멤버 함수를 바인딩하여 입력신호 값을 전달받도록 설계되어 있습니다.

이를 진행하기 위해 블루프린트 강좌에서 진행한 입력에 따른 캐릭터의 이동 기능을 C++로 구현해보겠습니다. 

 

먼저 프로젝트 설정의 입력으로 가서 Axis Mapping에 UpDown과 LeftRight 입력을 아래 그림과 같이 설정합시다.

입력의 설정

 

그리고 각 입력을 처리하도록 두 함수를 만들어 바인딩 시킵시다.
그리고 Tick 이벤트에서는 두 입력 값을 조합해 캐릭터를 회전시키는 로직을 추가합시다.
이를 위해서는 
ABPawn에 FloatingPawnMovement 컴포넌트를 추가해 주고,
스프링암의 값과 액터의 회전이 무관하게 세팅해준 후 AutoPossessPlayer 변수 값을 변경해
바로 조종할 수 있도록 만들어줍시다.
( 블루프린트강좌 2-10 예제에서는 컨트롤러의 회전을 사용해 캐릭터를 움직였지만, 이번 강좌에서는 컨트롤러의 회전이 아닌 액터의 회전을 사용했습니다. ) 

 

아래는 이를 구현한 코드입니다. 

 

//ABPawn.h
#pragma once

#include "GameFramework/Pawn.h"
#include "ABPawn.generated.h"

UCLASS(config=Game)
class ARENABATTLE_API AABPawn : public APawn
{
    GENERATED_BODY()

public:
    AABPawn();

    virtual void BeginPlay() override;
    virtual void Tick( float DeltaSeconds ) override;

    virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override;



    UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category="Collision")
    class UCapsuleComponent* Body;

    UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Visual")
    class USkeletalMeshComponent* Mesh;

    UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Helper")
    class UArrowComponent* Arrow;

    UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Camera")
    class USpringArmComponent* SpringArm;

    UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Camera")
    class UCameraComponent* Camera;
 
    UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Movement")  //추가
    class UFloatingPawnMovement* Movement;  //추가

    UPROPERTY(config, BlueprintReadWrite, EditDefaultsOnly, Category = "Stat")
    float MaxHP;

    UPROPERTY(BlueprintReadWrite, EditInstanceOnly, Category = "Stat")
    float CurrentHP;

private:
   UPROPERTY(config)
    TArray<FStringAssetReference> CharacterAssets;
	
    ////밑으로 추가됨
    float CurrentLeftRightVal;
    float CurrentUpDownVal;

    UFUNCTION()
    void UpDownInput(float NewInputVal);

    UFUNCTION()
    void LeftRightInput(float NewInputval);

};
// ABPawn.cpp

#include "ArenaBattle.h"
#include "ABGameInstance.h"
#include "Kismet/KismetMathLibrary.h"
#include "ABPawn.h"
 
AABPawn::AABPawn()
{
    PrimaryActorTick.bCanEverTick = true;
    Body = CreateDefaultSubobject<UCapsuleComponent>("Capsule");
    RootComponent = Body;
    
    Mesh = CreateDefaultSubobject<USkeletalMeshComponent>("Mesh");
    Mesh->SetupAttachment(Body);

    Arrow = CreateDefaultSubobject<UArrowComponent>("Arrow");
    Arrow->SetupAttachment(Body); 

    SpringArm = CreateDefaultSubobject<USpringArmComponent>("SpringArm");
    SpringArm->SetupAttachment(Body); 

    Camera = CreateDefaultSubobject<UCameraComponent>("Camera");
    Camera->SetupAttachment(SpringArm);

    Movement = CreateDefaultSubobject<UFloatingPawnMovement>("Movement"); 

    Body->SetCapsuleSize(34.0f, 88.0f);
    Mesh->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, -88.0f), FRotator(0.0f, -90.0f, 0.0f));

    static ConstructorHelpers::FObjectFinder<USkeletalMesh> SK_Warrior(TEXT("SkeletalMesh'/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Cardboard.SK_CharM_Cardboard'"));
    Mesh->SetSkeletalMesh(SK_Warrior.Object);

    SpringArm->SetRelativeRotation(FRotator(-45.0f, 0.0f, 0.0f));
    SpringArm->TargetArmLength = 650.0f;
    SpringArm->bInheritPitch = false;
    SpringArm->bInheritYaw = false;
    SpringArm->bInheritRoll = false; 

    MaxHP = 100.0f;
    AutoPossessPlayer = EAutoReceiveInput::Player0;
}

 


void AABPawn::Tick( float DeltaTime )
{
    Super::Tick( DeltaTime );
    FVector InputVector = FVector(CurrentUpDownVal, CurrentLeftRightVal, 0.0F);
    if (InputVector.SizeSquared() > 0.0F)
    {
        FRotator TargetRotation = UKismetMathLibrary::MakeRotFromX(InputVector);
        SetActorRotation(TargetRotation);
        AddMovementInput(GetActorForwardVector());
    }
}


void AABPawn::SetupPlayerInputComponent(class UInputComponent* InputComponent)
{
   Super::SetupPlayerInputComponent(InputComponent);
    InputComponent->BindAxis("LeftRight", this, &AABPawn::LeftRightInput);
    InputComponent->BindAxis("UpDown", this, &AABPawn::UpDownInput);
}
 

void AABPawn::LeftRightInput(float NewInputVal)
{
    CurrentLeftRightVal = NewInputVal;
} 

void AABPawn::UpDownInput(float NewInputVal)
{
    CurrentUpDownVal = NewInputVal;
}

 


딜리게이트를 이용한 비동기 리소스 로딩

 

이번에는 딜리게이트를 사용해 캐릭터에서 비동기 방식으로 리소스를 로딩하도록 변경해봅시다.
언리얼 엔진에서는 비동기로딩 방식을 위해 FStreamableManager 클래스에서 AsyncLoadRequest라는 API를 제공합니다.  

이 API에는 결과를 받아오기 위한 딜리게이트 FStreamableDelegate를 전달하는 것이 필요한데,
이 딜리게이트는 StreamableManager.h에 아래와 같이 선언되어 있습니다.

이는 리턴값이 void이고 인자가 없는 함수 형식을 의미합니다. 

DECLARE_DELEGATE( FStreamableDelegate );
//비동기로딩에 사용하기 위해 이미 선언된 딜리게이트

 

ABPawn의 멤버 변수로 해당 딜리게이트를 선언하고 이를 전달해도 되지만,
아래와 같이 Create로 시작하는 API 셋을 사용하면 딜리게이트를 즉시 생성해 필요한 곳에 전달하는 것이 가능합니다. 아래 코드에서 주석을 처리한 부분과 비교해보시기 바랍니다. 

 

 //ABPawn.h
{
public:
    UFUNCTION()
    void CharacterMeshDeferred();
    //FStreamableDelegate StreamableDelegate;
 
private:
    int32 NewIndex;
}
 

 

//ABPawn.cpp
void AABPawn::BeginPlay()
{
    Super::BeginPlay();
    CurrentHP = MaxHP;
    NewIndex = FMath::RandRange(0, CharacterAssets.Num() - 1);
    UABGameInstance* ABGameInstance = Cast<UABGameInstance>(GetGameInstance());
    if (ABGameInstance)
    {
        //StreamableDelegate.BindUObject(this, &AABPawn::CharacterMeshDeferred);
        //ABGameInstance->AssetLoader.RequestAsyncLoad(CharacterAssets[NewIndex], StreamableDelegate);
        ABGameInstance->AssetLoader.RequestAsyncLoad(CharacterAssets[NewIndex], FStreamableDelegate::CreateUObject(this, &AABPawn::CharacterMeshDeferred));
    }
}

void AABPawn::CharacterMeshDeferred()
{
    AB_LOG_CALLONLY(Warning);
    TAssetPtr<USkeletalMesh> NewCharacter(CharacterAssets[NewIndex]);
    if (NewCharacter)
    {
        Mesh->SetSkeletalMesh(NewCharacter.Get());
    }
}

지금까지 알아본 딜리게이트 선언은 하나의 딜리게이트에 하나의 함수만 연결(Binding)해 실행하는 기능이었습니다. 


멀티캐스트, 다이나믹, 다이나믹 멀티캐스트

 

이번 강좌 도입부에서 설명한 하나의 딜리게이트에 여러 개의 함수를 연결하는 기능은 우리가 지금까지 실습한

DECLARE_DELEGATE 매크로가 아닌 MULTICAST를 붙인
DECLARE_MULTICAST_DELEGATE 매크로를 사용해야 합니다.

MULTICAST 딜리게이트는 Execute API 대신에 Broadcast API를 사용해야 딜리게이트에 연결된 모든 함수가 실행됩니다. (Broadcast API에는 BroadcastIfBound라는 함수는 없습니다. 아무 연결이 없으면 그냥 아무일도 안 일어납니다. ) 

 

언리얼 엔진에서는 딜리게이트 종류에 MULTICAST 외에도 다이나믹(Dynamic) 딜리게이트라는 것을 제공합니다. 
다이나믹 딜리게이트함수포인터가 아닌, 함수의 이름을 기반으로 등록해 호출하는 방식입니다.

이름 기반이다보니 저장할 수 있다는 특징이 있습니다만, 반면에 동작이 느리다는 단점이 있습니다.
다이나믹 방식으로 딜리게이트를 선언하려면 인자의 이름까지 정확히 일치해야 합니다.
그래서 딜리게이트의 선언도 함수 인자 하나당 타입과 이름 정보가 들어가야 합니다.  

다이나믹 딜리게이트 시스템이 필요한 이유는
딜리게이트 시스템에 C++ 함수 뿐만 아니라 블루프린트 함수도 연결할 수 있게 하기 위해서입니다.
하지만 블루프린트에서 사용할 수 있게 하려면 기본적으로 MULTICAST 기능도 지원해주어야 합니다.
(1개가 아닌 여러 함수를 실행해야 해서?)

따라서 딜리게이트를 블루프린트의 함수와도 연동하고 싶은 경우에는 DYNAMIC과 MULTICAST가 합쳐진 DECLARE_DYNAMIC_MULTICAST_DELEGATE 매크로를 사용해야 합니다. 

아래는 WebConnection에 선언한 딜리게이트를 블루프린트에서도 사용할 수 있도록 확장한 예입니다.
함수를 연결할 때에는 AddDynamic API를 사용해주면 됩니다

 

//WebConnection.h
#include "UObject/NoExportTypes.h"
#include "WebConnection.generated.h"
 

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FTokenCompleteSignature, const FString& , Token);
 
UCLASS()
class WEBSERVICE_API UWebConnection : public UObject
{
    GENERATED_BODY()

public:
....

    UPROPERTY(BlueprintAssignable, Category="WebService")
    FTokenCompleteSignature TokenCompleteDelegate;

};
 
DECLARE_LOG_CATEGORY_EXTERN(WebConnection, Log, All);


//WebConnection.cpp
void UWebConnection::RequestToken(const FString& UserID)
{
	UE_LOG(WebConnection, Warning, TEXT("Request Token Call!"));

	TokenCompleteDelegate.Broadcast(TEXT("0LCJydGkiOiI2a3NjVE9pTUNESVZWM05qVTIyUnlTIn0.VJyMOicM"));
}

 

//ABGameInstance.h
{
public:
UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "WebService")
	class UWebConnection* WebConnection;
}    

//ABGameInstance.cpp
void UABGameInstance::Init()
{
    Super::Init();
    AB_LOG_CALLONLY(Warning);

    WebConnection->TokenCompleteDelegate.AddDynamic(this, &UABGameInstance::RequestTokenComplete);
    WebConnection->RequestToken(TEXT("destiny"));
}

 

추가로 WebConnection.h의 딜리게이트 변수 선언에 보이는 것처럼
UPROPERTY에 BlueprintAssignable 키워드를 추가해주어야 블루프린트에서 딜리게이트를 검색할 수 있됩니다.
아래 그림은 ABGameInstance를 확장한 블루프린트에서 블루프린트 함수로 이벤트를 받을 수 있게 연결한 예시입니다.

 

2개의 델리게이트 실행화면

 

블루프린트에서 블루프린트 함수를 딜리게이트에 연결하는 모습

 

 

이번에도 강좌가 길었는데, 정리하면 다음과 같습니다.

- 딜리게이트는 함수 포인터 대신 간편한 문법으로 안전하게 호출해주는 대리자 개념. 콜백, 이벤트 구독에 많이 사용됨.
- 하나의 딜리게이트가 모든 유형의 함수를 커버할 수 없기 때문에 대리할 함수 유형을 매크로로 지정해야 한다.
- 언리얼 엔진의 입력 시스템은 딜리게이트를 통한 입력 값의 전달 방식으로 이루어져있다.  
- 연결(바인딩)할 함수의 성격에 따라 다양한 API가 존재한다. 
- 딜리게이트형선언::Create~ 함수를 사용해 즉석에서 딜리게이트를 제작해 넘겨주는 것도 가능하다. 
- 같은 유형을 가진 여러 함수를 묶어서 발행/구독 모델처럼 사용할 때는 MULTICAST 계열 매크로를 사용한다.
- 블루프린트와 연동시에는 DYNAMIC_MULTICAST 계열 매크로를 사용한다. 

+ Recent posts