※월드와 에셋들이 어떻게 만들어졌는지를 항상 되새김질 하자!
아무 생각없이 따라하면 나중에 혼자 만들때 막막함!
언리얼에서 Actor.h를 열어보면 BeginPlay() 와 Tick() 이라는 메소드가 있다.
이는 유니티의 Start() 와 Update() 라는 메소드와 비슷하다. 주석에도 달려있는데, BeginPlay()는 시작할때 혹은 스폰될때 한번만 호출되고, Tick()은 지속적으로 호출되어 상태를 갱신하는데 쓰인다는 것에서 같다.
BeginPlay()의 주석 - // Called when the game starts or when spawned
Tick()의 주석 - // Called every frame
-> 유니티의 경우 어떤 오브젝트에 움직임을 부여하는 스크립트를 붙이고 싶으면 그냥 새로 만들어서 붙이면 끝이다. 이름을 바꾸고 싶으면 바꾸면 되고, 삭제하고 싶으면 삭제하면 된다.
-> 그러나 언리얼은 기본적으로 수정 삭제가 자유롭게 안된다. 만약 오타로 이름 한글자 고치고 싶어도 엔진을 꺼야한다. 언리얼은 '만들땐 마음대로 였겠지만 바꿀땐 아니란다.' 가 기본이다.
▶ 엔진을 끄라구요? 아니 엔진을 끄면 어디서 삭제해요? 😨😨😨???
-> 네 끄세요. 일단 끄셔야함. 예상과 달리 엔진 상에서 삭제 못합니다. 마음을 비우십쇼.
메뉴가 있는데 왜 누르질 못하니! 어쩐지 오늘은 개발이 잘 되더라니만(?)
▶ 그러므로 언리얼 엔지니어에게 주는 교훈 - 네이밍과 오타에 주의하자
잘못된 손가락 놀림 하나로 상기와 같은 복잡하고 다각적인 고통을 맛보게 될 수도 있다. 갓 블레스 유어 핑거.
유니티는 빈 깡통에서 시작해서 내가 원하는대로 만들어나가는 반면, 언리얼은 이미 어느정도 내부에 갖춰진 것을 가져와 쓰기 때문에 기본적으로 상속구조가 있다. 그리고 그 상속구조가 아주 중요하다.
물론 그 갖춰진 것을 가져와서 어떻게 쓰느냐는 사용자에게 달린 것이므로 상속구조라고는 해도 게임 제작이나 패턴이 다 정해진것만은 아니고 그 후는 유니티처럼 만들 수 있지만, 아무튼.
class TESTUE426_API AMyActor : public AActor
VS를 열어보면 위와 같이 되어있다. 이는 MyActor가 Actor 클래스를 상속받았다는 것!
-> TESTUE426_API라는 저 이름은 모듈과 관련이 있는데, 나중에 빌드를 할때 이 모듈명을 이용해서 뭔가를 한다는 개념이 있다.
지금은 몰라도 되니 있구나 정도만 보고 넘어가자😎
▶ C++은 C#과 달리 reflection을 기본적으로 지원하지 않는다. C++ 20가 되어서야 나왔다.
(reflection이 뭔데요?! -> 대충 컴파일러가 읽을 수 있는 주석이라고 생각하자. 이건 C#이나 다른언어를 좀 해봐야 이해할 수 있는 부분이긴 하다. 나 자바 하다가 왔는데 왜 모르지? 들었던 기억은 나는데... 가물가물... 어어어...)
▶ 아무튼 그래서 C++에는 reflection이 없으니까, 언리얼 엔진 내부에서 자체적으로(야매) 리플렉션을 만들어준게 UCLASS(), GENERATEDBODY(), UPROPERTY() 등등으로 보면 된다. 일단 U가 붙는건 대충 언리얼엔진의 클래스, 언리얼 엔진의 프로퍼티다 라는 식으로 생각하자.
-> 일단 UCLASS()와 GENERATEDBODY()는 셋트로 함께 간다고 기억해두자.
private:
UPROPERTY()
UStaticMeshComponent* Mesh;
▶ MyActor.h에 위와 같은 코드를 추가해주었다. 이는 내가 Mesh라는 이름의 언리얼엔진에서 쓰는 스태틱 메시 컴포넌트를 쓸 예정이란 얘기. 그런데 포인터로 써줬기 때문에 지금 당장 뭐가 일어나는건 아니다.
(아니 저기 잠깐만요 저 포인터가 뭔지 모르는데요!? 그냥 많은 사람들이 포인터에서 좌절하고 C++을 포기한다는것만 아는데요?!
-> int가 정수를, char가 문자 1개를 저장하듯 포인터는 주소값을 저장하는거라고 보면 된다.)
일단 이것을 참고하고 넘어가자 - 포인터에 대해
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MESH"));
그리고 솔루션 탐색기의 프로젝트명을 우클릭해서 빌드를 눌러보자.
지금은 #include "CoreMinimal.h" 가 상단에 들어가 있기 때문에 문제없이 빌드가 되었지만, 경우에 따라 헤더에 뭔가가 없는 경우 C++이 UStaticMeshComponent를 인지하지 못해, 이게 뭔지 몰라서 빌드에 실패하는 경우도 있다.
-> 그리고 당연한 얘기지만, CoreMinimal.h에 포함된게 많으면 많을수록 빌드 및 로딩이 오래 걸리므로, 경우에 따라서 CoreMinimal.h를 사용하지 않고 직접 추가해서 쓰기도 한다.
-> 만약 직접 추가해서 쓴다면 일반 C++처럼 전방 선언을 해준 다음에, UStaticMeshComponent가 추가된 이 헤더를 MyActor.cpp 상단에도 직접 추가해야 한다.
(이게 뭔소리에요? -> 아래를 참고하자.)
private:
UPROPERTY()
class UStaticMeshComponent* Mesh;
// 이렇게 쓴 다음에, MyActor.cpp 상단에
// #include Components/StaticMeshComponents.h 를 직접 써줘야 함
강사님의 경우 '비주얼 어시스트'를 사용하기 때문에 단축키로 진입 가능.
똑같이 비주얼 어시스트를 사용해도 되고, 구글링을 해서 직접 경로를 찾아 입력해도 되고 그건 본인 선택.
그러나 이건 include가 안됐을때 얘기고, 무사히 빌드가 됐다면 딱히 따로 추가할거 없이 그냥 쓰면 된다.
아무튼, 이런식으로 CreateDefaultSubobject라는 함수를 이용하면, 다음부터는 이 Mesh라는 대상의 메모리 관리를 우리가 직접 해줄 필요가 없게 된다. 포인터로 선언해줬으므로 일종의 스마트 포인터로 보면 된다. 자체적으로 관리해주는 메모리가 된 것.
(기억 나는가? 원래 C/C++은 메모리 관리도 프로그래머가 알아서 해야함. malloc free 라고 검색해보면 나와용.)
▶ 아무리 C++이라고 해도 new / delete를 써서 쌩포인터를 관리하던 시대는 지났다!
이젠 그렇게 하면 안됨. 언리얼엔진에서도 다 스마트 포인터를 사용하고, 그렇게 쌩포인터로 직접 관리하는 경우는 사실상 없다고 보면 된다. 너무 위험하기 때문! 정말정말 비추.
빌드하는건 VS상에서 빌드를 눌러도 되고, 언리얼엔진 상에서 컴파일을 눌러도 된다. 똑같음.
이렇게 빌드하고 엔진을 보면 MyActor1 인스턴스 밑에 Mesh(MESH)(상속됨) 이라고 써있는 것을 볼 수 있다! 오호라, 너도 이제 언리얼엔진의 관리 하에 들어온 메시가 되었구나.😎
그러나 안타깝게도 하단의 트랜스폼, 스태틱 메시, 피직스 등등 모든 것이 다 비활성화 되어있어 건드릴 수 없다😥 못 고치나?
-> 고칠 수 있다. 속성을 조금 바꿔주면 된다. VS로 돌아가 UPROPERTY() 안에 뭔갈 써주자.
UPROPERTY()는 언리얼의 자체 reflection 기능 덕분에 이런저런 옵션을 넣어줄 수 있다.
-> UPROPERTY(VisibleAnywhere) 이라고 수정해주고 빌드하면 수정할 수 있게 된다!
-> 혹시라도 오해하지 않아야 할 것은, VisibleAnywhere이라고 쓴다고 해서 이게 보이기만 하고 수정 불가능한 것이 아니다. 수정할 수 있다.
▶ 이러한 처리의 문제점 - MyActor를 드래그 앤 드롭으로 새로 만들면, 걔는 아무 설정이 적용되어 있지 않다!
-> 내가 첫번째로 끌어온 Actor는 스태틱 메시 모양도 타원으로 설정해주고, 머티리얼도 벽돌무늬로 설정해주었다.
그런데 새로 끌어와 만든 애는 VisibleAnywhere이 무색하게 보이지도 않잖아?! (설정은 할 수 있는데, 처음부터 다 다시 해줘야 한다. 무슨 모양인지, 어떤 무늬인지...)
-> 확장성이 아주 낮은 설정이라서, 이 방법은 좋지 않다.
일단, 툴에서 뭔가 할수 있다면 코드상에서도 할 수 있다는 믿음을 가지자 (가져도 되는건가...?)
▶ 마우스 올려보면 '다음으로 확장: L"MESH" 라고 되어있다. 이 말은, 내 C++ 환경에서는 저렇게 쓰면 L"MESH" 라는 형태로 치환된단 얘기임.
(근데요? 그게 뭔 상관이에요? 뭐라는겨?😕🤔)
-> 언리얼 엔진같은 경우는 PC게임만 만드는 툴이 아니다. 즉, 이 코드를 PC에서만 돌릴거라는 보장이 없다는 것. L"MESH" 로 치환되어 작동하는건 PC환경에서만 그럴수도 있다는 얘기임!
-> 그러니까 이걸 섣불리 '나는 PC게임 만들거니까 여따가 L"MESH" 라고 써야징 힣힣' 했다간 빌드가 안되고 에러가 불꽃처럼 터지는 모습을 볼 수도 있다는 것.
이걸 붙여주지 않을 경우 인코딩에 유동적 대응이 안되어, 특정 환경에서만 빌드가 안되고 에러가 왕창 터지는 모습을 볼 수 있을 것이다. 당신이 만약 안드로이드와 iOS와 PC와 콘솔과 기타등등 그 어디서라도 플레이할 수 있는 게임을 만들고 싶다면 특히. 궰뛣쒧꿹 수준으로 끝나지 않을것이야.
제목만 보면 겁나 어려운 것 같다. 허미 이게 뭔소리여? 뭘 받는다고?
하지만 겁먹지 말자. 이건 '아까 새 MyActor를 꺼낼때마다 아무 형태가 없었던 상황'을 해결하기 위해서, 새 MyActor에 애초에 뭔가 스태틱 메시의 모습중 하나를 지정해줌으로써 '꺼낼때부터 모습이 있는채로 꺼내지게 하는' 작업을 하려는 것!
그래서 대강 아래와 같은 코드를 작성하였다.
-> 이때 경로를 받아오는건, 원하는 스태틱 메시의 콘텐츠를 클릭한 다음에 그대로 Ctrl + C 하면 된다. 그게 오타가 없고 편하다.
static ConstructorHelpers::FObjectFinder<UStaticMesh> SM(TEXT("StaticMesh'/Game/StarterContent/Props/SM_Couch.SM_Couch'"));
// 앞으로 새로 꺼낼 액터는 스태틱 메시 이름이 SM_Couch인 모습을 받아서 나갈거야
if (SM.Succeeded()) {
Mesh->SetStaticMesh(SM.Object);
}
//소파 찾았어? 그러면 로드한 오브젝트의 스태틱 메시를 걔(소파)로 아예 Set해줘
이것도 약간 규칙임. 컨벤션중 하나인듯. 그래서 RootComponent = Mesh; 라고 써주었다.
▶ 어라, 그런데 int32 Hp; int32 Mp; 를 넣고 빌드해도 엔진 안에서 보이지 않는데? 🤔
-> 이 부분 또한 유니티와 비슷한 점인데, 이 변수들 또한 reflection 기능을 통해 위에 UPROPERTY(VisibleAnywhere) 를 붙여서 외부로 노출시켜주는 작업이 필요하다.
와오. 벤치에 HP와 MP가 붙었어. 멋지다.
다시한번 말하지만 이는 어디까지나 언리얼 엔진에만 있는 야매 리플렉션 문법임.
C++은 공식적으로는 20 미만의 버전에 reflection이 존재하지 않음.
Running UnrealHeaderTool
"C:\Users\lihao\Desktop\UEproject\CPP-UE4TEST\testue426\testue426.uproject"
UnrealHeaderTool이 뭔가를 parsing해서 만들어주고 있구나. 열일하는군.
언리얼도 가진 툴이 많다. 빌드하는 툴도 있고, 헤더를 parsing하는 툴도 있고.
-> 언리얼은 PROPERTY()라는 굉장히 묘한 문법을 이용해 reflection 기능을 구현하는 것이다.
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* Mesh;
UPROPERTY(EditAnywhere, Category = BattleStat)
int32 Hp;
UPROPERTY(EditAnywhere, Category = BattleStat)
int32 Mp;
그래서 위와 같이 썼다고 해서 Hp, Mp가 메모리 관리가 되는게 아니다.
MyActor.cpp 쪽에 따로 선언해서 사용하는 Mesh만 메모리 관리가 되는 것이라 생각하면 된다.