[AbyssDiver] 튜토리얼_ GameMode 수정

칼든개구리·2025년 8월 11일
0
post-thumbnail

언리얼 엔진 C++ 튜토리얼 시스템 리팩토링: GameMode 로직 개선기

초기 튜토리얼 시스템은 각 단계를 처리하는 기본적인 구조를 갖추고 있었지만, 기능이 확장되면서 여러 문제가 발생했습니다.

  • 튜토리얼에만 사용되는 액터(몬스터, 드론 등)가 단계 종료 후에도 남아 보이지 않는 벽처럼 길을 막는 문제
  • 인디케이터의 생명주기가 명확하지 않아 의도치 않게 사라지거나 계속 남아있는 문제
  • 영구적으로 존재해야 할 액터 (예: 바위)가 임시 액터와 함께 삭제되는 버그
  • 여러 함수에 ActorsToShowThisPhase.Add(...) 같은 코드가 반복되어 코드 중복성 증가

이러한 문제들을 해결하고, 더 안정적이고 확장성 있는 구조를 만들기 위해 AADTutorialGameMode 클래스를 중심으로 대대적인 리팩토링을 진행했습니다.


주요 변경 및 개선 사항

  1. 헬퍼 함수를 이용한 코드 중복 제거 및 역할 명확화
    반복적으로 사용되던 임시 액터 추가 로직을 TrackPhaseActor라는 private 헬퍼 함수로 분리하여 코드 중복을 줄이고 가독성을 높였습니다. 또한, 함수들을 private으로 옮겨 클래스 외부에서 불필요하게 호출될 가능성을 차단하여 캡슐화를 강화했습니다.
// ...
protected:
	// ...

	// Helper 함수들을 private으로 이동시켜 역할과 접근 범위를 명확히 함
private:
	void TrackPhaseActor(AActor* Actor) { if (IsValid(Actor)) { ActorsToShowThisPhase.Add(Actor); } }
	void BindIndicatorToOwner(AActor* OwnerActor, AActor* IndicatorActor);

// ...
  1. 인디케이터 자동 정리를 위한 델리게이트 시스템 도입 (핵심 개선)
    가장 큰 변화는 인디케이터의 생명주기 관리 방식입니다. 기존에는 모든 임시 액터를 하나의 배열(ActorsToShowThisPhase)에 넣고 일괄적으로 관리했지만, 이는 인디케이터와 그 주인(Owner)의 관계를 명확히 표현하지 못했습니다.
    문제점:
  • 주인 액터(예: 드론)가 게임의 다른 로직에 의해 파괴될 경우, 인디케이터만 홀로 남는 '고아'가 될 수 있었습니다.
  • 모든 인디케이터를 수동으로 ActorsToShowThisPhase 배열에 추가해야 해서 실수가 발생하기 쉬웠습니다.

해결책:
인디케이터를 주인 액터에 '종속'시키는 새로운 시스템을 도입했습니다. 이는 주인이 파괴되면 인디케이터도 자동으로 파괴되는, 훨씬 안정적인 방식입니다.

// ...
	// 주인이 파괴되었을 때 호출될 함수
	UFUNCTION()
	void OnTrackedOwnerDestroyed(AActor* DestroyedActor);

private:
	// 주인(Key)과 인디케이터(Value)를 짝지어 저장하는 TMap
	UPROPERTY()
	TMap<TWeakObjectPtr<AActor>, TWeakObjectPtr<AActor>> OwnerToIndicator;
	// ...
// 인디케이터(IndicatorActor)를 주인(OwnerActor)에게 묶는 함수
void AADTutorialGameMode::BindIndicatorToOwner(AActor* OwnerActor, AActor* IndicatorActor)
{
    if (!IsValid(OwnerActor) || !IsValid(IndicatorActor)) return;

    // 1. TMap에 주인과 인디케이터를 기록
    OwnerToIndicator.Add(OwnerActor, IndicatorActor);
    // 2. 주인의 OnDestroyed 이벤트에 우리의 함수(OnTrackedOwnerDestroyed)를 바인딩(연결)
    OwnerActor->OnDestroyed.AddDynamic(this, &AADTutorialGameMode::OnTrackedOwnerDestroyed);
}

// 주인이 파괴되었을 때 자동으로 호출되는 함수
void AADTutorialGameMode::OnTrackedOwnerDestroyed(AActor* DestroyedActor)
{
    // TMap에서 파괴된 주인을 찾아 연결된 인디케이터가 있는지 확인
    if (TWeakObjectPtr<AActor>* FoundIndicator = OwnerToIndicator.Find(DestroyedActor))
    {
        if (FoundIndicator->IsValid())
        {
            // 연결된 인디케이터를 파괴
            FoundIndicator->Get()->Destroy();
        }
        // TMap에서 기록 삭제
        OwnerToIndicator.Remove(DestroyedActor);
    }
}

이제 HandlePhase_Drone처럼 주인이 명확한 경우에는 BindIndicatorToOwner를 호출하여 인디케이터를 자동으로 관리하므로, ActorsToShowThisPhase 배열에 수동으로 추가할 필요가 없어졌습니다.

  1. 액터 생명주기 관리 로직 명확화
    새로운 인디케이터 시스템 도입에 따라, 액터 관리 로직을 역할에 맞게 명확히 분리했습니다.
  • 영구 액터: Dialogue_02의 돌(Rock)처럼 단계가 끝나도 남아있어야 하는 액터는, TrackPhaseActor를 호출하지 않아 HidePhaseActors의 정리 대상에서 제외되도록 수정했습니다.
  • 임시 액터 (주인): 드론, 스폰된 광석처럼 단계가 끝나면 사라져야 하는 '주인' 액터들은 TrackPhaseActor를 통해 관리합니다.
  • 임시 액터 (종속된 인디케이터): '주인'이 있는 인디케이터들은 BindIndicatorToOwner를 통해 관리되어 주인의 생명주기를 따라갑니다.
  • 임시 액터 (주인 없는 인디케이터): Dialogue_02의 위치 표시용 인디케이터처럼 주인이 없는 경우는 예전처럼 TrackPhaseActor로 관리합니다.
// 예시: HandlePhase_Drone 함수
void AADTutorialGameMode::HandlePhase_Drone()
{
    // ...
    AActor* Drone = FoundActors[0];
    Drone->SetActorHiddenInGame(false);
    Drone->Tags.Add(FName("Radar"));
    TrackPhaseActor(Drone); // 주인 액터인 드론은 '일괄 정리' 대상

    if (IndicatingTargetClass)
    {
        AIndicatingTarget* Indicator = GetWorld()->SpawnActor<AIndicatingTarget>(...);
        if (Indicator)
        {
            // ...
            // 인디케이터는 주인에게 묶어 '자동 정리' 대상으로 설정
            BindIndicatorToOwner(Drone, Indicator); 
        }
    }
}
  1. HidePhaseActors 함수 안정성 강화
    HidePhaseActors가 직접 파괴하는 액터가 OnDestroyed 델리게이트를 불필요하게 호출하는 것을 막기 위해, 파괴 전에 델리게이트 바인딩을 먼저 해제하는 코드를 추가하여 안정성을 높였습니다.
void AADTutorialGameMode::HidePhaseActors()
{
    for (AActor* ActorToDestroy : ActorsToShowThisPhase)
    {
        if (ActorToDestroy) 
        {
            // 우리가 직접 파괴할 것이므로, OnDestroyed 델리게이트가 또 호출되지 않도록 연결을 끊어줍니다.
            ActorToDestroy->OnDestroyed.RemoveAll(this);
            ActorToDestroy->Destroy();
        }
    }
    ActorsToShowThisPhase.Empty();
}
profile
메타쏭이

0개의 댓글