함수 라이브러리 생성 및 활용
블루프린트 함수 라이브러리에 상태 없는 공통 유틸리티를 정의해 여러 블루프린트에서 재사용하는 법을 익힙니다.
두 액터 사이의 거리를 구하는 함수를 플레이어 캐릭터에 만들어 두었는데, 적 AI와 퀘스트 관리자도 같은 계산이 필요해졌다고 해 봅니다. 함수를 블루프린트마다 복사하면 계산을 고칠 때 세 곳을 모두 찾아 고쳐야 하고, 플레이어 캐릭터의 함수를 빌려 쓰려면 적 AI가 플레이어 캐릭터의 참조를 먼저 구해야 합니다. 어느 액터에도 속하지 않는 공통 계산이라면, 어느 액터에도 두지 않는 편이 자연스럽습니다.
이번 장에서는 데이터 테이블, SaveGame 객체, 에셋 레퍼런싱으로 데이터를 관리하는 방법을 다뤘습니다. 이 글에서는 여러 블루프린트에서 함께 쓰는 유틸리티 함수를 한곳에 모아 두는 블루프린트 함수 라이브러리(Blueprint Function Library)를 만들고, 다른 블루프린트에서 호출해 봅니다.
인스턴스 없이 호출하는 함수 모음
블루프린트 함수 라이브러리는 특정 액터나 객체에 속하지 않는 정적(static) 함수의 모음입니다. 공식 문서도 함수 라이브러리를 게임플레이 객체에 묶이지 않는 유틸리티 기능을 담는 정적 함수의 모음으로 설명합니다. 함수 라이브러리의 함수는 라이브러리 객체를 따로 만들지 않고, 어느 블루프린트에서든 이름을 검색해 바로 호출할 수 있습니다.
일반 블루프린트의 함수와 비교하면 차이가 분명해집니다. BP_PlayerCharacter에 정의한 함수를 호출하려면 BP_PlayerCharacter의 인스턴스가 있어야 하고, 그 인스턴스의 참조를 Target 핀에 연결해야 합니다. 함수 라이브러리의 함수는 어떤 인스턴스에서 실행되는지가 정해져 있지 않으므로 이런 참조가 필요 없습니다. 다만 월드에 접근하는 함수나 특정 대상의 참조를 입력으로 받는 함수라면, 그 함수가 요구하는 호출 문맥과 입력은 호출하는 쪽이 갖춰 주어야 합니다.
공통 기능을 함수 라이브러리에 모으면 다음과 같은 점이 좋아집니다. 자주 쓰는 계산을 필요한 곳에서 바로 가져다 쓸 수 있고, 특정 액터에 의존하지 않으므로 블루프린트 사이의 연결이 줄어듭니다. 계산을 고쳐야 할 때도 라이브러리 한 곳만 고치면 모든 호출 위치에 반영되고, 반복되던 노드 묶음이 함수 노드 하나로 줄어 그래프가 읽기 쉬워집니다. 다만 공통 계산을 정적 함수로 옮기는 것은 수정 범위를 줄이려는 정리이지, 그것만으로 실행 속도가 빨라지는 것은 아닙니다.
함수 라이브러리 만들고 함수 정의하기
콘텐츠 브라우저의 빈 곳을 오른쪽 클릭하고, 블루프린트 하위 메뉴에서 Blueprint Function Library를 선택합니다. 에디터 버전에 따라 메뉴 배치가 조금 다를 수 있습니다. 이름은 BFL_GameUtilities로 짓습니다. 함수 라이브러리에는 흔히 BFL_ 접두사를 붙입니다.
BFL_GameUtilities를 열면 일반 블루프린트와 비슷한 에디터가 나타납니다. 마이 블루프린트(My Blueprint) 패널의 함수(Functions) 목록에서 추가 버튼으로 새 함수를 만들고, 이름을 CalculateDistanceBetweenActors로 정합니다. 디테일 패널에서는 입력으로 ActorA와 ActorB(둘 다 Actor Object Reference)를, 출력으로 Distance(Float)를 추가합니다.
함수 안의 로직은 두 위치의 차이 벡터를 구해 그 길이를 반환하는 것입니다. ActorA와 ActorB를 각각 Get Actor Location에 연결해 위치를 얻고, 두 벡터를 빼서 나온 차이 벡터를 Vector Length에 넣은 뒤, 그 결과를 반환 노드의 Distance에 연결합니다. 이때 위치 하나의 길이를 계산하는 것과 헷갈리지 않아야 합니다. 위치 벡터 하나의 길이는 월드 원점까지의 거리이지 두 액터 사이의 거리가 아닙니다.
이 함수는 두 액터가 유효하다고 가정하고 계산하므로, 유효성 확인은 호출하기 전에 호출하는 쪽에서 한다는 규칙을 정해 둡니다. 함수 라이브러리의 함수는 기본적으로 정적이라서, 그래프에 놓은 함수 노드에는 Target 핀이 없습니다. 함수를 다 정의했으면 컴파일하고 저장합니다.
참고로 엔진에는 이미 액터 사이의 거리를 구하는 Get Distance To 노드가 있습니다. 여기서 직접 만드는 것은 함수 라이브러리의 구조를 익히기 위해서이며, 실제 프로젝트에서도 엔진이 제공하는 노드가 있는지 먼저 찾아보는 편이 좋습니다.
거리 계산을 손으로 확인하기
함수가 맞게 계산하는지 숫자로 따라가 봅니다. 유효한 두 액터 A와 B의 월드 위치를 각각 (100, 200, 0) cm와 (400, 600, 0) cm로 둡니다. A에서 B를 빼면 차이 벡터는 (-300, -400, 0) cm이고, 그 길이는 √(300² + 400²) = 500 cm, 즉 5 m입니다. 이 함수가 500을 돌려주면 계산이 맞는 것입니다.
만약 A의 위치 벡터 길이만 읽었다면 √(100² + 200²), 약 223.6 cm가 나옵니다. 이것은 월드 원점에서 A까지의 거리이므로 두 액터 사이의 거리와 다릅니다. 이런 실수는 결과가 그럴듯한 숫자로 나오기 때문에 놓치기 쉬우므로, 손으로 계산한 값과 비교해 보는 습관을 들입니다.
다른 블루프린트에서 호출하기
이제 BP_PlayerCharacter나 BP_GameManager처럼 함수를 쓸 블루프린트를 열고, 이벤트 그래프의 빈 곳을 오른쪽 클릭해 CalculateDistanceBetweenActors를 검색합니다. 함수 라이브러리의 함수는 검색 목록에 그 함수가 속한 라이브러리와 함께 표시되므로, 이름이 비슷한 다른 함수와 구별할 수 있습니다. 함수를 골라 그래프에 놓습니다.
ActorA와 ActorB 입력에는 실제로 거리를 잴 액터의 참조를 연결합니다. 예를 들어 Get Player Character로 얻은 플레이어와 Get Actor Of Class로 찾은 적 액터를 연결할 수 있습니다. 두 참조가 유효한지 먼저 확인한 뒤 실행 선을 함수 호출에서 Print String으로 잇고, Distance 값을 문자열로 바꿔 출력합니다. Get Actor Of Class가 액터를 찾지 못하면 빈 참조를 돌려주므로, 그대로 함수에 넘기지 않도록 합니다. 컴파일하고 플레이해서 출력된 거리가 예상과 같은지 확인합니다.
함수 라이브러리를 설계할 때 고려할 것
순수 함수로 만들지 정하기
함수 라이브러리의 함수도 목적에 따라 순수 함수로 만들 수도 있고, 실행 핀이 있는 함수로 만들 수도 있습니다. 함수의 디테일 패널에서 Pure 옵션을 켜면 실행 핀이 없는 순수 함수 노드가 되고, 끄면 실행 핀이 생깁니다. 순수 함수는 입력으로 받은 값으로 출력을 계산하는 데 쓰고, 무언가를 저장하거나 바꾸는 부작용이 있는 함수는 실행 핀이 있는 함수로 만들어 실행 순서가 그래프에 드러나게 합니다.
Pure 표시가 결과를 저장해 두었다가 다시 쓰는 기능은 아니라는 점도 알아 둡니다. 같은 입력이면 같은 결과가 나온다는 보장도 하지 않습니다. 예를 들어 CalculateDistanceBetweenActors에 같은 두 액터의 참조를 넘겨도, 그 사이에 액터가 움직였다면 거리는 달라집니다. 순수 함수는 연결된 노드가 값을 필요로 할 때마다 다시 계산되기 때문입니다.
데이터 관리 기능과 묶기
앞에서 배운 데이터 테이블이나 SaveGame과 연결되는 유틸리티도 함수 라이브러리에 두기 좋습니다. 예를 들어 행 이름을 받아 아이템 데이터 테이블에서 데이터를 찾아 주는 Get Item Data (By Row Name) 같은 함수를 BFL_GameUtilities에 정의하면, 조회와 실패 처리를 여러 블루프린트가 같은 방식으로 쓸 수 있습니다.
상태는 라이브러리 밖에 두기
함수 라이브러리를 액터처럼 값을 보관하는 저장소로 쓰지 않습니다. 함수 안의 지역 변수와 입력, 출력은 쓸 수 있지만, 호출과 호출 사이에 유지되어야 하는 값은 라이브러리가 아니라 그 값을 가진 객체에 두어야 합니다. 함수가 외부 상태를 읽어야 한다면 그 상태를 가진 객체의 참조를 입력으로 받아, 무엇에 의존하는지가 함수 노드에 드러나게 합니다.
또 함수 라이브러리는 UBlueprintFunctionLibrary를 부모로 하는 공용 정적 함수의 모음일 뿐입니다. 액터마다 다르게 동작해야 하는 기능은 상속으로 함수를 재정의하는 설계가 맞고, 함수 라이브러리로 대신할 일이 아닙니다. 함수 라이브러리는 모든 로직을 몰아넣는 곳이 아니라, 여러 곳에서 부르는 작은 도구를 모아 두는 곳으로 쓸 때 프로젝트의 구조가 깔끔하게 유지됩니다.
정리
- 블루프린트 함수 라이브러리는 특정 액터에 속하지 않는 정적 함수의 모음이며, 인스턴스나 Target 핀 없이 어느 블루프린트에서든 호출합니다.
- 공통 계산을 라이브러리에 모으면 수정할 곳이 한 곳으로 줄지만, 정적 함수로 옮기는 것만으로 실행 속도가 빨라지지는 않습니다.
- 두 액터 사이의 거리는 두 위치의 차이 벡터의 길이이며, 위치 벡터 하나의 길이와 구별해야 합니다.
- 부작용이 없는 계산은 순수 함수로, 저장이나 변경이 있는 함수는 실행 핀이 있는 함수로 만들며,
Pure는 결과를 저장해 두는 기능이 아닙니다. - 호출 사이에 유지할 상태는 라이브러리가 아닌 객체에 두고, 필요하면 참조를 입력으로 받습니다.
연습 문제
1. B가 움직인 뒤의 거리
본문의 A는 (100, 200, 0) cm에 그대로 두고, B를 (100, 500, 0) cm로 옮겼습니다. CalculateDistanceBetweenActors의 결과는 얼마일까요? 같은 두 액터 참조를 넘긴다면 이전 결과인 500을 그대로 써도 될까요?
해설 보기
차이 벡터는 (0, -300, 0) cm이므로 결과는 300 cm, 즉 3 m입니다. 같은 액터 참조를 넘겨도 그 액터의 위치가 바뀌었으므로 계산 결과가 달라집니다.
Pure 표시는 이전 결과를 고정해 두는 기능이 아니므로, 값이 필요할 때마다 새 위치로 다시 계산됩니다. 실제로 확인할 때는 두 액터 참조가 유효한지 먼저 검사하고, 두 위치와 출력 단위를 함께 기록해 두면 결과를 비교하기 쉽습니다.
2. 라이브러리에 처치 수를 모으기
동료가 "적을 처치할 때마다 BFL_GameUtilities의 함수를 불러 전체 처치 수를 1씩 늘리자"고 제안했습니다. 이 설계에는 어떤 문제가 있으며, 처치 수는 어디에 두는 것이 알맞을까요?
해설 보기
함수 라이브러리는 인스턴스가 없는 정적 함수의 모음이라서, 호출과 호출 사이에 값을 보관하는 곳으로 쓰기에 맞지 않습니다. 함수 안의 지역 변수는 호출이 끝나면 사라지므로 처치 수가 누적되지 않습니다.
처치 수처럼 게임이 진행되는 동안 유지할 값은 그 값을 가질 객체에 둡니다. 레벨이 바뀌어도 유지해야 한다면 4장에서 다룬 GameInstance가, 한 판의 규칙에 속한 값이라면 9장에서 다룰 GameMode나 GameState가 후보입니다. 라이브러리 함수가 이 값을 바꿔야 한다면 그 객체의 참조를 입력으로 받게 합니다.
3. 순수 함수로 만들어도 될까
BFL_GameUtilities에 두 함수를 만들려고 합니다. (가) 체력과 최대 체력을 받아 0에서 1 사이의 비율을 돌려주는 GetHealthRatio, (나) 점수를 받아 SaveGame에 기록하고 성공 여부를 돌려주는 SaveScore. 각각 Pure를 켜는 것이 알맞을까요?
해설 보기
(가)는 입력만으로 출력을 계산하고 아무것도 바꾸지 않으므로 순수 함수로 만들기에 알맞습니다. 다만 최대 체력이 0일 때 나눗셈을 어떻게 처리할지는 함수 안에서 정해 두어야 합니다.
(나)는 저장 파일을 기록하는 부작용이 있으므로 Pure를 끄고 실행 핀이 있는 함수로 만듭니다. 순수 함수로 만들면 값을 쓰는 노드가 실행될 때마다 저장이 다시 일어날 수 있고, 언제 저장되는지가 실행 선으로 드러나지 않아 문제를 찾기 어려워집니다.
함수 라이브러리의 구조와 정적 함수 규칙은 언리얼 엔진 공식 문서의 블루프린트 함수 라이브러리에서 확인할 수 있습니다.
이것으로 5장을 마칩니다. 5장에서는 구조체와 열거형으로 데이터를 묶고 분류하는 방법에서 시작해, 데이터 테이블로 정형 데이터를 관리하고, SaveGame 객체로 진행 상황을 저장하고 불러오며, 그 바탕인 직렬화와 하드·소프트 에셋 참조를 살펴본 뒤, 공통 기능을 함수 라이브러리로 모으는 방법까지 데이터를 관리하는 기본기를 익혔습니다. 다음 장의 위젯 블루프린트의 기본 개념부터는 위젯 블루프린트로 화면 UI를 만들고, 동적 UI와 애니메이션, 게임플레이와의 상호작용을 거쳐 간단한 인벤토리까지 구현합니다.