[Unreal] 02.C++ 스크립트로 변환
본 문서는 어소트락 언리얼엔진 게임프로그래머 양성과정의 강의를 토대로 필기한 내용입니다
C++로 스크립트 생성
Player
- 새로운 C++ 파일을 생성
- 헤더 파일(.h)과 실행 파일(.cpp)이 생성됨
- 헤더 파일 : 클래스의 선언부
- 실행 파일 : 클래스의 구현부
#include "PlayerCharacter.generated.h": 자동으로 생성되는 헤더, 가장 아래에 있어야 한다
PlayerCharacter.h
#pragma once
// ../ 는 이전폴더를 의미한다.
#include "../GameInfo.h"
#include "GameFramework/Character.h"
#include "PlayerCharacter.generated.h"
UCLASS()
class UE20241_API APlayerCharacter : public ACharacter
{
GENERATED_BODY()
public:
APlayerCharacter(); // 생성자
protected:
// USpringArmComponent* mArm;
// 언리얼 UObject용 포인터 선언
UPROPERTY(VisibleAnywhere)
TObjectPtr<USpringArmComponent> mArm;
UPROPERTY(VisibleAnywhere)
TObjectPtr<UCameraComponent> mCamera;
protected:
virtual void BeginPlay() override; // 게임 시작 시 실행
public:
virtual void Tick(float DeltaTime) override; // 매 프레임 호출
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; // 충돌처리 (사용X)
};
- UPROPERTY : Unreal Property
- 언리얼 리플렉션 시스템에 해당 프로퍼티가 있음을 알림
- 디테일 탭에 나타낼 수 있음
UPROPERTY(VisibleAnywhere)
PlayerCharacter.cpp
#include "PlayerCharacter.h" // 생성한 헤더파일
APlayerCharacter::APlayerCharacter()
{
PrimaryActorTick.bCanEverTick = true;
// CreateDefaultSubobject 함수는 생성자에서만 사용한다
// 템플릿에 지정된 타입의 객체 하나를 생성하고 그 메모리 주소를 반환해준다.
mArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("Arm"));
mCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
mArm->SetupAttachment(RootComponent); // SpringArm은 RootComponent의 Child로 붙여줌
mCamera->SetupAttachment(mArm); // Camera는 SpringArm의 Child로 붙여줌
mArm->TargetArmLength = 500.f;
mArm->SetRelativeLocation(FVector(0.0, 0.0, 70.0));
mArm->SetRelativeRotation(FRotator(-10.0, 0.0, 0.0)); // Pitch(Y회전), Yaw(z회전), Roll(X회전)
}
// 게임 시작 혹은 스폰될 때 호출
void APlayerCharacter::BeginPlay()
{
Super::BeginPlay();
}
// 매 프레임 호출
void APlayerCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
// 기능을 입력에 바인딩할 때 호출
void APlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
}
- CreateDefaultSubobject
- UObject를 만들어 내는 함수
- 위 함수는 생성자에서만 사용한다
- 위 함수는 템플릿에 지정된 타입의 객체 하나를 생성하고 그 메모리 주소를 반환해 줌
mArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("Arm"));으로 헤더에서 선언한 mArm 포인터변수에 만든 객체 할당
게임 모드
- 게임 모드에서 BP_TestPawn을 디폴트 폰 클래스 값에 넣어 놨기 때문에 BP로 만든 Pawn이 컨트롤러에 빙의됨
- 게임할 때 필요한 공용 데이터를 따로 추가해 놓을 수 있다
GameInfo.h
- UE에서 만든 것이 아닌 VS에서 만든 파일은 UE상에 나타나지 않는다
- 상대 경로를 이용해 헤더파일 접근 가능
#pragma once
#include "EngineMinimal.h"
GameMode.h
#pragma once
#include "../GameInfo.h" // ../는 이전폴더를 의미한다
#include "GameFramework/GameModeBase.h"
#include "DefaultGameMode.generated.h"
UCLASS()
class UE20241_API ADefaultGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
ADefaultGameMode();
};
GameMode.cpp
#include "DefaultGameMode.h"
#include "../Player/PlayerCharacter.h"
ADefaultGameMode::ADefaultGameMode()
{
// 모든 언리얼 클래스들은 클래스이름::StaticClass() 함수를 이용해서 어떤 타입인지를 반환하게 만들어두었다.
DefaultPawnClass = APlayerCharacter::StaticClass();
}
입력
[ProjectName].Build.cs
프로젝트에 필요한 모듈이 담기는 파일
InputData.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "../GameInfo.h"
#include "InputMappingContext.h"
#include "InputAction.h"
#include "UObject/NoExportTypes.h"
#include "InputData.generated.h"
/**
*
*/
UCLASS()
class UE20241_API UDefaultInputData : public UObject
{
GENERATED_BODY()
public :
UDefaultInputData();
public:
UInputMappingContext* mDefaultContext = nullptr;
public:
UInputAction* mMoveFB = nullptr;
UInputAction* mMoveLR = nullptr;
UInputAction* mAttack = nullptr; // InputAction은 헤더안에서 이미 만들어져 있고 포인터 변수를 로딩만 해주면 된다 어떻게?
};
InputData.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "InputData.h"
UDefaultInputData::UDefaultInputData()
{
// 생성자에서 에셋을 로드하는 방법
// ConstructorHelpers : 오로지 생성자에서만 사용할 수 있는 구조체
ConstructorHelpers::FObjectFinder<UInputMappingContext>Context(TEXT("/Script/EnhancedInput.InputMappingContext'/Game/Test/Input/IMC_Test.IMC_Test'"));
// 참조를 성공했을 경우 Context.Object를 미리 만든 mDefaultContext에 할당
if (Context.Succeeded()) mDefaultContext = Context.Object;
ConstructorHelpers::FObjectFinder<UInputAction>MoveFB(TEXT("/Script/EnhancedInput.InputAction'/Game/Test/Input/IA_Move_FB.IA_Move_FB'"));
if (MoveFB.Succeeded()) mMoveFB = MoveFB.Object;
ConstructorHelpers::FObjectFinder<UInputAction>MoveLR(TEXT("/Script/EnhancedInput.InputAction'/Game/Test/Input/IA_Move_LR.IA_Move_LR'"));
if (MoveLR.Succeeded()) mMoveLR = MoveLR.Object;
ConstructorHelpers::FObjectFinder<UInputAction>Attack(TEXT("/Script/EnhancedInput.InputAction'/Game/Test/Input/IA_Attack.IA_Attack'"));
if (Attack.Succeeded()) mAttack = Attack.Object;
}
PlayerCharacter.cpp
- BeginPlay() 시 APlayerController
#include "EnhancedInputSubsystems.h"
#include "EnhancedInputComponent.h"
// Called when the game starts or when spawned
void APlayerCharacter::BeginPlay()
{
Super::BeginPlay();
// GetController로 컨트롤러를 찾고 PlayerController로 형 변환
// PlayerController로 부터 LocalPlayer를 얻어옴
// 미리 PlayerController를 얻어 두고, nullptr이 아니면(찾음) LocalPlayer를 얻어옴
APlayerController* PlayerController = GetController<APlayerController>();
if (nullptr != PlayerController)
{
ULocalPlayer* LocalPlayer = PlayerController->GetLocalPlayer();
// LocalPlayer를 이용해서 EnhancedInputLocalPlayerSubsystem을 얻어옴
UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(LocalPlayer);
// UDefaultInputData의 CDO를 꺼내온다
const UDefaultInputData* InputData = GetDefault<UDefaultInputData>();
Subsystem->AddMappingContext(InputData->mDefaultContext, 0);
}
}
// Called to bind functionality to input
void APlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// Cast : 언리얼 UObject 객체들의 형변환 함수
UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent);
// UDefaultInputData의 CDO를 꺼내온다
const UDefaultInputData* InputData = GetDefault<UDefaultInputData>();
// 원하는 InputAction이 동작할 때 호출될 함수의 함수 포인터를 지정
EnhancedInput->BindAction(InputData->mMoveFB, ETriggerEvent::Triggered, this, &APlayerCharacter::OnMoveFB);
}
void APlayerCharacter::OnMoveFB(const FInputActionVaule& InputValue)
{
const FVector ActionValue = InputValue.Get<FVector>();
float MoveDir = ActionValue.X + ActionValue.Y;
AddMovementInput(GetActorForwardVector(), MoveDir);
GEngine->AddOnScreenDebugMessage(-1, 20.f,
FColor::Red,
FString::Printf(TEXT("x : %.5f y : %.5f"), ActionValue.X, ActionValue.Y)
);
}
UE Reflection
Java나 C#의 리플렉션 기능을 C++ 표준 문법에서는 제공하지 않으므로 언리얼 엔진이 자체적으로 프레임워크를 만들어 제공
- UE Object 생성과정 : 실제 컴파일 전에 언리얼 헤더 툴에 의해 헤더 파일을 분석하는 과정이 선행되며, 이 과정이 완료되면 Intermediate 폴더에 언리얼 오브젝트의 정보를 담은 메타 파일이 생성됨
- 언리얼 엔진이 컴파일 전에 먼저 메타 소스 파일과 헤더 파일을 생성하는 목적은 여러가지가 있겠지만, 기존의 C++ 문법에서 제공하지 못하는 런타임에서의 빠른 클래스 정보의 검색이 있음
- 이 메타 정보는 언리얼 엔진이 지정한
UClass라는 특별한 클래스를 통해 보관됨 - UClass에는 언리얼 오브젝트에 대한 클래스 계층 구조 정보와 멤버 변수, 함수에 대한 정보를 모두 기록함. 하지만 단순히 검색하는 것에서 더 나아가, 런타임에서 특정 클래스를 검색해 형(Type)을 알아내 인스턴스의 멤버 변수 값을 변경하거나 특정 인스턴스의 멤버 함수를 호출하는 것이 가능.
- Java나 C#과 같은 C++ 다음 세대의 언어에서는 이와 유사한 기능을 리플렉션(Reflection)이라는 이름으로 제공
CDO (Class Default Object)
클래스의 기본 객체 : 컴파일 단계에서 언리얼 오브젝트마다 UClass가 생성된다면, 실행 초기의 런타임 과정에서는 언리얼 오브젝트마다 클래스 정보와 함께 언리얼 오브젝트의 인스턴스가 생성됨
언리얼 오브젝트의 기본 세팅을 지정하는데 사용됨
언리얼 엔진에서 CDO를 만드는 이유
- 언리얼 오브젝트를 생성할 때마다 매번 초기화 시키지 않고, 기본 인스턴스를 미리 만들어 놓고 복제하는 방식으로 메커니즘이 구성되어 있기 때문
- 하나의 언리얼 오브젝트가, 예를 들어 복잡한 기능을 수행하는 캐릭터까지 담당할 정도로 기능이 확장되면, 굉장히 큰 덩어리의 객체로 커질 수 있다. 만일 게임 실행 중, 런타임에서 이 캐릭터를 한번에 100명을 스폰시킨다고 가정했을 때, 캐릭터를 하나씩 처음부터 생성하고 초기화시키는 방법보다, 미리 큰 기본 객체 덩어리를 복제한 후에 속성 값만 변경하는 방법이 보다 효과적
- 정리하자면 하나의 언리얼 오브젝트가 초기화 될 때에는 두 개의 인스턴스가 항상 생성됨.
- 언리얼 오브젝트의 생성자는 인스턴스를 초기화해 CDO를 제작하기 위한 목적으로 사용됨
- 이 생성자 코드는 초기화에서만 실행되고 실제 게임 플레이에서 생성자 코드는 사용할 일이 없다고 보면 됨
참고로 언리얼 엔진에서 게임 플레이에서 사용할 초기화 함수는 생성자 대신 Init 이나 혹은 BeginPlay 함수를 제공
GetDefault(): const * 타입을 리턴하기 때문에 return 받을 값도 const로 지정
UE 로딩 과정
PlayerCharacter.h
실제 MoveFB 함수를 헤더에 선언 FInputActionValue 타입을
...
#include "InputActionValue.h"
...
UCLASS()
class STUDY_240601_API APlayerCharacter : public ACharacter
{
...
protected:
void OnMoveFB(const FInputActionValue& InputValue);
};