이벤트 디스패처의 기본 사용법
이벤트 디스패처를 선언·바인딩·호출해 한 액터의 변화를 여러 수신자에게 느슨하게 전달하는 법을 익힙니다.
문이 열릴 때 게임 매니저는 진행도를 기록하고, 사운드 매니저는 효과음을 내고, UI는 안내 문구를 띄워야 한다고 해 봅시다. 직접 참조를 통한 블루프린트 통신의 방식대로라면 문 블루프린트가 세 대상의 참조를 모두 가지고 각자의 함수를 하나씩 불러야 합니다. 반응할 대상이 하나 늘 때마다 문을 고쳐야 하고, 문은 자신과 상관없는 매니저들의 구체 타입까지 알아야 합니다. 이런 강한 결합(tight coupling)은 프로젝트가 커질수록 고치기 어려운 구조를 만듭니다.
이벤트 디스패처(Event Dispatcher)는 방향을 뒤집어 이 문제를 줄입니다. 문은 "열렸다"는 사실만 알리고, 그 소식이 필요한 쪽이 미리 등록해 두는 방식입니다. 이 글에서는 디스패처를 만들고, 호출하고, 다른 블루프린트에서 등록하고 해제하는 과정을 따라가며, 등록 시점과 해제에서 자주 생기는 실수를 함께 살펴봅니다.
사건을 알리는 신호기
이벤트 디스패처는 어떤 일이 일어났음을 알리는 신호기와 같습니다. 디스패처를 가진 블루프린트는 사건이 일어나면 신호를 보내고, 그 신호를 받고 싶은 블루프린트는 신호가 오면 실행할 자신의 이벤트를 미리 등록합니다. 신호를 보내는 쪽은 누가 등록했는지, 등록한 쪽이 무엇을 할지 알 필요가 없으므로 둘 사이의 결합이 느슨해집니다. 이것을 느슨한 결합(loose coupling)이라고 합니다.
다만 완전히 서로를 모르는 것은 아닙니다. 등록하려면 수신하는 쪽이 디스패처를 가진 액터의 참조를 알아야 합니다. 디스패처는 보내는 쪽이 받는 쪽을 몰라도 되게 만드는 것이지, 받는 쪽이 보내는 쪽을 몰라도 되게 만드는 것은 아닙니다.
디스패처를 다루는 동작은 네 가지입니다. 먼저 알릴 사건을 나타내는 디스패처를 만들고(생성), 수신하는 블루프린트가 자신의 이벤트를 디스패처에 등록하며(Bind), 디스패처를 가진 블루프린트가 사건이 일어났을 때 디스패처를 호출하고(Call), 더 이상 듣지 않을 때 등록을 해제합니다(Unbind). 이 밖에 Assign은 등록할 이벤트를 만드는 일과 Bind 노드를 함께 배치해 주는 편의 기능이며, Bind 뒤에 반드시 거쳐야 하는 별도 단계는 아닙니다.
문에 디스패처 만들기
디스패처는 사건을 알릴 액터 블루프린트 안에 정의합니다. 문이 열릴 때 알리고 싶다면 BP_Door 안에 만듭니다. 예제를 위해 BP_Door 액터 블루프린트를 새로 만들어 열어도 됩니다.
마이 블루프린트(My Blueprint) 패널의 이벤트 디스패처(Event Dispatchers) 항목에서 + 버튼을 누르고 이름을 OnDoorOpened로 정합니다. 사건과 함께 정보를 넘기고 싶다면 디스패처를 선택한 채 디테일 패널의 Inputs에서 입력을 추가합니다. 문을 연 액터가 누구인지 함께 알리고 싶다면 Actor 오브젝트 참조 타입의 입력 OpeningActor를 추가하는 식이며, 이렇게 정의한 입력은 디스패처를 호출할 때 수신자에게 함께 전달됩니다. 정의를 마치면 컴파일하고 저장합니다.
디스패처 호출하기
디스패처를 호출하는 것은 신호를 보내는 일입니다. 호출하면 그 순간 등록되어 있는 이벤트들이 실행됩니다. 이때 수신자들이 병렬로 실행되거나 특정 순서로 실행된다는 보장은 없으므로, 순서가 중요한 작업은 디스패처에 맡기지 말고 명시적인 호출 흐름으로 만듭니다.
BP_Door의 그래프에서 문을 여는 OpenDoor 함수의 끝이나, 상호작용 이벤트에서 문이 열리는 시점에 호출을 둡니다. 마이 블루프린트 패널의 OnDoorOpened를 그래프로 끌어다 놓으면 나오는 메뉴에서 Call을 고르면 Call OnDoorOpened 노드가 생깁니다. 이 노드를 문이 열리는 로직의 알맞은 위치에 실행 선으로 잇고, OpeningActor 입력이 있다면 그 핀에 문을 연 액터의 참조를 연결합니다.
다른 블루프린트에서 등록하기
이제 다른 블루프린트가 BP_Door의 OnDoorOpened를 듣고 반응하게 만들어 보겠습니다. 편의상 Actor를 부모로 BP_GameManager를 새로 만들어 씁니다.
등록하려면 먼저 이 블루프린트가 BP_Door 인스턴스를 알아야 합니다. 가장 간단한 방법은 Event BeginPlay에서 Get All Actors Of Class로 월드의 BP_Door들을 가져오는 것입니다. 배열이 비어 있지 않은지 확인한 뒤, 정해 둔 기준으로 원하는 문을 고르거나 For Each Loop로 모든 문에 등록합니다. 앞 글에서 본 것처럼 배열의 첫 요소가 어느 문인지는 정해져 있지 않으므로 순서를 기준으로 삼지 않습니다. 고른 문의 참조는 BP_Door 오브젝트 참조 타입의 변수 MyDoorRef로 승격해 저장해 두면 편리합니다.
다음으로 MyDoorRef의 Get 노드 출력 핀에서 드래그해 Bind Event to OnDoorOpened 노드를 만들고, Target 핀에 MyDoorRef를 연결합니다. 이 Bind는 MyDoorRef가 유효할 때만 실행되게 합니다. Bind Event to OnDoorOpened의 Event 핀에는 디스패처가 호출될 때 실행할 커스텀 이벤트(Custom Event)를 연결해야 합니다. Event 핀에서 드래그해 Add Custom Event를 고르면 새 커스텀 이벤트 노드가 생기는데, 이름을 HandleDoorOpenEvent로 바꿉니다. 디스패처에 OpeningActor 입력이 있었다면 이 커스텀 이벤트에도 같은 매개변수 핀이 자동으로 생깁니다.
HandleDoorOpenEvent의 실행 핀 뒤에는 신호를 받았을 때 할 일을 만듭니다. 예를 들어 Print String으로 "게임 매니저: 문이 열렸습니다! 연 사람: " 뒤에 이름을 붙여 출력할 수 있습니다. OpeningActor가 유효하면 Get Display Name으로 얻은 문자열을 Append로 붙이고, 유효하지 않다면 "알 수 없음" 같은 대체 문구를 붙입니다. 두 블루프린트를 모두 컴파일하고 저장합니다.
확인하려면 레벨에 BP_Door와 BP_GameManager를 배치하고 플레이한 뒤 문이 열리게 합니다. 예를 들어 블루프린트 인터페이스 활용에서 만든 것처럼 BP_Door에 Interact를 구현하고, 플레이어가 상호작용 키를 누르면 Interact 메시지를 보내게 할 수 있습니다. 문이 열리는 순간 BP_GameManager에 등록된 커스텀 이벤트가 실행되어 게임 매니저의 메시지가 출력됩니다.
등록하기 전에 보낸 알림
디스패처는 호출하는 순간 등록되어 있는 이벤트만 실행합니다. 그래서 등록하기 전에 일어난 알림은 나중에 등록한 수신자에게 다시 전달되지 않습니다. 여러 액터의 BeginPlay가 어떤 순서로 실행되는지에 기대어 "매니저가 먼저 등록하겠지"라고 가정하면, 순서가 바뀌는 순간 알림을 놓칩니다. 등록을 마친 뒤에야 문 열기 같은 사건을 허용하거나, 수신자가 등록할 때 현재 상태를 따로 읽어 초기화하는 식으로 설계합니다.
문의 움직임이나 입력을 아직 만들지 않았다면, 등록과 알림만 먼저 확인할 수 있습니다. BP_Door에 출력이 없는 OpenDoor 함수를 만들고, 그 안에서 문구를 출력한 뒤 Call OnDoorOpened를 실행하게 잇습니다. OpeningActor 입력을 쓴다면 OpenDoor에도 같은 입력을 추가해 디스패처에 그대로 넘깁니다. 그리고 BP_GameManager에서 유효한 MyDoorRef를 저장하고 Bind를 마친 실행 출력 뒤에 MyDoorRef의 OpenDoor를 한 번 호출하며, 입력에는 매니저 자신인 Self를 넣습니다.
두 액터를 배치하고 저장한 뒤 플레이하면, 등록된 HandleDoorOpenEvent의 출력이 한 번 나타나야 합니다. 이 순서에서는 등록이 끝난 뒤에 같은 실행 흐름 안에서 사건을 일으키므로, 다른 액터의 BeginPlay 순서에 기대지 않습니다. 확인을 마치면 원래 목표인 상호작용 경로로 사건을 일으켜 봅니다.
중복 등록과 해제
같은 대상의 같은 이벤트를 같은 디스패처에 여러 번 Bind해도 한 번만 등록됩니다. 반면 서로 다른 이벤트는 여러 개 등록할 수 있으므로, 한 수신자가 이벤트 두 개를 각각 Bind하면 알림 한 번에 두 이벤트가 모두 실행됩니다.
수신을 끝낼 때는 자신이 등록한 이벤트를 Unbind Event로 해제합니다. 수신자가 먼저 사라지거나 더 이상 반응하면 안 될 때 해제를 빠뜨리면, 원하지 않는 반응이 계속 실행될 수 있습니다. 이와 비슷한 이름의 Unbind All Events는 그 디스패처에 등록된 모든 이벤트를 해제하므로, 다른 수신자의 등록까지 함께 지워 버립니다. 내 등록만 끊으려면 Unbind Event를 씁니다.
등록된 두 수신자에게 알림 보내기
지금까지의 흐름을 그림으로 정리해 보겠습니다. 아래 그림은 문 인스턴스 하나의 OnDoorOpened에 게임 매니저와 사운드 매니저가 미리 등록을 마친 상태를 보여 줍니다.
등록된 수신자에게 알림 전달
본문의 Bind·Call·Unbind에서 현재 등록 집합이 바뀌는 점을 보여 주어 등록 전 알림이 재생되지 않으며 해제가 필요하다는 점을 드러낸다. 두 수신자는 같은 알림 단계에 묶고 호출 순서를 만들지 않는다.
BOUND EVENTS · ONE OWNER
등록을 마친 수신자에게 문 열림을 알립니다
두 수신자가 이 문 인스턴스의 OnDoorOpened에 미리 바인드된 상태입니다. Call은 현재 등록된 이벤트들을 호출하며 수신자 순서나 병렬 실행을 보장하지 않습니다.
화살표는 바인딩을 마친 뒤의 알림 호출입니다. 화살표의 배치는 호출 순서나 병렬 실행을 뜻하지 않습니다.
등록과 해제도 설계합니다
등록 전 발생한 알림은 재전달되지 않습니다. 각 수신자는 문 참조를 얻어 바인드하고, 수신을 끝낼 때 자신의 이벤트를 해제합니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 등록 Bind로 현재 수신자 집합을 만든다. |
|
|
| 알림 호출 Call이 현재 두 수신자에게 같은 알림을 전달한다. |
|
|
| 해제 Unbind 뒤 수신자는 다음 알림 대상에서 빠진다. |
|
|
등록된 수신자에게 알림 전달
본문의 Bind·Call·Unbind에서 현재 등록 집합이 바뀌는 점을 보여 주어 등록 전 알림이 재생되지 않으며 해제가 필요하다는 점을 드러낸다. 두 수신자는 같은 알림 단계에 묶고 호출 순서를 만들지 않는다.
BOUND EVENTS · ONE OWNER
등록을 마친 수신자에게 문 열림을 알립니다
두 수신자가 이 문 인스턴스의 OnDoorOpened에 미리 바인드된 상태입니다. Call은 현재 등록된 이벤트들을 호출하며 수신자 순서나 병렬 실행을 보장하지 않습니다.
화살표는 바인딩을 마친 뒤의 알림 호출입니다. 화살표의 배치는 호출 순서나 병렬 실행을 뜻하지 않습니다.
등록과 해제도 설계합니다
등록 전 발생한 알림은 재전달되지 않습니다. 각 수신자는 문 참조를 얻어 바인드하고, 수신을 끝낼 때 자신의 이벤트를 해제합니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 등록 Bind로 현재 수신자 집합을 만든다. |
|
|
| 알림 호출 Call이 현재 두 수신자에게 같은 알림을 전달한다. |
|
|
| 해제 Unbind 뒤 수신자는 다음 알림 대상에서 빠진다. |
|
|
그림에서 문 인스턴스의 OnDoorOpened는 게임 매니저와 사운드 매니저가 등록한 이벤트로 각각 알림을 전달합니다. 화살표는 등록을 마친 뒤의 알림 호출이며, 화살표가 놓인 위치는 호출 순서나 병렬 실행을 뜻하지 않습니다. 즉 Call은 현재 등록된 이벤트들을 호출할 뿐, 게임 매니저가 사운드 매니저보다 먼저 실행된다는 보장은 없습니다. 그림 아래의 설명처럼 등록 전에 일어난 알림은 다시 전달되지 않고, 각 수신자는 문 참조를 얻어 등록하며 수신을 끝낼 때 자신의 이벤트를 해제합니다.
단계 그림은 수신자 집합이 바뀌는 과정을 세 단계로 보여 줍니다. 먼저 Bind로 현재의 수신자 집합을 만들고, 다음으로 Call이 그 집합에 있는 두 수신자에게 같은 알림을 전달합니다. 마지막으로 한 수신자가 Unbind하면, 그 수신자는 다음 알림부터 대상에서 빠집니다.
디스패처를 쓰면 좋아지는 점
디스패처를 쓰면 문을 여는 로직이 게임 매니저나 사운드 매니저의 구체적인 처리 함수를 직접 부르지 않아도 됩니다. 하나의 디스패처에 여러 블루프린트가 등록할 수 있으므로, 문이 열릴 때 게임 매니저와 사운드 매니저, 이펙트 매니저가 모두 알게 하는 것도 간단합니다. 나중에 새로운 액터가 문 열림을 듣고 싶다면 그 액터에서 Bind만 추가하면 되며, 기존 문 블루프린트는 고칠 필요가 없습니다.
그래서 디스패처는 한 사건을 여러 대상에게 알려야 할 때 특히 알맞습니다. 공식 문서도 보스가 죽었을 때 문, HUD, 캐릭터가 각자 반응하는 경우를 디스패처의 예로 듭니다. 디스패처를 설계할 때는 만들기와 호출뿐 아니라, 누가 언제 등록하고 언제 해제하는지까지 함께 정해 두어야 흐름이 안정적입니다.
정리
- 이벤트 디스패처는 사건을 가진 블루프린트가 신호를 보내고, 듣고 싶은 블루프린트가 자신의 이벤트를 등록하는 통신 방법입니다.
- 디스패처는 마이 블루프린트 패널에서 만들고, 끌어다 놓아
Call,Bind,Unbind같은 노드를 만듭니다. 수신자는 디스패처를 가진 액터의 참조가 있어야 등록할 수 있습니다. - Call은 그 순간 등록된 이벤트만 실행하며, 수신자 사이의 실행 순서는 보장되지 않습니다. 등록 전에 보낸 알림은 다시 전달되지 않습니다.
- 같은 대상의 같은 이벤트는 여러 번 Bind해도 한 번만 등록되고, 다른 이벤트는 여러 개 등록할 수 있습니다.
- 수신을 끝낼 때는
Unbind Event로 자신의 이벤트만 해제하며,Unbind All Events는 다른 수신자의 등록까지 지웁니다.
연습 문제
1. 늦게 등록한 수신자
디스패처를 한 번 Call한 뒤, 수신자 A와 B의 서로 다른 이벤트를 각각 한 번 Bind하고 다시 한 번 Call했습니다. 수신자 안에서 다른 호출을 하거나 중복으로 등록하지는 않습니다. A와 B는 각각 몇 번 반응할까요?
해설 보기
각각 한 번입니다. 첫 Call 때는 두 수신자 모두 아직 등록하지 않았으므로 알림을 받지 못하고, 등록한 뒤의 두 번째 Call만 전달됩니다. 디스패처는 호출하는 순간의 등록 목록으로만 판단합니다.
이전 알림을 다시 받는 기능이나 현재 상태를 읽는 기능은 디스패처가 해 주지 않으므로 따로 만들어야 합니다. 또 이 조건만으로는 A와 B 가운데 어느 이벤트가 먼저 실행되는지 정할 수 없습니다.
2. 사라진 사운드 매니저의 등록
UI 위젯이 문 열림에 반응하지 않게 하려고, 위젯이 닫힐 때 Unbind All Events로 OnDoorOpened의 등록을 해제했습니다. 그 뒤로 문이 열려도 사운드 매니저가 효과음을 내지 않는다는 보고가 들어왔습니다. 무엇이 문제일까요?
해설 보기
Unbind All Events는 그 디스패처에 등록된 모든 이벤트를 해제합니다. 그래서 위젯의 등록뿐 아니라 사운드 매니저와 게임 매니저의 등록까지 함께 지워졌습니다.
위젯은 자신이 등록한 이벤트만 Unbind Event로 해제해야 합니다. 이렇게 하면 다른 수신자들의 등록은 그대로 남아, 문이 열릴 때 사운드 매니저가 계속 반응합니다.
3. 같은 이벤트를 두 번 등록하면
게임 매니저의 BeginPlay와 다른 초기화 함수에서 같은 HandleDoorOpenEvent를 같은 문의 OnDoorOpened에 한 번씩, 모두 두 번 Bind했습니다. 문이 한 번 열리면 HandleDoorOpenEvent는 몇 번 실행될까요? 게임 매니저가 서로 다른 이벤트 두 개를 하나씩 Bind했다면 어떨까요?
해설 보기
같은 대상의 같은 이벤트는 여러 번 Bind해도 한 번만 등록되므로 HandleDoorOpenEvent는 한 번 실행됩니다.
반면 서로 다른 이벤트 두 개를 각각 Bind했다면 두 이벤트가 모두 등록되어 있으므로, 문이 한 번 열릴 때 두 이벤트가 각각 한 번씩 실행됩니다. 이때도 두 이벤트의 실행 순서는 보장되지 않습니다.
디스패처를 만들고 입력을 추가하는 방법과 각 노드의 뜻은 언리얼 엔진 공식 문서의 이벤트 디스패처 설명에서 확인할 수 있습니다.
다음 글 블루프린트 인터페이스를 통한 통신에서는 여러 종류의 대상에게 같은 요청을 보내고 각자 다르게 반응하게 하는 인터페이스 통신을 살펴봅니다.