제3장: 메모리 관리와 스마트 포인터

반투50·2024년 8월 15일
0

제3장: 메모리 관리와 스마트 포인터

이 장에서는 다음 주제들을 다룰 것입니다:

비관리 메모리 - malloc()/free() 사용
비관리 메모리 - new/delete 사용
관리 메모리 - NewObject< >와 ConstructObject< > 사용
관리 메모리 - 메모리 할당 해제
관리 메모리 - 객체 추적을 위한 스마트 포인터(TSharedPtr, TWeakPtr, TAutoPtr) 사용
TScopedPointer를 사용한 객체 추적
언리얼의 가비지 컬렉션 시스템과 UPROPERTY()
가비지 컬렉션 강제 실행
중단점과 코드 단계별 실행
버그 찾기와 콜 스택 사용
프로파일러를 사용한 핫스팟 식별

소개
메모리 관리는 항상 컴퓨터 프로그램의 안정성과 버그 없는 작동을 보장하기 위해 올바르게 구현해야 하는 가장 중요한 요소 중 하나입니다. 메모리에서 제거된 것을 참조하는 포인터인 댕글링 포인터는 발생 시 추적하기 어려운 버그의 한 예입니다.
어떤 컴퓨터 프로그램에서든 메모리 관리는 매우 중요합니다. UE4의 UObject 참조 카운팅 시스템은 액터와 UObject 파생 클래스에 대한 메모리를 관리하는 기본 방식입니다. 이것이 UE4 프로그램 내에서 메모리가 관리되는 기본 방식입니다.
UObject에서 파생되지 않은 자체 사용자 정의 C++ 클래스를 작성하는 경우, TSharedPtr / TWeakPtr 참조 카운팅 클래스가 유용할 수 있습니다. 이 클래스들은 참조 카운팅과 0 참조 객체에 대한 자동 삭제를 제공합니다.
이 장에서는 UE4 내의 메모리 관리를 위한 레시피를 제공합니다.
비관리 메모리 - malloc()/free() 사용
C(와 C++)에서 프로그램에 메모리를 할당하는 기본 방법은 malloc()을 사용하는 것입니다. malloc()은 컴퓨터 시스템의 메모리 블록을 프로그램 사용을 위해 지정합니다. 프로그램이 메모리 세그먼트를 사용하고 있으면 다른 프로그램은 해당 메모리 세그먼트를 사용하거나 접근할 수 없습니다. 프로그램에 할당되지 않은 메모리 세그먼트에 접근하려는 시도는 "세그멘테이션 폴트"를 생성하며, 대부분의 시스템에서 불법적인 연산을 나타냅니다.
준비하기
다음은 int 포인터 변수 i를 할당하고, malloc()을 사용하여 메모리를 할당한 다음, 역참조 연산자 를 사용하여 int 내부에 값을 저장하는 예제 코드입니다:

cppCopy// int 변수 i를 위한 메모리 생성 및 할당
int* i; // i 포인터 변수 선언
i = (int*)malloc(sizeof(int)); // 시스템 메모리 할당
*i = 0; // i 변수에 0 값 할당
printf("i contains %d", *i); // i 변수 사용, 사용 시 역참조 연산자 * 사용 필요

// i가 차지한 메모리를 시스템에 반환
free(i); // i 사용이 끝났으면 할당된 메모리를 시스템에 반환합니다.
i = 0; // 포인터의 참조를 주소 0으로 설정

작동 방식
위 코드는 다음과 같이 작동합니다:

첫 번째 줄은 int 포인터 변수 i를 생성합니다. 이는 프로그램이 참조하기에 유효하지 않을 수 있는 메모리 세그먼트를 가리키는 댕글링 포인터로 시작합니다.
두 번째 다이어그램에서는 malloc() 호출을 사용하여 정확히 int 변수 크기의 메모리 세그먼트를 가리키도록 변수 i를 초기화합니다. 이 세그먼트는 프로그램이 참조하기에 유효합니다.
그런 다음
i = 0; 명령을 사용하여 해당 메모리 세그먼트의 내용을 값 0으로 초기화합니다.

포인터 변수에 대한 할당(i =)과 포인터 변수가 가리키는 메모리 주소 내부에 대한 할당(*i =)의 차이점에 주목하세요.
메모리를 시스템에 다시 해제해야 할 때, 다음 다이어그램과 같이 free() 할당 해제 호출을 사용합니다. 그런 다음 i는 메모리 주소 0을 가리키도록 할당됩니다 (다이어그램에서 전기 접지 기호 참조로 표시됨).
변수 i가 유효한 메모리 세그먼트를 참조하지 않음을 명확히 하기 위해 i 변수를 NULL 참조를 가리키도록 설정합니다.
비관리 메모리 - new/delete 사용
new 연산자는 malloc 호출과 거의 같지만, 메모리 할당 직후 생성된 객체에 대해 생성자 호출을 호출한다는 점이 다릅니다. new 연산자로 할당된 객체는 delete 연산자로 할당 해제해야 합니다 (free()가 아님).
준비하기
C++에서는 malloc() 사용이 모범 사례로 new 연산자 사용으로 대체되었습니다. malloc()과 new 연산자의 주요 차이점은 new가 메모리 할당 후 객체 타입에 대한 생성자를 호출한다는 것입니다.
malloc:

사용을 위한 인접 공간의 영역을 할당합니다.

new:

사용을 위한 인접 공간의 영역을 할당합니다.
new 연산자의 인자로 사용된 객체 타입에 대한 생성자를 호출합니다.

방법
다음 코드에서는 간단한 Object 클래스를 선언한 다음 new 연산자를 사용하여 인스턴스를 생성합니다:
cppCopyclass Object
{
Object() {
puts("Object constructed");
}
~Object() {
puts("Object destructed");
}
};
Object object = new Object(); // 생성자 호출
delete object; // 소멸자 호출
object = 0; // 객체를 null 포인터로 재설정
작동 방식
new 연산자는 malloc()과 마찬가지로 공간을 할당합니다. new와 함께 사용된 타입이 객체 타입인 경우 키워드 new 사용 시 생성자가 자동으로 호출되지만, malloc() 사용 시에는 생성자가 절대 호출되지 않습니다.
추가 정보
키워드 new를 사용한 naked 힙 할당 (또는 malloc 사용)을 피해야 합니다. 엔진 내에서는 모든 메모리 사용이 추적되고 깨끗하도록 관리 메모리가 선호됩니다. UObject 파생 클래스를 할당하는 경우 확실히 NewObject< > 또는 ConstructObject< >를 사용해야 합니다 (이후 레시피에서 설명).
관리 메모리 - NewObject< >와 ConstructObject< > 사용
관리 메모리는 C++의 new, delete, malloc 및 free 호출 위에 있는 일부 프로그래밍된 하위 시스템에 의해 할당 및 할당 해제되는 메모리를 말합니다. 이러한 하위 시스템은 일반적으로 프로그래머가 메모리를 할당한 후 해제하는 것을 잊지 않도록 하기 위해 생성됩니다. 할당되었지만 사용되지 않은 메모리 청크를 메모리 누수라고 합니다. 예를 들어:
cppCopyfor(int i = 0; i < 100; i++)
int** leak = new int[500]; // 엄청난 메모리 누수 생성!
위의 예에서 할당된 메모리는 어떤 변수도 참조할 수 없습니다! 따라서 for 루프 이후에 할당된 메모리를 사용할 수도 없고 해제할 수도 없습니다. 프로그램이 사용 가능한 모든 시스템 메모리를 할당하면 시스템의 메모리가 완전히 소진되고 OS가 프로그램을 플래그하고 너무 많은 메모리를 사용한다는 이유로 닫을 것입니다.
메모리 관리는 메모리 해제를 잊는 것을 방지합니다. 메모리 관리 프로그램에서는 일반적으로 동적으로 할당된 객체가 해당 객체를 참조하는 포인터 수를 기억합니다. 객체를 참조하는 포인터가 0개가 되면 즉시 자동으로 삭제되거나 다음 가비지 컬렉터 실행 시 삭제되도록 플래그가 지정됩니다.
관리 메모리 사용은 UE4 내에서 자동으로 이루어집니다. 엔진 내에서 사용할 객체의 할당은 NewObject< >() 또는 SpawnActor< >()를 사용하여 수행해야 합니다. 객체 해제는 객체에 대한 참조를 제거한 다음 가끔 가비지 정리 루틴을 호출하여 수행됩니다 (이 장의 뒷부분에서 나열).
준비하기
Actor 클래스의 파생이 아닌 UObject 파생 클래스를 구성해야 할 때 항상 NewObject< >를 사용해야 합니다. SpawnActor< >는 객체가 Actor 또는 그 파생인 경우에만 사용해야 합니다.
방법
UObject에서 파생된 UAction 타입의 객체를 구성하려고 한다고 가정해 보겠습니다. 예를 들어, 다음과 같은 클래스입니다:
cppCopyUCLASS(BlueprintType, Blueprintable, meta=(ShortTooltip="Base class for any Action type"))
Class WRYV_API UAction : public UObject
{
GENERATED_UCLASS_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Properties)
FString Text;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Properties)
FKey ShortcutKey;
};
UAction 클래스의 인스턴스를 구성하려면 다음과 같이 할 것입니다:
cppCopyUAction
action = NewObject(GetTransientPackage(), UAction::StaticClass() / RF_ flags /);
작동 방식
여기서 UAction::StaticClass()는 UAction 객체에 대한 기본 UClass
를 가져옵니다. NewObject< >의 첫 번째 인자는 GetTransientPackage()로, 게임의 임시 패키지를 단순히 검색합니다. UE4의 패키지(UPackage)는 단순히 데이터 덩어리입니다. 여기서는 힙 할당 데이터를 저장하기 위해 임시 패키지를 사용합니다. 블루프린트에서 UPROPERTY() TSubclassOf를 사용하여 UClass 인스턴스를 선택할 수도 있습니다.
세 번째 인자(선택적)는 UObject가 메모리 관리 시스템에서 처리되는 방식을 나타내는 매개변수의 조합입니다.
추가 정보
NewObject< >와 매우 유사한 ConstructObject< >라는 다른 함수도 있습니다. ConstructObject< >는 구성 시 더 많은 매개변수를 제공하며, 이러한 매개변수를 지정해야 하는 경우 유용할 수 있습니다. 그렇지 않으면 NewObject로도 충 CopyRetryClaude’s response was limited as it hit the maximum length allowed at this time. Claude does not have the ability to run the code it generates yet.Q계속번역 Edit분히 잘 작동합니다.
참조

RF_* 플래그에 대한 문서는 https://docs.unrealengine.com/latest/INT/Programming/UnrealArchitecture/Objects/Creation/index.html#objectflags 에서 확인할 수 있습니다.

관리 메모리 - 메모리 할당 해제
UObject는 참조 카운트되며 UObject 인스턴스에 대한 참조가 더 이상 없을 때 가비지 수집됩니다. ConstructObject<> 또는 NewObject< >를 사용하여 UObject 클래스 파생에 할당된 메모리도 UObject::ConditionalBeginDestroy() 멤버 함수를 호출하여 수동으로 할당 해제할 수 있습니다(참조 카운트가 0으로 떨어지기 전에).
준비하기
UObject 또는 UObject 클래스 파생 인스턴스를 더 이상 메모리에 두고 싶지 않다고 확신할 때만 이 작업을 수행해야 합니다. ConditionalBeginDestroy() 함수를 사용하여 메모리를 해제하세요.
방법
다음 코드는 UObject 클래스의 할당 해제를 보여줍니다:
cppCopyUObject *o = NewObject< UObject >( ... );
o->ConditionalBeginDestroy();
작동 방식
ConditionalBeginDestroy() 명령은 BeginDestroy() 및 FinishDestroy() 오버라이드 가능한 함수를 호출하여 할당 해제 프로세스를 시작합니다.
추가 정보
다른 객체의 포인터가 여전히 메모리에서 참조하고 있는 객체에 대해 UObject::ConditionalBeginDestroy()를 호출하지 않도록 주의하세요.
관리 메모리 - 객체 추적을 위한 스마트 포인터(TSharedPtr, TWeakPtr, TAutoPtr) 사용
표준 C++ 객체를 생성할 때 delete 호출을 잊어버릴까 봐 걱정되는 경우 종종 메모리 누수를 방지하기 위해 스마트 포인터를 사용합니다. TSharedPtr은 UObject 파생을 제외한 모든 사용자 정의 C++ 객체를 참조 카운트하는 데 매우 유용한 C++ 클래스입니다. UObject 파생은 이미 참조 카운트됩니다. TWeakPtr이라는 대체 클래스도 제공되며, 이는 삭제를 방지할 수 없다는 특이한 속성을 가진 참조 카운트된 객체를 가리키는 데 사용됩니다(따라서 "약한" 포인터).
UObject 및 그 파생 클래스(NewObject 또는 ConstructObject로 생성된 모든 것)는 TSharedPtr을 사용할 수 없습니다!
준비하기
UObject 파생을 사용하지 않는 C++ 코드에서 원시 포인터를 사용하지 않고 수동으로 삭제를 추적하고 싶지 않다면, 해당 코드는 TSharedPtr, TSharedRef 등과 같은 스마트 포인터를 사용하기에 좋은 후보입니다. 키워드 new를 사용하여 동적으로 할당된 객체를 사용할 때 참조 카운트된 포인터로 감싸서 할당 해제가 자동으로 발생하도록 할 수 있습니다. 다양한 유형의 스마트 포인터는 스마트 포인터 동작과 삭제 호출 시간을 결정합니다. 다음과 같습니다:

TSharedPtr: 공유 객체를 나타내는 스레드 안전(ESPMode::ThreadSafe를 템플릿의 두 번째 인자로 제공한 경우)한 참조 카운트 포인터 유형입니다. 공유 객체는 더 이상 참조가 없을 때 할당 해제됩니다.
TAutoPtr: 스레드 안전하지 않은 공유 포인터입니다.

방법
이전에 언급한 네 가지 유형의 스마트 포인터 사용을 짧은 코드 세그먼트로 보여줄 수 있습니다. 이 모든 코드에서 시작 포인터는 원시 포인터이거나 다른 스마트 포인터의 복사본일 수 있습니다. C++ 원시 포인터를 가져와 TSharedPtr, TSharedRef, TWeakPtr 또는 TAutoPtr의 생성자 호출로 감싸기만 하면 됩니다.
예를 들어:
cppCopy// UObject에서 파생되지 않은 C++ 클래스
class MyClass { };
TSharedPtr sharedPtr(new MyClass());
작동 방식
약한 포인터와 공유 포인터 사이에는 몇 가지 차이점이 있습니다. 약한 포인터는 참조 카운트가 0으로 떨어질 때 객체를 메모리에 유지할 수 있는 능력이 없습니다.
약한 포인터를 사용하는 장점(원시 포인터에 비해)은 약한 포인터 아래의 객체가 수동으로 삭제될 때(ConditionalBeginDestroy()를 사용하여) 약한 포인터의 참조가 NULL 참조가 된다는 것입니다. 이를 통해 다음과 같은 형태의 문장을 확인하여 포인터 아래의 리소스가 여전히 제대로 할당되어 있는지 확인할 수 있습니다:
cppCopyif(ptr.IsValid()) // 포인터가 유효한지 확인
{
}
추가 정보
공유 포인터는 스레드 안전합니다. 이는 기본 객체를 별도의 스레드에서 안전하게 조작할 수 있음을 의미합니다. TSharedRef, TSharedPtr, TWeakPtr 클래스를 사용하여 원시 포인터를 감쌀 수 있는 것은 UObject나 UObject 파생이 아닌 사용자 정의 C++ 클래스나 FStructure에서만 가능하다는 점을 항상 기억하세요. 스마트 포인터를 사용하여 객체를 가리키려면 TWeakObjectPointer 또는 UPROPERTY()를 시작점으로 사용해야 합니다.
TSharedPtr의 스레드 안전 보장이 필요하지 않은 경우 TAutoPtr을 사용할 수 있습니다. TAutoPtr은 참조 수가 0으로 떨어질 때 객체를 자동으로 삭제합니다.
TScopedPointer를 사용한 객체 추적
스코프 포인터는 선언된 블록의 끝에서 자동 삭제되는 포인터입니다. 스코프는 변수가 "살아있는" 코드 섹션입니다. 스코프는 처음 나오는 닫는 중괄호 }까지 지속됩니다.
예를 들어, 다음 블록에는 두 개의 스코프가 있습니다. 외부 스코프는 정수 변수 x를 선언하고(전체 외부 블록에 대해 유효), 내부 스코프는 정수 변수 y를 선언합니다(선언된 줄 이후의 내부 블록에 대해 유효):
cppCopy{
int x;
{
int y;
} // y의 스코프 끝
} // x의 스코프 끝
준비하기
스코프 포인터는 스코프를 벗어날 위험이 있는 참조 카운트된 객체를 사용 기간 동안 유지해야 할 때 유용합니다.
방법
스코프 포인터를 선언하려면 다음과 같은 구문을 사용하면 됩니다:
cppCopyTScopedPointer warrior(this);
이는 각괄호 안에 선언된 유형의 객체를 참조하는 스코프 포인터를 선언합니다: .
작동 방식
TScopedPointer 변수 유형은 가리키는 변수에 대한 참조 카운트를 자동으로 추가합니다. 이는 스코프 포인터의 수명 동안 적어도 기본 객체의 할당 해제를 방지합니다.
언리얼의 가비지 컬렉션 시스템과 UPROPERTY()
UCLASS()의 UPROPERTY() 멤버로 객체(예: TArray< >)를 가질 때, 해당 멤버를 UPROPERTY()로 선언해야 합니다(블루프린트에서 편집하지 않더라도). 그렇지 않으면 TArray가 제대로 할당된 상태로 유지되지 않습니다.
방법
예를 들어 다음과 같은 UCLASS() 매크로가 있다고 가정해 봅시다:
cppCopyUCLASS()
class MYPROJECT_API AWarrior : public AActor
{
//TArray< FSoundEffect > Greets; // 잘못됨
UPROPERTY() TArray< FSoundEffect > Greets; // 올바름
};
TArray 멤버를 UPROPERTY()로 나열해야 제대로 참조 카운트됩니다. UPROPERTY() 선언 없이는 TArray가 제대로 작동하지 않습니다.
작동 방식
UPROPERTY() 선언은 UE4에 TArray를 제대로 메모리 관리해야 한다고 알려줍니다. UPROPERTY() 선언 없이는 TArray가 제대로 작동하지 않습니다.
가비지 컬렉션 강제 실행
메모리가 가득 차고 일부를 해제하고 싶을 때 가비지 컬렉션을 강제로 실행할 수 있습니다. 이를 자주 할 필요는 없지만, 참조 카운트된 매우 큰 텍스처(또는 텍스처 세트)를 지워야 하는 경우 이 작업을 수행할 수 있습니다.
준비하기
할당 해제하려는 모든 UObject에 대해 ConditionalBeginDestroy()를 호출하거나 참조 카운트를 0으로 설정하기만 하면 됩니다.
방법
가비지 컬렉션은 다음과 같이 호출하여 수행됩니다:
cppCopyGetWorld()->ForceGarbageCollection(true);
중단점과 코드 단계별 실행
중단점은 C++ 프로그램을 일시 중지하여 코드 실행을 일시적으로 중지하고 프로그램의 작동을 분석하고 검사할 수 있는 방법입니다. 변수를 들여다보고, 코드를 단계별로 실행하고, 변수 값을 변경할 수 있습니다.
준비하기
Visual Studio에서 중단점을 설정하는 것은 쉽습니다. 작동을 일시 중지하려는 코드 줄에서 F9를 누르거나 작동을 일시 중지하려는 코드 줄 왼쪽의 회색 여백을 클릭하면 됩니다. 작동이 표시된 줄에 도달하면 코드가 일시 중지됩니다.
방법

실행을 일시 중지할 줄에서 F9를 누릅니다. 이렇게 하면 코드에 중단점이 추가되며, 빨간 점으로 표시됩니다. 빨간 점을 클릭하면 토글됩니다.
빌드 구성을 Debug가 포함된 구성 중 하나로 설정합니다(DebugGame Editor 또는 에디터 없이 실행할 경우 단순히 DebugGame).
F5를 눌러(Ctrl을 누르지 않고) 코드를 실행하거나 Debug | Start Debugging 메뉴 옵션을 선택합니다.
코드가 빨간 점에 도달하면 코드 실행이 일시 중지됩니다.
일시 중지된 뷰는 디버그 모드의 코드 편집기로 이동합니다. 이 모드에서는 창이 재배열되어 있을 수 있으며, 솔루션 탐색기가 오른쪽으로 이동하고 Locals, Watch 1, Call Stack을 포함한 새 창이 아래에 나타날 수 있습니다. 이 창들이 나타나지 않으면 Debug | Windows 하위 메뉴에서 찾을 수 있습니다.
Locals 창에서 변수를 확인합니다(Debug | Windows | Locals).
F10을 눌러 코드 한 줄을 넘깁니
CopyRetryClaude’s response was limited as it hit the maximum length allowed at this time. Claude does not have the ability to run the code it generates yet. Claude does not have internet access. Links provided may not be accurate or up to date.Q계속 Edit다.

F11을 눌러 코드 한 줄로 들어갑니다.

작동 방식
디버거는 코드가 실행되는 동안 변수 상태를 포함한 모든 것을 볼 수 있게 해주는 강력한 도구입니다.
코드 한 줄을 넘기는 것(F10)은 코드 한 줄을 완전히 실행한 다음 프로그램을 즉시 다음 줄에서 다시 일시 중지합니다. 그 줄이 함수 호출인 경우 함수 호출의 첫 번째 코드 줄에서 일시 중지하지 않고 함수가 실행됩니다. 예를 들어:
cppCopyvoid f() {
// F11은 여기서 일시 중지
UE_LOG(LogTemp, Warning, TEXT("Log message"));
}

int main() {
f(); // 여기에 중단점: F10은 실행하고 다음 줄로 건너뜁니다
}
코드 한 줄로 들어가는 것(F11)은 실행된 다음 코드 줄에서 실행을 일시 중지합니다.
버그 찾기와 콜 스택 사용
코드에 버그가 있을 때 Visual Studio는 중지되고 코드를 검사할 수 있게 해줍니다. Visual Studio가 중지하는 위치가 항상 버그의 정확한 위치는 아니지만 가까울 수 있습니다. 적어도 제대로 실행되지 않는 코드 줄에 있을 것입니다.
준비하기
이 레시피에서는 Call Stack에 대해 설명하고 오류가 어디서 발생했는지 추적하는 방법을 알아봅니다. 코드에 버그를 추가하거나 검사하고 싶은 흥미로운 위치에 중단점을 추가해 보세요.
방법

F5를 누르거나 Debug | Start Debugging 메뉴 옵션을 선택하여 버그가 발생하는 지점까지 코드를 실행합니다. 예를 들어 다음과 같은 코드 줄을 추가합니다:

cppCopyUObject *o = 0; // 불법적인 null 포인터로 초기화
o->GetName(); // 객체의 이름을 가져오려고 시도 (버그 있음)

코드는 두 번째 줄(o->GetName())에서 일시 중지됩니다.
코드가 일시 중지되면 Call Stack 창으로 이동합니다(Debug | Windows | Call Stack).

작동 방식
Call Stack은 실행된 함수 호출 목록입니다. 버그가 발생하면 발생한 줄이 Call Stack의 맨 위에 나열됩니다.
프로파일러를 사용한 핫스팟 식별
C++ 프로파일러는 많은 처리 시간이 필요한 코드 섹션을 찾는 데 매우 유용합니다. 프로파일러를 사용하면 최적화 중에 집중해야 할 코드 섹션을 찾는 데 도움이 될 수 있습니다. 코드의 한 영역이 느리게 실행된다고 의심되는 경우 프로파일러에서 강조 표시되지 않으면 실제로 느리지 않다는 것을 확인할 수 있습니다.
방법

Debug | Start Diagnostic Tools Without Debugging...으로 이동합니다.
표시된 대화 상자에서 분석하려는 유형을 선택합니다. CPU 사용량, GPU 사용량, 메모리 사용량을 분석하거나 보고 싶은 항목을 선택하는 데 도움이 되는 성능 마법사를 단계별로 실행할 수 있습니다.
대화 상자 하단의 Start 버튼을 클릭합니다.
샘플 수집을 중지하려면 잠시 후(1-2분 이내)에 코드를 중지합니다.

너무 많은 샘플을 수집하지 마세요. 그렇지 않으면 프로파일러 시작에 시간이 오래 걸릴 수 있습니다.

.diagsession 파일에 나타나는 결과를 검사합니다. 열리는 모든 탭을 반드시 살펴보세요. 사용 가능한 탭은 수행된 분석 유형에 따라 다릅니다.

작동 방식
C++ 프로파일러는 실행 중인 코드를 샘플링하고 분석하여 코드 성능에 대한 일련의 다이어그램과 수치를 제시합니다. CopyRetryClaude does not have the ability to run the code it generates yet.Q끝이야? Edit네, 그것으로 3장의 번역이 끝났습니다. 이 장은 메모리 관리와 스마트 포인터에 대해 다루고 있으며, 마지막 부분에서는 디버깅과 프로파일링에 대한 내용도 포함하고 있습니다. 주요 주제들은 다음과 같습니다:

비관리 메모리 사용 (malloc/free, new/delete)
관리 메모리 사용 (NewObject, ConstructObject)
스마트 포인터 (TSharedPtr, TWeakPtr, TAutoPtr, TScopedPointer)
언리얼의 가비지 컬렉션 시스템
중단점과 디버깅 기법
프로파일러 사용

이 장은 언리얼 엔진에서의 메모리 관리 방법과 디버깅 도구 사용법에 대한 중요한 정보를 제공합니다. 더 자세한 내용이나 다른 장에 대해 알고 싶으신 것이 있나요? CopyRetryQ4장번역 Edit네, 4장을 번역해 드리겠습니다:
제4장: 액터와 컴포넌트
이 장에서는 다음 레시피들을 다룰 것입니다:

C++에서 사용자 정의 액터 만들기
SpawnActor를 사용하여 액터 인스턴스화하기
Destroy와 Timer를 사용하여 액터 파괴하기
SetLifeSpan을 사용하여 지연 후 액터 파괴하기
구성을 통해 액터 기능 구현하기
FObjectFinder를 사용하여 컴포넌트에 에셋 로딩하기
상속을 통해 액터 기능 구현하기
계층 구조를 만들기 위해 컴포넌트 부착하기
사용자 정의 액터 컴포넌트 만들기
사용자 정의 씬 컴포넌트 만들기
사용자 정의 프리미티브 컴포넌트 만들기
RPG를 위한 인벤토리 컴포넌트 만들기
궤도 운동 컴포넌트 만들기
유닛을 생성하는 건물 만들기

소개
액터는 게임 세계에 존재감이 있는 클래스입니다. 액터는 컴포넌트를 통합함으로써 특별한 기능을 얻습니다. 이 장에서는 사용자 정의 액터와 컴포넌트를 만드는 방법, 그들의 목적, 그리고 그들이 어떻게 함께 작동하는지를 다룹니다.
C++에서 사용자 정의 액터 만들기
언리얼의 기본 설치에는 다양한 종류의 액터가 포함되어 있지만, 프로젝트 개발 중에 사용자 정의 액터를 만들어야 할 때가 있을 것입니다. 기존 클래스에 기능을 추가하거나, 기본 하위 클래스에 없는 조합으로 컴포넌트를 결합하거나, 클래스에 추가 멤버 변수를 추가해야 할 때 이런 일이 발생할 수 있습니다. 다음 두 레시피는 구성이나 상속을 사용하여 액터를 사용자 정의하는 방법을 보여줍니다.
준비하기
1장 "UE4 개발 도구"의 레시피에 따라 Visual Studio와 Unreal 4를 설치했는지 확인하세요. 또한 기존 프로젝트가 있거나 언리얼에서 제공하는 마법사를 사용하여 새 프로젝트를 만들어야 합니다.
방법

언리얼 에디터 내에서 프로젝트를 열고 콘텐츠 브라우저에서 Add New 버튼을 클릭합니다.
New C++ Class...를 선택합니다.
열리는 대화 상자에서 목록에서 Actor를 선택합니다.
액터에 이름을 지정합니다(예: MyFirstActor). 그런 다음 OK를 클릭하여 Visual Studio를 실행합니다.

관례상 액터 하위 클래스의 클래스 이름은 A로 시작합니다. 이 클래스 생성 마법사를 사용할 때 A 접두사를 클래스에 붙이지 마세요. 엔진이 자동으로 접두사를 추가합니다.

Visual Studio가 로드되면 다음과 매우 유사한 것을 볼 수 있습니다:

MyFirstActor.h
cppCopy#pragma once

#include "GameFramework/Actor.h"
#include "MyFirstActor.generated.h"

UCLASS()
class UE4COOKBOOK_API AMyFirstActor : public AActor
{
GENERATED_BODY()

public:
AMyFirstActor();
};
MyFirstActor.cpp
cppCopy#include "UE4Cookbook.h"
#include "MyFirstActor.h"

AMyFirstActor::AMyFirstActor()
{
PrimaryActorTick.bCanEverTick = true;
}
작동 방식
시간이 지나면 표준 코드에 익숙해져서 언리얼 마법사를 사용하지 않고도 Visual Studio에서 새 클래스를 만들 수 있게 될 것입니다.

#pragma once: 이 전처리기 문(또는 pragma)은 include 파일이 여러 번 참조되어 오류를 일으키는 것을 방지하는 include guard를 구현하는 언리얼의 예상 방법입니다.
#include "GameFramework/Actor.h": 액터 하위 클래스를 만들 것이므로 당연히 상속받을 클래스의 헤더 파일을 포함해야 합니다.
#include "MyFirstActor.generated.h": 모든 액터 클래스는 generated.h 파일을 포함해야 합니다. 이 파일은 언리얼 헤더 도구(UHT)에 의해 파일에서 감지한 매크로를 기반으로 자동 생성됩니다.
UCLASS(): UCLASS는 그러한 매크로 중 하나로, 클래스가 언리얼의 리플렉션 시스템에 노출될 것임을 나타냅니다. 리플렉션을 통해 런타임 중에 객체 속성을 검사하고 반복할 수 있으며 가비지 컬렉션을 위해 객체에 대한 참조를 관리할 수 있습니다.
class UE4COOKBOOK_API AMyFirstActor : public AActor: 이것이 실제 클래스 선언입니다. UE4COOKBOOK_API 매크로는 UHT에 의해 생성되며, Windows에서 프로젝트를 올바르게 컴파일하기 위해 필요합니다. 프로젝트 모듈의 클래스가 DLL에서 올바르게 내보내지도록 보장합니다. MyFirstActor와 Actor 모두 A 접두사가 붙어 있음을 알 수 있습니다. 이는 액터에서 상속된 네이티브 클래스에 대해 언리얼이 요구하는 명명 규칙입니다.
GENERATED_BODY(): GENERATED_BODY는 기본 UE 타입 시스템이 요구하는 자동 생성된 함수를 포함하도록 확장된 또 다른 UHT 매크로입니다.
PrimaryActorTick.bCanEverTick = true;: 생성자 구현 내에서 이 줄은 이 액터에 대한 틱킹을 활성화합니다. 모든 액터에는 Tick이라는 함수가 있으며, 이 부울 변수는 필요에 따라 액터가 매 프레임마다 해당 함수를 호출하도록 합니다. 성능 최적화를 위해 이는 기본적으로 비활성화되어 있습니다.
CopyRetryClaude does not have the ability to run the code it generates yet.Q계석 Edit네, 계속해서 4장의 번역을 이어가겠습니다:
SpawnActor를 사용하여 액터 인스턴스화하기
이 레시피를 위해서는 인스턴스화할 액터 하위 클래스가 필요합니다. 내장된 클래스인 StaticMeshActor를 사용하거나, 이전 레시피에서 만든 사용자 정의 액터를 사용하면 도움이 될 것입니다.
방법

이전 레시피와 마찬가지로 새 C++ 클래스를 만듭니다. 이번에는 GameMode를 기본 클래스로 선택하고 UE4CookbookGameMode와 같은 이름을 지정합니다.
새 GameMode 클래스에 함수 오버라이드를 선언합니다:
cppCopyvirtual void BeginPlay() override;

cpp 파일에서 BeginPlay를 구현합니다:
cppCopyvoid AUE4CookbookGameMode::BeginPlay()
{
Super::BeginPlay();
GEngine->AddOnScreenDebugMessage(-1, -1, FColor::Red, TEXT("Actor Spawning"));
FTransform SpawnLocation;
GetWorld()->SpawnActor(AMyFirstActor::StaticClass(), &SpawnLocation);
}

Visual Studio를 통해 또는 언리얼 에디터의 Compile 버튼을 클릭하여 코드를 컴파일합니다.
현재 레벨에 대한 World Settings 패널을 열려면 설정 툴바 아이콘을 클릭한 다음 드롭다운 메뉴에서 World Settings를 선택합니다. GameMode Override 섹션에서 방금 만든 GameMode 하위 클래스로 게임 모드를 변경합니다.
레벨을 시작하고 World Outliner 패널을 보고 GameMode가 게임 월드에 액터 복사본을 생성했는지 확인합니다. BeginPlay 함수가 실행되고 있음을 화면에 표시되는 "Actor Spawning" 텍스트를 통해 확인할 수 있습니다. 액터가 생성되지 않으면 액터가 생성되는 것을 방해하는 장애물이 월드 원점에 없는지 확인하세요. World Outliner 패널 상단의 검색 창을 사용하여 월드의 객체 목록을 검색하여 표시된 엔티티를 필터링할 수 있습니다.

작동 방식

GameMode는 언리얼 게임 프레임워크의 일부인 특별한 타입의 액터입니다. 맵의 GameMode는 게임이 시작될 때 엔진에 의해 자동으로 인스턴스화됩니다.
사용자 정의 GameMode의 BeginPlay 메서드에 코드를 배치하면 게임이 시작될 때 자동으로 실행됩니다.
BeginPlay 내에서 SpawnActor 함수에 사용할 FTransform을 생성합니다. 기본적으로 FTransform은 회전이 없고 원점에 위치하도록 구성됩니다.
그런 다음 GetWorld를 사용하여 현재 레벨의 UWorld 인스턴스에 대한 참조를 가져온 다음 SpawnActor 함수를 호출합니다. 이전에 만든 FTransform을 전달하여 객체가 해당 위치, 즉 원점에 생성되도록 지정합니다.

Destroy와 Timer를 사용하여 액터 파괴하기
이 레시피는 이전 레시피의 GameMode를 재사용할 것이므로 먼저 그것을 완료해야 합니다.
방법

GameMode 선언에 다음 멤버를 추가합니다:
cppCopyUPROPERTY()
AMyFirstActor* SpawnedActor;

UFUNCTION()
void DestroyActorFunction();

구현 파일의 include 섹션에 #include "MyFirstActor.h"를 추가합니다.
SpawnActor의 결과를 새 SpawnedActor 변수에 할당합니다:
cppCopySpawnedActor = GetWorld()->SpawnActor(AMyFirstActor::StaticClass(), SpawnLocation);

BeginPlay 함수 끝에 다음을 추가합니다:
cppCopyFTimerHandle Timer;
GetWorldTimerManager().SetTimer(Timer, this, &AUE4CookbookGameMode::DestroyActorFunction, 10);

마지막으로 DestroyActorFunction을 구현합니다:
cppCopyvoid AUE4CookbookGameMode::DestroyActorFunction()
{
if (SpawnedActor != nullptr)
{
SpawnedActor->Destroy();
}
}

이전 레시피에서 만든 레벨을 로드하고 게임 모드가 사용자 정의 UE4CookbookGameMode로 설정되어 있는지 확인합니다.
레벨을 실행하고 Outliner를 사용하여 SpawnedActor가 10초 후에 삭제되는지 확인합니다.

작동 방식

UPROPERTY를 선언하여 생성된 액터 인스턴스를 저장하고, 사용자 정의 함수를 호출하여 UFUNCTION을 선언하여 타이머에서 Destroy()를 호출할 수 있게 합니다:
cppCopyUPROPERTY()
AMyFirstActor* SpawnedActor;

UFUNCTION()
void DestroyActorFunction();

BeginPlay에서 생성된 액터를 새 UPROPERTY에 할당합니다:
cppCopySpawnedActor = GetWorld()->SpawnActor(AMyFirstActor::StaticClass(), SpawnLocation);

그런 다음 TimerHandle 객체를 선언하고 GetWorldTimerManager::SetTimer에 전달합니다. SetTimer는 10초 후에 this 포인터가 가리키는 객체에서 DestroyActorFunction을 호출합니다. SetTimer는 필요한 경우 타이머를 취소할 수 있는 객체인 핸들을 반환합니다. SetTimer 함수는 TimerHandle 객체를 참조 매개변수로 받으므로 미리 선언하여 함수에 올바르게 전달할 수 있습니다:
cppCopyFTimerHandle Timer;
GetWorldTimerManager().SetTimer(Timer, this, &AUE4CookbookGameMode::DestroyActorFunction, 10);

DestroyActorFunction은 생성된 액터에 대한 유효한 참조가 있는지 확인합니다:
cppCopyvoid AUE4CookbookGameMode::DestroyActorFunction()
{
if (SpawnedActor != nullptr)

참조가 있다면 인스턴스에 대해 Destroy를 호출하여 파괴되고 결국 가비지 컬렉션됩니다:
cppCopySpawnedActor->Destroy();

SetLifeSpan을 사용하여 지연 후 액터 파괴하기
액터를 파괴하는 방법을 살펴보겠습니다.
방법

마법사를 사용하여 새 C++ 클래스를 만듭니다. 기본 클래스로 Actor를 선택합니다.
액터의 구현에서 BeginPlay 함수에 다음 코드를 추가합니다:
cppCopySetLifeSpan(10);

에디터 내의 뷰포트로 사용자 정의 액터의 복사본을 드래그합니다.
레벨을 실행하고 Outliner를 보고 액터 인스턴스가 10초 후에 스스로 파괴되어 사라지는지 확인합니다.

작동 방식

게임이 시작될 때 실행되도록 BeginPlay 함수에 코드를 삽입합니다.
SetLifeSpan(10);: SetLifeSpan 함수를 사용하면 초 단위로 기간을 지정할 수 있으며, 이 기간이 지나면 액터가 자체적으로 Destroy() 메서드를 호출합니다.

구성을 통해 액터 기능 구현하기
컴포넌트가 없는 사용자 정의 액터는 위치가 없으며 다른 액터에 부착될 수 없습니다. 루트 컴포넌트가 없으면 액터는 기본 변환을 가지지 않으므로 위치가 없습니다. 따라서 대부분의 액터는 유용하기 위해 최소한 하나의 컴포넌트가 필요합니다.
구성을 통해 사용자 정의 액터를 만들 수 있습니다. 즉, 필요한 기능의 일부를 제공하는 여러 컴포넌트를 액터에 추가합니다.
준비하기
이 레시피는 "C++에서 사용자 정의 액터 만들기" 레시피에서 만든 Actor 클래스를 사용합니다.
방법

C++에서 사용자 정의 클래스에 새 멤버를 추가하여 다음과 같이 변경합니다:
cppCopyUPROPERTY()
UStaticMeshComponent* Mesh;

cpp 파일 내의 생성자 안에 다음 줄을 추가합니다:
cppCopyMesh = CreateDefaultSubobject("BaseMeshComponent");

코드가 다음 스니펫과 같은지 확인하고 에디터에서 Compile 버튼을 사용하거나 Visual Studio에서 프로젝트를 빌드하여 컴파일합니다:
cppCopyUCLASS()
class UE4COOKBOOK_API AMyFirstActor : public AActor
{
GENERATED_BODY()

public:
AMyFirstActor();

UPROPERTY()
UStaticMeshComponent* Mesh;

};

#include "UE4Cookbook.h"
#include "MyFirstActor.h"

AMyFirstActor::AMyFirstActor()
{
PrimaryActorTick.bCanEverTick = true;
Mesh = CreateDefaultSubobject("BaseMeshComponent");
}

이 코드를 컴파일한 후 클래스의 인스턴스를 콘텐츠 브라우저에서 게임 환경으로 드래그하면 이제 변환과 Static Mesh와 같은 다른 속성이 있음을 확인할 수 있습니다. 이는 우리가 추가한 StaticMeshComponent에서 비롯됩니다.

작동 방식

클래스 선언에 UPROPERTY 매크로를 추가하여 서브오브젝트로 사용할 컴포넌트에 대한 포인터를 보유합니다.
cppCopyUPROPERTY()
UStaticMeshComponent* Mesh;

UPROPERTY() 매크로를 사용하면 포인터로 선언된 객체가 참조된 것으로 간주되어 가비지 컬렉션되지 않고(즉, 삭제되지 않고) 포인터가 댕글링 상태가 되지 않습니다.
Static Mesh 컴포넌트를 사용하고 있지만, 액터 컴포넌트의 하위 클래스라면 어느 것이든 작동할 것입니다. Epic의 스타일 가이드에 따라 변수 유형에 별표가 연결되어 있음을 주목하세요.
생성자에서 템플릿 함수인 CreateDefaultSubobject(FName SubobjectName, bool bTransient = false)를 사용하여 포인터를 알려진 유효한 값으로 초기화합니다.
이 함수는 컴포넌트를 적절하게 초기화하기 위해 엔진 코드를 호출하고 새로 구성된 객체에 대한 포인터를 반환하여 우리 컴포넌트 포인터에 기본값을 제공할 수 있습니다. 이는 포인터가 항상 유효한 값을 가지도록 하는 것이 중요하며, 초기화되지 않은 메모리를 역참조하는 위험을 최소화합니다.
이 함수는 생성할 객체 유형을 기반으로 템플릿되지만, 두 개의 매개변수도 받습니다. 첫 번째는 서브오브젝트의 이름으로, 이상적으로는 사람이 읽을 수 있어야 하며, 두 번째는 객체가 일시적인지 여부입니다(즉, 부모 객체와 함께 저장되지 않는지).

참고
다음 레시 CopyRetryClaude’s response was limited as it hit the maximum length allowed at this time. Claude does not have the ability to run the code it generates yet.Q계속 Edit피는 Static Mesh 컴포넌트에 메시 에셋을 참조하여 에디터에서 사용자가 메시를 지정하지 않고도 표시할 수 있도록 하는 방법을 보여줍니다.
FObjectFinder를 사용하여 컴포넌트에 에셋 로딩하기
마지막 레시피에서 우리는 Static Mesh 컴포넌트를 만들었지만, 컴포넌트가 표시할 메시를 로드하려고 하지는 않았습니다. 에디터에서 이 작업을 수행할 수 있지만, 때로는 C++에서 기본값을 지정하는 것이 도움이 될 수 있습니다.
준비하기
이전 레시피를 따라 Static Mesh 컴포넌트가 있는 사용자 정의 액터 하위 클래스를 준비하세요.
콘텐츠 브라우저에서 View Options 버튼을 클릭하고 Show Engine Content를 선택합니다. Engine Content로 이동한 다음 BasicShapes로 이동하여 이 레시피에서 사용할 Cube를 찾습니다.
방법

클래스의 생성자에 다음 코드를 추가합니다:
cppCopyauto MeshAsset = ConstructorHelpers::FObjectFinder(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
if (MeshAsset.Object != nullptr)
{
Mesh->SetStaticMesh(MeshAsset.Object);
}

컴파일하고 에디터에서 클래스의 인스턴스가 이제 시각적 표현으로 메시를 가지고 있는지 확인합니다.

작동 방식

FObjectFinder 클래스의 인스턴스를 만들고, 로드하려는 에셋 유형을 템플릿 매개변수로 전달합니다.
FObjectFinder는 에셋을 로드하는 데 도움이 되는 클래스 템플릿입니다. 생성할 때 로드하려는 에셋의 경로가 포함된 문자열을 전달합니다.
문자열의 형식은 "{ObjectType}'/Path/To/Asset.Asset'"입니다. 문자열에 작은따옴표를 사용하는 것에 주의하세요.
에디터에 이미 존재하는 에셋에 대한 문자열을 얻으려면 콘텐츠 브라우저에서 에셋을 마우스 오른쪽 버튼으로 클릭하고 Copy Reference를 선택하면 됩니다. 이렇게 하면 코드에 붙여넣을 수 있는 문자열이 제공됩니다.
C++11의 auto 키워드를 사용하여 선언에서 전체 객체 유형을 입력하지 않도록 합니다; 컴파일러가 우리를 위해 유추합니다. auto 없이는 다음과 같은 코드를 대신 사용해야 합니다:
cppCopyConstructorHelpers::FObjectFinder MeshAsset = ConstructorHelpers::FObjectFinder(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));

FObjectFinder 클래스에는 원하는 에셋에 대한 포인터를 가지고 있거나 에셋을 찾을 수 없는 경우 NULL이 될 Object라는 속성이 있습니다.
이는 nullptr와 비교하여 확인할 수 있으며, null이 아닌 경우 SetStaticMesh를 사용하여 Mesh에 할당할 수 있습니다.

참고

액터와 컴포넌트에 대한 자세한 논의는 5장 "이벤트와 델리게이트 처리"를 참조하세요. 이 장에서는 NotifyActorOverlap과 같은 이벤트에 대해 설명합니다.

상속을 통해 액터 기능 구현하기
상속은 사용자 정의 액터를 구현하는 두 번째 방법입니다. 이는 일반적으로 기존 액터 클래스에 멤버 변수, 함수 또는 컴포넌트를 추가하여 새 하위 클래스를 만들 때 사용됩니다. 이 레시피에서는 사용자 정의 GameState 하위 클래스에 변수를 추가합니다.
방법

언리얼 에디터에서 콘텐츠 브라우저의 Add New를 클릭한 다음 New C++ Class...를 클릭하고 GameState를 기본 클래스로 선택한 다음 새 클래스의 이름을 지정합니다.
새 클래스 헤더에 다음 코드를 추가합니다:
cppCopyAMyGameState();

UFUNCTION()
void SetScore(int32 NewScore);

UFUNCTION()
int32 GetScore();

private:
UPROPERTY()
int32 CurrentScore;

cpp 파일에 다음 코드를 추가합니다:
cppCopyAMyGameState::AMyGameState()
{
CurrentScore = 0;
}

int32 AMyGameState::GetScore()
{
return CurrentScore;
}

void AMyGameState::SetScore(int32 NewScore)
{
CurrentScore = NewScore;
}

코드가 다음 목록과 같은지 확인하고 언리얼 에디터에서 Compile 버튼을 사용하여 컴파일합니다:
MyGameState.h
cppCopy#pragma once
#include "GameFramework/GameState.h"
#include "MyGameState.generated.h"

/**

  • */
    UCLASS()
    class UE4COOKBOOK_API AMyGameState : public AGameState
    {
    GENERATED_BODY()

public:
AMyGameState();

UPROPERTY()
int32 CurrentScore;

UFUNCTION()
int32 GetScore();

UFUNCTION()
void SetScore(uint32 NewScore);

};
MyGameState.cpp
cppCopy#include "UE4Cookbook.h"
#include "MyGameState.h"

AMyGameState::AMyGameState()
{
CurrentScore = 0;
}

int32 AMyGameState::GetScore()
{
return CurrentScore;
}

void AMyGameState::SetScore(uint32 NewScore)
{
CurrentScore = NewScore;
}

작동 방식

먼저 기본 생성자의 선언을 추가합니다:
cppCopyAMyGameState();

이를 통해 객체 초기화 시 새 멤버 변수를 안전한 기본값 0으로 설정할 수 있습니다:
cppCopyAMyGameState::AMyGameState()
{
CurrentScore = 0;
}

새 변수를 선언할 때 int32 타입을 사용합니다. 이는 언리얼 엔진이 지원하는 다양한 컴파일러 간의 이식성을 보장하기 위함입니다. 이 변수는 게임 실행 중 현재 게임 점수를 저장하는 역할을 합니다. 항상 그렇듯이 UPROPERTY로 변수를 표시하여 적절히 가비지 컬렉션되도록 합니다. 이 변수는 private로 표시되어 있어 함수를 통해서만 값을 변경할 수 있습니다:
cppCopyUPROPERTY()
int32 CurrentScore;

GetScore 함수는 현재 점수를 검색하여 호출자에게 반환합니다. 단순한 접근자로 구현되어 기본 멤버 변수를 반환합니다.
두 번째 함수인 SetScore는 멤버 변수의 값을 변경하여 외부 객체가 점수 변경을 요청할 수 있도록 합니다. 점수 설정 요청을 함수로 배치하면 GameState가 그러한 요청을 검증하여 부정 행위를 방지할 수 있습니다. 이러한 검사의 세부 사항은 이 레시피의 범위를 벗어나지만 SetScore 함수는 그러한 검사를 수행하기에 적절한 위치입니다.
우리의 점수 함수는 여러 가지 이유로 UFUNCTION 매크로를 사용하여 선언됩니다. 첫째, UFUNCTION은 추가 코드를 통해 블루프린트에서 호출하거나 재정의할 수 있습니다. 둘째, UFUNCTION은 exec로 표시될 수 있습니다. 이는 플레이어나 개발자가 플레이 세션 중에 콘솔 명령으로 실행할 수 있음을 의미하며, 디버깅을 가능하게 합니다.

참고

8장 "C++와 언리얼 에디터 통합"에는 exec와 콘솔 명령 기능에 대한 자세한 정보를 제공하는 "새 콘솔 명령 만들기" 레시피가 있습니다.

계층 구조를 만들기 위해 컴포넌트 부착하기
컴포넌트로 사용자 정의 액터를 만들 때 부착(Attaching)의 개념을 고려하는 것이 중요합니다. 컴포넌트를 함께 부착하면 부모 컴포넌트에 적용된 변환이 부착된 컴포넌트에도 영향을 미치는 관계가 생성됩니다.
방법

에디터를 사용하여 Actor를 기반으로 하는 HierarchyActor라는 새 클래스를 만듭니다.
새 클래스에 다음 속성을 추가합니다:
cppCopyUPROPERTY()
USceneComponent* Root;

UPROPERTY()
USceneComponent* ChildSceneComponent;

UPROPERTY()
UStaticMeshComponent* BoxOne;

UPROPERTY()
UStaticMeshComponent* BoxTwo;

클래스 생성자에 다음 코드를 추가합니다:
cppCopyRoot = CreateDefaultSubobject("Root");
ChildSceneComponent = CreateDefaultSubobject("ChildSceneComponent");
BoxOne = CreateDefaultSubobject("BoxOne");
BoxTwo = CreateDefaultSubobject("BoxTwo");

auto MeshAsset = ConstructorHelpers::FObjectFinder(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
if (MeshAsset.Object != nullptr)
{
BoxOne->SetStaticMesh(MeshAsset.Object);
BoxTwo->SetStaticMesh(MeshAsset.Object);
}

RootComponent = Root;
BoxOne->AttachTo(Root);
BoxTwo->AttachTo(ChildSceneComponent);
ChildSceneComponent->AttachTo(Root);
ChildSceneComponent->SetRelativeTransform(FTransform(FRotator(0, 0, 0), FVector(250, 0, 0), FVector(0.1f)));

코드가 다음과 같은지 확인합니다:
HierarchyActor.h
cppCopy#pragma once
#include "GameFramework/Actor.h"
#include "HierarchyActor.generated.h"

UCLASS()
class UE4COOKBOOK_API AHierarchyActor : public AActor
{
GENERATED_BODY()

public:
AHierarchyActor();
virtual void BeginPlay() override;
virtual void Tick( float DeltaSeconds ) override;

UPROPERTY()
USceneComponent* Root;

UPROPERTY()
USceneComponent* ChildSceneComponent;

UPROPERTY()
UStaticMeshComponent* BoxOne;

UPROPERTY()
UStaticMeshComponent* BoxTwo;

};
HierarchyActor.cpp
cppCopy#include "UE4Cookbook.h"
#include "HierarchyActor.h"

AHierarchyActor::AHierarchyActor()
{
PrimaryActorTick.bCanEverTick = true;
Root = CreateDefaultSubobject("Root");
ChildSceneComponent = CreateDefaultSubobject("ChildSceneComponent");
BoxOne = CreateDefaultSubobject("BoxOne");
BoxTwo = CreateDefaultSubobject("BoxTwo");

auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
if (MeshAsset.Object != nullptr)
{
    BoxOne->SetStaticMesh(MeshAsset.Object);
    BoxOne->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);
    BoxTwo->SetStaticMesh(MeshAsset.Object);
    BoxTwo->Set

CopyRetryClaude’s response was limited as it hit the maximum length allowed at this time. Claude does not have the ability to run the code it generates yet.Q계속 EditCollisionProfileName(UCollisionProfile::Pawn_ProfileName);
}
Copy RootComponent = Root;
BoxOne->AttachTo(Root);
BoxTwo->AttachTo(ChildSceneComponent);
ChildSceneComponent->AttachTo(Root);
ChildSceneComponent->SetRelativeTransform(FTransform(FRotator(0, 0, 0), FVector(250, 0, 0), FVector(0.1f)));
}
void AHierarchyActor::BeginPlay()
{
Super::BeginPlay();
}
void AHierarchyActor::Tick( float DeltaTime )
{
Super::Tick( DeltaTime );
}
Copy
5. 코드를 컴파일하고 에디터를 실행합니다. HierarchyActor의 복사본을 씬으로 드래그합니다.

  1. 액터에 계층 구조로 된 컴포넌트가 있고 두 번째 상자의 크기가 더 작은지 확인합니다.

작동 방식

  1. 평소와 마찬가지로 액터에 대해 UPROPERTY로 태그가 지정된 몇 가지 컴포넌트를 생성합니다. 두 개의 Scene 컴포넌트와 두 개의 Static Mesh 컴포넌트를 만듭니다.

  2. 생성자에서 평소와 같이 각 컴포넌트에 대한 기본 서브오브젝트를 생성합니다.

  3. 그런 다음 정적 메시를 로드하고, 로드가 성공하면 두 정적 메시 컴포넌트에 할당하여 시각적 표현을 갖게 합니다.

  4. 그 다음 액터 내에서 컴포넌트를 부착하여 계층 구조를 구성합니다.

  5. 첫 번째 Scene 컴포넌트를 액터의 루트로 설정합니다. 이 컴포넌트는 계층 구조의 다른 모든 컴포넌트에 적용되는 변환을 결정합니다.

  6. 그런 다음 첫 번째 상자를 새 루트 컴포넌트에 부착하고, 두 번째 씬 컴포넌트를 첫 번째 컴포넌트의 자식으로 만듭니다.

  7. 두 번째 상자를 자식 씬 컴포넌트에 부착하여 해당 씬 컴포넌트의 변환이 자식에 영향을 미치지만 다른 객체의 컴포넌트에는 영향을 미치지 않음을 보여줍니다.

  8. 마지막으로 해당 씬 컴포넌트의 상대 변환을 설정하여 원점에서 일정 거리 떨어진 곳으로 이동하고 크기를 10분의 1로 축소합니다.

  9. 이는 에디터에서 BoxTwo 컴포넌트가 부모 컴포넌트인 ChildSceneComponent의 이동 및 스케일링을 상속받았음을 볼 수 있음을 의미합니다.

사용자 정의 액터 컴포넌트 만들기

액터 컴포넌트는 액터 간에 공유되어야 하는 공통 기능을 구현하는 쉬운 방법입니다. 액터 컴포넌트는 렌더링되지 않지만 이벤트 구독이나 액터 내의 다른 컴포넌트와의 통신과 같은 작업을 수행할 수 있습니다.

방법

  1. 에디터 마법사를 사용하여 RandomMovementComponent라는 이름의 ActorComponent를 만듭니다. UCLASS 매크로에 다음 클래스 지정자를 추가합니다:
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )

클래스 헤더에 다음 UPROPERTY를 추가합니다:
cppCopyUPROPERTY()
float MovementRadius;

생성자의 구현에 다음을 추가합니다:
cppCopyMovementRadius = 0;

마지막으로 TickComponent()의 구현에 다음을 추가합니다:
cppCopyAActor* Parent = GetOwner();
if (Parent)
{
    Parent->SetActorLocation(
        Parent->GetActorLocation() +
        FVector(
            FMath::FRandRange(-1, 1) * MovementRadius,
            FMath::FRandRange(-1, 1) * MovementRadius,
            FMath::FRandRange(-1, 1) * MovementRadius
        )
    );
}

코드가 다음과 같은지 확인합니다:
cppCopy#pragma once
#include "Components/ActorComponent.h"
#include "RandomMovementComponent.generated.h"

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class UE4COOKBOOK_API URandomMovementComponent : public UActorComponent
{
    GENERATED_BODY()

public:
    URandomMovementComponent();
    virtual void BeginPlay() override;
    virtual void TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction ) override;

    UPROPERTY()
    float MovementRadius;
};

#include "UE4Cookbook.h"
#include "RandomMovementComponent.h"

URandomMovementComponent::URandomMovementComponent()
{
    bWantsBeginPlay = true;
    PrimaryComponentTick.bCanEverTick = true;
    MovementRadius = 5;
}

void URandomMovementComponent::BeginPlay()
{
    Super::BeginPlay();
}

void URandomMovementComponent::TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction )
{
    Super::TickComponent( DeltaTime, TickType, ThisTickFunction );
    AActor* Parent = GetOwner();
    if (Parent)
    {
        Parent->SetActorLocation(
            Parent->GetActorLocation() +
            FVector(
                FMath::FRandRange(-1, 1) * MovementRadius,
                FMath::FRandRange(-1, 1) * MovementRadius,
                FMath::FRandRange(-1, 1) * MovementRadius
            )
        );
    }
}

프로젝트를 컴파일합니다. 에디터에서 빈 액터를 만들고 Random Movement Component를 추가합니다. 이렇게 하려면 Empty Actor를 Placement 탭에서 레벨로 드래그한 다음 Details 패널에서 Add Component를 클릭하고 Random Movement를 선택합니다. 액터의 위치를 시각화할 수 있도록 Cube Component도 같은 방식으로 추가합니다.
레벨을 실행하고 TickComponent 함수가 호출될 때마다 액터가 무작위로 이동하는 것을 관찰합니다.

작동 방식

먼저 컴포넌트의 선언에 사용된 UCLASS 매크로에 몇 가지 지정자를 추가합니다. BlueprintSpawnableComponent를 클래스의 메타 값에 추가하면 에디터의 블루프린트 클래스에 컴포넌트의 인스턴스를 추가할 수 있습니다. ClassGroup 지정자를 사용하면 클래스 목록에서 컴포넌트가 속한 클래스 카테고리를 지정할 수 있습니다:
cppCopyUCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )

MovementRadius를 새 컴포넌트의 속성으로 추가하면 컴포넌트가 단일 프레임에서 이동할 수 있는 거리를 지정할 수 있습니다:
cppCopyUPROPERTY()
float MovementRadius;

생성자에서 이 속성을 안전한 기본값으로 초기화합니다:
cppCopyMovementRadius = 5;

TickComponent는 액터의 Tick과 마찬가지로 엔진에 의해 매 프레임마다 호출되는 함수입니다. 구현에서는 컴포넌트의 소유자, 즉 컴포넌트를 포함하는 액터의 현재 위치를 검색하고 월드 공간에서 오프셋을 생성합니다:
cppCopyAActor* Parent = GetOwner();
if (Parent)
{
    Parent->SetActorLocation(
        Parent->GetActorLocation() +
        FVector(
            FMath::FRandRange(-1, 1) * MovementRadius,
            FMath::FRandRange(-1, 1) * MovementRadius,
            FMath::FRandRange(-1, 1) * MovementRadius
        )
    );
}

현재 위치에 무작위 오프셋을 더하여 새 위치를 결정하고 소유 액터를 그 위치로 이동시킵니다. 이로 인해 액터의 위치가 프레임마다 무작위로 변경되어 춤을 추는 것처럼 보입니다.

사용자 정의 씬 컴포넌트 만들기
씬 컴포넌트는 액터 컴포넌트의 하위 클래스로, 변환(상대적 위치, 회전 및 스케일)을 가집니다. 액터 컴포넌트와 마찬가지로 씬 컴포넌트는 자체적으로 렌더링되지 않지만 변환을 사용하여 액터로부터 고정된 오프셋에서 다른 객체를 생성하는 등의 다양한 작업을 수행할 수 있습니다.
방법

ActorSpawnerComponent라는 사용자 정의 SceneComponent를 만듭니다. 헤더에 다음과 같은 변경을 합니다:
cppCopyUFUNCTION()
void Spawn();

UPROPERTY()
TSubclassOf<AActor> ActorToSpawn;

cpp 파일에 다음 함수 구현을 추가합니다:
cppCopyvoid UActorSpawnerComponent::Spawn()
{
    UWorld* TheWorld = GetWorld();
    if (TheWorld != nullptr)
    {
        FTransform ComponentTransform(this->GetComponentTransform());
        TheWorld->SpawnActor(ActorToSpawn, &ComponentTransform);
    }
}

코드를 다음 스니펫과 비교하여 확인합니다:
ActorSpawnerComponent.h
cppCopy#pragma once
#include "Components/SceneComponent.h"
#include "ActorSpawnerComponent.generated.h"

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class UE4COOKBOOK_API UActorSpawnerComponent : public USceneComponent
{
    GENERATED_BODY()

public:
    UActorSpawnerComponent();
    virtual void BeginPlay() override;
    virtual void TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction ) override;

    UFUNCTION(BlueprintCallable, Category=Cookbook)
    void Spawn();

    UPROPERTY(EditAnywhere)
    TSubclassOf<AActor> ActorToSpawn;
};

ActorSpawnerComponent.cpp
#include "UE4Cookbook.h"
#include "ActorSpawnerComponent.h"

UActorSpawnerComponent::UActorSpawnerComponent()
{
    bWantsBeginPlay = true;
    PrimaryComponentTick.bCanEverTick = true;
}

void UActorSpawnerComponent::BeginPlay()
{
    Super::BeginPlay();
}

void UActorSpawnerComponent::TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction )
{
    Super::TickComponent( DeltaTime, TickType, ThisTickFunction );
}

void UActorSpawnerComponent::Spawn()
{
    UWorld* TheWorld = GetWorld();
    if (TheWorld != nullptr)
    {
        FTransform ComponentTransform(this->GetComponentTransform());
        TheWorld->SpawnActor(ActorToSpawn, &ComponentTransform);
    }
}

컴파일하고 프로젝트를 엽니다. 씬에 빈 액터를 드래그하고 ActorSpawnerComponent를 추가합니다. Details 패널에서 새 컴포넌트를 선택하고 ActorToSpawn에 값을 할당합니다. 이제 컴포넌트의 인스턴스에서 Spawn()이 호출될 때마다 ActorToSpawn에 지정된 액터 클래스의 복사본을 인스턴스화합니다.

작동 방식

Spawn UFUNCTION과 ActorToSpawn이라는 변수를 만듭니다. ActorToSpawn UPROPERTY는 TSubclassOf< > 유형입니다. TSubclassOf< >는 기본 클래스 또는 그 하위 클래스로 포인터를 제한할 수 있는 템플릿 유형입니다. 이는 또한 에디터 내에서 선택할 수 있는 사전 필터링된 클
profile
취미로 개발

0개의 댓글