Sparta Unreal 부트캠프 92일차

정찬호·2026년 4월 13일

오늘 한 일

  • 발판 상하 이동 구현
  • 메인 맵에 미션 배치
  • 미션 기믹 시스템 버그 수정
  • 코딩 테스트 문제풀기 (프로그래머스 - 두 큐 합 같게 만들기)
  • Challenge 분반 수업 듣기(State Tree)

1. 미션 기믹 시스템 작업

Pressure 판 상하 이동 완료

UTimelineComponent를 활용해 압력판이 밟히면 Z축으로 내려가고, 떠나면 원래 위치로 복원되는 애니메이션을 구현했습니다.
압력판이 밟힐 때 GimmickStatTag가 변화하기 때문에 OnRep_StateTag에서 해당 로직을 호출하도록 구현했습니다.

Sandbag HP 위젯 (BP 구현)

C++ 위젯 클래스 없이 블루프린트만으로 구현했다.

  • WB_SangbagHP : GetPercent 함수에서 TargetSandbagStatComponent를 참조해 CurrentHP / MaxHP 비율을 반환
  • BP_Sandbag : Event Tick마다 플레이어 카메라를 향하도록 WidgetComponent의 월드 로테이션을 설정. OnDead 이벤트에서 위젯을 숨김 처리

ItemCarry 부착 위치 어긋남 버그 수정

1차적인 원인

TryPlaceItemToSlot 내부의 호출 순서가 문제였다.

// 기존 (버그 있음)
CarryItem->PlaceOnTarget(this, Slot.AttachOffset); // Gimmick에 부착
EquipComp->Server_DropItem(HandSlot);              // DetachFromActor 호출 → 방금 붙인 걸 다시 뜯어냄
CarryItem->SetActorRelativeLocation(Slot.AttachOffset); // 이미 detach된 상태에서 위치 설정

PlaceOnTarget에서 Gimmick에 AttachToActor를 완료했는데, 이후 Server_DropItem → HandleItemAttachment(false)DetachFromActor를 호출해 부착을 끊어버렸습니다.
그 상태에서 SetActorRelativeLocation을 호출하니 부착 기준점 없이 월드 좌표로 밀려나는 현상이 발생했고, 이 타이밍이 클라이언트마다 달랐기 때문에 어긋나는 정도도 제각각이었습니다.

수정

// 수정 후
EquipComp->Server_DropItem(HandSlot);              // 먼저 캐릭터 손에서 분리
CarryItem->PlaceOnTarget(this, Slot.AttachOffset); // 그 다음 Gimmick에 부착 (간섭 없음)
// SetActorRelativeLocation 중복 호출 제거 (PlaceOnTarget 내부에서 처리)

Server_DropItem을 먼저 호출해 캐릭터와의 부착을 완전히 끊은 뒤 PlaceOnTarget을 호출하는 것으로 해결하게 했습니다.
Attach 대상과 상대 위치를

2차적 원인
여기저기서 Attach를 시도하고, Attach 대상과 상대 위치 복사를 따로 하고 있기에 복제되는 타이밍이 일치하지 않는 경우가 발생했습니다. 때문에 엉뚱한 위치에 붙거나 Attach 대상에 파뭍히는 등의 문제가 발생했습니다.

해결
중복되는 호출코드를 제거하고 Attach 대상과 상대 위치를 하나의 구조체에 담아서 복제하도록 변경했습니다.

#include "VGMissionItemCarry.h"
#include "Common/VGGameplayTags.h"
#include "Mission/Gimmick/VGMissionGimmickBase.h"
#include "Net/UnrealNetwork.h"

AVGMissionItemCarry::AVGMissionItemCarry()
{
	PrimaryActorTick.bCanEverTick = false;
}

void AVGMissionItemCarry::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	
	DOREPLIFETIME(ThisClass, PlaceInfo);
}

void AVGMissionItemCarry::PlaceOnTarget(AVGMissionGimmickBase* TargetGimmick, FVector TargetRelativeLocation)
{
	if (!HasAuthority())
	{
		return;
	}
	
	if (UPrimitiveComponent* RootComp = Cast<UPrimitiveComponent>(GetRootComponent()))
	{
		RootComp->SetSimulatePhysics(false);
		RootComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	}
	
	// 캐릭터에서 분리 후 Gimmick에 부착
	DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
	
	Carrier = nullptr;
	OnRep_Carrier();
	
	FVGCarryPlaceInfo Info;
	Info.AttachmentTargetActor = TargetGimmick;
	Info.RelativeLocation = TargetRelativeLocation;
	PlaceInfo = Info;
	OnRep_PlaceInfo();
	
	SetStateTag(VigilantMissionTags::ItemPlaced);
	UE_LOG(LogTemp, Log, TEXT("[%s] item placed!"),*GetName());
}


void AVGMissionItemCarry::OnRep_PlaceInfo()
{
	if (UPrimitiveComponent* RootComp =
		Cast<UPrimitiveComponent>(GetRootComponent()))
	{
		RootComp->SetSimulatePhysics(false);
		RootComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	}
	
	AttachToActor(PlaceInfo.AttachmentTargetActor,
		FAttachmentTransformRules::KeepWorldTransform);
    	
	SetActorRelativeLocation(PlaceInfo.RelativeLocation);
	SetActorRelativeRotation(FRotator::ZeroRotator);
	
	UE_LOG(LogTemp, Warning, TEXT("[%s] Attachment placed! At %s %s"),*GetName(),
	 *PlaceInfo.AttachmentTargetActor.GetName(),
	  *PlaceInfo.RelativeLocation.ToString());
}

2. 코딩 테스트 - 두 큐 합 같게 만들기

실패코드

#include <string>
#include <vector>
#include <list>

using namespace std;


int solution(vector<int> queue1, vector<int> queue2) {
    int answer = -1;
    
    list<int> q1, q2;
    long long sum1 = 0, sum2 = 0;
    for(int i = 0; i < queue1.size(); i++)
    {
        sum1 += queue1[i];
        sum2 += queue2[i];
        q1.push_back(queue1[i]);
        q2.push_back(queue2[i]);
    }
    
    if((sum1 + sum2) % 2 != 0)
    {
        return answer;
    }
    
    int cnt = 0;
    while(sum1 != sum2)
    {
        if(cnt > queue1.size() * 2)
        {
            return answer;
        }
        
        if(sum1 < sum2)
        {
            int num = q2.front();
            q2.pop_front();
            q1.push_back(num);
            sum1 += num;
            sum2 -= num;
        }
        else
        {
            int num = q1.front();
            q1.pop_front();
            q2.push_back(num);
            sum2 += num;
            sum1 -= num;
        }
        cnt++;
    }
    
    answer = cnt;
    return answer;
}

정답률 96.7

테스트 1 〉	실패 (0.01ms, 4.2MB)
테스트 2 〉	통과 (0.01ms, 3.63MB)
테스트 3 〉	통과 (0.01ms, 3.59MB)
테스트 4 〉	통과 (0.01ms, 4.2MB)
테스트 5 〉	통과 (0.02ms, 4.15MB)
테스트 6 〉	통과 (0.02ms, 4.15MB)
테스트 7 〉	통과 (0.03ms, 4.21MB)
테스트 8 〉	통과 (0.06ms, 4.21MB)
테스트 9 〉	통과 (0.15ms, 4.21MB)
테스트 10 〉	통과 (0.23ms, 4.23MB)
테스트 11 〉	통과 (12.60ms, 9.53MB)
테스트 12 〉	통과 (6.67ms, 9.57MB)
테스트 13 〉	통과 (6.97ms, 11.9MB)
테스트 14 〉	통과 (7.56ms, 13.1MB)
테스트 15 〉	통과 (9.57ms, 14.6MB)
테스트 16 〉	통과 (9.90ms, 15.3MB)
테스트 17 〉	통과 (9.12ms, 15.8MB)
테스트 18 〉	통과 (31.33ms, 41.5MB)
테스트 19 〉	통과 (32.57ms, 41.7MB)
테스트 20 〉	통과 (34.79ms, 41.7MB)
테스트 21 〉	통과 (30.94ms, 42MB)
테스트 22 〉	통과 (36.54ms, 41.8MB)
테스트 23 〉	통과 (36.57ms, 42.8MB)
테스트 24 〉	통과 (38.29ms, 42.5MB)
테스트 25 〉	통과 (0.02ms, 4.15MB)
테스트 26 〉	통과 (0.02ms, 4.16MB)
테스트 27 〉	통과 (0.02ms, 4.21MB)
테스트 28 〉	통과 (16.79ms, 16.1MB)
테스트 29 〉	통과 (1.08ms, 4.51MB)
테스트 30 〉	통과 (14.84ms, 16MB)

삽입 삭제가 빈번하니 입력받은 vector를 list로 변경하는 과정을 넣었습니다.
양쪽 큐의 합이 홀수 이면 양쪽에 똑같이 나눌 수가 없으니 아예 연산을 하지 않도록 예외처리도 추가했고요.
이전에 풀었던 코드와 비교해도 크게 다른 것이 없는데, 반복 횟수 제한을 큐 크기(양쪽이 같음) 3에서 2로 변경한 것이 문제였던 걸까요?

정답 코드

#include <string>
#include <vector>
#include <list>

using namespace std;


int solution(vector<int> queue1, vector<int> queue2) {
    int answer = -1;
    
    list<int> q1, q2;
    long long sum1 = 0, sum2 = 0;
    for(int i = 0; i < queue1.size(); i++)
    {
        sum1 += queue1[i];
        sum2 += queue2[i];
        q1.push_back(queue1[i]);
        q2.push_back(queue2[i]);
    }
    
    if((sum1 + sum2) % 2 != 0)
    {
        return answer;
    }
    
    int cnt = 0;
    while(sum1 != sum2)
    {
        if(cnt > queue1.size() * 3)
        {
            return answer;
        }
        
        if(sum1 < sum2)
        {
            int num = q2.front();
            q2.pop_front();
            q1.push_back(num);
            sum1 += num;
            sum2 -= num;
        }
        else
        {
            int num = q1.front();
            q1.pop_front();
            q2.push_back(num);
            sum2 += num;
            sum1 -= num;
        }
        cnt++;
    }
    
    answer = cnt;
    return answer;
}

3. State Tree 스터디

State Tree에 대해 학습했습니다.

특징

  • GAS와 연동이 용이하고 직관적인 구조로 디버깅이 쉽다
  • 큰 상태 안에 하위 상태를 계층적으로 관리할 수 있다
  • 우선순위 지정, Tick/이벤트 기반 전환 선택이 가능하다
  • 외부에서 데이터를 가져오는 명세를 스키마(Schema) 라고 부른다
  • AI용으로 사용할 때는 기본 State Tree 대신 State Tree AI Component로 생성해야 AIController가 스키마에 포함된다

UE 5 이전에는 잘 쓰이지 않았으나 5로 넘어오면서 Enhanced Input과 마찬가지로 활발히 사용되기 시작했다.

필기는 여기까지만 하고 나머지는 직접 따라서 만드는 시간을 가졌습니다.
정상적으로 동작하게 만드는데에 실패했기에 추후 복습을 진행할 예정입니다.


profile
게임 개발 지망생입니다.

0개의 댓글