함수 정의 및 기본 호출 방법
함수의 입력·출력과 지역 변수를 설계하고 호출 노드로 값을 주고받으며 반복 로직을 재사용하는 법을 익힙니다.
플레이어가 아이템을 주울 때마다 점수를 더하고, 효과음을 재생하고, 인벤토리에 아이템을 넣는 처리가 필요하다고 해 봅시다. 아이템 종류가 다섯 가지라면 같은 노드 묶음을 다섯 번 복사해 붙이게 되고, 점수 계산 방식을 바꿀 때마다 다섯 곳을 모두 고쳐야 합니다. 한 곳이라도 빠뜨리면 아이템마다 점수가 다르게 오르는 버그가 생깁니다.
함수(function)는 이런 반복을 없애는 방법입니다. 변수가 데이터를 담는 상자라면, 함수는 특정 작업을 하는 절차를 하나로 묶어 이름을 붙인 것입니다. 한 번 정의해 두면 필요한 곳에서 호출만 하면 되고, 고칠 때도 함수 한 곳만 고치면 됩니다. 요리로 비유하면 복잡한 레시피에서 "소스 만들기"를 별도의 과정으로 떼어 두고, 필요한 순간에 가져다 쓰는 것과 같습니다. 이 글에서는 점수를 더하는 AddScore 함수를 만들고 호출하면서, 함수의 입력과 출력, 지역 변수, 실행 핀이 있는 함수와 없는 함수의 차이를 익힙니다.
함수가 하는 일
블루프린트 함수는 특정 작업을 하는 노드들의 묶음입니다. 함수는 입력(input)을 받아 작업을 처리하고, 그 결과를 출력(output)으로 돌려줄 수 있습니다. 입력은 함수가 작업하는 데 필요한 정보이고, 출력은 작업을 마친 뒤 내놓는 결과입니다. 공식 문서에 따르면 함수의 입력과 출력에는 데이터 매개변수만 둘 수 있고, 각각 기본값을 정하거나 참조로 넘기게 할 수도 있습니다.
함수로 묶을 만한 경우는 크게 세 가지입니다. 먼저 같거나 비슷한 로직이 여러 곳에서 되풀이될 때 함수로 묶어 재사용합니다. 다음으로 하나의 긴 로직을 작은 함수 여러 개로 나누어 각 부분이 무엇을 하는지 이름으로 드러나게 합니다. 마지막으로 대미지 계산이나 아이템 사용처럼 한 가지 기능을 함수로 감싸 두면, 호출하는 쪽은 내부 구현을 몰라도 입력과 출력만 알고 쓸 수 있습니다. 이것을 캡슐화(encapsulation)라고 합니다.
함수 만들기
함수는 마이 블루프린트(My Blueprint) 패널에서 만듭니다. Functions 묶음 옆의 + 버튼을 누르고 이름을 입력하면, 예제에서는 AddScore, 그 함수의 그래프가 새 탭으로 열립니다. 이벤트 그래프와 비슷하게 생긴 이 공간이 함수의 내용을 정의하는 곳입니다. 그래프에는 함수 이름이 붙은 진입 노드(function entry node)가 하나 놓여 있는데, 함수가 호출되면 실행이 이 노드에서 시작됩니다.
입력과 출력은 진입 노드를 선택한 상태에서 디테일 패널에서 추가합니다. Inputs 항목의 + 버튼을 눌러 새 입력을 만들고 이름을 ScoreToAdd, 타입을 Integer로 정합니다. 결과를 돌려줄 함수라면 같은 디테일 패널의 Outputs에 출력을 추가하며, 출력을 추가하면 결과를 내보내는 반환 노드(Return Node)가 그래프에 생깁니다. AddScore는 점수 변수를 바꾸는 것이 목적이므로 반환할 데이터가 없어도 됩니다. 입력이나 출력의 개수를 바꾼 뒤에는 컴파일해야 이 함수를 호출하는 노드의 핀도 함께 바뀝니다.
AddScore의 내용 채우기
AddScore는 받은 ScoreToAdd를 지금의 PlayerScore에 더해 다시 PlayerScore에 저장합니다. 함수 그래프에 Get PlayerScore 노드를 놓고, 진입 노드의 ScoreToAdd 핀에서 끌어 + 노드(정수 덧셈)를 만든 뒤, 덧셈의 두 입력에 Get PlayerScore와 ScoreToAdd를 연결합니다. 다음으로 Set PlayerScore 노드를 놓고 덧셈 결과를 값 입력에 연결합니다. 마지막으로 진입 노드의 실행 핀을 Set PlayerScore의 실행 입력에 잇습니다. 반환 노드가 있다면 Set PlayerScore의 출력 실행 핀을 반환 노드에 이어 함수가 끝나는 지점을 분명히 합니다.
함수를 정의할 때는 입력, 내부 로직, 출력, 그리고 컴파일과 저장까지를 하나의 단위로 봅니다. 함수 정의를 먼저 끝내고 나서 이벤트 그래프의 어디서 호출할지를 정하면 흐름이 덜 헷갈립니다.
함수 안에서 잠깐 쓰고 버릴 계산 값은 지역 변수(local variable)에 둡니다. 함수 그래프를 열면 마이 블루프린트에 Local Variables 묶음이 나타나는데, 여기서 만든 변수는 함수가 호출될 때마다 새로 만들어지고 함수가 끝나면 사라집니다. 반대로 PlayerScore처럼 호출이 끝난 뒤에도 유지해야 하는 값은 블루프린트의 변수에 둡니다. 또 함수는 호출한 그 자리에서 끝까지 실행되어야 하므로, Delay처럼 시간이 지난 뒤에 이어서 실행되는 잠재(latent) 노드는 함수 안에 넣을 수 없습니다. 기다림이 필요한 흐름은 다음 글에서 볼 이벤트에 둡니다.
함수 호출하기
정의한 함수는 같은 블루프린트의 다른 그래프에서 호출합니다. 이벤트 그래프 탭으로 돌아와 빈 곳을 오른쪽 클릭하고 검색창에 AddScore를 입력해 선택하면 호출 노드가 만들어집니다. 마이 블루프린트의 Functions 목록에서 AddScore를 그래프로 끌어다 놓아도 됩니다.
AddScore처럼 변수를 바꾸는 함수의 호출 노드에는 실행 핀이 있으며, 이런 함수를 실행 핀이 있다는 뜻에서 비순수(impure) 함수라고 부릅니다. 이전 노드의 출력 실행 핀을 호출 노드의 실행 입력에 이어 함수가 언제 실행될지 정하고, 입력 핀에는 넘길 값을 연결하거나 직접 입력합니다. 함수에 출력이 있다면 호출 노드의 출력 핀을 다음 노드의 입력에 연결해 결과를 씁니다.
반대로 값을 계산해 돌려주기만 하고 블루프린트의 상태를 바꾸지 않는 함수는 디테일 패널에서 Pure를 켜 순수(pure) 함수로 만들 수 있습니다. 순수 함수의 호출 노드에는 실행 핀이 없고, 1장에서 본 것처럼 그 값을 쓰는 노드가 실행될 때 연결된 노드마다 한 번씩 평가됩니다. 그래서 상태를 바꾸는 함수를 순수 함수로 만들면 언제 몇 번 실행될지 예측하기 어려워지므로, 순수 함수에는 계산만 둡니다.
디테일 패널의 Access Specifier로 함수를 어디서 호출할 수 있는지도 정합니다. 기본값인 Public은 어느 오브젝트에서나 호출할 수 있고, Protected는 이 블루프린트와 이를 상속한 블루프린트에서만, Private는 이 블루프린트 안에서만 호출할 수 있습니다. 다른 블루프린트의 함수를 호출하려면 그 블루프린트의 인스턴스에 대한 참조가 있어야 하며, 이 방법은 4장에서 다룹니다.
BP_WelcomeMessage에서 AddScore 호출하기
이제 첫 번째 블루프린트 생성 및 실행에서 만든 BP_WelcomeMessage에서 AddScore를 호출해 봅니다. 블루프린트를 열고 앞 글의 PlayerScore(Integer)를 그대로 쓰거나 새로 만들어 기본값을 0으로 맞춥니다. 앞 글에서 BeginPlay에 만든 점수 증가 경로는 이번 호출 경로로 바꿔서, 점수가 두 번 더해지지 않게 합니다.
앞 절의 방법으로 AddScore 함수와 ScoreToAdd 입력을 만들고 내용을 채운 뒤 컴파일하고 저장합니다. 이벤트 그래프로 돌아와 Event BeginPlay 뒤에 AddScore 호출 노드를 잇고, ScoreToAdd 입력 칸에 100을 입력합니다. AddScore의 출력 실행 핀에 Print String을 잇고, Get PlayerScore를 In String에 연결합니다. 기본값 0에서 AddScore를 한 번 호출한 뒤 출력하므로 플레이하면 100이 나와야 합니다.
이벤트 그래프에는 이제 "BeginPlay에서 점수 100을 더하고 출력한다"는 흐름만 남고, 점수를 어떻게 더하는지는 AddScore 안에 숨었습니다. 나중에 점수에 배율을 곱하기로 했다면 AddScore 한 곳만 고치면 이 함수를 호출하는 모든 곳에 반영됩니다.
정리
- 함수는 특정 작업을 하는 노드를 묶어 이름을 붙인 것으로, 입력을 받아 처리하고 출력을 돌려줄 수 있습니다.
- 함수는 마이 블루프린트의 Functions
+로 만들고, 진입 노드를 선택해 디테일 패널에서 입력과 출력을 추가합니다. 출력을 추가하면 반환 노드가 생깁니다. - 함수 안에서만 쓰는 값은 지역 변수에, 호출 뒤에도 유지할 값은 블루프린트 변수에 둡니다. 함수에는
Delay같은 잠재 노드를 넣을 수 없습니다. - 상태를 바꾸는 비순수 함수는 실행 핀으로 호출 시점을 정하고, 계산만 하는 순수 함수는 실행 핀 없이 값을 쓸 때 평가됩니다.
Access Specifier로 Public, Protected, Private 가운데 호출할 수 있는 범위를 정합니다.
연습 문제
1. 호출을 두 번 이었을 때
같은 인스턴스의 PlayerScore가 10인 상태에서 AddScore(25), AddScore(-5), Print String 순서로 실행합니다. 다른 곳에서 점수를 바꾸지 않고, 함수는 입력을 현재 점수에 더해 Set합니다. 마지막에 출력되는 값은 얼마일까요?
해설 보기
첫 호출은 10 + 25 = 35를 저장합니다. 두 번째 호출은 기본값 10이 아니라 그 시점의 현재 값 35를 읽으므로 35 - 5 = 30을 저장하고, 마지막 출력은 30입니다.
만약 두 번째 호출 노드의 입력 칸에 -5가 적혀 있어도 실행 선이 첫 호출에서 그 노드를 거치지 않고 Print String으로 바로 이어졌다면, 그 호출은 일어나지 않아 출력은 35가 됩니다. 호출 노드가 그래프에 있는지와 실행이 실제로 그 노드에 도달하는지를 함께 확인해야 합니다.
2. 지역 변수와 블루프린트 변수
AddScore 안에 지역 변수 Total을 만들어 덧셈 결과를 저장하고, PlayerScore에는 아무것도 Set하지 않도록 바꿨습니다. AddScore(100)을 호출한 뒤 BeginPlay에서 PlayerScore를 출력하면 몇이 나올까요? Total 값은 함수가 끝난 뒤에도 남아 있을까요?
해설 보기
PlayerScore에 저장하지 않았으므로 기본값 0이 그대로 출력됩니다. Total은 지역 변수라서 함수가 호출될 때 만들어지고 함수가 끝나면 사라지므로, 함수 밖에서 그 값을 읽을 수 없습니다.
호출이 끝난 뒤에도 남아야 하는 점수는 블루프린트 변수 PlayerScore에 Set해야 합니다. 지역 변수는 함수 안에서 계산 중간값을 잠깐 담아 둘 때 씁니다.
3. 순수 함수로 바꿔도 될까
팀원이 "AddScore도 값을 계산하니 Pure를 켜서 실행 선을 줄이자"고 제안했습니다. 이 함수를 순수 함수로 바꾸면 어떤 문제가 생길 수 있을까요? 대신 순수 함수로 만들기 알맞은 함수는 어떤 것일까요?
해설 보기
AddScore는 PlayerScore를 바꾸는 함수입니다. 순수 함수는 실행 핀이 없고 그 출력값을 쓰는 노드가 실행될 때에만 평가되므로, 출력이 없는 AddScore를 순수 함수로 바꾸면 호출 노드를 놓아도 점수가 더해지지 않습니다. 결과를 내보내는 출력을 추가해 두 곳에 연결하면, 이번에는 연결된 노드마다 한 번씩 평가되어 점수가 두 번 더해집니다. 어느 쪽이든 점수가 언제 몇 번 바뀔지 그래프만 보고 예측하기 어려워집니다.
순수 함수로는 두 값을 받아 대미지를 계산해 돌려주기만 하는 CalculateDamage처럼, 블루프린트의 상태를 바꾸지 않고 입력에서 결과를 계산하는 함수가 알맞습니다.
함수의 입력과 출력, 순수 함수, 접근 지정자는 언리얼 엔진 공식 문서의 함수 설명에서 확인할 수 있습니다.
다음 글 이벤트 시스템의 기초 이해에서는 함수와 비슷해 보이지만 반환값이 없고 시간이 걸리는 흐름도 담을 수 있는 이벤트를 살펴봅니다.