== 연산자 미지원 문제AVGMissionLeverCombo에서 현재 활성화된 레버 인덱스 집합과 정답 인덱스 집합을 비교하려 했는데, TSet은 TArray처럼 순서 기반 비교를 하는 컨테이너가 아니라 순서 없는 집합이기 때문에 == 연산자가 없었습니다.
대신 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)는 this가 Other의 모든 원소를 포함하면 true를 반환합니다. 개수가 같고 포함 관계가 성립하면 두 집합이 동일하다는 의미가 됩니다.
AVGMissionTimedPressure와 AVGMissionTimedLeverAll 두 클래스의 로직이 완전히 겹치는 것을 확인했습니다.
| 항목 | TimedPressure | TimedLeverAll |
|---|---|---|
| 트리거 | 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로 변경했습니다.
MissionBase → GimmickBase (BeginPlay에서 SetGimmickIndex 호출)
MissionBase → MissionItemBase (BeginPlay에서 Delegate 바인딩)
MissionBase → MissionSubsystem (RegisterMission 호출)
GimmickBase → MissionBase ← 제거 완료 (Delegate로 대체)
ItemBase → MissionBase ← OwnerMission 참조 제거 완료
이전에는 Gimmick이 OwnerMission->OnConditionMet(this)를 직접 호출하는 방식과OnGimmickStateChanged Delegate 방식을 혼용해 사용했습니다. 온전히 Delegate로 교체하면서 Gimmick은 Mission을 전혀 알지 못하게 됐습니다.
변경 전: Gimmick → OwnerMission->OnConditionMet() (직접 참조)
변경 후: Gimmick → OnGimmickStateChanged.Broadcast() (Delegate)
↓
Mission::OnGimmickStateChanged() 에서 감지 및 판정
팀원의 AVGEquippableActor를 머지받아 AVGMissionItemBase의 부모를 AVGInteractableActorBase에서 AVGEquippableActor로 교체했습니다. OwnerMission 직접 참조도 함께 제거하고, OnItemStateChanged Delegate만으로 미션과 통신하는 구조로 정리됐습니다.
class VIGILANT_API AVGMissionItemBase
: public AVGEquippableActor
, public IVGMissionObjectInterface
{
...
};
ReportConditionMet()과 SetOwnerMission()을 제거하고, 상태 태그 계약만 남겼습니다.
class VIGILANT_API IVGMissionObjectInterface
{
public:
virtual FGameplayTag GetStateTag() = 0;
virtual void SetStateTag(FGameplayTag NewStateTag) = 0;
};
요청한 이펙트 유형별 구현 방향을 정리했다.
상태 변화 시 전체 클라이언트 파티클
OnRep_GimmickStateTag()는 ReplicatedUsing으로 모든 클라이언트에서 자동 호출되므로, 여기서 파티클을 재생하면 모든 클라이언트에 동기화된 이펙트를 줄 수 있다. GimmickBase에 InactiveParticle, 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);
}
UVGMissionSubsystem의 OnMissionStateChanged Delegate를 UI 위젯이 구독하는 패턴으로 설계 중입니다. Subsystem이 이미 GetMissionProgress(), GetMissionCountByState() 등 조회 API를 갖추고 있습니다. 테스트 위젯을 만들어 실제 작동을 확인할 준비가 진행 중입니다.
MissionState 변경
→ MissionBase::OnMissionStateChanged.Broadcast()
→ Subsystem::HandleMissionStateChanged() (래퍼)
→ Subsystem::OnMissionStateChanged.Broadcast()
→ [위젯] HUD 갱신
석상 돌리기 기믹 구현을 시작했습니다. 핵심 설계는 다음과 같습니다.
| 변수 | 역할 | 복제 여부 |
|---|---|---|
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() 구현, 회전 완료 후의 처리 등이 있습니다.
| 버그 | 파일 | 수정 내용 |
|---|---|---|
TSet == 연산자 없음 | VGMissionLeverCombo.cpp | Includes() 방식으로 변경 |
ItemStateTag DOREPLIFETIME 누락 | VGMissionItemBase.cpp | 추가 |
OwnerMission 잔존 | VGMissionItemBase | 제거 완료 |
GimmickBase.cpp 불필요한 MissionBase include | VGMissionGimmickBase.cpp | 제거 |
AVGMissionGimmickStatue 완성 (OnInteractWith 로직, IsAtAnswerAngle 구현)AVGMissionStatueAlign 구현AVGMissionItemBase 완성AVGMissionItemBase 자식 클래스 구현