unreal 5기

251226 언리얼엔진 본캠프 96일차 멀티플레이 팀프로젝트8

parkjinnam 2025. 12. 26. 20:34

무기를 줍고 버리기 기능을 구현하자

지난 시간에는 렉돌과 관련된 오류들을 수정했었고 성공적인 렉돌과 기절을 구현할 수 있었다. 이번에는 지난번 작성한 공격함수에서 얘기했듯 다양한 무기들을 줍고 버릴 수 있는 기능을 구현하고자 한다. 사실 이미 줍기 기능은 팀원이 임시로 오버랩되는 순간 바로 장착이 되도록 구현해 둔 상태이다. 이를 조금 변형하여 키 입력이 발생하면 장착할 수 있도록 수정해보려고 한다. 우리가 이 코드를 구현하면서 고려해야 할 사항은 크게 3가지가 된다.

 

1. 서버와 클라이언트 간의 위치 동기화

 

2. 오른손에는 검, 왼손에는 방패가 장착되도록 분리 구현(이미 되어있음)

 

3. 물리상태의 On/Off 전환

 

작성되어있는 줍기 시스템의 구조 파악

먼저 코드를 수정하기 전에 팀원의 코드가 어떤 식으로 작동되는지 파악해야 한다. 성공적인 코드 리팩토링을 위해서는 해당 구조를 잘 파악하고 있어야 하는 것인 기본 중에 기본이다. 팀원의 코드는 다음과 같은 구조로 되어있었다. BeginOverlap이 호출되어 BroadCast를 하면 이에 바인딩된 Shield와 Sword 액터가 Attach함수를 호출하여 플레이어 캐릭터의 소켓에 장착되는 형식으로 구현되어 있었다. 우리는 여기서 BroadCast를 가로채서 키 입력을 받을 때 특정 함수를 호출하고 그 BroadCast를 할 수 있도록 수정할 계획이다.

 

누구나 늘 계획은 있다. 쳐맞기 전까지는

간단하게 끝나리라 생각된 작업은 두 개의 난관에 봉착하고야 말았다. 아이템을 줍으려고 시도하는 순간 데디케이트 서버가 꺼지게 되는 것이었다. 하루 종일 새로 작성한 코드를 뒤지며 디버깅도 해보고 로그도 찍어본 결과 문제의 함수를 찾게 되었다.

void ATTPlayerCharacter::ServerPickUp_Implementation()
{
    if (IsValid(OverlappingPickupComponent))
    {
        OverlappingPickupComponent->ForcePickUp(this);
        
        AActor* PickedActor = OverlappingPickupComponent->GetOwner(); 

        if (ATTSword* NewSword = Cast<ATTSword>(PickedActor))
        {
			...
        }
    }
}

 

문제의 함수가 바로 이것 무엇이 문제일까? 문제는 바로 ForcePickup(브로드캐스트를 실행할 함수)와 그다음 줄의 GetOwner()를 호출하는 부분의 순서 때문이다. ForcePickup이 호출되면 무기의 Collision을 NoCollision이 되게 되어있으며 콜리전이 없어지면 엔진은 바로 EndOverlap을 호출하고 이를 가리키던 포인터에 nullptr이 대입된다. 그러한 nullptr을 참조하려고 시도를 하니 서버단에서 크래시를 일으키고 강제 종료가 되는 것 간단히 두 함수의 호출 순서를 바꾸어서 해결하게 되었다.

 

다음 문제는 분명히 작동 잘되던 코드에서 BroadCast를 호출하는 함수만 바뀌었을 뿐인데 전혀 줍기가 실행되지 않는다는 것이다. 중단점을 걸어도보고 로그를 통해서 호출되는지 확인을 해봐도 함수 자체는 모두 정상적으로 호출되는데 장착만 되지 않는 것이다. 이 문제를 해결하기 위해서 이틀을 고생했지만 너무 허무하게 해결되었다. 그놈의 원인은 바로

이 녀 석

멀티 환경에서는 안되던 코드가 StandAlone에서는 작동하는 것을 확인, 문제는 서버와 클라이언트간의 레플리케이션이 정상적이게 작동하지 않다고 생각했고, 생성자를 통해서 bReplicated를 true로 해도 현상이 지속되자 다른 변수가 있나 확인해 보니 컴포넌트의 디테일 패널에 해당 옵션이 있는 것을 발견하게 되었다. 정확히는 해당 장착 로직을 팀원이 구성할 때 PickupComponent를 따로 생성하여 이를 이용하여 BP를 생성했는데 해당 옵션이 꺼져있었고 이로 인해서 장착되는 것이 레플리케이션 되지 않고 있던 것!(크아아아아악)

해당 옵션을 체크해 주니 정상적인 장착이 이루어지게 되었다.

 

다사다난한 줍기, 그에 비해 편안했던 버리기

줍기 때 고생한 덕분일까? 버리기를 구현할 때는 순조롭게 구현되었다. 조립의 역순이 해체이듯 줍기의 역순으로 구현했는데, DetachFromActor로 떼어내고, 물리 시뮬레이션을 켜주고, 콜리전도 다시 켜주고, 아이템을 플레이어가 바라보는 방향으로 살짝 던져지게끔 AddImpulse를 적용해 주었다. 또한 버리기 함수인 DropItem이 호출될 때 오른손의 무기를 먼저 버리고 왼손의 무기를 버리게끔 구현하였다.

 

마치며

이번 작업의 내용은 그렇게 대단하지는 않지만 줍기를 구현하면서 마주한 문제를 해결하기 위해 2일이라는 금과 같은 시간을 소비했다. 하지만 어떠한 방식으로 코드를 작성해도 중요한 옵션 하나로 인해서 전혀 작동하지 않는다는 사실을 경험함으로써 좀 더 세밀하게 코드를 작성하고 테스트하게끔 된 거 같다. 또한 같은 코드라고 하더라도 어떤 코드가 먼저 오느냐에 따라 결과 값이 천차만별이라는 사실 또한 중요한 경험이었다. 이를 Actor의 생명주기라는 전문 용어가 따로 있다고 하는데 이를 신경 쓰면서 코드를 작성해야 하는 것 또한 알게 되었다. 다음에는 방어 기능을 구현해보려 한다.