이 문서는 프로그래밍 입문 문서의 서순을 준수하며, 해당 문서를 기반으로 작성되었습니다.
C++ 프로그래밍 및 더욱 자세한 내용은 프로그래밍 입문 문서를 참고해주세요.
블루프린트는 언리얼 엔진 4부터 지원하는 새로운 비주얼 스크립트 언어로, C++ 런타임을 기반으로 작동합니다.
기본적으로 블루프린트는 블루프린트에서 노출되도록 허용된 C++의 함수나 프로퍼티를 다음과 같이 노드로 치환합니다.
// GameplayStatics.h에 선언된 블루프린트 호출 가능 함수
class UGameplayStatics : public UBlueprintFunctionLibrary
{
/* ... */
UFUNCTION(BlueprintCallable, Category = "Audio", meta = (WorldContext = "WorldContextObject"))
static ENGINE_API void SetSoundMixClassOverride(const UObject* WorldContextObject, class USoundMix* InSoundMixModifier, class USoundClass* InSoundClass, float Volume = 1.0f, float Pitch = 1.0f, float FadeInTime = 1.0f, bool bApplyToChildren = true);
/* ... */
}
// 유저 클래스에서의 호출
UGameplayStatics::SetSoundMixClassOverride(GetWorld(), SCMIX.Get(), SoundAmbient.Get(), CommonSettings.AmbientSound);
유저 클래스에서 호출하는 C++ 함수 SetSoundMixClassOverride()
는 블루프린트에서 다음과 같이 치환됩니다.
블루프린트는 단순한 게임플레이 인터랙션 프로그래밍뿐만 아니라 레벨과 통합되어 레벨의 액터들을 제어하는 레벨 블루프린트, 캐릭터의 애니메이션을 상황에 따라 제어하는 애니메이션 블루프린트, UI를 만들고 제어하는 위젯 블루프린트 등 엔진의 다양한 영역에 사용됩니다.
블루프린트는 C++ 기반이지만 비주얼 스크립팅 언어로 구현하기 위해 C++ 네이티브보다는 같은 기능의 코드여도 몇 단계를 더 거칩니다. 따라서 블루프린트는 C++에 비해 동작 속도가 느린 편입니다. 하지만 C++ 및 엔진의 코어 기능을 잘 모르는 디자이너나 프로그래밍 비전공자들도 기초만 알면 편하게 프로그래밍할 수 있는 것은 분명한 장점이라고 할 수 있을 것입니다. 대규모 프로젝트의 경우 간단한 구현은 블루프린트로, 엔진의 코어 기능을 다루는 구현이나 복잡한 로직은 C++로 구현하는 편입니다.
블루프린트 클래스를 만드는 방법은 여러가지가 있습니다. 먼저 Content Drawer의 +Add 버튼을 통한 것입니다:
부모 클래스를 선택하면 Content Drawer에 새로운 블루프린트 애셋이 추가되며 이름을 짓고 편집할 수 있습니다.
블루프린트 에디터는 보통의 경우 위와 같습니다.
그래프 모드에서 노드를 배치할 때 거의 항상 블루프린트 그래프 안에 RMB시 나타나는 컨텍스트 메뉴를 사용합니다.
위 컨텍스트 메뉴에서 카테고리를 펼치거나 노드를 검색하여 추가하고자 하는 노드를 선택할 수 있습니다.
창 우측 상단에 Context Sensitive라는 옵션은 현재 상황 (Current Context)에서 사용할 수 있는 것들만 메뉴에 표시되도록 필터를 적용해 주는 옵션입니다.
기존 노드의 핀을 끌어놓을 때에도 컨텍스트 메뉴를 사용할 수 있습니다.
OOP (Object-Oriented Programming), 즉 객체지향 프로그래밍에서 사용되는 중요한 개념으로, 객체를 정의하기 위해 변수와 방법을 정의하는 템플릿입니다.
예시로, 사람이라는 종이 있습니다. 여기에는 사람의 장기, 사지 등 모든 사람들의 기본적인 공통점이 정리되어 있습니다. 하지만 인종, 더 나아가 개개인에 따라 세부적인 특성은 모두 다릅니다. 여기서 사람이라는 종은 기본적인 템플릿이고, 이 사람이라는 템플릿을 기반으로 개개인에게 차이를 두는 것입니다.
클래스는 세부적인 개념으로 파생될 수 있습니다. 위 예시로는 사람이라는 종을 프로그램에서 클래스라고 가정했을 때 사람 그 자체는 부모 클래스라고 부릅니다. 여기서 파생되는 황인, 백인, 흑인, 더 나아가 개개인이라는 개체로 분류되는데, 이를 자식 클래스로 비유할 수 있습니다. 상위 클래스의 공통점을 모두 가진 채 자식 클래스로 파생되는 것을 상속 (Inherit), 또는 확장 (Extend)라고 부릅니다. 상속된다고 해서 부모 클래스는 상속해준 기능을 잃는 것이 아니라 공유하는 것입니다.
블루프린트를 작성할 때에는 본인이 작성한 코드를 보기 쉽게 기능 및 작성 의도를 설명하는 것이 좋습니다. 이러한 설명을 주석이라고 부르며, 이 주석은 코드를 컴파일할 때 포함되지 않습니다. 주석은 코드를 작성하는 본인의 헷갈림을 해소해 주며, 협업 시 코드를 넘겨받는 협업자나 클라이언트에게 코드를 설명하기 용이해지는 장점이 있습니다.
이벤트 노드는 게임플레이 코드에서 호출되어 특정 동작을 실행하는 노드입니다.
핀의 구조는 다음과 같습니다.
AActor
의 하위 클래스, UActorComponent
의 하위 클래스 또는 레벨 블루프린트 등에서 사용되는 기본 제공 이벤트는 다음과 같습니다:
Event BeginPlay
: 이 클래스가 플레이 중인 레벨에 스폰될 때 한 번 호출됨 (플레이하지 않는 맵 (에디터 환경)이거나 기본값을 설정할 때에는 ConstructionSctipt
를 사용함)
void setup()
과 유사함Event Tick
: 게임의 매 프레임마다 호출됨
Event Tick
은 0.01666...초마다 호출됩니다.void loop()
, Processing의 void draw()
과 유사함Delta Seconds
: 프레임이 렌더링되는 데 걸리는 시간
Delta Seconds
는 0.01666...입니다.수학의 미지수와 비슷한 개념으로, 자연수가 될 수도 소수가 될 수도 있지만 자연수로 채택된 변수는 소수가 될 수 없습니다. 마찬가지로 반대도 불가능합니다. 이러한 변수의 등록 형식을 변수의 타입이라 부르며, 자연수와 소수를 같이 연산하려면 둘 중 하나의 타입을 바꾸어야 합니다.
변수는 이벤트그래프 죄측 하단 VARIABLES 탭에서 생성하거나 관리할 수 있습니다.
타입 | 들어갈 수 있는 값 |
---|---|
Boolean |
true , false |
Integer(64) |
-1 , 0 , 1000000 등 |
Float |
-1.0 , 3.14 등 |
FName |
"None" , "ScalarParameter" 등 |
FString |
"Hello" 등 |
FText |
"Hello" 등 (현지화 기능) |
FVector |
(X=0.00000, Y=0.00000, Z=0.00000) 등 (벡터 연산 기능) |
FRotator |
(Roll=0.0000, Pitch=0.00000, Yaw=0.00000 등 |
FTransform |
FVector : Location, FRotator : Rotation, FVector : Scale |
여기에서, 비슷하게 문자열을 다루는 FName
, FString
, FText
는 다음과 같이 구분할 수 있습니다.
FText
: 다국어 지원이 포함되어, 주로 위젯에 사용된다.FString
: 일반적인 문자열 처리FName
: 객체 이름, Niagara 사용자 변수, 머티리얼 스케일러 파라미터 등에 사용되는 성능이 중요한 상황에서 고유하 이름을 관리한다. 문장열 조작이 불가능하다.모든 변수는 전역 변수 (Global Variable)와 지역 변수 (Local Variable)로 나뉩니다. 전역 변수는 코드 내 어디에서든 접근하고 수정이 가능한 변수입니다. 반면 지역 변수는 선언된 함수 내에서만 접근 및 수정이 가능합니다.
일반적으로 변수에 값을 할당하는 것은 값을 복사하는 것입니다. 일반적으로 대입 연산자 (=
)는 값을 복사한다는 의미입니다. 따라서 복사된 값을 변경하는 것은 원본에 영향을 끼치지 않습니다. 일례로, 파일 탐색기에서 *.txt
파일을 복사한 * 복사본.txt
을 수정하는 것은 *.txt
에 아무런 영향을 미치지 않는 것과 같습니다.
따라서 언리얼 엔진에서 다른 액터나 클래스를 관리할 때, Object Reference
변수를 사용합니다. 일종의 바로가기와 비슷합니다. 다른 액터나 클래스에 접근할 때 참조 (Reference)하는 방식을 사용하는 이유는 오브젝트 레퍼런스가 아닌 값을 복사하는 방법으로는 원래 있는 클래스를 말 그대로 복사하는 것이므로, 접근하고자 하는 클래스를 복사하여 그 복사본을 수정하게 된다면 원본에는 아무런 영향이 가지 않기 때문입니다.
가령 현재 프로젝트의 구조가 이렇게 되어있다고 가정합시다.
BP_Character_0.txt
(ACharacter
에서 상속된 블루프린트 클래스 인스턴스)A
B
BP_Character
Object Reference) BP_Character 오브젝트 레퍼런스
Integer A
에 B
의 값을 할당하고, A
의 값을 바꾼다고 해서 B
의 값까지 바뀌지는 않습니다. 하지만 BP_Character
Object Reference 변수의 경우 아래와 같이 바로 가기처럼 작동합니다.
오브젝트 레퍼런스는 일종의 바로 가기 (Shortcut)처러 작동합니다. 바로 가기에 올바른 주소 (위 사진의 경우 Original 탭의 주소)가 할당되어 있다면 이 변수를 통해 레벨에 배치된 액터를 제어할 수 있습니다.
하지만 바로 가기의 주소를 할당하지 않으면 오브젝트 레퍼런스 변수는 그 어떤 것도 제어할 수 없게 됩니다. 주소가 할당되어야 그 주소를 통해 원본에 다다를 수 있기 때문입니다.
오브젝트 레퍼런스 변수에 주소가 제대로 할당되었는지에 대한 검사는 IsValid
함수 및 매크로를 통해 진행할 수 있습니다. 이 검사를 유효성 검사라고 부릅니다. 유효성 검사에 실패하는 경우는 보통 다음과 같습니다.
Destruct
및 Destroy
) 경우nullptr
) 경우유효하지 않은 오브젝트 레퍼런스에 접근하는 것은 위험합니다. 블루프린트의 경우 Blueprint Runtime Error: "Accessed None trying to read property ..." Blueprint: ...
오류가 발생합니다.
메모리 관리를 직접 해야 하는 C++의 경우, 원시 포인터를 사용한다는 가정 하에 해당 주소에 무관한 오브젝트나 변수가 있을 수도 있습니다. 이런 주소에 잘못 접근한다면 치명적 오류가 발생할 수 있습니다.
같은 타입의 변수 및 클래스 및 구조체로 이루어진 집합입니다. 리스트라고 생각해도 되지만 엄밀히는 다른 개념입니다.
예시: FString
배열의 경우
호출 번호 | 값 |
---|---|
0 |
"Hello," |
1 |
"Asterum!" |
2 |
"PLAVE" |
기본적으로 배열은 위 예시에서 0번을 지울 경우 1번이었던 "Asterum!"
은 계속 1번이지만, 언리얼 엔진은 자체 배열 시스템인 TArray
를 사용하므로 0번을 REMOVE
할 경우 1번이었던 "Asterum!"
은 0번으로 다시 할당됩니다.
배열의 기본적인 사용법은 다음과 같습니다.
작업을 수행하기 위해 설계된 코드의 집합으로, Arduino에서 썼던 println()
, Processing의 rect()
등은 모두 함수입니다. 함수의 목적에 따라 입력 값과 출력 값 (Return 값)이 있을 수 있습니다. 함수를 잘 쓰면 코드의 양이 획기적으로 줄고 가독성이 좋아집니다.
언리얼에서 이벤트와 함수의 대표적인 차이점은 다음과 같습니다:
Delay
가 잆는 한 기다리지 않고 바로 다음 코드를 호출한다.Latent
타입의 함수를 호출할 수 없다.함수는 단순히 코드를 묶는 용도가 아닌, 값을 계산하여 클래스의 프로퍼티 (변수)의 값을 바꾸거나, 그 값을 가져오거나, 클래스에 탑재된 기능을 실행하는 시작 지점 (엔트리 포인트)으로서 사용하는 것이 바람직합니다.
코딩하다 보면 비슷한 코드를 여기에도 쓰고 저기에도 쓰는 경우가 생깁니다. 이 때, 비슷한 코드의 다른 부분을 변수로 대체하여 어떤 상황에서든 함수가 유연하게 작동하도록 만들 수 있습니다.
클래스의 프로퍼티, 특히 private
맟 protected
필드의 프로퍼티는 특별한 이유 없이는 공개되어서는 안 됩니다. 이는 클래스의 기능 캡슐화를 위한 방법으로, 하드 코딩의 가능성을 줄입니다. 가령, 클래스의 프로퍼티를 바꾸면, 특정 기능은 바뀐 값을 토대로 작동되어야 한다고 가정해보겠습니다. 그렇다면 다른 함수나 클래스에서 변수의 프로퍼티를 수정할 때 일일이 그에 맞는 기능을 찾아서 구현해야 하는데, 실행해야 하는 기능이 바뀌면 프로퍼티를 수정하는 모든 코드를 수정해야 할 수 있습니다. 다시 말해, 프로퍼티의 값을 바꾸는 이유를 명확히 제시해야 한다는 것입니다.
따라서 프로퍼티의 값을 바꾸고 필요한 기능을 수행하는 코드를 함수로 묶으면 해당 클래스의 외부에서는 이 함수를 실행시키기만 하면 프로퍼티의 값을 바꾸고 필요한 기능을 바로 수행할 수 있습니다. 기능이 바뀌어야 한다면 함수에서 코드를 바꾸기만 하면 됩니다.
위 예시는 함수를 통해 프로퍼티의 값을 변경하고, 변경된 값을 사용하는 BlueprintSetter
의 예제입니다.
private
나 protected
필드의 프로퍼티는 외부 클래스에서 수정뿐만 아니라 값을 읽어오는 것 또한 불가능합니다. 따라서 프로퍼티의 값 또는 레퍼런스 (주소)를 반환하는 함수도 작성합니다.
하지만, 이 글에 따르면 Getter는 단순히 변수의 값을 반환만 하는 것이 아닌, 이 값을 사용하여 필요한 결과를 도출하여 반환하는 것을 권장합니다. 이는 프로퍼티를 만든 목적에 따라 다르니 상황에 맞게 유연하게 Setter와 Getter를 구현합니다.
Set "변수이름"
으로 생성된 노드의 왼쪽 유색 핀에 값을 넣으면 됩니다.
+
: 덧셈-
: 뺄셈*
: 곱셈/
: 나눗셈%
: 나눗셈의 나머지<
및 >
: 초과나 미만으로 좌우의 값을 비교했을 때 참이면 true
를, 거짓이면 false
를 반환합니다.
3 < 4 == true
3 > 4 == false
<=
및 >=
: 이상이나 이하로 좌우의 값을 비교했을 때 참이면 true
를, 거짓이면 false
를 반환합니다.
3 >= 4 == false
3 >= 3 == true
3 <= 4 == true
==
: 좌우의 값을 비교하고 같으면 true
를, 다르면 false
를 반환합니다.
int
형 변수 a
에 4가 저장되었을 때 a == 4
는 true
, a == 10
은 false
를 반환합니다.!=
: 좌우의 값을 비교하고 다르면 true
를, 같으면 false
를 반환합니다.true
와 false
두 가지를 비교할 때
&&
): 양쪽 다 true
일 때 true
를 반환합니다.
true && true == true
true && false == false
false && false == false
||
): 둘 중 하나만 true
여도 true
를 반환합니다.
true || false == true
true || true == true
false || false == false
true
의 개수가 홀수일 때 true
를 반환합니다.Branch에 들어가는 Condition: bool
핀이 true
이면 True
핀을, false
이면 False
핀을 실행합니다.
switch
에 들어가는 어떤 한 변수에 대해서 case
와 동일 값의 핀을 실행하고 동일한 조건이 없다면 default
를 실행합니다.
First Index
부터 Last Index
까지 값을 1씩 더하거나 빼며, First Index
가 Last Index
에 도달할 때까지 Loop Body
핀을 계속 반복하여 실행합니다. 이 때, Index
핀의 값을 통해 현재의 Index 값을 알 수 있습니다. 반복이 끝나면 Completed
핀을 실행합니다.
Array
의 배열을 기반으로, 배열의 길이만큼 Loop Body
핀을 반복합니다. 이 때, Array Index
핀의 값을 통해 현재 반복 주기의 배열 번호를, Array Element
핀의 값을 통해 배열의 Array Index
값을 가져올 수 있습니다. 반복이 끝나면 Completed
핀을 실행합니다.
이 문서에서는 클래스에 대해 간략하게 서술합니다.
클래스에 대한 자세한 내용은 프로그래밍 입문 문서를 참고해주세요.
클래스란 어떠한 단일 기능을 위한 코드의 집합으로, 클래스의 기능을 기반으로 세부적으로는 다르거나 추가된 기능으로 작동하는 클래스가 필요할 때, 클래스를 상속 (Inherit
또는 Extend
)할 수도 있습니다.
언리얼 런타임에 직접적으로 사용되는 클래스는 모두 UObject
기반입니다.
UObject
: 언리얼 런타임의 베이스 클래스
AActor
: 레벨에 직접적으로 배치되는 최상위 단위
APawn
: 플레이어나 AI가 제어할 수 있는 액터
ACharacter
: 기본적인 움직임이 가미된 폰AController
: 플레이어나 AI 개체 입장에서 볼 때 Controller, 컨트롤러는 본질적으로 뇌를 말합니다.
APlayerController
: 플레이어에게 받은 입력을 이동, 아이템 사용, 무기 발사와 같은 동작으로 변환하는 함수성을 구현합니다.AAIController
: 사람 플레이어의 입력 없이 주변 월드를 관찰하고 의사를 결정한 뒤 알맞게 반응합니다.UVisual
UWidget
UUserWidget
: 위젯 블루프린트의 기반이 되는 클래스UActorComponent
: 액터 내 탑재될 수 있는 컴포넌트
USceneComponent
UPrimitiveComponent
UMeshComponent
UWidgetComponent
이 계층은 부계/모계 -> 자식에게 주는 유전과 같습니다. 상속이라고 해서 상위 클래스가 사라지는 게 아니라, 기능을 더하고 더하며 분화되는 것입니다.
아래와 같이 치환하면 이해하기 쉬울 것입니다.
기본적으로 플레이어 캐릭터를 만들 때 언리얼 엔진에 기본 탑재된 ACharacter
클래스를 상속한 블루프린트 클래스를 만들어 캐릭터를 만듭니다. 게임 UI를 만들 때에는 UUserWidget
클래스를 상속한 위젯 블루프린트를 만들어 UI를 제작합니다.
블루프린트 클래스를 제작하거나 수정할 때 에디터의 우상단에 존재하는 Parent Class는 현재 수정 중인 블루프린트 클래스가 상속받은 베이스가 되는 클래스입니다. Event BeginPlay
, Event Tick
등의 이벤트는 모두 상위 클래스에서 상속된 기능의 일부입니다.
클래스의 형변환은 큰 집합(부모 클래스)과 작은 집합(상속 클래스)의 경계를 넘나들어 변환하는 기능입니다.
나는 사람이다.
나 -> 사람
업캐스팅이란, 상속된 클래스를 부모 클래스로 변환하는 것을 말합니다. AActor
는 UObject
에서 상속된 클래스이므로, UObject
레퍼런스 변수에 AActor
인스턴스의 주소를 대입할 수 있습니다. 다만 UObject
클래스 변수에 들어간 AActor
인스턴스는 UObject
의 공통 기능만을 사용할 수 있습니다. SetActorLocation()
등은 사용할 수 없습니다.
예로 고등어과는 동물계에 속하므로, 고등어는 동물이라고 말할 수 있습니다. 동물의 공통적인 특성은 스스로 움직일 수 있다는 것입니다. 하지만 고등어->동물로 업캐스팅되었으면 이 고등어에게는 움직여라 정도의 지시만 가능합니다. 고등어의 고유 특성인 지느러미를 움직여라, 아가미로 호흡해라 등은 동물의 공통 특징이 아니므로 지시할 수 없다는 것입니다.
업캐스팅은 다양한 상속된 클래스를 배열에 모아 부모 클래스의 공통된 함수를 실행하거나 변수를 조작할 때 주로 사용합니다.
나는 사람이지만, 모든 사람이 나인 것은 아니다.
하지만 다음과 같은 경우는 가능하다: 나 -> 사람 -> 나
예컨대 트리거 박스의 OnActorBeginOverlap
델리게이트를 통해 오버래핑된 액터를 검출한다고 가정합시다. 우리가 만든 블루프린트 클래스는 모두 언리얼 엔진의 클래스를 상속하여 만들어진 클래스입니다. 하지만, 언리얼 엔진에서 기본적으로 제공하는 함수와 델리게이트는 우리가 만든 클래스를 바로 감지하지 못하며, OnActorBeginOverlap
으로는 Other Actor: AActor
만 검출할 수 있습니다. 이를 해결하고 우리가 만든 클래스의 기능을 모두 사용하려면 업캐스팅과 다운캐스팅의 개념에 대해 알아야 합니다.
여기에서 AActor
를 동물, 우리가 만든 블루프린트 액터를 고등어라고 비유해보겠습니다. 동물을 감지할 수 있는 OnActorBeginOverlap
은 무조건 동물만을 반환하므로, 이 돌물이 고등어인지 아닌지 수동으로 검사해야 합니다. 동물을 반환한다고 해서 고등어의 특성을 모두 잃은 상태로 반환하는 것이 아닙니다. 단지 업캐스팅된 상태일 뿐입니다. 따라서 이 동물에 대해 고등어로 다운캐스팅에 성공하면, 이 동물은 고등어이므로 고등어의 모든 기능을 사용할 수 있습니다.
하지만 OnActorBeginOverlap
에 닿은 동물이 고등어가 아닐 때가 있습니다. 쉽게 말해, 모든 동물은 고등어가 아니라는 것입니다. 이 때 다운캐스팅을 진행하면 캐스팅에 실패합니다. 우리가 만든 블루프린트 액터는 AActor
이지만, AActor
그 자체는 우리가 만든 블루프린트 액터가 아닙니다. 이 특성을 잘 알아야 합니다.