
변수(Variable): 간단하게 게임에서 캐릭터의 체력, 공격력, 공격속도와 같은 값과 상태를 저장하고 조작하는 데 사용한다
(게임이나 프로그램에 필요한 여러 종류의 값들을 담아두는 것)
코딩을 조금 해봤던 사람들이라면 int, float, double, char, bool 등등 변수의 타입이 있다는 것을 알고 있을 것이다
언리얼엔진에서도 결국 C++코드를 가지고 진행을 하기 때문에 이 변수들을 사용하여 제작한다
이외에도 언리얼 엔진에서 제공하는 여러 클래스도 있고 다른 변수들을 묶음으로 다룰 수 있는 컨테이너들 역시 변수로 사용된다
직접 C++ 클래스를 Visual Studio를 사용하여 코드를 짠 다음 에디터와 연결시켜 작업을 해 보자
그럼 먼저 언리얼 엔진을 실행하고 새 프로젝트를 생성한다
카테고리는 게임, 다른 코드 없이 완전히 비어있는 기본 템플릿으로 선택 후 프로젝트 타입을 C++로 변경한 다음 생성한다
프로젝트가 생성되면 UE5-8 C++ <C++ 클래스 생성/기본 이벤트> 에서 배운 것처럼 Actor 클래스를 상속받아서 C++ 클래스를 하나 생성한다 (클래스의 이름은 언리얼 엔진이 기본으로 추천해 주는 MyActor를 그대로 써보자)
그리고 이 클래스 안에 멤버 변수를 한번 만들어보자
멤버 변수란, 클래스나 구조체 내에서 선언되며 해당 클래스의 객체(Instance)에 속하는 변수이다. 그래서 모든 오브젝트는 이러한 멤버 변수를 서로 공유하며, 그 객체가 유지되는 동안만 데이터를 보존한다
그전에 언리얼엔진에서 사용되는 변수의 이름은 몇 가지 규칙이 존재한다
- 변수명은 영문자(a~Z), 숫자(0~9), 밑줄("_")로 구성된다
- 첫 글자로는 영문자 혹은 밑줄("_")이 가능하며, 밑줄("_")을 사용했을 때 실제 언리얼 에디터에서는 "공백"으로 남는다
- 예약어(int, if, for, void, class, namespace, *, throw 등)나 특수문자(~, !, @, #, $, <, - 등)를 변수명으로 사용할 수 없다
- 변수명의 길이에는 제한이 없지만 변수 이름은 해당 변수가 나타내는 내용을 이해하기 쉽게 나타내는 것이 좋고, 가능한 한 명확하고 간결한 이름을 사용하는 것이 제일 좋다
그리고 나중에 나올 UPROPERTY() 매크로를 붙였을 때의 프로퍼티도 몇 가지 규칙이 존재한다
- 변수 자체로는 소문자와 대문자를 구분하지만, UPROPERTY() 매크로를 붙였을 때 프로퍼티의 이름은 전부 소문자로 인식되기 때문에 동일한 알파벳을 사용할 수 없다
- 가장 첫번째 나오는 알파벳은 변수의 선언과는 관계없이 항상 대문자로 '표시'가 된다(변수를 선언한 글자 자체가 변하는 것은 아님)
- 밑줄("_")을 사용하는 것 뿐만 아니라, 소문자 뒤에 대문자가 나올 경우에도 그 사이 공백이 생긴다.
- 공백 이후에 첫 글자는 항상 대문자로 '표시'된다. 만약 '소문자_소문자' 형태일 경우 공백이 "1번만" 적용된다
이러한 규칙들 이외에도 네이밍 컨벤션(Naming Convention)을 통해 이름을 일관되게 작성할 수도 있다
이 멤버변수를 만들기 위해서는 클래스의 헤더 파일에서 선언해주어야 한다
그러면 테스트를 위해서 생성자 함수에서 int32 타입의 TotalDamage 멤버 변수 하나를 선언한다
기본 변수 중에서는 정수를 선언하는 short, int, long이 있지만, 이러한 기본타입들은 게임을 작동시키는 플랫폼마다 길이가 달라질 수 있기 때문에 언리얼엔진에서는 길이가 고정되어 있는 타입으로 int8, int16, int32, int64를 사용한다
int 뒤에 붙는 숫자들은 정수를 표현하는데 몇 bit를 사용할 것인가를 묻는 것인데, 음수로 표현할 필요가 없다면 u를 붙임으로써 양수로 범위를 정할 수 있다 [ex) uint32, uint64]
그냥 편하게 범위 상관없이 많은 비트를 사용하면 되지 않을까 생각하지만, 게임의 최적화를 위해 적절한 범위의 타입을 사용해야 한다(데이터 유형이 나타낼 수 있는 최댓값이나 최솟값을 넘어버리는 가능성을 배제할 수 없을 때, 범위 지정(예외처리)을 통해 그 숫자의 범위를 벗어나지 못하도록 예방하는 것이 좋다)
예시로 8bit는 (-128~127)까지인데 실수로 127에 1이 더해져 -128이 되는 경우가 있고 -> Overflow(오버플로우)
-128에 1이 빠져 127이 되는 경우도 발생한다 -> Underflow(언더플로우)
실수를 표현하는 변수타입은 float(32bit), double(64bit)이 존재한다. 보통의 경우에는 float(소수점 5자리)를 사용하지만
정말 세밀한 작업을 할 때는 double(소수점 10자리)을 사용하기도 한다
문자열을 표현하는 변수타입은 C++에서 보통 string을 사용하지만 언리얼엔진에서는 다양한 변수타입들이 존재한다
- FStirng: 저장되는 글자의 길이에 따라 변수의 길이가 자동으로 변환된다 [TEXT( )매크로를 사용해야만 한다]
- FTEXT: 현지화 텍스트를 위해 사용된다
- FName: 자주 사용되는 문자열을 식별자로 지정하여 문자열을 비교할 때 소모되는 메모리와 CPU처리시간을 절약한다
- TCHAR: 문자열 세트와 상관없이 문자열을 저장하는 용도
논리변수(True/False): 참 또는 거짓 두 가지 경우밖에 존재하지 않는다
이제 이 변수들을 Visual Studio가 아닌 언리얼 에디터에서 보이게 할 수 있도록 만들어보자
UE5-9 C++ <C++ 클래스 생성/기본 이벤트>에서 접근지정자를 public으로 지정한다고 해서 에디터에서 바로 사용할 수 있는 것은 아니라고 했는데, C++ 클래스에서 변수를 만들고 언리얼 엔진에 노출시키기 위해서는
변수를 만들 클래스의 헤더 파일에서 변수의 타입과 이름을 적어주고, 그 앞에 UPROPERTY 매크로를 붙여주면 된다 (대부분의 언리얼 프로그래밍에서 사용되는 변수의 기본형)
UPROPERTY( )를 붙이는 이유는 프로퍼티가 언리얼엔진이나 에디터에게 이러한 프로퍼티가 있음을 알리고 프로퍼티가 엔진과 연결되었을 때 어떻게 작동할지 지정자를 통해 명령을 해주기 위함이다.
지정자를 넣기 위해서 선언했던 모든 변수에 UPROPERTY 매크로를 붙이고,
TotalDamage와 DamageTimeInSecond 프로퍼티에 EditAnywhere, BlueprintReadWrite, Category="Damage" 지정자DamagePerSecond 프로퍼티에 BlueprintReadOnly, VisibleAnywhere, Transient, Category="Damage" 지정자
나머지 두 프로퍼티인 CharacterName과 bAttackable에 EditAnywhere, BlueprintReadWrite 지정자를 입력해 준다
UPROPERTY( ) 지정자
- EditAnywhere: Archetype(아직 instance되지 않은 블루프린트의 원본)과 instance된 양쪽 모두의 프로퍼티 창에서 편집할 수 있다 [instance: 클래스가 게임 레벨에 배치된 상태]
- BlueprintReadWrite: 이 프로퍼티를 블루프린트에서 읽기와 쓰기가 모두 가능하도록 한다
- Caregory = "N": 블루프린트 편집들이나 디테일 패널에서 이 프로퍼티를 N이라는 카테고리로 묶어서 보여준다(에디터에서 작업할 때 필요한 프로퍼티를 빠르게 찾을 수 있다)
- BlueprintReadOnly: 프로퍼티를 블루프린트에서 읽기만 가능하다
- VisibleAnywhere: 프로퍼티는 모든 프로퍼티 창에서 보이지만 편집할 수 없게한다
- Transient: 휘발성 프로퍼티로 저장 또는 로드되지 않는다
이제 이렇게 작성한 프로퍼티들이 에디터에서 어떻게 보이는지 확인해 보자
이렇게 수정한 코드를 에디터에 적용하기 위해서는 [Ctrl + Shift + S]로 모두 저장하고
에디터로 돌아간 뒤 여러 컴파일 방법 중 하나를 선택해서 컴파일을 하면 된다
컴파일이 끝난 뒤에 C++ 클래스의 MyActor를 월드에 끌어다가 배치하고 디테일 패널에서 보면
Category="Damage" 지정자로 묶어준 프로퍼티들은 Damage 카테고리에 모여있고, 그렇지 않은 프로퍼티들은 클래스 이름인 MyActor 카테고리에 있는 것을 볼 수 있다
그리고 EditAnywhere로 지정한 다른 프로퍼티와는 다르게 VisibleAnywhere로 지정한 DamagePerSecond 프로퍼티는 짙은 회색으로 표시돼서 수정할 수 없는 것을 확인할 수 있다
이렇게 소스코드 에디터에서 초기화 하지 않은 값들은 자동으로 0으로 초기화 된다.
여기서 값들을 수정할 수 있지만 초기값을 수정하는 것은 아니기 때문에, 소스코드 에디터에서 프로퍼티의 기본값을 지정하는 방법을 알아보자
언리얼 C++ 클래스에서 반환형이 없고 클래스 이름과 같은 이름을 가진 '생성자' 함수는 UE5-7 C++ <C++ 클래스 생성/기본 이벤트>에서 설명한 대로 클래스의 객체가 생성될 때 한 번 호출되는 함수이며 주로 생성된 액터의 프로퍼티, 즉 변수의 기본 값을 설정해 주는 데 사용된다
생성자 함수가 선언되어 있는 이 헤더파일에서도 변수를 선언함과 동시에 값을 넣어주는 '초기화' 작업을 해도 되지만,
.cpp 파일의 생성자 함수에서 프로퍼티의 기본 값을 설정해 보자
우선 MyActor 클래스의 소스 파일로 넘어가서 AMyActor 생성자에서 작업을 진행한다
프로퍼티의 기본값(초기화)을 설정하는 방법은 2가지가 존재하는데,
1. 생성자 옆에 콜론을 입력한 뒤 프로퍼티의 이름을 적고 괄호에 기본값을 넣는다
2. 생성자의 몸체에서 기본값을 직접 대입한다
이 두 가지 방법 중에 선호하는 방법을 사용하면 된다 (방법을 섞어서 사용해도 무관하다)
[주의: CharacterName에 한글을 입력하면 컴파일 시 오류가 발생할 수 있으므로 영어로 입력하는 것을 추천한다 (나중에 다룰 내용)]
생성자에서 값을 입력해 주고 저장 후 언리얼 에디터로 돌아가서 컴파일해 주면 수정할 수 없었던 DamagePerSecond 값도
생성자에 입력했던 값으로 변경된 것을 볼 수 있다
다만 여기서 한 가지 알아보아야 할 것이 컴파일을 했을 때 나오는 이 라이브 코딩 문제이다
이 라이브 코딩 기능은 간단하게 말해서, 개발 중에 코드를 수정하고 변경한 내용을 즉시 반영시켜서 에디터를 다시 실행하지 않고도 결과를 확인 할 수 있는 기능이다. 하지만 이 라이브 코딩이 단순히 '현재' 에디터에서 수정한 내용만을 적용한다는 것이다
빠른 이해를 돕기 위해 먼저 이런 식으로 기본값들을 에디터에서 아무렇게 변경한 다음 프로젝트를 저장한다
이제 이 맵을 기본 맵으로 설정하기 위해 [Edit > Project Settings...]를 누르고 Project 섹션에서 Maps & Modes 항목을 선택한다
그리고 Default Maps에서 Editor Startup Map과 Game Default Map을 저장했던 맵으로 변경해준 다음 언리얼 에디터를 닫아준다
그런데 프로젝트를 다시 열고 Content Browser을 보면 C++ Classes 폴더가 사라져 있는 것을 볼 수 있다
이 상태에서 컴파일을 한번 하면 다시 C++ Classes 폴더가 생기지만 배치했던 MyActor가 사라져있고, 다시 배치해도 언리얼 에디터를 닫기 전에 수정했던 값들이 사라져 있는 것을 볼 수 있다
이는 라이브 코딩이 에디터의 변경사항에만 영향을 미치고, 디스크에는 남아있지 않기 때문이다
그렇다면 앞으로 프로젝트를 열 때마다 컴파일을 하고 배치를 한 후 값들을 수정해야 하는 걸까?
이를 해결하기 위해 먼저 초기화된 값들이 언리얼 에디터에서 자동저장 되지 않도록 에디터를 바로 닫거나, 이미 초기값으로 자동 저장 된 경우에는 다시 값들을 변경한 후 레벨을 저장하고 언리얼엔진 에디터를 닫는다 (중요)
그리고 소스코드 에디터에서 [Build > Build Solution]을 선택하거나 (Ctrl + Shift + 'B') 단축키를 이용해서 솔루션을 빌드해준다
다시 프로젝트를 열면 지정했던 기본 맵이 열리면서, 컴파일 버튼을 누르지 않아도 자동으로 C++ Classes 폴더가 생성되어 있고
아웃라이너에서도 자동으로 배치가 되어 있으며, 값들도 사라지지 않고 수정했던 그대로 남아있는 것을 볼 수 있다
VR게임 개발을 위한 언리얼엔진/C++ 공부한 내용 끄적이기...
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!