Sparta Unreal 부트캠프 86일차

정찬호·2026년 4월 3일

팀프로젝트 — VGMission 시스템 리팩토링 및 구조 개선

1. TSet — == 연산자 미지원 문제

AVGMissionLeverCombo에서 현재 활성화된 레버 인덱스 집합과 정답 인덱스 집합을 비교하려 했는데, TSetTArray처럼 순서 기반 비교를 하는 컨테이너가 아니라 순서 없는 집합이기 때문에 == 연산자가 없었습니다.

대신 TSet::Includes()를 활용해서 한 집합이 다른 집합을 완전히 포함하는지 확인하는 방식으로 해결했습니다.

void AVGMissionLeverCombo::OnGimmickStateChanged(
    AVGMissionGimmickBase* Gimmick, FGameplayTag Tag)
{
    if (Gimmick->GetStateTag() == VigilantMissionTags::GimmickActive)
        CurrentOnLeverIndexes.Add(Gimmick->GetGimmickIndex());
    else
        CurrentOnLeverIndexes.Remove(Gimmick->GetGimmickIndex());

    // TSet == 미지원 → Num() 같고 Includes()로 포함 관계 검사
    if (CurrentOnLeverIndexes.Num() == RequiredOnLeverIndexes.Num()
        && RequiredOnLeverIndexes.Includes(CurrentOnLeverIndexes))
    {
        CompleteMission();
    }
}

Includes(Other)thisOther의 모든 원소를 포함하면 true를 반환합니다. 개수가 같고 포함 관계가 성립하면 두 집합이 동일하다는 의미가 됩니다.


2. TimedPressure / TimedLeverAll → AVGMissionTimedAll 통합

AVGMissionTimedPressureAVGMissionTimedLeverAll 두 클래스의 로직이 완전히 겹치는 것을 확인했습니다.

항목TimedPressureTimedLeverAll
트리거GimmickActive 시 타이머 시작GimmickActive 시 타이머 시작
완료 조건모든 Gimmick Active/Completed모든 Gimmick Active/Completed
타이머 만료상태 리셋상태 리셋

차이는 함수 이름뿐이었고 실제 동작은 동일해요. MissionGimmicks 배열에 어떤 Gimmick 타입을 넣느냐에 따라 자동으로 발판 미션도, 레버 미션도 동작하므로 AVGMissionTimedAll로 통합했습니다.

void AVGMissionTimedAll::OnGimmickStateChanged(
    AVGMissionGimmickBase* Gimmick, FGameplayTag Tag)
{
    if (Tag != VigilantMissionTags::GimmickActive) return;

    if (CurrentStateTag == VigilantMissionTags::MissionInactive)
    {
        StartTimer();
        SetMissionState(VigilantMissionTags::MissionActive);
    }

    if (AreAllGimmicActive())
    {
        CompleteMission();
    }
}

MissionGimmicks에 Pressure를 넣으면 발판 미션, Lever를 넣으면 레버 미션으로 동작하므로 에디터에서 Gimmick 타입만 구분해주면 충분합니다. 기존 두 클래스 파일을 제거하고 AVGMissionTimedAll로 변경했습니다.


3. 디커플링 분석 및 개선

현재 의존 관계

MissionBase  →  GimmickBase      (BeginPlay에서 SetGimmickIndex 호출)
MissionBase  →  MissionItemBase  (BeginPlay에서 Delegate 바인딩)
MissionBase  →  MissionSubsystem (RegisterMission 호출)
GimmickBase  →  MissionBase      ← 제거 완료 (Delegate로 대체)
ItemBase     →  MissionBase      ← OwnerMission 참조 제거 완료

GimmickBase → MissionBase 의존 완전 제거

이전에는 Gimmick이 OwnerMission->OnConditionMet(this)를 직접 호출하는 방식과OnGimmickStateChanged Delegate 방식을 혼용해 사용했습니다. 온전히 Delegate로 교체하면서 Gimmick은 Mission을 전혀 알지 못하게 됐습니다.

변경 전: Gimmick → OwnerMission->OnConditionMet()   (직접 참조)
변경 후: Gimmick → OnGimmickStateChanged.Broadcast() (Delegate)
                       ↓
             Mission::OnGimmickStateChanged() 에서 감지 및 판정

ItemBase 구조 개선 — EquippableActor 상속

팀원의 AVGEquippableActor를 머지받아 AVGMissionItemBase의 부모를 AVGInteractableActorBase에서 AVGEquippableActor로 교체했습니다. OwnerMission 직접 참조도 함께 제거하고, OnItemStateChanged Delegate만으로 미션과 통신하는 구조로 정리됐습니다.

class VIGILANT_API AVGMissionItemBase
    : public AVGEquippableActor
    , public IVGMissionObjectInterface
{
    ...
};

MissionObjectInterface 최종 정리

ReportConditionMet()SetOwnerMission()을 제거하고, 상태 태그 계약만 남겼습니다.

class VIGILANT_API IVGMissionObjectInterface
{
public:
    virtual FGameplayTag GetStateTag() = 0;
    virtual void SetStateTag(FGameplayTag NewStateTag) = 0;
};

4. 이펙트 구현 방향 설계

요청한 이펙트 유형별 구현 방향을 정리했다.

상태 변화 시 전체 클라이언트 파티클

OnRep_GimmickStateTag()ReplicatedUsing으로 모든 클라이언트에서 자동 호출되므로, 여기서 파티클을 재생하면 모든 클라이언트에 동기화된 이펙트를 줄 수 있다. GimmickBaseInactiveParticle, ActiveParticle, CompleteParticle 변수를 미리 선언해뒀습니다. 후에 DataAsset으로 분리할 계획입니다.

상호작용한 클라이언트에게만 보이는 파티클

서버 전용인 OnInteractWith()에서 Interactor의 PlayerController를 통해 Client RPC를 호출하는 방식을 고민중입니다. 관련 코드를 확인 후 Core 담당 팀원과 협의가 필요해보입니다.

[서버] OnInteractWith(Interactor)
    → Interactor->GetController() → PC 획득
    → PC->Client_PlayInteractEffect(위치)   ← Client RPC
        → [해당 클라이언트만] 파티클 재생

미션 활성화 시 OverlayMaterial

미션 상태가 MissionActive로 바뀔 때 OnRep_CurrentStateTag()에서 등록된 모든 Gimmick에 오버레이 머티리얼을 적용하는 방식을 고민 중입니다. UStaticMeshComponent::SetOverlayMaterial()은 기본 기능이라 별도 구현 없이 사용 가능하다고 합니다.

// MissionBase::OnRep_CurrentStateTag()에 추가 예정
if (CurrentStateTag == VigilantMissionTags::MissionActive)
{
    for (AVGMissionGimmickBase* Gimmick : MissionGimmicks)
        Gimmick->ApplyOverlayMaterial(ActiveOverlayMaterial);
}

5. 미션 위젯 구상

UVGMissionSubsystemOnMissionStateChanged Delegate를 UI 위젯이 구독하는 패턴으로 설계 중입니다. Subsystem이 이미 GetMissionProgress(), GetMissionCountByState() 등 조회 API를 갖추고 있습니다. 테스트 위젯을 만들어 실제 작동을 확인할 준비가 진행 중입니다.

MissionState 변경
    → MissionBase::OnMissionStateChanged.Broadcast()
        → Subsystem::HandleMissionStateChanged() (래퍼)
            → Subsystem::OnMissionStateChanged.Broadcast()
                → [위젯] HUD 갱신

6. GimmickStatue 구현 현황

석상 돌리기 기믹 구현을 시작했습니다. 핵심 설계는 다음과 같습니다.

변수역할복제 여부
TargetAngle현재 목표 각도ReplicatedUsing = OnRep_TargetAngle
AnswerAngle정답 각도에디터 지정, 복제 불필요
InitialAngle시작 각도 (리셋용)에디터 지정
bIsRotating회전 중 플래그클라이언트 로컬

TargetAngle만 복제하고, 클라이언트는 OnRep_TargetAngle() 수신 시 Tick을 켜서 보간 회전을 직접 처리합니다. 서버는 회전 완료 후 정답 여부를 체크해 Mission에 보고합니다.

void AVGMissionGimmickStatue::OnRep_TargetAngle()
{
    bIsRotating = true;
    SetActorTickEnabled(true);
}

void AVGMissionGimmickStatue::Tick(float DeltaTime)
{
    if (!bIsRotating) return;

    FRotator New = FMath::RInterpConstantTo(
        GetActorRotation(),
        FRotator(0.f, TargetAngle, 0.f),
        DeltaTime, RotationSpeed);
    SetActorRotation(New);

    if (FMath::Abs(New.Yaw - TargetAngle) < AngleTolerance)
    {
        bIsRotating = false;
        SetActorTickEnabled(false);
        // 서버에서 정답 체크 → SetStateTag 또는 Broadcast
    }
}

현재 남은 작업으로 OnInteractWith()에서 TargetAngle += RotateStep 처리, IsAtAnswerAngle() 구현, 회전 완료 후의 처리 등이 있습니다.


7. 오늘 수정된 버그 목록

버그파일수정 내용
TSet == 연산자 없음VGMissionLeverCombo.cppIncludes() 방식으로 변경
ItemStateTag DOREPLIFETIME 누락VGMissionItemBase.cpp추가
OwnerMission 잔존VGMissionItemBase제거 완료
GimmickBase.cpp 불필요한 MissionBase includeVGMissionGimmickBase.cpp제거

다음 작업 예정

  • AVGMissionGimmickStatue 완성 (OnInteractWith 로직, IsAtAnswerAngle 구현)
  • 석상 각도 미션 AVGMissionStatueAlign 구현
  • 파티클 및 OverlayMaterial 이펙트 적용
  • PlayerController Client RPC 확인
  • AVGMissionItemBase 완성
  • AVGMissionItemBase 자식 클래스 구현
  • MissionItem을 사용하는 미션 구현(물건 옮기기, 열쇠로 문/상자 열기)
profile
게임 개발 지망생입니다.

0개의 댓글