Sparta Unreal 부트캠프 88일차

정찬호·2026년 4월 7일

코딩 테스트

프로그래머스 - 삼각 달팽이

실패 코드

#include <string>
#include <vector>
#include <cmath>
#include <iostream>

using namespace std;

vector<int> solution(int n) {
    vector<int> answer;
    int answerSize = 0;
    for(int i = 1; i <= n; i++)
    {
        answerSize += i;
    }
    answer.resize(answerSize, 0);
    
    int currentIdx = 1;
    
    int leftDown = 0, bottomRight = 0, leftUp = 0;
    for(int i = 0; i <= ceil(n /4); i++)
    {
        leftDown = leftUp;
        for(int j = i; j < n - i; j++)
        {
            if(leftDown + j < answerSize
              && answer[leftDown + j] == 0)
            {
                leftDown += j;
                answer[leftDown] = currentIdx++;
            }
        }
        
        bottomRight = leftDown;
        for(int j = 0; j < n - i * 2; j++)
        {
            if(bottomRight + 1 < answerSize
              && answer[bottomRight + 1] == 0)
            {
                bottomRight++;
                answer[bottomRight] = currentIdx++;
            }
        }
        
        leftUp = bottomRight;
        for(int j = n - i; j > i; j--)
        {
            if(leftUp - j > 0
              && answer[leftUp - j] == 0)
            {
                leftUp -= j;
                answer[leftUp] = currentIdx++;
            }
        }
    }
    
    return answer;
}

정답률은 33.3%였습니다.

TA 분반 수업

디졸브 적용시키기 - 복습 필요

수업을 들으며 작성한 필기입니다.

Power 한 것이 어떻게 적용되서 Dissolve의 범위가 되는 것인가?
노이즈 텍스처로 들어오는 값은 0~1 사이의 선형적인 값
노이즈를 색상으로 표현하면 Power를 적용하면 입력된 Range의 값 만큼 제곱이 되어 어두운 부분은 더 어두워지게 됩니다.
어두운 영역이 좀 더 늘어나게 됨.
디좁르 자체는 이미시브 값을 사용, 오파시티도 마찬가지
어두운 부분과 중간값 사이의 부분이 존재
Power는 이미시브 영역을 추출하기 위함. 오프셋만큼의 영역
텍스처는 기준점. 디졸브를 어느 부분은 나타나게 하고, 어느 부분은 어둡게 해서 오파시티를 처리할지에 대한 기준점.
PallaxOcclusionMapping이라는 제공되는 MF가 있는 열어서 확인 가능. PallaxOcclusionMapping이라는은 언리얼 공식 잡기술이라고 합니다.
Marked는 알파 오버드로우가 발생하지 않음. Translucent는 반투명이 겹쳐서 알파 오버드로우가 되는 경우가 있습니다.
알파 오버드로우가 되면 뒷면이 비쳐 보인다던가, 일부가 투명해지는 것처럼 보이게 됩니다.

Translucent로 설정된 상태

Marked가 적용된 상태

팀프로젝트 — VGMission 시스템 클라이언트 동기화 및 버그 수정

1. UWorldSubsystem은 Replicate가 불가능하다

UVGMissionSubsystemRegisteredMissions 배열을 DOREPLIFETIME으로 복제하는 방법을 생각해보았지만 알아본 결과 UWorldSubsystem은 구조적으로 복제가 불가능하다고 합니다.

복제 시스템(GetLifetimeReplicatedProps, DOREPLIFETIME)은 AActorUActorComponent 계열에서만 동작합니다. UWorldSubsystemUObject → USubsystem → UWorldSubsystem 계열이라 네트워크 복제 시스템에 연결되어 있지 않습니다.

복제 가능: AActor, UActorComponent
복제 불가: UObject, USubsystem, UWorldSubsystem

2. 클라이언트 MissionSubsystem 동기화 해결책 — 자가 등록(Self-Registration) 방식

미션 액터 자체가 이미 bReplicates = true로 설정되어 서버에서 클라이언트로 복제됩니다. 이를 활용해 미션 액터가 클라이언트의 로컬 Subsystem에 스스로 등록하도록 변경했습니다.

핵심 아이디어: MissionID가 복제되는 타이밍을 트리거로 사용

클라이언트 BeginPlay() 시점에는 서버에서 설정한 MissionID 값이 아직 복제되지 않아서 기본값 -1입니다. Subsystem의 Server_RegisterMission()ID < 0이면 등록을 거부하므로, MissionID가 복제된 직후인 OnRep_MissionID()에서 등록해야 합니다.

// VGMissionBase.h
UPROPERTY(EditInstanceOnly, BlueprintReadOnly, Category = "Mission",
          ReplicatedUsing = OnRep_MissionID)
int32 MissionID = -1;

UFUNCTION()
virtual void OnRep_MissionID();
// VGMissionBase.cpp
void AVGMissionBase::GetLifetimeReplicatedProps(
    TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(ThisClass, CurrentStateTag);
    DOREPLIFETIME(ThisClass, MissionID); // 추가
}

void AVGMissionBase::OnRep_MissionID()
{
    // GIsServer 체크 불필요 — OnRep_는 클라이언트에서만 자동 호출됨
    if (UVGMissionSubsystem* Subsystem =
        GetWorld()->GetSubsystem<UVGMissionSubsystem>())
    {
        Subsystem->Client_RegisterMission(this);
    }
}

처음에 실수로 if (GIsServer)를 조건으로 넣었지만 빠르게 제거했습니다. GIsServer는 에디터 PIE에서 항상 true를 반환하여 클라이언트 등록이 차단되게 만듭니다. OnRep_는 클라이언트에서만 자동 호출되는 함수이므로 별도의 서버 체크가 필요 없습니다.


3. MissionSubsystem 서버/클라이언트 역할 분리

피드백을 반영해서 Subsystem을 서버 전용 함수와 클라이언트 전용 함수로 명확히 분리습니다.

서버 Subsystem: 게임 로직 담당
    Server_RegisterMission() — MissionID 유효성 검증 + 완료 이벤트 바인딩
    Server_OnMissionCompleted() — 전체 완료 체크 → OnAllMissionCompleted 발화

클라이언트 Subsystem: UI 데이터 캐싱 담당
    Client_RegisterMission() — UI용 상태 변경 이벤트만 바인딩
    OnMissionRegistered.Broadcast() — 위젯 생성 트리거

CompletedMissions TSet 제거 — 실시간 계산으로 전환

완료된 미션 ID를 별도로 모아둘 필요가 없었습니다. RegisteredMissions 배열의 각 미션이 CurrentStateTag를 가지고 있으므로 달성률은 필요할 때마다 실시간으로 계산합니다.

float UVGMissionSubsystem::GetMissionProgress() const
{
    if (RegisteredMissions.IsEmpty()) return 0.f;

    // TSet을 쓰지 않고 현재 상태 태그를 실시간 검사
    int32 CompletedCount = GetMissionCountByState(
        VigilantMissionTags::MissionCompleted);
    return static_cast<float>(CompletedCount) / RegisteredMissions.Num();
}

이렇게 하면 서버/클라이언트 양쪽에서 동일한 달성률을 보장받을 수 있습니다.


4. MissionListWidget 타이밍 문제 해결

문제: 위젯이 생성되는 시점이 미션 액터들이 클라이언트에 복제되어 BeginPlay를 호출하는 시점보다 빠를 수 있습니다. 초기화 시점에 Subsystem의 목록을 순회해도 빈 배열을 받아 위젯이 아무것도 표시하지 않을 것입니다.

해결: OnMissionRegistered Delegate를 추가하여 미션이 등록될 때마다 위젯에 알리게 합니다.

// VGMissionSubsystem.h
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMissionRegistered, AVGMissionBase*, Mission);

UPROPERTY(BlueprintAssignable)
FOnMissionRegistered OnMissionRegistered;
// VGMissionListWidget.cpp
void UVGMissionListWidget::InitializeWidget()
{
    if (UVGMissionSubsystem* MissionSubsystem = ...)
    {
        // 1. 앞으로 등록될 미션을 위해 먼저 Delegate 바인딩
        MissionSubsystem->OnMissionRegistered.AddDynamic(
            this, &UVGMissionListWidget::AddMissionWidget);

        // 2. 이미 등록된 미션들이 있다면 즉시 UI 생성
        const TArray<AVGMissionBase*>& Missions = MissionSubsystem->GetAllMissions();
        for (AVGMissionBase* Mission : Missions)
        {
            AddMissionWidget(Mission);
        }
    }
}

UFUNCTION() // AddDynamic 바인딩에 필요
void UVGMissionListWidget::AddMissionWidget(AVGMissionBase* Mission)
{
    if (!Mission) return;
    UVGMissionInfoWidget* InfoWidget =
        CreateWidget<UVGMissionInfoWidget>(GetOwningPlayer(), MissionInfoWidgetClass);
    if (!InfoWidget) return;
    MissionInfoScrollBox->AddChild(InfoWidget);
    InfoWidget->RegisterMission(Mission);
}

순서가 중요합니다. 선 바인딩 → 후 순회 순서로 해야 위젯 생성 후 미션 등록 이벤트를 놓치지 않습니다.


5. Gimmick 머티리얼 변경 버그 수정

테스트에서 Gimmick 상태 변화에 따른 머티리얼 색상 변경이 적용되지 않는 것을 확인했습니다. 원인 분석:

원인 1 — SetMaterial() 호출 누락

UMaterialInstanceDynamic::Create()로 다이나믹 머티리얼 인스턴스를 생성했지만, 메시에 실제로 적용하는 SetMaterial() 호출이 빠져있었습니다. 코드를 수정하는 과정에설 실수로 제거한 것 같습니다.

// 수정 전 — Create만 하고 SetMaterial 없음
DynamicMaterialInstance = UMaterialInstanceDynamic::Create(BaseMaterial, this);

// 수정 후
DynamicMaterialInstance = UMaterialInstanceDynamic::Create(BaseMaterial, this);
if (!DynamicMaterialInstance) return;
MeshComponent->SetMaterial(0, DynamicMaterialInstance); // 추가

원인 2 — FColorFLinearColor 혼용

// 수정 전 — FColor를 FLinearColor에 암묵적 변환 (감마 보정으로 색 왜곡)
Color = FColor::Black;
Color = FColor::Cyan;

// 수정 후 — FLinearColor 명시
Color = FLinearColor::Black;
Color = FLinearColor(0.f, 1.f, 1.f); // Cyan

FColorFLinearColor에 암묵적으로 변환하면 감마 보정이 적용되어 의도한 색상과 달라진다고 합니다.


6. MissionSandbag 설계 — GimmickBase 상속 안 한 이유

샌드백 미션 구현을 시작했습니다. AVGMissionSandbagAActor를 직접 상속하고 있습니다.

AVGMissionGimmickBase를 상속하지 않은 이유:

GimmickBaseMissionSandbag
상호작용E키로 플레이어가 직접 조작공격 히트로 자동 감지
상태 관리GimmickStateTag + ReplicateStatComponent::HP + Replicate
미션 보고OnGimmickStateChanged DelegateOnSandbagDefeated Delegate

샌드백은 플레이어가 때리는 대상이지 상호작용하는 대상이 아닙니다. CombatComponent가 Sweep으로 히트를 감지하고 StatComponent::ApplyDamage()를 직접 호출하는 흐름이라 IVGInteractable이 불필요합니다.

// 막타 플레이어 추적
void AVGMissionSandbag::RegisterAttacker(AVGCharacterBase* Attacker)
{
    if (!HasAuthority()) return;
    LastAttacker = Attacker; // 데미지를 줄 때마다 갱신
}

void AVGMissionSandbag::OnDead()
{
    OnSandbagDefeated.Broadcast(LastAttacker); // 미션에 막타 보고
}

7. 잔여 작업 및 계획

항목상태
StatComponentLastDamageSource 추가 요청요청 완료, 미반영
CombatComponent::Server_ProcessHitRegisterAttacker 연동대기 중
GimmickAltar 다중 슬롯 구조 개선🟢 설계 완료, 구현 예정
profile
게임 개발 지망생입니다.

0개의 댓글