UE4 Chapter 2. 액터의 설계

Date:     Updated:

카테고리:

태그:

이득우님의 책 이득우의 언리얼 C++ 게임 개발의 정석 을 공부하고 정리한 필기입니다. 😀
언리얼 엔진 공식 문서 https://docs.unrealengine.com/ko/index.html

Chater 2. 액터의 설계

🔔 언리얼 컨텐츠의 구성 요소

1. 월드

뷰포느 윈도우에 보이는 작업 공간 = 가상 공간 = 월드

  • 구성요소
    1. 공간
      • 월드의 3차원 공간안에 존재하려면 Transform 컴포넌트를 가졎야 하며
      • 공간의 기본 단위는 cm이다.
    2. 시간
      • 월드(가상공간) 안에서 흐른 ㄴ시간
      • 시간 스케일 조절 가능.
    3. 물리
      • 중력같은 물리적 영향을 받으려면 Collision 컴포넌트가 붙어있어야.
    4. 렌더링
      • 시각적인 기능. 현실 세계와 유사한 빛.
      • Physically Based Rendering
  • 세팅 - 월드 세팅
    • 에서 월드의 더 많은 요소를 볼 수 있다.


2. Actor

콘텐츠를 구성하는 자신에게 주어진 역할을 수행하는 최소 단위의 물체(오브젝트)

  • 월드 아웃라이너 윈도우에서 액터들의 목록을 볼 수 있다.
    • 흰 글씨 액터들은 현재 레벨에 플레이 전에 현재 레벨에 이미 존재하고 있는 액터들을 뜻하며
    • 노란 글씨의 액터들은 플레이 도중 새롭게 생겨난 액터들을 뜻한다.
  • 디테일 윈도우에서 선택한 액터의 정보를 볼 수 있다.
  • 액터에 붙어있는 컴포넌트(C++ 클래스, C++ 코드)는 이 액터의 기능 및 역할이 된다.
  • 액터들은 트랜스폼을 가진다.
  • ⭐액터는 내부적으로는 컴포넌트들로 조합되어 이루어져 있지만 하나의 독립된 배우로서 게임 콘텐츠에 기여한다.


3. 레벨

플레이어에게 주어진 스테이지.

  • 월드에 배치된 액터들의 집합.
  • 레벨을 설게하다 = 스테이지를 조립하다.


4. 컴포넌트

액터에게 기능을 부여하려면 컴포넌트를 붙여주면 된다. 언리얼에서 지원하는 컴폰넌트들도 있고 직접 C++ 클래스 코드를 작성하여 사용자 지정 컴포넌트를 붙여줄 수도 있다.

  • 주요 컴포넌트
    • StaticMesh 컴포넌트
      • 애니메이션이 없는 모델링 에셋인 스태틱 메시를 사용하여 시각적, 물리적 기능 제공
      • 게임 제작에 있어 가장 많이 사용 됨.
      • 주로 배경에 사용
    • SkeletalMesh 컴포넌트
      • 애니메이션이 있는 모델링 에셋인 스태틱 메시를 사용하여 시각적, 물리적 기능 제공
      • 플레이어, 적 등등
    • Camera, Collision, Audio, Particle, Light, Movement, … 등등

루트 컴포넌트

  • 언리얼에서는 어떤 액터에 붙어있는 컴포넌트들 중 그 액터에서 대표되는 컴포넌트 1 개를 반드시 지정해야 한다. 이를 루트 컴포넌트 라고 한다.
    • 다른 컴포넌트들을 이 루트 컴포넌트의 자식으로 넣어주어야 한다.


🔔 액터의 설계

언리얼에선 코드를 엔진에 반영하려면 저장하는걸로는 안되고 꼭 빌드 해서 컴파일을 마쳐야 한다.

  • Fountain 액터를 만들어 보자.
    • 분수대는 분수대 구조물로 구성되어 있다.
      • 분수대 구조물 👉 StaticMesh 컴포넌트 (루트 컴포넌트로 지정)
        • 분수대 구조물의 비주얼
        • 충돌 처리
      • 👉 StaticMesh 컴포넌트
        • 물의 비주얼
  • 📜Fountain.h
    • 컴포넌트 선언
  • 📜Fountain.cpp
    • 컴포넌트 구현


📜Fountain.h

#pragma once

#include "ArenaBattle.h"
#include "EngineMinimal.h"
#include "GameFramework/Actor.h"
#include "Fountain.generated.h"

UCLASS()
class ARENABATTLE_API AFountain : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AFountain();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	UPROPERTY()
	UStaticMeshComponent *Body;

	UPROPERTY()
	UStaticMeshComponent *Water;
};

EngineMinimal.h

#include "EngineMinimal.h"
  • CoreMinimal.h
    • 언리얼 오브젝트가 동작할 수 있는 최소한의 기능만 선언된 공용 헤더
  • EngineMinimal.h
    • 언리얼 오브젝트의 다양한 엔진 클래스 선언을 모아둔 공용 헤더
    • 여기선 다양한 엔진 기능을 사용하기 위해 “EngineMinimal.h”를 사용했다.

컴포넌트 선언 및 동적 할당

	UPROPERTY()
	UStaticMeshComponent *Body;

	UPROPERTY()
	UStaticMeshComponent *Water;
  • 컴포넌트 포인터
    • 선언
      • UStaticMeshComponent *Body;
        • 분수대 구조대의 비주얼과 충돌처리 기능을 할 컴포넌트
      • UStaticMeshComponent *Water
        • 의 비주얼 기능을 할 컴포넌트
  • 컴포넌트를 동적 할당 받아 포인터에 주소를 리턴할 것
    • C++ 에서는 힙 메모리로 동적 할당 받은 객체들은 개발자가 직접 delete 시켜주어 메모리 누수를 방지해야 하는데
    • 언리얼에서는 UPROPERTY() 매크로를 포인터 위에 붙여주면 객체가 더 이상 사용되지 않으면 자동으로 메모리를 해제시켜 준다.
      • UObject에서 가비지 컬렉션을 제공하기 때문!
      • 개발자가 직접 delete시킬 필요가 없음
      • 이 매크로는 언리얼 오브젝트 객체에만 사용이 가능하다.

언리얼 오브젝트

언리얼 오브젝트 : 언리얼 환경에 의해 관리되는 C++ 객체. 컴포넌트들도 전부 언리얼 오브젝트다.

  #include "ArenaBattle.h"
  #include "EngineMinimal.h"
  #include "GameFramework/Actor.h"
  #include "Fountain.generated.h" // ⭐

  UCLASS() // ⭐
  class ARENABATTLE_API AFountain : public AActor // ⭐
  {
	GENERATED_BODY()  // ⭐
  • C++ 클래스가 언리얼 오브젝트 클래스가 되려면 지켜야 하는 규칙
    1. 클래스 위에 UCLASS() 매크로, 클래스 내부에 GENERATED_BODY() 매크로 선언해 이 클래스가 언리얼 오브젝트임을 선언한다.
    2. 액터 클래스 이름 앞에는 A를 붙이고, 액터가 아닌 클래스 이름 앞에는 U를 붙여 구분한다. 언리얼 오브젝트와 전혀 상관이 없는 일반 C++ 클래스는 F를 붙인다.
      • AFountain : 액터 클래스
      • AActor : 액터 클래스
      • UStaticMeshComponent : 컴포넌트 클래스
      • FVector : 일반 C++ 벡터 클래스
    3. Fountain.generated.h
      • 코드를 작성할 땐 없지만 언리얼 환경의 컴파일 과정에서 생기는 파일이다. 마지막 #inlcude 구문에서 반드시 선언해주어야 하는 헤더파일이다.
    4. ARENABATTLE_API
      • 클래스 선언 앞에 이 키워드가 있어야 다른 모듈에서 해당 객체에 접근할 수 있다.


📜Fountain.cpp

  • #include “Fountain.h”
  • cpp 파일에서 액터의 구축(생성자), 컴포넌트들의 구현을 담당한다.
// Fill out your copyright notice in the Description page of Project Settings.

#include "Fountain.h"

// Sets default values
AFountain::AFountain()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BODY"));
	Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("WATER"));

    RootComponent = Body;
	Water->SetupAttachment(Body);
}

// Called when the game starts or when spawned
void AFountain::BeginPlay()
{
	Super::BeginPlay();

}

// Called every frame
void AFountain::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

CreateDeaultSubobject 함수

  • 생성자 에서 헤더 파일에서 선언 했었던 컴포넌트들을 동적 할당해 생성해주는데
    • 언리얼에선 new대신 CreateDeaultSubobject 함수를 사용하여 컴포넌트를 생성한다.
Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BODY"));
Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("WATER"));
  • 동적 할당하고 포인터를 리턴
  • UStaticMeshComponent 컴포넌트 생성
  • TEXT(“BODY”)
    • 액터에 속한 컴포넌트들을 구별하기 위한 Hash 값 생성에 사용되는 문자열 값이다.
    • 다른 컴포넌트들과 중복되지 않는 유일한 값을 사용해야 하며 어떤 값을 넣어도 상관은 없다.

루트 컴포넌트 지정

    RootComponent = Body;
	Water->SetupAttachment(Body);
  • 분수대 구조물인 Body를 루트 컴포넌트로 지정한 후
    • SetupAttachment 함수를 사용하여 WaterBody의 자식 컴포넌트로 지정해준다.

나머지 컴포넌트들은 루트 컴포넌트의 자식으로 들어가야 한다.


빌드

  • 비주얼 스튜디오에서 빌드 - 솔루션빌드 해서 컴파일 해준 후
  • 콘텐츠 브라우저에서 Fountain 액터 클래스 아이콘을 뷰포트로 드래그 해보면 해당 컴포넌트들이 생성되어 붙어있는 것을 볼 수 있다.

image


🔔 액터와 에디터 연동

📜Fountain.h

#pragma once

#include "ArenaBattle.h"
#include "EngineMinimal.h"
#include "GameFramework/Actor.h"
#include "Fountain.generated.h"

UCLASS()
class ARENABATTLE_API AFountain : public AActor
{
	GENERATED_BODY()

public:
	// Sets default values for this actor's properties
	AFountain();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent *Body;

	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent *Water;
};

VisibleAnywhere

	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent *Body;

	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent *Water;

UPROPERTY에 VisibleAnywhere 키워드를 추가해주어야 컴포넌트의 멤버 변수 속성 값들을 편집할 수 있다.

  • 이 키워드를 추가해주기 전엔 언리얼 엔진에서 컴포넌트들이 편집 못 되게 전부 회색 글씨로 되어있고 막혀있을 것.

분수대 비주얼

Fountain 액터(오브젝트)의 Body, Water 포인터 이름을 가지고 있는 StaticMesh 컴포넌트가 각각 두개 붙어있는 상태다.

  1. Body 컴포넌트를 더블 클릭해 선택하고
  2. StaticMeshComponent 컴포넌트 부분에서 드롭다운에서 Fountain 에셋을 검색하여 에셋을 선택하여 이렇게 분수대가 보여지게 끔 한다.
    • image
  3. 분수대가 그려진 것을 볼 수 있다.
    • image

물 비주얼

Water를 선택하고 위와 같은 방법으로 StaticMeshComponent 컴포넌트 부분에서 에셋을 선택해준다. 트랜스폼의 위치 z 값을 135로 지정해 위로 끌어올려준다.

image


🔔 액터 기능의 확장

SetRelativeLocation 함수

해당 컴포넌트의 기본 Transform 값을 설정할 수있다. 인수로 FVector 타입의 구조체로 넘긴다.

📜Fountain.cpp

Water->SetRelativeLocation(FVector(0.0f, 0.0f, 135.0f));

Water컴포넌트는 Fountain액터의 루트 컴포넌트이자 부모인 Body를 기준으로 z 축으로 135 떨어진 곳에 위치하게 될 것이다. 즉 Body보다 z 축으로 135 떨어져 있는게 디폴트 위치. (0, 0, 135)가 기본 값이 됨.

조명 & 물이 찰랑이는 이펙트 추가하기

  • Fountain 액터에 조명 & 물이 찰랑이는 이펙트 추가하기
    • Body, Water와 동일하게
      • 포인터로 선언하여 컴포넌트를 동적 할당 받는다.

조명 👉 UPointLightComponent 클래스(컴포넌트) 사용

물 찰랑이는 이펙트 👉 UParticleSystemComponent 클래스(컴포넌트) 사용

📜Fountain.h

  • 컴포넌트 포인터 선언 및 매크로 설정(가비지 컬렉션 기능 UPROPERTY)
	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent *Body;

	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent *Water;

	UPROPERTY(VisibleAnywhere)
	UPointLightComponent *Light;

	UPROPERTY(VisibleAnywhere)
	UParticleSystemComponent *Splash;

📜Fountain.cpp

  • 생성자에서 컴포넌트를 동적 할당받아 생성해준 후
  • 루트 컴포넌트인 Body의 자식으로 넣어주자
  • 컴포넌트의 기본 위치도 설정해주자.
	Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BODY"));
	Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("WATER"));
	Light = CreateDefaultSubobject<UPointLightComponent>(TEXT("LIGHT"));
	Splash = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("SPLASH"));

	RootComponent = Body;
	Water->SetupAttachment(Body);
	Light->SetupAttachment(Body);
	Splash->SetupAttachment(Body);

    Water->SetRelativeLocation(FVector(0.0f, 0.0f, 135.0f));
	Light->SetRelativeLocation(FVector(0.0f, 0.0f, 195.0f));
	Splash->SetRelativeLocation(FVector(0.0f, 0.0f, 195.0f));

image

  • Splash를 더블 클릭해 선택하고 Particles 부분에서 Fountain 이펙트 에셋를 검색하여 추가해준다.


🔔 객체 유형과 값 유형

  • 언리얼 오브젝트의 속성 값
    1. 객체를 관리하는 객체 유형
      • 대표적으로 포인터
    2. 값을 관리하는 값 유형
      • 바이트 unit8
      • 정수 int32
      • 실수 float
      • 문자열 FString, FName
      • 구조체 FVector, FRotator, FTransform

멤버 변수 추가하기

📜Fountain.cpp

  • Fountain 액터에 ID 라는 이름의 int32 데이터형 멤버 변수를 추가.
    • 프로퍼티는 VisibleAnywhere이 아닌 EditAnywhere 로 해준다.
UPROPERTY(EditAnywhere)
int32 ID;

VisibleAnywhere과 EditAnywhere의 차이

UPROPERTY(VisibleAnywhere)
UStaticMeshComponent *Water;

UPROPERTY(VisibleAnywhere)
int32 A;

UPROPERTY(EditAnywhere)
int32 B;
  • VisibleAnywhere
    • 이 속성이 언리얼 디테일 윈도우뷰에서 보이지만 편집은 할 수 없다.
    • Water 컴포넌트 포인터의 값을 변경할 수 없다.
    • 즉!! Water 포인터를 기존의 UStaticMeshComponent 컴포넌트 말고 다른 컴포넌트를 가리키도록 주소 값을 변경할 수 없다.
    • 단, 위의 예시들처럼 컴포넌트의 내부의 속성 값들은 변경 할 수 있다.
      • 위에서 에셋 추가하고 Transform 편집했듯이.
    • A 멤버 변수의 값을 변경할 수 없다.
      • 언리얼 디테일 윈도우뷰에서 멤버 변수의 값을 편집은 할 수 없다.
  • EditAnywhere
    • 이 속성은 언리얼 디테일 윈도우뷰에서 보이고 편집도 가능하다.
    • 디테일창에서 B 멤버 변수의 값을 변경할 수 있다.
    • 포인터가 EditAnywhere이면 값을 변경할 수 있기 때문에 해당 포인터가 다른 컴포넌트를 가리키도록 변경할 수 있게 된다.
UPROPERTY(EditAnywhere, Category = ID)
int32 A;
  • 이렇게 카테고리를 주어 지정한 분류로서 속성값을 관리할 수 있다.


🔔 에셋의 지정

에셋을 일일이 언리얼 엔진 툴로 지정할 필요 없이 분수대에 관련된 에셋이 자동으로 로딩 되도록 하기. 마치 유니티의 GetComponent 처럼!

에셋 목록

image

  • 콘텐츠 브라우저에서 확인할 수 있다.
  • 레벨 에셋을 제외하고 모든 에셋은 uasset이라는 동일한 확장자를 가진다.
  • 그러나 에셋마다 품고 있는 데이터의 종류는 다르다.
    • 위 사진과 같이 색깔별로 어떤 데이터를 품고있는 에셋인지 알 수 있다.
      • ex) 하늘색은 스태틱 메시 에셋, 하얀색은 파티클 시스템 에셋

에셋 로딩하기

에셋을 불러들일 때 사용하는 문자열 KEY 값으로 에셋의 경로를 사용한다.

  • 에셋 경로 복사 방법 2 가지
    1. 에셋 우클릭 - 레퍼런스 복사 클릭
    2. 그냥 에셋 선택후 Ctrl + C 하면 경로가 복사된다.

📜Fountain.cpp

	// 생성자 안에 정의

	static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_BODY(TEXT("/Game/InfinityBladeGrassLands/Environments/Plains/Env_Plains_Ruins/StaticMesh/SM_Plains_Castle_Fountain_01.SM_Plains_Castle_Fountain_01"));

	if (SM_BODY.Succeeded())
	{
		Body->SetStaticMesh(SM_BODY.Object);
	}
  • ConstructorHelpers::FObjectFinder 타입의 객체 SM_BODY
    • static 으로 선언하는 것이 좋다. 여러번 에셋을 불러오면 큰 부담이 되기 때문에 최초에 한번만 초기화하게끔.
    • ConstructorHelpers 클래스
      • 메모리에서 에셋을 찾아본 다음 없으면 로드한다.
      • 생성자 안에서만 사용 가능하다.
    • FObjectFinder
      • 에셋의 내용물을 가져온다.
    • SM_BODY 생성자로 에셋의 경로를 인수로 전달한다.
  • 꼭 성공적으로 불러와 졌는지 체크를 하는 것이 좋다.
    • SM_BODY.Succeeded()
      • 성공적으로 불러오면 true 리턴
  • Body->SetStaticMesh(SM_BODY.Object);
    • SM_BODY.Object : 불러온 에셋의 포인터
    • 이를 Body에 대입해준다.
      • SetStaticMesh 타입의 에셋이니 SetStaticMesh 함수를 사용해 대입.
  • 물, 파티클 컴포넌트도 똑같은 방법으로 해준다.


📜 최종 코드

📜Fountain.h

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

#pragma once

#include "ArenaBattle.h"
#include "GameFramework/Actor.h"
#include "Fountain.generated.h"

UCLASS()
class ARENABATTLE_API AFountain : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AFountain();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent *Body;

	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent *Water;

	UPROPERTY(VisibleAnywhere)
	UPointLightComponent *Light;

	UPROPERTY(VisibleAnywhere)
	UParticleSystemComponent *Splash;

	UPROPERTY(EditAnywhere, Category=ID)
	int32 ID;
};

📜Fountain.cpp

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

#include "Fountain.h"


// Sets default values
AFountain::AFountain()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BODY"));
	Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("WATER"));
	Light = CreateDefaultSubobject<UPointLightComponent>(TEXT("LIGHT"));
	Splash = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("SPLASH"));

	RootComponent = Body;
	Water->SetupAttachment(Body);
	Light->SetupAttachment(Body);
	Splash->SetupAttachment(Body);

	Water->SetRelativeLocation(FVector(0.0f, 0.0f, 135.0f));
	Light->SetRelativeLocation(FVector(0.0f, 0.0f, 195.0f));
	Splash->SetRelativeLocation(FVector(0.0f, 0.0f, 195.0f));

	static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_BODY(TEXT("/Game/InfinityBladeGrassLands/Environments/Plains/Env_Plains_Ruins/StaticMesh/SM_Plains_Castle_Fountain_01.SM_Plains_Castle_Fountain_01"));

	if (SM_BODY.Succeeded())
	{
		Body->SetStaticMesh(SM_BODY.Object);
	}

	static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_WATER(TEXT("/Game/InfinityBladeGrassLands/Effects/FX_Meshes/Env/SM_Plains_Fountain_02.SM_Plains_Fountain_02"));
	
	if (SM_WATER.Succeeded())
	{
		Water->SetStaticMesh(SM_WATER.Object);
	}

	static ConstructorHelpers::FObjectFinder<UParticleSystem> PS_SPLASH(TEXT("/Game/InfinityBladeGrassLands/Effects/FX_Ambient/Water/P_Water_Fountain_Splash_Base_01.P_Water_Fountain_Splash_Base_01"));

	if (PS_SPLASH.Succeeded())
	{
		Splash->SetTemplate(PS_SPLASH.Object);
	}
}

// Called when the game starts or when spawned
void AFountain::BeginPlay()
{
	Super::BeginPlay();

}

// Called every frame
void AFountain::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}


🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

UE4 Lesson 2 카테고리 내 다른 글 보러가기

댓글 남기기