UE4 Chapter 2. 액터의 설계
카테고리: UE4 Lesson 2
태그: Cpp UE4 Game Engine
이득우님의 책 이득우의 언리얼 C++ 게임 개발의 정석 을 공부하고 정리한 필기입니다. 😀
언리얼 엔진 공식 문서 https://docs.unrealengine.com/ko/index.html
Chater 2. 액터의 설계
🔔 언리얼 컨텐츠의 구성 요소
1. 월드
뷰포느 윈도우에 보이는 작업 공간 =
가상 공간
=월드
- 구성요소
- 공간
- 월드의 3차원 공간안에 존재하려면
Transform
컴포넌트를 가졎야 하며 - 공간의 기본 단위는 cm이다.
- 월드의 3차원 공간안에 존재하려면
- 시간
- 월드(가상공간) 안에서 흐른 ㄴ시간
- 시간 스케일 조절 가능.
- 물리
- 중력같은 물리적 영향을 받으려면
Collision
컴포넌트가 붙어있어야.
- 중력같은 물리적 영향을 받으려면
- 렌더링
- 시각적인 기능. 현실 세계와 유사한 빛.
Physically Based Rendering
- 공간
- 세팅 - 월드 세팅
- 에서 월드의 더 많은 요소를 볼 수 있다.
2. Actor
콘텐츠를 구성하는 자신에게 주어진 역할을 수행하는 최소 단위의 물체(오브젝트)
- 월드 아웃라이너 윈도우에서 액터들의 목록을 볼 수 있다.
- 흰 글씨 액터들은 현재 레벨에 플레이 전에 현재 레벨에 이미 존재하고 있는 액터들을 뜻하며
- 노란 글씨의 액터들은 플레이 도중 새롭게 생겨난 액터들을 뜻한다.
- 디테일 윈도우에서 선택한 액터의 정보를 볼 수 있다.
- 액터에 붙어있는 컴포넌트(C++ 클래스, C++ 코드)는 이 액터의 기능 및 역할이 된다.
- 액터들은 트랜스폼을 가진다.
- ⭐액터는 내부적으로는 컴포넌트들로 조합되어 이루어져 있지만 하나의 독립된 배우로서 게임 콘텐츠에 기여한다.
3. 레벨
플레이어에게 주어진 스테이지.
- 월드에 배치된 액터들의 집합.
- 레벨을 설게하다 = 스테이지를 조립하다.
4. 컴포넌트
액터에게 기능을 부여하려면 컴포넌트를 붙여주면 된다. 언리얼에서 지원하는 컴폰넌트들도 있고 직접 C++ 클래스 코드를 작성하여 사용자 지정 컴포넌트를 붙여줄 수도 있다.
- 주요 컴포넌트
- StaticMesh 컴포넌트
- 애니메이션이 없는 모델링 에셋인 스태틱 메시를 사용하여 시각적, 물리적 기능 제공
- 게임 제작에 있어 가장 많이 사용 됨.
- 주로 배경에 사용
- SkeletalMesh 컴포넌트
- 애니메이션이 있는 모델링 에셋인 스태틱 메시를 사용하여 시각적, 물리적 기능 제공
- 플레이어, 적 등등
- Camera, Collision, Audio, Particle, Light, Movement, … 등등
- StaticMesh 컴포넌트
루트 컴포넌트
- 언리얼에서는 어떤 액터에 붙어있는 컴포넌트들 중 그 액터에서 대표되는 컴포넌트 1 개를 반드시 지정해야 한다. 이를 루트 컴포넌트 라고 한다.
- 다른 컴포넌트들을 이 루트 컴포넌트의 자식으로 넣어주어야 한다.
🔔 액터의 설계
언리얼에선 코드를 엔진에 반영하려면 저장하는걸로는 안되고 꼭 빌드 해서 컴파일을 마쳐야 한다.
Fountain
액터를 만들어 보자.- 분수대는
분수대 구조물
과물
로 구성되어 있다.- 분수대 구조물 👉 StaticMesh 컴포넌트 (루트 컴포넌트로 지정)
- 분수대 구조물의 비주얼
- 충돌 처리
- 물 👉 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
물
의 비주얼 기능을 할 컴포넌트
- UStaticMeshComponent *Body;
- 선언
- 컴포넌트를 동적 할당 받아 포인터에 주소를 리턴할 것
- C++ 에서는 힙 메모리로 동적 할당 받은 객체들은 개발자가 직접
delete
시켜주어 메모리 누수를 방지해야 하는데 - 언리얼에서는
UPROPERTY()
매크로를 포인터 위에 붙여주면 객체가 더 이상 사용되지 않으면 자동으로 메모리를 해제시켜 준다.UObject
에서 가비지 컬렉션을 제공하기 때문!- 개발자가 직접
delete
시킬 필요가 없음 - 이 매크로는 언리얼 오브젝트 객체에만 사용이 가능하다.
- C++ 에서는 힙 메모리로 동적 할당 받은 객체들은 개발자가 직접
언리얼 오브젝트
언리얼 오브젝트
: 언리얼 환경에 의해 관리되는 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++ 클래스가 언리얼 오브젝트 클래스가 되려면 지켜야 하는 규칙
- 클래스 위에
UCLASS()
매크로, 클래스 내부에GENERATED_BODY()
매크로 선언해 이 클래스가 언리얼 오브젝트임을 선언한다. - 액터 클래스 이름 앞에는
A
를 붙이고, 액터가 아닌 클래스 이름 앞에는U
를 붙여 구분한다. 언리얼 오브젝트와 전혀 상관이 없는 일반 C++ 클래스는F
를 붙인다.AFountain
: 액터 클래스AActor
: 액터 클래스UStaticMeshComponent
: 컴포넌트 클래스FVector
: 일반 C++ 벡터 클래스
Fountain.generated.h
- 코드를 작성할 땐 없지만 언리얼 환경의 컴파일 과정에서 생기는 파일이다. 마지막 #inlcude 구문에서 반드시 선언해주어야 하는 헤더파일이다.
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
함수를 사용하여Water
를Body
의 자식 컴포넌트로 지정해준다.
나머지 컴포넌트들은 루트 컴포넌트의 자식으로 들어가야 한다.
빌드
- 비주얼 스튜디오에서
빌드 - 솔루션빌드
해서 컴파일 해준 후 - 콘텐츠 브라우저에서 Fountain 액터 클래스 아이콘을 뷰포트로 드래그 해보면 해당 컴포넌트들이 생성되어 붙어있는 것을 볼 수 있다.
🔔 액터와 에디터 연동
📜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 컴포넌트가 각각 두개 붙어있는 상태다.
Body
컴포넌트를 더블 클릭해 선택하고StaticMeshComponent
컴포넌트 부분에서 드롭다운에서 Fountain 에셋을 검색하여 에셋을 선택하여 이렇게 분수대가 보여지게 끔 한다.- 분수대가 그려진 것을 볼 수 있다.
물 비주얼
Water
를 선택하고 위와 같은 방법으로 StaticMeshComponent
컴포넌트 부분에서 에셋을 선택해준다. 트랜스폼의 위치 z 값을 135로 지정해 위로 끌어올려준다.
🔔 액터 기능의 확장
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));
Splash
를 더블 클릭해 선택하고Particles
부분에서 Fountain 이펙트 에셋를 검색하여 추가해준다.
🔔 객체 유형과 값 유형
- 언리얼 오브젝트의 속성 값
- 객체를 관리하는 객체 유형
- 대표적으로
포인터
- 대표적으로
- 값을 관리하는 값 유형
- 바이트 unit8
- 정수 int32
- 실수 float
- 문자열 FString, FName
- 구조체 FVector, FRotator, FTransform
- 객체를 관리하는 객체 유형
멤버 변수 추가하기
📜Fountain.cpp
Fountain
액터에 ID 라는 이름의 int32 데이터형 멤버 변수를 추가.- 프로퍼티는 VisibleAnywhere이 아닌
EditAnywhere
로 해준다.
- 프로퍼티는 VisibleAnywhere이 아닌
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 처럼!
에셋 목록
- 콘텐츠 브라우저에서 확인할 수 있다.
- 레벨 에셋을 제외하고 모든 에셋은
uasset
이라는 동일한 확장자를 가진다. - 그러나 에셋마다 품고 있는 데이터의 종류는 다르다.
- 위 사진과 같이 색깔별로 어떤 데이터를 품고있는 에셋인지 알 수 있다.
- ex) 하늘색은 스태틱 메시 에셋, 하얀색은 파티클 시스템 에셋
- 위 사진과 같이 색깔별로 어떤 데이터를 품고있는 에셋인지 알 수 있다.
에셋 로딩하기
에셋을 불러들일 때 사용하는 문자열 KEY 값으로 에셋의 경로를 사용한다.
- 에셋 경로 복사 방법 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 리턴
- SM_BODY.Succeeded()
- 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);
}
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기