카테고리 없음

Oblivio Delegate 활용한 레벨 기믹과 의존성 분

eclipse2 2026. 5. 17. 19:21

1. 문제 인식: 스파게티 코드의 위험성 프로젝트 초반에는 캐릭터가 특정 아이템(메멘토)을 획득했을 때 레벨의 기믹(예: 2층 홍수 이벤트)을 작동시키기 위해, 캐릭터의 Interact() 함수 내부에서 직접 태그를 검사(ActorHasTag("FloodTrigger"))하고 특정 액터(AFloodLevelActor)를 찾아 실행하는 방식(하드코딩)을 사용했다. 하지만 층수가 늘어나고 9층(전화기 울림), 10층 등 각기 다른 기믹이 추가될수록 캐릭터 클래스가 모든 맵의 이벤트를 알아야 하는 구조적 모순이 발생했다. 이는 코드가 무한정 비대해지고 유지보수가 불가능해지는 '스파게티 코드'의 전형적인 원인이었다.

2. 리팩토링 설계: 의존성 역전과 Delegate(방송국) 도입 "캐릭터와 아이템은 세상에 무슨 일이 일어나는지 알 필요가 없다"는 객체지향의 원칙(의존성 분리)을 적용했다. C++ 클래스에는 오직 '방송국(Delegate)'만 개설하고, 이벤트를 송출(Broadcast)하는 역할만 부여했다. 기믹의 실제 연출은 맵 전체를 관장하는 '레벨 블루프린트(Level Blueprint)'가 방송을 수신(Bind)하여 직접 지휘하도록 아키텍처를 전면 수정했다.

3. 적용 사례 1: 메멘토(유품) 획득 기믹의 중앙 통제

  • 변경 전: 캐릭터가 홍수 아이템인지 직접 확인 후 홍수 액터를 조작. 블루프린트 인터페이스(BPI)와 혼용됨.
  • 변경 후: OblivioItemBase에 멀티캐스트 델리게이트(OnMementoCollected)를 선언. 아이템이 상호작용 될 때 단순히 "나 획득됨!"이라고 방송을 쏜다.
  • 효과: 2층의 홍수 발동, 9층의 일기장 획득 시 전화기 사운드 재생 등 완전히 다른 연출들을 C++ 수정 단 한 줄 없이 각 층의 레벨 블루프린트 노드 연결만으로 완벽하게 제어할 수 있게 되었다.

4. 적용 사례 2: 플레이어 사망 시의 레벨 기믹 초기화

  • 변경 전: HandleDeath() 함수에서 캐릭터가 직접 AFloodLevelActor를 찾아 홍수 이벤트를 끄도록 명령. (10층에서 죽어도 2층 기믹을 찾는 낭비 발생)
  • 변경 후: OblivioCharacter에 OnPlayerDied 델리게이트를 선언하고 사망 시 방송 송출. 레벨 블루프린트가 이를 듣고 알아서 자기 층의 기믹을 정지(StopFloodEffects)시키도록 책임을 넘겼다.

5. 인사이트 델리게이트를 통한 로직 분리는 프로그래머와 레벨 디자이너의 완벽한 분업을 가능하게 한다. 프로그래머는 뼈대(이벤트 트리거)만 제공하고, 기획자/디자이너는 블루프린트에서 이를 자유롭게 조립할 수 있는 파이프라인을 구축했다. 이를 통해 기능 확장에 따른 버그 발생 확률을 획기적으로 낮출 수 있었다.


1. 아이템 베이스 델리게이트 선언 (OblivioItemBase.h)

// 델리게이트 선언: 누가 주웠는지 캐릭터 정보를 함께 전달
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMementoCollected, class AOblivioCharacter*, Interactor);

UCLASS()
class OBLIVIO_API AOblivioItemBase : public AActor
{
	GENERATED_BODY()

public:
	// 블루프린트에서 노드로 바인딩할 수 있는 이벤트 디스패처
	UPROPERTY(BlueprintAssignable, Category = "Events")
	FOnMementoCollected OnMementoCollected;

	UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
	void OnInteract(AActor* Interactor);
	virtual void OnInteract_Implementation(AActor* Interactor);
};

2. 아이템 획득 시 이벤트 방송 (OblivioItemBase.cpp)

void AOblivioItemBase::OnInteract_Implementation(AActor* Interactor)
{
	// 아이템이 획득되었음을 월드에 방송. (어떤 기믹이 터질지는 레벨 BP가 결정)
	if (AOblivioCharacter* Player = Cast<AOblivioCharacter>(Interactor))
	{
		OnMementoCollected.Broadcast(Player);
	}
}

3. 캐릭터 사망 델리게이트 선언 및 송출 (OblivioCharacter.h &. cpp)

// OblivioCharacter.h
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnPlayerDied);

public:
	UPROPERTY(BlueprintAssignable, Category = "Events")
	FOnPlayerDied OnPlayerDied;
    
    
    // OblivioCharacter.cpp 의 HandleDeath() 내부
void AOblivioCharacter::HandleDeath()
{
	if (bIsDead) return;
	bIsDead = true;

	if (USkeletalMeshComponent* MeshComp = GetMesh())
	{
		MeshComp->bPauseAnims = true;
		MeshComp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
		MeshComp->SetCollisionProfileName(TEXT("Ragdoll"));
		MeshComp->SetSimulatePhysics(true);
	}

	// 하드코딩된 FloodActor 검색 코드를 삭제하고 사망 이벤트 방송으로 대체
	OnPlayerDied.Broadcast();

	if (AOblivioGameMode* GM = Cast<AOblivioGameMode>(UGameplayStatics::GetGameMode(GetWorld())))
	{
		GM->GameOver();
	}
}

2층 홍수 이벤트
9층 일기장-> 전화기 연동