본문 바로가기

블루프린트 인터페이스를 통한 통신

인터페이스 메시지로 서로 다른 블루프린트에 공통 기능을 요청하고 직접 참조·디스패처와의 차이를 비교합니다.

발사체가 무언가에 맞았을 때 대미지를 주고 싶다고 해 봅시다. 맞은 것이 적일 수도, 플레이어일 수도, 부서지는 상자일 수도 있습니다. 발사체가 직접 참조로 처리하려면 "적인가? 플레이어인가? 상자인가?"를 하나씩 캐스트해 봐야 하고, 이벤트 디스패처의 기본 사용법의 디스패처는 한 사건을 미리 등록한 여러 수신자에게 알리는 방법이라서 "지금 맞은 이 대상"에게 요청하는 일에는 맞지 않습니다.

이럴 때 쓰는 것이 블루프린트 인터페이스(Blueprint Interface)를 통한 통신입니다. 블루프린트 인터페이스 활용에서 인터페이스를 만들고 구현하는 방법을 익혔다면, 이 글에서는 그것을 대미지 처리라는 통신에 적용해 봅니다. 그리고 지금까지 본 직접 참조, 이벤트 디스패처, 인터페이스 세 방법을 대상과 연결 조건으로 비교해, 상황에 맞는 방법을 고르는 기준을 정리합니다.


같은 요청을 다른 대상에게 보내기

인터페이스 통신은 기능에 대한 약속을 바탕으로 합니다. 인터페이스는 여러 클래스가 구현할 함수의 선언(declaration)만 담고, 실제 구현은 각 클래스에 맡깁니다. 다른 블루프린트가 이 인터페이스의 기능을 부를 때는 호출할 대상이 인터페이스를 구현했는지만 따집니다. 구현했다면 그 클래스가 내부에서 어떻게 처리하는지와 상관없이 같은 방식으로 호출할 수 있습니다.

충전기가 휴대폰의 브랜드를 몰라도 충전 규격만 맞으면 충전할 수 있는 것과 같습니다. 발사체도 맞은 대상이 적인지 플레이어인지 몰라도, 대미지를 받을 수 있다는 약속을 구현한 대상이라면 같은 메시지로 대미지를 요청할 수 있습니다. 그리고 메시지를 받은 적은 적의 방식으로, 플레이어는 플레이어의 방식으로 체력을 줄입니다.


대미지 인터페이스 정의하기

먼저 통신에 쓸 함수를 인터페이스에 선언합니다. 콘텐츠 브라우저에서 마우스 오른쪽 버튼을 누르고 Blueprint 메뉴의 Blueprint Interface를 골라 BPI_Damageable을 만듭니다. 인터페이스 에디터에서 함수 ApplyDamage를 추가하고, Float 타입의 입력 DamageAmount를 하나 둡니다. 출력은 두지 않습니다.

여기서 ApplyDamage는 직접 정의한 인터페이스 함수입니다. 엔진에도 Apply Damage라는 이름의 기본 노드가 따로 있으므로, 나중에 노드를 검색할 때 둘을 헷갈리지 않게 구별해서 고릅니다. 이름이 겹치는 것이 걱정된다면 ReceiveHitDamage처럼 다른 이름을 써도 됩니다.


적과 플레이어에 구현하기

대미지를 받을 블루프린트, 예를 들어 BP_Enemy를 엽니다. 툴바의 Class Settings를 누르고 디테일 패널의 Implemented Interfaces에 BPI_Damageable을 추가한 뒤 컴파일하면, 마이 블루프린트 패널의 Interfaces 항목에 ApplyDamage가 나타납니다. ApplyDamage를 오른쪽 클릭해 Implement Event를 고르면 이벤트 그래프에 Event ApplyDamage 노드가 생깁니다.

이 이벤트 뒤에 실제 대미지 처리를 만듭니다. CurrentHealth에서 DamageAmount를 뺀 결과를 Set CurrentHealth에 연결해 체력을 갱신하는 것이 가장 단순한 형태입니다. BP_PlayerCharacter에도 같은 방식으로 BPI_Damageable을 추가하고, 플레이어에 맞는 Event ApplyDamage 로직을 만듭니다. 두 클래스는 서로 상속 관계가 없어도 같은 인터페이스를 구현할 수 있습니다.


발사체에서 메시지 보내기

이제 발사체 BP_Projectile에서 맞은 대상에게 메시지를 보냅니다. 발사체가 다른 액터와 부딪쳤을 때 Hit 이벤트가 오려면, 먼저 콜리전 채널의 응답이 Block인지와 Hit 이벤트가 발생하도록 설정되어 있는지 확인합니다. 물리 시뮬레이션으로 부딪치는 경우라면 Simulation Generates Hit Events도 켜져 있어야 합니다.

Event Hit 노드의 Other 핀에는 부딪친 상대 액터가 들어옵니다. 이 핀에서 드래그해 Does Implement Interface 노드를 만들고, Interface 핀에서 BPI_Damageable을 고릅니다. 이 노드는 연결된 오브젝트가 그 인터페이스를 구현했는지를 불리언으로 돌려줍니다. 그다음 Event Hit의 실행 핀을 Branch의 실행 입력에 잇고, Does Implement Interface의 Return Value를 Branch의 Condition에 연결합니다. 이 Branch가 부딪친 액터가 대미지를 받을 수 있는지에 따라 실행 흐름을 나눕니다.

Branch의 True 출력에서 드래그해 ApplyDamage를 검색하고, ApplyDamage (Message) 노드를 고릅니다. Target 핀에는 Event Hit의 Other를, DamageAmount에는 발사체가 줄 대미지 값 20.0을 넣고 컴파일한 뒤 저장합니다. 메시지 노드는 대상이 인터페이스를 구현하지 않았으면 조용히 실패하므로, Does Implement Interface와 Branch 없이 바로 메시지 노드를 연결해도 동작은 합니다. 다만 명시적으로 확인해 두면 "대미지를 받지 않는 대상에 맞았다"는 경우를 따로 처리할 자리가 생깁니다.

메시지 한 번은 Target에 연결한 오브젝트 하나에만 전달됩니다. 적에게 맞으면 그 적의 구현이, 플레이어에게 맞으면 그 플레이어의 구현이 실행되며, BPI_Damageable을 구현한 모든 액터에 방송되는 것은 아닙니다.


체력 하나로 결과 확인하기

처음 확인할 때는 대상에 Float 변수 CurrentHealth를 만들고 기본값을 50.0으로 둡니다. Event ApplyDamage에서 Set CurrentHealth를 거쳐 Print String으로 실행 선을 잇고, Set CurrentHealth에 저장할 값으로는 기존 CurrentHealth에서 DamageAmount를 뺀 결과를 연결합니다. Print String에는 갱신된 CurrentHealth를 넣습니다.

발사체가 한 번 맞아 메시지가 20.0을 전달했다면 50.0 - 20.0 = 30.0이 출력되어야 합니다. 이 단순한 예제에는 체력이 0 아래로 내려가지 않게 하는 처리, 방어력, 사망 처리, 네트워크 권한이 들어 있지 않습니다. 실제 게임에서는 이런 처리를 구현 쪽에 더해야 하며, 네트워크 권한은 블루프린트에서의 리플리케이션에서 다룹니다.


인터페이스 통신이 주는 것

인터페이스 통신의 가장 큰 장점은 다형성(polymorphism)입니다. ApplyDamage (Message) 노드 하나가 BP_Enemy와 BP_PlayerCharacter 모두에 같은 방식으로 동작하고, 발사체는 대상이 적인지 플레이어인지 알 필요 없이 BPI_Damageable을 구현했는지만 확인하면 됩니다.

이것은 곧 느슨한 결합(loose coupling)이기도 합니다. 호출하는 쪽인 발사체는 호출받는 쪽의 구체 클래스를 몰라도 되고, 인터페이스만 알면 됩니다. 콜리전 설정과 대상을 고르는 규칙, 함수의 입력과 출력이 같다면, 나중에 부서지는 상자 BP_DestructibleBox가 BPI_Damageable을 구현하는 것만으로 같은 발사체 로직이 상자에도 동작합니다.

유지보수에도 유리합니다. 함수의 입력, 출력과 호출의 의미를 그대로 두는 한, 적의 대미지 공식을 바꾸는 일은 적의 구현 안에서 끝납니다. 반대로 인터페이스 함수의 입력이나 출력을 바꾸면 인터페이스와 모든 구현, 호출부를 함께 점검해야 합니다. 또 인터페이스는 "이 기능을 지원하는 클래스는 이 함수를 가진다"는 약속을 분명하게 보여 주므로, 팀 작업에서 각 클래스가 무엇을 맡는지 정하는 데도 도움이 됩니다.


세 통신 방법 비교하기

지금까지 본 세 방법은 누구에게 보내는지와 어떻게 연결되는지가 다릅니다.

REFERENCE · SUBSCRIPTION · CONTRACT

호출 대상과 연결 조건으로 통신 방식을 선택합니다

직접 참조와 인터페이스 메시지는 지정한 객체 하나를 대상으로 합니다. 이벤트 디스패처는 소유자에 현재 바인드된 수신자들에게 사건을 알립니다.

통신 대상과 연결 조건 비교
방식연결과 호출 범위선택할 상황·확인할 조건
직접 참조구체 타입의 인스턴스 하나를 참조하여 호출그 타입의 변수·기능이 필요할 때. 호출 전 유효성과 접근 권한 확인
이벤트 디스패처수신자가 소유자 참조로 바인드하고, 호출 시 현재 구독자들에게 알림사건에 여러 대상이 반응할 때. 등록 전 알림은 재생되지 않으며 해제 시점 관리
인터페이스대상 참조 하나에 공통 계약의 메시지 호출서로 다른 클래스의 같은 능력을 요청할 때. 미구현 대상의 Message는 조용히 무시됨
방식: 직접 참조
연결과 호출 범위: 구체 타입의 인스턴스 하나를 참조하여 호출
선택할 상황·확인할 조건: 그 타입의 변수·기능이 필요할 때. 호출 전 유효성과 접근 권한 확인
방식: 이벤트 디스패처
연결과 호출 범위: 수신자가 소유자 참조로 바인드하고, 호출 시 현재 구독자들에게 알림
선택할 상황·확인할 조건: 사건에 여러 대상이 반응할 때. 등록 전 알림은 재생되지 않으며 해제 시점 관리
방식: 인터페이스
연결과 호출 범위: 대상 참조 하나에 공통 계약의 메시지 호출
선택할 상황·확인할 조건: 서로 다른 클래스의 같은 능력을 요청할 때. 미구현 대상의 Message는 조용히 무시됨

인터페이스에도 대상 참조가 필요합니다. 세 방식 모두 자동 네트워크 RPC는 아니며, 멀티플레이어 동기화는 별도로 설계합니다.

위 그림은 세 방법을 연결과 호출 범위, 그리고 선택할 상황과 확인할 조건으로 나누어 비교합니다. 직접 참조는 구체 타입의 인스턴스 하나를 참조해 호출하는 방식으로, 그 타입만 가진 변수나 기능이 필요할 때 고르며 호출 전에 유효성과 접근 권한을 확인해야 합니다. 이벤트 디스패처는 수신자가 소유자의 참조로 등록하고, 호출하면 현재 등록된 구독자들에게 알리는 방식입니다. 한 사건에 여러 대상이 반응할 때 고르며, 등록 전의 알림은 다시 전달되지 않으므로 해제 시점까지 관리해야 합니다.

인터페이스는 대상 참조 하나에 공통 약속의 메시지를 보내는 방식으로, 서로 다른 클래스의 같은 능력을 요청할 때 고릅니다. 인터페이스를 구현하지 않은 대상에 보낸 메시지는 조용히 무시됩니다. 그림 아래의 설명처럼 인터페이스에도 대상 참조는 필요하며, 세 방법 모두 네트워크 RPC를 자동으로 보내는 기능은 아니므로 멀티플레이어 동기화는 별도의 복제와 RPC로 설계해야 합니다.

정리하면, 상호작용이나 대미지 받기, 켜기와 끄기처럼 여러 종류의 오브젝트가 같은 행동을 각자의 방식으로 해야 할 때 인터페이스가 가장 잘 맞습니다. 어느 한 방법이 항상 낫지는 않으므로, 대상이 하나인지 여럿인지, 대상의 구체 타입을 알아야 하는지를 먼저 따져 고릅니다.


정리

  • 인터페이스 통신은 대상이 인터페이스를 구현했는지만 보고 같은 메시지를 보내며, 실제 처리는 대상의 구현이 맡습니다.
  • Does Implement Interface와 Branch로 대상이 인터페이스를 구현했는지 명시적으로 확인하면, 대미지를 받지 않는 대상에 대한 처리를 따로 둘 수 있습니다.
  • 메시지 한 번은 Target의 오브젝트 하나에만 전달되며, 구현하지 않은 대상에서는 조용히 무시됩니다.
  • 직접 참조는 특정 인스턴스와 구체 타입의 기능이, 디스패처는 한 사건에 여러 수신자가, 인터페이스는 여러 클래스의 같은 능력이 필요할 때 알맞습니다.
  • 세 방법 모두 네트워크 동기화를 자동으로 하지 않습니다.

연습 문제

1. 유효한 벽에 맞은 발사체

발사체가 맞은 대상은 유효한 벽 액터이지만 BPI_Damageable을 구현하지 않습니다. 본문의 Does Implement Interface에서 Branch로 가는 경로에서, 20.0을 전달하는 True 경로가 실행될까요? 벽이 유효한 액터라는 사실이 대미지를 처리할 수 있다는 뜻일까요?

해설 보기

True 경로는 실행되지 않습니다. 유효한 오브젝트인지와 인터페이스를 구현했는지는 서로 다른 조건이므로, 벽은 유효하지만 Does Implement Interface는 거짓을 돌려줍니다.

검사를 빼고 메시지만 보내더라도, 구현하지 않은 벽에서는 대미지 처리가 실행되지 않고 메시지가 조용히 무시됩니다. 명시적인 Branch는 이럴 때 실패 피드백이나 다른 처리를 둘 자리를 만들어 줍니다. 또 한 발이 한 번만 대미지를 주어야 한다면, 같은 발사체가 여러 번 Hit을 일으키는 경우를 허용할지와 처리한 뒤 발사체를 어떻게 할지도 따로 정해야 합니다.

2. 두 번 깎인 체력

체력 50.0인 적에게 발사체 한 발을 쐈는데 Print String이 30.0과 10.0을 차례로 출력했습니다. 대미지는 한 번 메시지에 20.0입니다. 어떤 일이 일어났을까요?

해설 보기

메시지가 두 번 전달된 것입니다. 50.0 - 20.0 = 30.0이 한 번, 30.0 - 20.0 = 10.0이 또 한 번 계산되었습니다. 발사체가 대상과 부딪친 뒤에도 남아 있어 Hit 이벤트가 다시 발생했을 가능성이 큽니다.

한 발이 한 번만 대미지를 주게 하려면 메시지를 보낸 뒤 발사체를 파괴하거나, 이미 맞힌 대상을 기록해 다시 보내지 않게 하거나, 충돌을 꺼서 반복 Hit을 막는 식으로 처리합니다.

3. 상황에 맞는 통신 방법

다음 세 상황에 직접 참조, 이벤트 디스패처, 인터페이스 가운데 무엇이 알맞은지 골라 보세요. 보스가 죽으면 출구 문과 HUD, 배경 음악이 각자 반응하는 경우, 레벨에 놓인 스위치가 바로 옆의 특정 문 하나를 여는 경우, 폭발이 범위 안의 나무와 적, 상자에 각자 다른 피해를 주는 경우.

해설 보기

보스의 죽음은 한 사건에 여러 대상이 반응하는 경우이므로 이벤트 디스패처가 알맞습니다. 보스는 죽었다는 사실만 알리고, 문과 HUD, 음악이 각자 등록해 반응합니다.

스위치와 특정 문은 일대일 관계이고 스위치가 그 문을 정확히 알고 있으므로 직접 참조가 가장 간단합니다. 폭발은 종류가 다른 여러 대상에게 같은 요청을 보내고 각자 다르게 반응해야 하므로 인터페이스가 알맞습니다. 이때 범위 안의 대상들을 찾아 각각에 메시지를 보내야 하며, 메시지 한 번이 모두에게 방송되지는 않습니다.

통신 방법별로 알맞은 상황은 언리얼 엔진 공식 문서의 블루프린트 통신 사용법에서 확인할 수 있습니다.

다음 글 게임 인스턴스 활용 데이터 공유에서는 레벨이 바뀌어도 사라지지 않는 게임 인스턴스에 데이터를 두고 여러 블루프린트가 함께 쓰는 방법을 살펴봅니다.