2 분 소요

Unreal/C++ 개인 과제 10 - 언리얼 모듈과 플러그인

Git Page

과제 소개 및 목표

Unreal의 제품은
기본적으로 아래의 구조를 취함

솔루션 - 프로젝트/플러그인 - 모듈 - 소스코드

구현 기능

  • Test 모듈 제작
  • 모듈 연결
  • Temporary 플러그인 제작
  • 플러그인 연결
  • UObject 용 플러그인 추가
  • Character 와 연결하기

트러블 슈팅 1 - TestActor 가 빌드되지 않는 상황

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TestActor.generated.h"

UCLASS()
class TEST_API ATestActor : public AActor
{
	GENERATED_BODY()
	// 왜 에러...??

public:	
	ATestActor();

protected:
	virtual void BeginPlay() override;

};
  • 간단하기 그지 없는 Actor 이나 이상하게 빌드 에러가 발생하였다

  • generated.h 가 존재하지 않는다는 ‘컴파일 에러’였는데
    아무리 생각해도 빌드가 되지 않는것이 이상하였다

알고보니…

"AdditionalDependencies": [
		"Engine",
		"Test"
]
  • .uproject 에서 해당 부분을 추가하지 않아 발생했던 문제였다

  • ‘Test’ 모듈 자체는 존재하나,
    UHT가 ‘Test’ 모듈의 ‘빌드 필요성’을 인지하지 못하여
    ‘빌드’를 하지 않아 ‘Generated.h’가 만들어지지 않은것!
    • “‘Test’ 모듈이 있는건 ok, 근데 이거 빌드할 필요 없잖아?”인 상태
  • 그렇기에 해당 방식으로 의존성을 걸어두게 되면
    그제서야 빌드를 제대로 해준다
    • ‘아 현재 메인 게임 모듈 사용하려면 저 Test 모듈이 필요하구나?’

Build.cs 에서 설정하는 dependency와 비교?

  • Build.cs 에 넣으면
    ‘컴파일’ 할때, 해당 모듈의 헤더와 라이브버리를 사용하겠다는 뜻!
    • 대표적으로 ‘UMG’ 내부의 라이브러리를 사용하려 한다면
      Build.cs 에 넣어주어야 컴파일 에러가 발생하지 않음
  • uproject 에서 설정한 경우
    ‘현재 모듈’ 실행 시, UHT가 같이 처리하도록 세팅하는 방식
    • 이 모듈을 쓸거라면 ‘의존성 등록된’ 모듈들도 미리 ‘준비’해줘
      (해당 모듈들이 ‘로드’되어 있어야 함)

정리 표

구분 위치 누가 읽음? 언제 쓰임? 의미
Build.cs Source/MyModule/MyModule.Build.cs UBT (빌드툴) C++ 컴파일/링크 할 때 “이 모듈이 빌드되려면 어떤 모듈의 헤더/라이브러리가 필요하다”
.uproject / .uplugin 의 AdditionalDependencies MyGame.uproject, MyPlugin.uplugin UHT + 모듈 로더 리플렉션 생성, 에디터/게임에서 모듈 로드할 때 “이 타겟/모듈이 실행/리플렉션 될 때, 어떤 모듈이 반드시 같이 준비되어 있어야 한다”

트러블 슈팅 2 - UObject 타입을 사용할 때, 엔진이 다운되는 현상

APluginTestCharacter::APluginTestCharacter()
{
CharaData = NewObject<UUCharacterData>(this);

CharaData->SetHP(1000);
CharaData->SetAttack(200);
CharaData->SetDefend(100);
}
  • 해당 코드를 넣고, 빌드 완료 후 엔진을 실행하니 크래쉬가 발생하였다!

  • 찾아보니 생성자에서 NewObject를 쓰는 것은 금지된 패턴이라 한다
    (사용시 Engine 쪽에서 Fatal Error 를 발생시킴)

  • 왜 그럴까?

    • CDO(Class Default Object) 떄문에 그렇다!

언리얼의 생명주기 관련

[1] 클래스 로드
    ↓
[2] CDO 생성 (생성자 실행 1회)
    ↓
[3] Actor Spawn 요청
    ↓
[4] CDO 메모리 복제 (MemCopy)
    ↓
[5] 생성자 다시 실행 (인스턴스용)
    ↓
[6] PostInitProperties
    ↓
[7] PostInitializeComponents
    ↓
[8] BeginPlay
  • 기본적으로 인스턴스들이 CDO 기반 복제 -> 생성자 호출 을 통해 제작됨
    • CDO 로 ‘기본 데이터 템플릿’ 을 세팅
    • 생성자를 통해 ‘인스턴스화 된 시점’의 초기화 로직 실행
  • 그런데 생성자에서 NewObject 를 사용하면 안되는 이유?
    • CDO 만들 때 한번, 이후 인스턴스를 만들때마다 호출
    • 그런데 NewObject 생성 시, CDO에 Outer로 붙어버림
    • 그리고, 인스턴스 에 호출된 것은 인스턴스에 Outer
  • CDO에 NewObject 하나 붙는 건데 뭔가 문제 있나??
    • CDO에 붙는 UObject 는 ‘DefaultSubObject’ 취급
      • 규칙 상으로 ‘고유 이름 + 계층 구조’가 필요
      • CDO <-> 인스턴스 간의 ‘복제/매핑’ 룰 유지 필요
    • 이는 ‘생성 순서’ 고정 등 다양한 규칙을 통해 ‘SubObject’들의 안정성을
      NewObject 키워드로는 규칙을 보장하지 못함
      • 생성자에서 사용 금지!
  • NewObject는 런타임에 객체를 생성하는 함수이며,
    이는 Subobject인지, 일시적인 런타임 객체인지 확인할 수 없음

  • 그렇기에 CreateDefaultSubobject<T>()가 생성자에서 강제됨
    • ‘생성 순서’, ‘타입 결정’, ‘이름’ 등의 규칙이 보장되기 때문
    • 다만 Actor/Component 기반이기에, 내가 만든 UObject는 BeginPlay에 넣어서 문제 해결
CreateDefaultSubobject<USceneComponent>(TEXT("Root"));  // ✅

CreateDefaultSubobject<USceneComponent>();              // ❌ 컴파일 불가

해결 코드

APluginTestCharacter::BeginPlay()
{
CharaData = NewObject<UUCharacterData>(this);

CharaData->SetHP(1000);
CharaData->SetAttack(200);
CharaData->SetDefend(100);
}

댓글남기기