컨텐츠 검색
[UE5 BP] _#8. 언리얼 엔진 블루프린트 통신 (2) - 형 변환(cast), 블루프린트 인터페이스, 이벤트 디스패처

2025. 11. 26. 10:01Unreal Engine/개념

1. 형 변환(Cast)과 레퍼런스를 이용한 문 열기 상호작용

1.1. 🧩 문제 상황

  • 버튼(BP_Trigger)을 밟으면 문(BP_Door)이 열리는 기능을 구현 중.
  • 캐릭터가 버튼 위에 올라가면 문이 열려야 하지만,
    단순 오브젝트(공)이 버튼에 닿았을 때는 열리면 안 됨.
  • 버튼이 어떤 문을 열지 결정하기 위해 BP_Door를 가리키는 DoorToOpen 변수가 필요함.
  • 캐릭터가 키(HasKey)를 가지고 있을 때만 문이 열리도록 조건을 추가해야 함.

공이 버튼에 닿았을 땐 문이 안 열리고 캐릭터가 닿아야 문이 열리는 모습


1.2. 🎯 구현 목적

  • 조건 1: 버튼을 밟은 액터가 캐릭터(BP_Communications_Character)일 것
  • 조건 2: 캐릭터가 HasKey = true일 것
  • 조건 3: 조건을 모두 통과하면 연결된 문(BP_Door)을 열기(OpenDoor)

즉, "키를 가진 캐릭터만 버튼을 밟았을 때 문이 열린다."

BP_Communications_Character 블루프린트 클래스에 추가한 변수
현재로서는 플레이어가 키를 가지고 있다는 설정이므로 기본값을 true로 설정


1.3. 📌 Cast가 필요한 이유 (Actor → Character)

  • On Component Begin Overlap의 Other Actor는 Actor 타입이다.
  • Actor 타입에는 HasKey 변수가 없음.
  • HasKey는 BP_Communications_Character 전용 변수이므로
    → 반드시 Cast To BP_Communications_Character를 통해 타입을 변환해야 한다.
  • Cast 성공 시점에만
    → As BP_Communications_Character 핀에서 HasKey에 접근 가능.

Cast가 없으면 HasKey 접근 불가 → 문 열기 조건 검사가 불가능


1.4. 📌 DoorToOpen이 문을 여는 이유 (Reference 변수 구조)

  • BP_Trigger 안에는 DoorToOpen이라는 퍼블릭 변수가 있음.
  • 변수 타입은 BP_Door (Object Reference)
    → 즉, “어떤 문을 가리키는지 저장하는 포인터/레퍼런스”
  • 레벨 에디터에서 BP_Trigger를 선택하고
    DoorToOpen에 실제 문 액터(Trigger_Door)를 드래그하여 연결.

레퍼런스이기 때문에:

DoorToOpen 변수는 BP_Door가 가진 OpenDoor 함수를 호출할 수 있다.

즉, "문이 어디 있는지"를 알고 있는 게 아니라 "어떤 문을 열라고 호출해야 하는지"를 알고 있음.

BP_Trigger 블루프린트 클래스의 퍼블릭 변수
BP_Trigger 버튼의 퍼블릭 변수 Door to Open에 이미지에 보이는 문인 Trigger_Door를 연결


1.5. ⚙️ 전체 처리 흐름

  1. 버튼의 Box Collision에서 Overlap 발생
    → On Component Begin Overlap
  2. Overlap한 액터 확인
    → Other Actor (Actor 타입)
  3. “캐릭터인지” 판별
    → Cast To BP_Communications_Character
  4. 캐스트 성공 후
    → HasKey 변수 접근
  5. HasKey가 true이면 Branch 통과
  6. BP_Trigger 내부 변수 DoorToOpen을 Get
  7. DoorToOpen이 가리키는 BP_Door 인스턴스에게
    → OpenDoor 함수 호출
  8. BP_Door 내부에서 문 열림 처리
    (Visibility, Collision 변경)

Other Actor는 BP_Communications_Character로 형 변환하여 캐릭터가 갖고 있는 HasKey 변수에 접근했고 디폴트 값이 True이므로 분기를 통과한 후 DoorToOpen 변수에 연결된 Trigger_Door가 열린다.


1.6. 💡 배운 점

(1) Cast는 “타입 변환 + 안전성 검사”

  • Overlap의 Other Actor는 Actor 최상위 타입이므로
    캐릭터 전용 변수에 접근하기 위해서는 Cast가 필수.

(2) 레퍼런스 변수(DoorToOpen)의 역할

  • BP_Trigger가 어떤 문을 열지 “하드코딩”하지 않기 위해
    → 문을 가리키는 레퍼런스를 변수로 분리
  • 이 변수 덕분에 트리거 하나로 여러 문을 제어하거나
    에디터에서 문을 갈아끼울 수 있는 유연성이 생김.

(3) OpenDoor 함수는 “문 액터(BP_Door)의 멤버 함수”

  • DoorToOpen은 BP_Door를 가리키고 있으므로
    DoorToOpen → OpenDoor 호출이 가능함.

(4) 전체 구조는 다음 패턴에 해당됨

  • 캐릭터 판별(Cast) → 상태 검사(HasKey) → 레퍼런스를 통한 대상 선택(DoorToOpen) → 대상 액터의 기능 실행(OpenDoor)

2. 블루프린트 인터페이스(Interface)를 이용한 상호작용 시스템 구조 이해

2.1. 🧩 문제 상황

언리얼 엔진에서 다음과 같은 상호작용을 구현해야 했다.

  • 플레이어가 상호작용 키(E, 마우스 좌클릭)를 눌렀을 때,
  • 특정 오브젝트(BP_TriggerPickup)가 반응해서,
  • 맵 밖에 있던 Ramp2가 지정된 위치로 이동하며 맵 위에 나타나야 한다.

추가 조건:

  • 오브젝트는 플레이어가 특정 아이템("Key")을 가지고 있을 때만 동작해야 한다.
  • 하지만 BP_TriggerPickup은 플레이어의 내부 상태(변수, 인벤토리 구조)를 직접 알 수 없고, 특정 캐릭터 BP에 강하게 의존하는 것도 피하고 싶다.

즉, “플레이어가 이 아이템을 갖고 있는지, 공통된 방식으로 물어보고 응답받는 시스템”이 필요했다.


2.2. 🎯 구현 목적

  1. 플레이어 상호작용 입력 처리
    • 플레이어가 BP_TriggerPickup에 가까이 가서 상호작용 키를 누르면
      → BP_TriggerPickup이 “상호작용 이벤트(Event InteractWith)”를 받는다.
  2. 아이템 보유 여부 질의
    • BP_TriggerPickup은 “지금 상호작용한 캐릭터가 특정 아이템("Key")을 가지고 있는지”를
      공통 규약(인터페이스)을 통해 물어본다.
  3. 캐릭터 쪽에서 HasItem 로직을 구현해 응답
    • 캐릭터 BP는 “들어온 문자열이 "Key"인지 확인하고”
      → 맞으면 true, 아니면 false를 반환한다.
  4. 결과에 따른 오브젝트 동작
    • HasItem("Key") == true인 경우에만
      → BP_TriggerPickup이 Ramp를 SetActorLocation으로 옮겨서 Ramp2가 맵 위에 나타난다.

이때 어디에서도 HasKey bool 변수는 사용하지 않고,
모든 체크를 문자열 기반 HasItem("Key") 로직 하나로 처리한다.


2.3. 📌 인터페이스가 필요한 이유

2.3.1. 캐릭터 구조를 모르는 상태에서 “아이템 보유 여부”를 묻기 위해

  • BP_TriggerPickup은:
    • 플레이어의 인벤토리 구조를 몰라도,
    • 어떤 캐릭터인지(구체 클래스)를 몰라도,
    • 단지 “BPI_Inventory 인터페이스를 구현했는지”만 알면 된다.
  • 그 인터페이스 안에는 HasItem(Item: string) → bool이라는 공통된 약속만 정의되어 있다.

즉, “이 객체가 BPI_Inventory를 구현했다면, HasItem("Key")를 보내면 true/false로 대답해줄 것이다.”

라는 전제만으로 통신이 가능해진다.

2.3.2. 서로 다른 블루프린트에 동일한 메시지를 보내기 위해

  • 상호작용 이벤트 자체는
    BPI_LearningKit_PlayerInteractions의 InteractWith(Interactor)로 통일.
  • 아이템 확인 로직은
    BPI_Inventory의 HasItem(Item)으로 통일.

이렇게 해두면:

  • 나중에 “인벤토리가 있는 다른 캐릭터”나 “아이템을 가지고 있을 수 있는 다른 오브젝트”가 생겨도,
  • 해당 애들이 BPI_Inventory만 구현하면
    같은 방식으로 HasItem 호출을 받을 수 있다.
  • BP_TriggerPickup의 그래프는 그대로 둔 채,
    새로운 액터만 인터페이스 구현하면 확장된다.

2.3.3. 구현과 규약의 분리

  • 인터페이스(BPI_Inventory, BPI_LearningKit_PlayerInteractions)
    • 함수 이름, 입력, 출력 → “형태(Signature)”만 정의
    • 로직 없음
  • 구현하는 블루프린트(BP_Communications_Character, BP_TriggerPickup)
    • 같은 이름의 함수를 실제로 “어떻게 동작시킬지” 정의

이를 통해:

  • 규약(함수 형태)은 고정된 채,
  • 구현체를 자유롭게 교체/추가할 수 있다.

2.4. ⚙️ 전체 처리 흐름

2.4.1. 인터페이스 정의

  1. BPI_Inventory
    • 함수: HasItem
      • 입력: Item (string)
      • 출력: HasItem (bool)
    • 역할:
    • “이 액터가 Item이라는 이름의 아이템을 가지고 있나요?”를 물어보는 규약.

BPI_Inventory 블루프린트 인터페이스

  1. BPI_LearningKit_PlayerInteractions
    • 함수(이벤트): InteractWith(Interactor)
    • 역할:
    • “어떤 액터(Interactor)가 나와 상호작용했다”는 이벤트를 전달하는 규약.

2.4.2. 캐릭터에서 BPI_Inventory 구현

BP_Communications_Character (또는 이와 같은 캐릭터 BP):

  • Class Settings → Implemented Interfaces에 BPI_Inventory 추가.
  • 그 결과, 블루프린트에 HasItem 함수 구현부가 생김.
  • 구현 내용:
    • 입력: Item (string)
    • 로직:
    • 즉,“이 캐릭터는 HasItem("Key")를 물어보면 true를, 그 외엔 false를 반환한다.”
if Item == "Key":
    return true
else:
    return false

※ 여기서 HasKey bool 변수는 사용되지 않았고,
모든 아이템 보유 체크는 문자열 비교 기반 HasItem 구현으로 처리되었다.

BP_Communications_Character 블루프린트 클래스에서 BPI_Inventory 블루프린트 인터페이스의 구현

2.4.3. BP_TriggerPickup에서 상호작용 인터페이스 구현

BP_TriggerPickup:

  • 퍼블릭 변수:
    • Ramp (Actor Reference)
      → 에디터에서 실제 Ramp2 메쉬를 가진 액터에 연결
  • 구현된 인터페이스:
    • BPI_LearningKit_PlayerInteractions
  • Event Graph의 핵심 흐름:
Event InteractWith (from BPI_LearningKit_PlayerInteractions)
    └ HasItem (Target = Interactor, Item = "Key")   // BPI_Inventory 메시지 호출
        └ Branch (Condition = HasItem 반환값)
            └ True: SetActorLocation(Target = Ramp)
            └ False: 아무 동작 없음

설명:

  1. 플레이어가 상호작용 키를 눌러서
    → 캐릭터가 BP_TriggerPickup에 InteractWith(Interactor = 자기 자신) 메시지를 보냄.
  2. BP_TriggerPickup은 Interactor를 대상으로
    HasItem("Key") 메시지를 호출.
  3. 캐릭터는 BPI_Inventory.HasItem 구현에 따라:
    • "Key"가 맞으면 true 반환.
  4. true이면 Branch 통과 후:
    • SetActorLocation(Target = Ramp) 실행
    • → Ramp2가 지정된 위치로 이동하며 맵 위에 보이게 됨.

BP_TriggerPickup 블루프린트 클래스에서 상호작용 시 Interactor를 대상으로 HasItem 함수 메시지를 호출하여 True이면 Ramp2를 지정된 위치 변경


2.5. 💡 배운 점

  1. HasKey 같은 개별 변수 없이도, 문자열 기반 인터페이스 하나로 아이템 체크를 일반화할 수 있다.
    • 이번 구조에서는 HasKey bool은 실제로 사용되지 않고,
      HasItem(Item: string) → bool만으로
      “Key를 가진 상태인지”를 표현했다.
  2. 인터페이스는 로직이 아니라 “형태(서명)”만 정의한다.
    • BPI_Inventory 자체에는 비교 로직이 없다.
    • 비교 로직(Item == "Key")은 전부 이 인터페이스를 구현한 캐릭터 BP 안에 존재한다.
  3. 오브젝트는 캐릭터의 타입이나 내부 구조를 몰라도 된다.
    • BP_TriggerPickup은
      • “상호작용 시 InteractWith 이벤트가 온다”
      • “Interactor가 BPI_Inventory를 구현했다면 HasItem을 호출할 수 있다”
        이 두 가지만 알면 된다.
  4. 인터페이스 기반 메시지 호출 덕분에 확장성이 좋아진다.
    • 나중에 인벤토리를 가진 다른 액터(예: NPC, AI, 다른 플레이어)를 추가하더라도
      • 그 액터가 BPI_Inventory를 구현하고
      • HasItem을 적절히 정의하기만 하면
        같은 BP_TriggerPickup 로직이 그대로 재사용 가능하다.
  5. 상호작용 로직과 조건 검사 로직이 깔끔히 분리된다.
    • BP_TriggerPickup:
    • “조건이 true면 Ramp를 움직인다.”
    • 캐릭터 BP:
    • “넘어온 문자열이 'Key'인지 여부를 판정한다.”

3. 블루프린트 이벤트 디스패처(Event Dispatcher)

 3.1. 🧩 문제 상황

  • 여러 Blueprint가 수집품(Collectible)의 변화를 동시에 알아야 하는 상황이 발생했다.
  • 수집품은 게임 시작 시 등록되어야 하고, 플레이어가 겹쳐서 획득할 때마다 감소해야 했다.
  • 이 변화는 UI 텍스트에도 즉시 반영되어야 했고, 이후 다른 오브젝트(문 등)에서도 사용할 수 있어야 했다.
  • 하지만 기존 방식처럼 서로 직접 참조하도록 구성하면 Blueprint 간 의존성이 커졌고,
  • 수집품 → UI → 문 → 게임 모드로 이어지는 구조가 하드 결합되어 유지보수가 어려워지는 문제가 발생했다.
  • 한 이벤트를 처리하기 위해 여러 Blueprint를 동시에 수정해야 하는 비효율도 있었다.


 3.2. 🎯 구현 목적

  • 하나의 이벤트(Collectible 개수 변화)를 기반으로 UI, 문, 게임 로직이
    각자 독립적으로 반응하도록 만드는 것을 목표로 했다.
    • Blueprint 간 직접 참조를 제거하고
    • 수집품 등록·수집 로직을 GameMode에 모으고
    • UI는 변화가 있을 때만 자동으로 업데이트되며
    • 이후 기능 확장이 쉬운 구조를 만드는 것
    즉, 느슨한 결합(Decoupling)이벤트 기반 구조(Event-Driven Architecture) 를 구현하는 것이 목적이었다.

3.3. 📌 이벤트 디스패처가 필요한 이유

  • 수집품 변화 이벤트는 UI, 문, 게임 로직 등 여러 곳에서 사용될 수 있기 때문에
    하나의 Blueprint에 모든 기능을 몰아 넣는 방식은 구조가 너무 무거워졌다.
  • Event Dispatcher를 사용하면 GameMode는 단순히
    “수집품 변화가 일어났다” 는 사실을 Broadcast만 하면 되고,
    UI나 문처럼 반응해야 하는 Blueprint는 원할 때 Bind해서 듣기만 하면 되는 구조를 만들 수 있다.
  • 이 덕분에 Blueprint 간 직접 연결 없이도 여러 시스템이 동일한 이벤트에 반응할 수 있는 확장성이 생긴다.

3.4. 📌 이벤트 디스패처의 기본 개념

  • Event Dispatcher는 Broadcast(발신)Bind(구독) 으로 이루어진 메시징 시스템이다.
    • 예: GameMode는 Count 값을 매개변수로 전달하며
      CollectibleCountUpdated 이벤트를 Broadcast 한다.
    • UI, 문, 기타 Blueprint는 해당 이벤트를 Bind하여
      호출될 때마다 자동으로 로직을 실행한다.
    발신자는 수신자를 몰라도 되고,
    수신자는 발신자의 내부 구조를 몰라도 되기 때문에
    Blueprint 간 결합도가 크게 낮아지는 구조를 제공한다.


 3.5. ⚙️ 전체 처리 흐름

(1) BP_Collectible — 수집품 등록 & 수집 처리

🔸 게임 시작 시 등록 처리

  • Event BeginPlay
    → Delay(월드 생성 보정)
    Get GameMode → Cast To GM_BPCommunication
    CollectibleRegister(커스텀 이벤트) 호출
  • 결과: 게임 시작 시 GameMode가 총 수집품 개수를 정확히 파악하게 되었다.

🔸 플레이어가 겹쳤을 때 수집 처리

  • OnComponentBeginOverlap(Sphere)
    → Cast To BP_Communication_Character
    Get Game Mode → Cast To GM_BPCommunication
    CollectibleCollected(커스텀 이벤트) 호출
  • 결과: 겹칠 때마다 Count가 감소되도록 구조화되었다.


(2) GM_BPCommunication — 수집품 개수 관리 & Dispatcher Broadcast

🔸 수집품 개수 증가

  • CollectiblesLeft 변수를 ++ 증가
  • Call CollectibleCountUpdated(Dispatcher)
    • Count(int) 매개변수 전달
      → 다른 Blueprint들이 “등록됨”을 들을 수 있도록 Broadcast된다.

GM_BPCommunication  블루프린트 클래스의 CollectibleRegister 커스텀 이벤트

🔸 수집품 개수 감소

  • CollectiblesLeft 변수를 -- 감소
  • Call CollectibleCountUpdated(Dispatcher)
    → 수집될 때마다 UI와 시스템이 자동으로 업데이트된다.

GM_BPCommunication  블루프린트 클래스의 CollectibleCollected 커스텀 이벤트

🔸 UI 생성 및 이벤트 바인딩

  • Event BeginPlay
    Bind Event to Collectible Count Updated
    • CollectibleCountUpdated_Event 생성
      UMG_PlayerUI Construct → Add to Viewport
  • 결과: UI 위젯이 생성되며 Dispatcher를 구독하게 되고 이후 Count가 바뀔 때마다 자동 갱신된다.

GM_BPCommunication  블루프린트 클래스의 CollectibleCountUpdated_Event 커스텀 이벤트


(3) UMG_PlayerUI — UI 텍스트 갱신

  • Event Construct
    → Get GameMode → Cast To GM_BPCommunication
    Bind Event to Collectible Count Updated
    → CollectibleCountUpdated_Event
  • CollectibleCountUpdated_Event 내부:
    • 전달받은 Count(int)를 → Text로 변환(ToText)
    • TXT_COLLECTIBLES SetText(Text)
  • 결과: 수집품의 개수가 변할 때마다 UI 텍스트가 즉시 갱신되는 구조가 완성되었다.

UI 디자인


3.6. 📍 구현 시 주의해야 할 점

  • Dispatcher를 생성한 것만으로는 동작하지 않으며
    Bind가 반드시 필요하다.
  • UI가 Dispatcher를 받으려면 BeginPlay가 아닌
    Event Construct에서 Bind해야 한다.
    (UI는 BeginPlay가 호출되지 않기 때문)
  • Dispatcher Signature(매개변수)를 변경할 경우
    기존 Bind 이벤트도 함께 수정해야 오류가 발생하지 않는다.
  • Count는 수집품 "등록"과 "수집" 두 경우 모두에서 Broadcast해야
    UI 갱신이 정상 동작한다.
  • GameMode 캐스팅 후 null 체크가 필요하고
    Blueprint 실행 순서가 꼬이지 않도록 Delay 구간이 필요하다.

3.7. 💡 배운 점

  • Event Dispatcher는 Blueprint 간 통신을 깔끔하게 구조화하는 핵심 메커니즘임을 이해했다.
  • 직접 참조 없이도 여러 시스템을 유기적으로 연결할 수 있어 유지보수성과 확장성이 크게 향상된다.
  • UI가 Tick 없이도 즉시 갱신되는 구조를 구현할 수 있었다.
  • 수집품 등록/수집 → GameMode 처리 → Dispatcher Broadcast → UI 반영
    이 전체 흐름이 이벤트 기반으로 자연스럽게 이어지는 것을 확인했다.