TIL
07.15 TIL
2026. 7. 15. 20:24

일정표

시간 할 일 비고
08:30~10:00 코드카타, 팀프로젝트  
10:00~13:00 개인 프로젝트  
14:00~18:00 2시 스크럼, 팀프로젝트  
19:00~21:00 저녁 스크럼, 팀프로젝트, TIL  
21:00~22:00 운동  
22:00~23:30 개인 프로젝트  

*오늘의 코드카타*

 

문제. 로또의 최고 순위와 최저 순위

더보기

로또 6/45(이하 '로또'로 표기)는 1부터 45까지의 숫자 중 6개를 찍어서 맞히는 대표적인 복권입니다. 로또를 구매한 민우는 당첨 번호 발표일을 학수고대하고 있었습니다. 하지만, 민우의 동생이 로또에 낙서를 하여, 일부 번호를 알아볼 수 없게 되었습니다. 당첨 번호 발표 후, 민우는 자신이 구매했던 로또로 당첨이 가능했던 최고 순위와 최저 순위를 알아보고 싶어 졌습니다.

순서와 상관없이, 구매한 로또에 당첨 번호와 일치하는 번호가 있으면 맞힌 걸로 인정됩니다.

알아볼 수 없는 두 개의 번호를 각각 10, 6이라고 가정하면 3등에 당첨될 수 있습니다.3등을 만드는 다른 방법들도 존재합니다. 하지만, 2등 이상으로 만드는 것은 불가능합니다.

알아볼 수 없는 두 개의 번호를 각각 11, 7이라고 가정하면 5등에 당첨될 수 있습니다.5등을 만드는 다른 방법들도 존재합니다. 하지만, 6등(낙첨)으로 만드는 것은 불가능합니다.

민우가 구매한 로또 번호를 담은 배열 lottos, 당첨 번호를 담은 배열 win_nums가 매개변수로 주어집니다. 이때, 당첨 가능한 최고 순위와 최저 순위를 차례대로 배열에 담아서 return 하도록 solution 함수를 완성해주세요

#include <vector>
#include <algorithm>

using namespace std;

vector<int> solution(vector<int> lottos, vector<int> win_nums) {
    
    vector<int> rank = {6, 6, 5, 4, 3, 2, 1};
    
    int zero_count = count(lottos.begin(), lottos.end(), 0);
    
    int match_count = 0;
    for (int num : lottos)
    {
        if (num != 0 && find(win_nums.begin(), win_nums.end(), num) != win_nums.end())
        {
            match_count++;
        }
    }
    
    int max_match = match_count + zero_count;
    int min_match = match_count;
    
    return {rank[max_match], rank[min_match]};
}

 

std::count 함수 활용 : <algorithm> 헤더의 std::count 함수를 사용하면 단 한 줄로 배열 내 특정 값의 개수를 셀 수 있어 가독성이 향상된다.

 

std::find 함수를 통한 당첨 번호 검색 : 구매한 로또 번호(num)가 당첨 번호 리스트(win_nums)에 포함되어 있는지 std::find로 판별한다.

 

임시 벡터 반환 최적화 (return {x, y};) : 반환해야 할 벡터를 번거롭게 선언하고 대입(push_back)하는 오버헤드 없이, 중괄호 {} 초기화 리스트를 사용해 즉시 반환하여 메모리 효율과 컴파일 속도를 높일 수 있다.

 

시간 복잡도: O(N \times M) 으로, 로또 번호 개수 N=6, 당첨 번호 개수 M=6 이기 때문에 단 36번의 비교 연산 내외로 빠른 연산을 한다.

 

 


*팀 프로젝트*

작업한 내용들

 

[1. 차징 게이지]

 

차징 게이지 UI 구현을 위해 기존에 작성되어있는 코드를 다시 한번 살펴보았다.

 

UParcelHeroComponent (데이터 송신부):

던지기 충전 상태가 바뀔 때마다 신호를 보낼 델리게이트(OnThrowChargeChanged)를 선언. StartThrow(), TickComponent(), ReleaseThrow() 및 상호작용 예외 처리 시점에 이 델리게이트를 통해 bIsCharging과 ChargeRatio(0.0 ~ 1.0)를 브로드캐스트.

[안전장치 추가 필요]: 차징 도중 함정에 피격당해 상자를 강제로 떨어뜨리거나 래그돌 상태가 되면, 안전하게 차징 상태를 취소하고 UI를 숨기도록 예외 처리 코드를 강화해야 함.

 

UParcelHUDWidget (중간 다리 및 바인딩):

초기 안전 바인딩 단계(TryBindUIEvents)에서 로컬 플레이어의 UParcelHeroComponent를 감지하고 델리게이트를 구독. 이벤트가 감지되면 블루프린트 이벤트인 K2_OnThrowChargeChanged를 실행시켜 에디터(WBP)로 던져줘야 함.

 

WBP_InGameHUD (시각적 연출부):

C++ 부모 클래스가 호출해 주는 K2_OnThrowChargeChanged 이벤트를 받아, 화면 중앙의 게이지 바(ProgressBar)의 Percent를 채우고, 충전 중이 아닐 때는 투명하게 숨겨주는 비주얼 노드 배치.

 

1. HeroComponent 보완

더보기
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InputActionValue.h"
#include "ParcelHeroComponent.generated.h"

class USpringArmComponent;
class UCameraComponent;
class UInputMappingContext;
class UInputAction;

// [UI] 던지기 충전 상태 델리게이트
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnThrowChargeChangedSignature, bool, bIsCharging, float, ChargeRatio);

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class PARCEL_KNIGHT_API UParcelHeroComponent : public UActorComponent
{
	GENERATED_BODY()
	
public:	
	UParcelHeroComponent();
	virtual void BeginPlay() override;
	
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
	
	// 캐릭터의 입력을 바인딩 (SetupPlayerInputComponent 시점에 호출)
	void InitializePlayerInput(UInputComponent* PlayerInputComponent);
	void AddInputMappingContext();
	
	void ResetCameraAttachment();
	
	void EnterRagdollCameraMode();
	void ExitRagdollCameraMode();
	
	// [UI] HUD 위젯 바인딩
	UPROPERTY(BlueprintAssignable, Category = "Carry|Throw")
	FOnThrowChargeChangedSignature OnThrowChargeChanged;

	// [UI] 현재 로컬 차징 상태 게터 함수들
	UFUNCTION(BlueprintPure, Category = "Carry|Throw")
	FORCEINLINE bool IsChargingThrow() const { return bIsChargingThrow; }
	
	UFUNCTION(BlueprintPure, Category = "Carry|Throw")
	FORCEINLINE float GetThrowChargeRatio() const { return MaxThrowChargeTime > 0.f ? FMath::Clamp(CurrentThrowChargeTime / MaxThrowChargeTime, 0.f, 1.f) : 0.f; }
	
protected:
	// 카메라 컴포넌트 (인게임에 시점 변경이 필요하다면 이것도 분리 가능)
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
	TObjectPtr<USpringArmComponent> SpringArm;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
	TObjectPtr<UCameraComponent> FollowCamera;
	
	// EnhanceInput (인게임에서 조작 변경이 필요하다면 이것도 분리 가능)
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
	TObjectPtr<UInputMappingContext> InputMappingContext;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
	TObjectPtr<UInputAction> MoveAction;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
	TObjectPtr<UInputAction> LookAction;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
	TObjectPtr<UInputAction> JumpAction;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
	TObjectPtr<UInputAction> SprintAction;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
	TObjectPtr<UInputAction> RagdollAction;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
	TObjectPtr<UInputAction> InteractAction;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
	TObjectPtr<UInputAction> ThrowAction;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Carry|Throw")
	float MinThrowForce = 800.f;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Carry|Throw")
	float MaxThrowForce = 2200.f;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Carry|Throw")
	float MaxThrowChargeTime = 1.5f;

	bool bIsChargingThrow = false;
	float CurrentThrowChargeTime = 0.f;
	
	void Move(const FInputActionValue& Value);
	void Look(const FInputActionValue& Value);
	void StartJump(const FInputActionValue& Value);
	void StopJump(const FInputActionValue& Value);
	void StartSprint(const FInputActionValue& Value);
	void StopSprint(const FInputActionValue& Value);
	void TestRagdoll(const FInputActionValue& Value);
	void Interact(const FInputActionValue& Value);
	void StartThrow(const FInputActionValue& Value);
	void ReleaseThrow(const FInputActionValue& Value);
	
	// Ragdoll 카메라 세팅 에디터 노출
	UPROPERTY(EditAnywhere, Category = "Camera|Ragdoll")
	float RagdollCameraHeightOffset = 20.f;

	UPROPERTY(EditAnywhere, Category = "Camera|Ragdoll")
	float RagdollCameraBackOffset = 90.f;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Camera|Collision")
	float CameraCollisionProbeSize = 18.f;
	
private:
	// 달리기 기능을 위한 Server RPC
	UFUNCTION(Server, Reliable)
	void ServerSetSprinting(bool bNewIsSprinting);

	void ApplySprintSpeed(bool bNewIsSprinting);
	bool CanProcessLocalInput() const;
};
#include "Character/ParcelHeroComponent.h"
#include "ParcelLog.h"
#include "GameFramework/Character.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "EnhancedInputSubsystems.h"
#include "EnhancedInputComponent.h"
#include "Character/ParcelCharacter.h"
#include "Character/RagdollComponent.h"
#include "Character/ParcelInteractionComponent.h"
#include "Character/ParcelMovementStatComponent.h"
#include "Character/CharacterCarryComponent.h"
#include "Character/ParcelPlayerStateComponent.h"
#include "Components/DFStatusEffectComponent.h"

DEFINE_LOG_CATEGORY(LogHeroComp);

UParcelHeroComponent::UParcelHeroComponent()
{
    PrimaryComponentTick.bCanEverTick = true;
    PrimaryComponentTick.bStartWithTickEnabled = false;
    
    // [Server] : 컴포넌트에서 Server RPC 가동
    SetIsReplicatedByDefault(true);

    // 카메라
    SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
    SpringArm->TargetArmLength = 350.f;
    SpringArm->bDoCollisionTest = true;
    SpringArm->ProbeChannel = ECC_Camera;
    SpringArm->ProbeSize = CameraCollisionProbeSize;
    SpringArm->bUsePawnControlRotation = true;
    SpringArm->bEnableCameraLag = true;
    SpringArm->CameraLagSpeed = 10.f;

    FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
    FollowCamera->bUsePawnControlRotation = false;
}

void UParcelHeroComponent::BeginPlay()
{
    Super::BeginPlay();

    // 부모 캐릭터 루트에 카메라 부착
    if (ACharacter* Character = Cast<ACharacter>(GetOwner()))
    {
       SpringArm->AttachToComponent(Character->GetRootComponent(), FAttachmentTransformRules::SnapToTargetNotIncludingScale);
       FollowCamera->AttachToComponent(SpringArm, FAttachmentTransformRules::SnapToTargetNotIncludingScale, USpringArmComponent::SocketName);
    
       HEROCOMP_LOG(Log, TEXT("[%s] 캐릭터에 카메라 컴포넌트 부착 완료."), *Character->GetName());
    }

    AddInputMappingContext();
}

void UParcelHeroComponent::ResetCameraAttachment()
{
    if (ACharacter* Character = Cast<ACharacter>(GetOwner()))
    {
       if (SpringArm)
       {
          SpringArm->AttachToComponent(Character->GetRootComponent(), FAttachmentTransformRules::SnapToTargetNotIncludingScale);
          SpringArm->SetRelativeLocation(FVector::ZeroVector);
       }
    }
}

void UParcelHeroComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    ACharacter* Character = Cast<ACharacter>(GetOwner());
    if (!Character) return;

    if (Character->IsLocallyControlled() && bIsChargingThrow)
    {
        // [UI] 방어 코드 : 던지는 도중 맞거나 래그돌 등 상자 놓친 경우 예외 처리
        UCharacterCarryComponent* CarryComp = Character->FindComponentByClass<UCharacterCarryComponent>();
        if (!CarryComp || !CarryComp->IsCarrying())
        {
            HEROCOMP_LOG(Warning, TEXT("차징 연출을 강제 취소합니다."));
            bIsChargingThrow = false;
            CurrentThrowChargeTime = 0.f;
            
            OnThrowChargeChanged.Broadcast(false, 0.0f);
            
            URagdollComponent* RagdollComp = Character->FindComponentByClass<URagdollComponent>();
            bool bNeedsTick = RagdollComp && RagdollComp->IsRagdoll();
            if (!bNeedsTick)
            {
                PrimaryComponentTick.SetTickFunctionEnable(false);
            }
            return;
        }
        
        CurrentThrowChargeTime += DeltaTime;
        if (CurrentThrowChargeTime > MaxThrowChargeTime)
        {
            CurrentThrowChargeTime = MaxThrowChargeTime;
        }
        
        // [UI] 프레임마다 변경되는 ChargeRatio 전달
        float ChargeRatio = MaxThrowChargeTime > 0.f ? FMath::Clamp(CurrentThrowChargeTime / MaxThrowChargeTime, 0.f, 1.f) : 0.f;
        OnThrowChargeChanged.Broadcast(true, ChargeRatio);
        
        if (GEngine)
        {
            int32 Percentage = FMath::RoundToInt(ChargeRatio * 100.f);
            FString ProgressBar = TEXT("[");
            int32 BarCount = Percentage / 10;
            for (int32 i = 0; i < 10; ++i)
            {
                ProgressBar += (i < BarCount) ? TEXT("■") : TEXT("□");
            }
            ProgressBar += TEXT("]");

            FString ChargeMsg = FString::Printf(TEXT("던지기 충전 중... %s %d%%"), *ProgressBar, Percentage);
            GEngine->AddOnScreenDebugMessage(8888, 0.1f, FColor::Yellow, ChargeMsg);
        }
    }

    URagdollComponent* RagdollComp = Character->FindComponentByClass<URagdollComponent>();

    // 래그돌 상태가 활성화 되었을 때만 카메라 연산 가동함(카메라 보정)
    if (Character->IsLocallyControlled() && RagdollComp && RagdollComp->IsRagdoll() && Character->GetMesh() && SpringArm)
    {
        const FVector HeadLocation = Character->GetMesh()->GetSocketLocation(TEXT("head"));
        const FRotator ViewRotation = Character->GetController() ? Character->GetController()->GetControlRotation() : Character->GetActorRotation();
        const FVector CameraBackDirection = -FRotationMatrix(ViewRotation).GetUnitAxis(EAxis::X);
        const FVector TargetLocation = HeadLocation + FVector::UpVector * RagdollCameraHeightOffset + CameraBackDirection * RagdollCameraBackOffset;

        SpringArm->SetWorldLocation(TargetLocation);
    }
}

void UParcelHeroComponent::InitializePlayerInput(UInputComponent* PlayerInputComponent)
{
    AddInputMappingContext();

    if (!CanProcessLocalInput()) return;

    // IA 바인딩
    UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent);
    if (!EnhancedInputComponent) return;
    
    if (MoveAction) EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &UParcelHeroComponent::Move);
    
    if (LookAction) EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &UParcelHeroComponent::Look);
    
    if (JumpAction)
    {
       EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &UParcelHeroComponent::StartJump);
       EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &UParcelHeroComponent::StopJump);
    }
    
    if (SprintAction)
    {
       EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Started, this, &UParcelHeroComponent::StartSprint);
       EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Completed, this, &UParcelHeroComponent::StopSprint);
       EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Canceled, this, &UParcelHeroComponent::StopSprint);
    }
    
    if (RagdollAction) EnhancedInputComponent->BindAction(RagdollAction, ETriggerEvent::Started, this, &UParcelHeroComponent::TestRagdoll);
    
    if (InteractAction) 
    {
       EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &UParcelHeroComponent::Interact);
    }
    
    if (ThrowAction)
    {
       EnhancedInputComponent->BindAction(ThrowAction, ETriggerEvent::Started, this, &UParcelHeroComponent::StartThrow);
       EnhancedInputComponent->BindAction(ThrowAction, ETriggerEvent::Completed, this, &UParcelHeroComponent::ReleaseThrow);
    }
    
    HEROCOMP_LOG(Log, TEXT("Enhanced Input 바인딩 완료."));
}

void UParcelHeroComponent::Move(const FInputActionValue& Value)
{
    ACharacter* Character = Cast<ACharacter>(GetOwner());
    if (!CanProcessLocalInput() || !Character || !Character->GetController()) return;

    // [Add] 방어 코드 : 래그돌 상태에서는 입력 처리 불가
    URagdollComponent* RagdollComp = Character->FindComponentByClass<URagdollComponent>();
    if (RagdollComp && RagdollComp->IsRagdoll()) return;
    
	// 반전 함정
	FVector2D MoveValue = Value.Get<FVector2D>();
	if (const UDFStatusEffectComponent* StatusEffectComponent = Character->FindComponentByClass<UDFStatusEffectComponent>())
	{
		if (StatusEffectComponent->IsInputInverted())
		{
			MoveValue *= -1.0f;
		}
	}

    const FRotator ControlRotation = Character->GetController()->GetControlRotation();
    const FRotator YawRotation(0.f, ControlRotation.Yaw, 0.f);

    const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
    const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

    Character->AddMovementInput(ForwardDirection, MoveValue.Y);
    Character->AddMovementInput(RightDirection, MoveValue.X);
}

void UParcelHeroComponent::Look(const FInputActionValue& Value)
{
    ACharacter* Character = Cast<ACharacter>(GetOwner());
    if (!CanProcessLocalInput() || !Character) return;

    const FVector2D LookValue = Value.Get<FVector2D>();
    Character->AddControllerYawInput(LookValue.X);
    Character->AddControllerPitchInput(LookValue.Y);
}

void UParcelHeroComponent::StartJump(const FInputActionValue& Value)
{
    ACharacter* Character = Cast<ACharacter>(GetOwner());
    if (!CanProcessLocalInput() || !Character) return;

    // Throwing 액션 중에는 물리적인 점프 발동을 차단
    if (AParcelCharacter* ParcelChar = Cast<AParcelCharacter>(Character))
    {
        if (UParcelPlayerStateComponent* StateComp = ParcelChar->GetParcelPlayerStateComponent())
        {
            if (StateComp->HasStateTag(FGameplayTag::RequestGameplayTag(TEXT("Character.State.Throwing")))) return;
        } //TODO: 게임플레이 태그 Action 추가 및 State.Throwing 변경 필요
    }

    Character->Jump();
}

void UParcelHeroComponent::StopJump(const FInputActionValue& Value)
{
    ACharacter* Character = Cast<ACharacter>(GetOwner());
    if (CanProcessLocalInput() && Character) Character->StopJumping();
}

void UParcelHeroComponent::StartSprint(const FInputActionValue& Value)
{
    ACharacter* Character = Cast<ACharacter>(GetOwner());
    if (!Character) return;

    URagdollComponent* RagdollComp = Character->FindComponentByClass<URagdollComponent>();
    if (!CanProcessLocalInput() || (RagdollComp && RagdollComp->IsRagdoll())) return;

    ApplySprintSpeed(true);
    if (!Character->HasAuthority()) ServerSetSprinting(true);
}

void UParcelHeroComponent::StopSprint(const FInputActionValue& Value)
{
    ACharacter* Character = Cast<ACharacter>(GetOwner());
    if (!CanProcessLocalInput() || !Character) return;

    ApplySprintSpeed(false);
    if (!Character->HasAuthority()) ServerSetSprinting(false);
}

void UParcelHeroComponent::TestRagdoll(const FInputActionValue& Value)
{
    ACharacter* Character = Cast<ACharacter>(GetOwner());
    if (!CanProcessLocalInput() || !Character) return;

    URagdollComponent* RagdollComp = Character->FindComponentByClass<URagdollComponent>();
    if (!RagdollComp) return;

    const bool bWasRagdoll = RagdollComp->IsRagdoll();
	if (bWasRagdoll && !RagdollComp->IsRagdollCloseToGround()) 
	{
		HEROCOMP_LOG(Warning, TEXT("래그돌 해제 실패 : 현재 공중에 떠 있는 상태입니다. (지면과 너무 멂)"));
		return;
	}

    RagdollComp->ToggleRagdoll();

    // 래그돌이 켜질 때만 컴포넌트 틱을 킴
    if (RagdollComp->IsRagdoll())
    {
       HEROCOMP_LOG(Log, TEXT("래그돌 상태 진입: 카메라 보정을 위한 컴포넌트 틱 활성화"));
       PrimaryComponentTick.SetTickFunctionEnable(true);
    }
    else
    {
       HEROCOMP_LOG(Log, TEXT("래그돌 상태 해제: 카메라 위치 복구 및 컴포넌트 틱 비활성화"));
       PrimaryComponentTick.SetTickFunctionEnable(false);
       if (SpringArm)
       {
          SpringArm->AttachToComponent(Character->GetRootComponent(), FAttachmentTransformRules::SnapToTargetNotIncludingScale);
          SpringArm->SetRelativeLocation(FVector::ZeroVector);
       }
    }
}

void UParcelHeroComponent::Interact(const FInputActionValue& Value)
{
    if (!CanProcessLocalInput()) return;
    
    HEROCOMP_LOG(Log, TEXT("상호작용 조작(E키) 감지: InteractionComponent 호출"));
    
    // [Add] 방어 코드 : 캐릭터가 없으면 상호작용 중단
    
    ACharacter* Character = Cast<ACharacter>(GetOwner());
    if (!Character) return;
    
    // [Add] 방어 코드 : 래그돌 도중에는 상호작용 불가
    URagdollComponent* RagdollComp = Character->FindComponentByClass<URagdollComponent>();
    if (RagdollComp && RagdollComp->IsRagdoll()) return;

    // 만약 이미 상자를 들고 있다면 내려놓기(Drop) 실행
    if (UCharacterCarryComponent* CarryComp = Character->FindComponentByClass<UCharacterCarryComponent>())
    {
        if (CarryComp->IsCarrying())
        {
            HEROCOMP_LOG(Log, TEXT("이미 상자를 운반 중: 내려놓기(Drop) 실행"));
            
            // [방어코드] : 던지기 충전 중이었다면 충전 상태 해제
            if (bIsChargingThrow)
            {
                bIsChargingThrow = false;
                CurrentThrowChargeTime = 0.f;
                OnThrowChargeChanged.Broadcast(false, 0.0f);
                if (!RagdollComp || !RagdollComp->IsRagdoll())
                {
                    PrimaryComponentTick.SetTickFunctionEnable(false);
                }
            }
            
            CarryComp->Drop();
            return;
        }
    }

    // InAir 상태에서는 집기 상호작용 불가
    if (AParcelCharacter* ParcelChar = Cast<AParcelCharacter>(Character))
    {
        if (UParcelPlayerStateComponent* StateComp = ParcelChar->GetParcelPlayerStateComponent())
        {
            if (StateComp->HasStateTag(FGameplayTag::RequestGameplayTag(TEXT("Character.State.InAir")))) return;
        }
    }
    
    // 장착된 InteractionComponent 호출
    if (AParcelCharacter* OwnerChar = Cast<AParcelCharacter>(GetOwner()))
    {
       if (UParcelInteractionComponent* InteractComp = OwnerChar->GetParcelInteractionComponent())
       {
          InteractComp->PrimaryInteract();
       }
    }
}


void UParcelHeroComponent::ServerSetSprinting_Implementation(bool bNewIsSprinting)
{
    HEROCOMP_LOG(Log, TEXT("[Server] 클라이언트의 요청으로 달리기 상태 변경 적용: %s"), bNewIsSprinting ? TEXT("True") : TEXT("False"));
    ApplySprintSpeed(bNewIsSprinting);
}

void UParcelHeroComponent::ApplySprintSpeed(bool bNewIsSprinting)
{
    AParcelCharacter* ParcelChar = Cast<AParcelCharacter>(GetOwner());
    if (!ParcelChar) return;

    // 서버 전용 권한 확인 후 중앙 상태 창고 컴포넌트에 실시간 달리기 태그 토글 제어
    if (ParcelChar->HasAuthority())
    {
       if (UParcelPlayerStateComponent* StateComp = ParcelChar->GetParcelPlayerStateComponent())
       {
          FGameplayTag SprintTag = FGameplayTag::RequestGameplayTag(TEXT("Character.State.Sprinting"));
          if (bNewIsSprinting) StateComp->AddStateTag(SprintTag);
          else StateComp->RemoveStateTag(SprintTag);
       }
        
        // MovementStat을 담당하는 매니저에 속도 계산 위임
        if (UParcelMovementStatComponent* StatComp = ParcelChar->GetParcelMovementStatComponent())
        {
            StatComp->RefreshMoveSpeed();
        }
    }
}

void UParcelHeroComponent::AddInputMappingContext()
{
    ACharacter* Character = Cast<ACharacter>(GetOwner());
    if (!Character || !Character->IsLocallyControlled()) return;

    APlayerController* PlayerController = Cast<APlayerController>(Character->GetController());
    if (!PlayerController || !PlayerController->GetLocalPlayer()) return;

    UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer());
    if (!Subsystem || !InputMappingContext) return;

    Subsystem->RemoveMappingContext(InputMappingContext);
    Subsystem->AddMappingContext(InputMappingContext, 0);
}

bool UParcelHeroComponent::CanProcessLocalInput() const
{
    ACharacter* Character = Cast<ACharacter>(GetOwner());
    return Character && Character->GetController() && Character->IsLocallyControlled();
}

void UParcelHeroComponent::EnterRagdollCameraMode()
{
	PrimaryComponentTick.SetTickFunctionEnable(true);
	HEROCOMP_LOG(Log, TEXT("카메라 래그돌 모드 진입"));
}

void UParcelHeroComponent::ExitRagdollCameraMode()
{
	PrimaryComponentTick.SetTickFunctionEnable(false);
	ResetCameraAttachment();
	HEROCOMP_LOG(Log, TEXT("카메라 래그돌 모드 해제"));
}

void UParcelHeroComponent::StartThrow(const FInputActionValue& Value)
{
    if (!CanProcessLocalInput()) return;

    ACharacter* Character = Cast<ACharacter>(GetOwner());
    if (!Character) return;

    UCharacterCarryComponent* CarryComp = Character->FindComponentByClass<UCharacterCarryComponent>();
    if (CarryComp && CarryComp->IsCarrying())
    {
        bIsChargingThrow = true;
        CurrentThrowChargeTime = 0.f;
        
        // 게이지 모으는 중 틱 활성화
        PrimaryComponentTick.SetTickFunctionEnable(true);
        HEROCOMP_LOG(Log, TEXT("던지기 충전 시작: 틱 활성화"));
        
        // [UI] 던지기 차징 게이지 브로드캐스트
        OnThrowChargeChanged.Broadcast(true, 0.0f);
    }
}

void UParcelHeroComponent::ReleaseThrow(const FInputActionValue& Value)
{
    if (!bIsChargingThrow) return;

    ACharacter* Character = Cast<ACharacter>(GetOwner());
    if (!Character) return;

    UCharacterCarryComponent* CarryComp = Character->FindComponentByClass<UCharacterCarryComponent>();
    if (CarryComp && CarryComp->IsCarrying())
    {
        float ChargeRatio = FMath::Clamp(CurrentThrowChargeTime / MaxThrowChargeTime, 0.f, 1.f);
        float ForceMag = FMath::Lerp(MinThrowForce, MaxThrowForce, ChargeRatio);
        
        // 카메라의 조준 방향 계산 (약간 위로 향해 포물선을 그리도록 보정)
        FVector ThrowDir = FollowCamera->GetForwardVector();
        ThrowDir.Z += 0.2f;
        ThrowDir.Normalize();
        
        FVector ThrowForce = ThrowDir * ForceMag;
        CarryComp->Throw(ThrowForce);
        HEROCOMP_LOG(Log, TEXT("던지기 실행! 충전 비율: %f, 최종 힘: %f"), ChargeRatio, ForceMag);
    }

    // [UI] 던지기 차징 종료 브로드캐스트
    OnThrowChargeChanged.Broadcast(false, 0.0f);
    
    bIsChargingThrow = false;
    CurrentThrowChargeTime = 0.f;
    
    URagdollComponent* RagdollComp = Character->FindComponentByClass<URagdollComponent>();
    bool bNeedsTick = RagdollComp && RagdollComp->IsRagdoll();
    if (!bNeedsTick)
    {
        PrimaryComponentTick.SetTickFunctionEnable(false);
    }
}

2. HUDWidget 코드 수정

더보기
#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "GameplayTagContainer.h"
#include "ParcelHUDWidget.generated.h"

class AParcelGameState;
class ADeliveryBox;
/**
 *  UParcelWidget
 *  인게임 HUD 요소의 이벤트 바인딩하고 관리하는 베이스 위젯
 *  담당자 : JYW
 */
UCLASS()
class PARCEL_KNIGHT_API UParcelHUDWidget : public UUserWidget
{
	GENERATED_BODY()
	
protected:
	// 위젯의 BeginPlay 함수
	virtual void NativeConstruct() override;
	
	// 로컬 플레이어와 데이터를 바인딩
	void TryBindUIEvents();
	
	// WBP에서 사용할 수 있도록 열어두는 이벤트 (델리게이트 -> 브로드캐스트. K2는 Kismet2 약자입니다)
	UFUNCTION(BlueprintImplementableEvent, Category = "UI")
	void K2_OnHPChanged(float CurrentHP, float MaxHP);
	
	UFUNCTION(BlueprintImplementableEvent, Category = "UI")
	void K2_OnTeamScoreChanged(const FText& DisplayText);
	
	UFUNCTION(BlueprintImplementableEvent, Category = "ParcelUI")
	void K2_OnRemainingTimeChanged(const FText& DisplayText, float RawRemainingTime);
	
	UFUNCTION(BlueprintImplementableEvent, Category = "ParcelUI")
	void K2_OnCrosshairStateChanged(bool bCanInteract, const FText& InteractionPrompt);
	
	UFUNCTION(BlueprintImplementableEvent, Category = "ParcelUI")
	void K2_OnCarriedBoxInfoChanged(bool bIsCarrying, const FText& BoxTypeName, const FText& DestinationText, FGameplayTag BoxTypeTag);
	
	UFUNCTION(BlueprintImplementableEvent, Category = "ParcelUI")
	void K2_OnComboChanged(int32 NewComboCount, const FText& DisplayText);
	
	UFUNCTION(BlueprintImplementableEvent, Category = "ParcelUI")
	void K2_OnCharacterStateChanged(const FGameplayTagContainer& ActiveTags);
	
	UFUNCTION(BlueprintImplementableEvent, Category = "ParcelUI")
	void K2_OnThrowChargeChanged(bool bIsCharging, float ChargeRatio);
	
private:
	
	UFUNCTION()
	void HandleOnTeamScoreChanged(int32 NewTeamScore);
	
	UFUNCTION()
	void HandleOnComboChanged(int32 NewComboCount);
	
	// [Timestamp] 핸들러(종료 시각)
	UFUNCTION()
	void HandleOnExpirationTimeChanged(float NewExpirationTime);
	
	UFUNCTION() 
	void HandleOnInteractionFocusChanged(AActor* NewFocusedActor);
	
	UFUNCTION() 
	void HandleOnCarriedBoxChanged(ADeliveryBox* NewCarriedBox);
	
	UFUNCTION()
	void HandleOnCharacterStateChanged(const FGameplayTagContainer& ActiveTags);
	
	UFUNCTION()
	void HandleOnThrowChargeChanged(bool bIsCharging, float ChargeRatio);
	
	// [Timestamp] (남은 시간)
	void UpdateLocalTimer();
	
	// 안전한 접근을 위해 캐싱
	UPROPERTY()
	TWeakObjectPtr<AParcelGameState> CachedGameState;
	
	// 안전장치용 타이머 핸들
	FTimerHandle RetryBindTimerHandle;
	
	// [Timestamp] UI 자체 로컬 타이머 핸들 및 종료 시간 저장 변수
	FTimerHandle UILocalTimerHandle;
	float CachedExpirationTime = 0.0f;
};
#include "UI/ParcelHUDWidget.h"
#include "ParcelLog.h"
#include "GameFramework/GameStateBase.h"
#include "Core/ParcelGameState.h"
#include "Core/TeamScoreComponent.h"
#include "Character/ParcelInteractionComponent.h" 
#include "Core/HealthComponent.h"
#include "Character/CharacterCarryComponent.h"
#include "Character/ParcelCharacter.h"
#include "Character/ParcelHeroComponent.h"
#include "Character/ParcelPlayerStateComponent.h"
#include "Delivery/DeliveryBox.h"
#include "GameFramework/Pawn.h"

DEFINE_LOG_CATEGORY(LogInGameHUD);

void UParcelHUDWidget::NativeConstruct()
{
	Super::NativeConstruct();
	TryBindUIEvents();
}

void UParcelHUDWidget::TryBindUIEvents()
{
	bool bInteractionBound = false;
	bool bHealthBound = false;
	bool bCarryBound = false;
	bool bComboBound = false;
	bool bCharacterStateBound = false;
	bool bHeroBound = false;
	
	// GameState와 TeamScore 바인딩
	if (!CachedGameState.IsValid())
	{
		CachedGameState = Cast<AParcelGameState>(GetWorld()->GetGameState());
		if (CachedGameState.IsValid())
		{
			// Todo : 디커플링을 위해서 FindComponentByClass를 사용했습니다. 배포 버전을 만들 때 게터로 리팩토링이 필요합니다.
			UTeamScoreComponent* TeamScoreComp = CachedGameState->FindComponentByClass<UTeamScoreComponent>();
			
			if (TeamScoreComp)
			{
				// 이벤트 바인딩
				TeamScoreComp->OnTeamScoreChanged.RemoveDynamic(this, &UParcelHUDWidget::HandleOnTeamScoreChanged);
				TeamScoreComp->OnTeamScoreChanged.AddDynamic(this, &UParcelHUDWidget::HandleOnTeamScoreChanged);
				HandleOnTeamScoreChanged(TeamScoreComp->GetTeamScore());
				
				// 콤보 시스템 바인딩
				TeamScoreComp->OnComboChanged.RemoveDynamic(this, &UParcelHUDWidget::HandleOnComboChanged);
				TeamScoreComp->OnComboChanged.AddDynamic(this, &UParcelHUDWidget::HandleOnComboChanged);
				HandleOnComboChanged(TeamScoreComp->GetComboCount());

				// 라운드 만료 시간 바인딩
				TeamScoreComp->OnRemainingTimeChanged.RemoveDynamic(this, &UParcelHUDWidget::HandleOnExpirationTimeChanged);
				TeamScoreComp->OnRemainingTimeChanged.AddDynamic(this, &UParcelHUDWidget::HandleOnExpirationTimeChanged);
				HandleOnExpirationTimeChanged(TeamScoreComp->GetRemainingTime());
				
				bComboBound = true;
			}
		}
	}

	// 2. PlayerState 바인딩(삭제)
	
	// 3. 컴포넌트 바인딩
	if (APawn* OwningPawn = GetOwningPlayerPawn())
	{
		if (AParcelCharacter* ParcelChar = Cast<AParcelCharacter>(OwningPawn))
		{
			if (UParcelPlayerStateComponent* StateComp = ParcelChar->GetParcelPlayerStateComponent())
			{
				StateComp->OnCharacterStateTagsChanged.RemoveDynamic(this, &UParcelHUDWidget::HandleOnCharacterStateChanged);
				StateComp->OnCharacterStateTagsChanged.AddDynamic(this, &UParcelHUDWidget::HandleOnCharacterStateChanged);
             
				// 진입 시점의 최초 캐릭터 상태 태그 강제 초기화
				HandleOnCharacterStateChanged(StateComp->GetCharacterStateTags());
				bCharacterStateBound = true;
			}
		}
		
		// 상호작용
		if (UParcelInteractionComponent* InteractComp = OwningPawn->FindComponentByClass<UParcelInteractionComponent>())
		{
			InteractComp->OnFocusChanged.RemoveDynamic(this, &UParcelHUDWidget::HandleOnInteractionFocusChanged);
			InteractComp->OnFocusChanged.AddDynamic(this, &UParcelHUDWidget::HandleOnInteractionFocusChanged);
			HandleOnInteractionFocusChanged(InteractComp->GetCurrentFocusedActor());
			bInteractionBound = true;
		}

		// 체력
		if (UHealthComponent* HealthComp = OwningPawn->FindComponentByClass<UHealthComponent>())
		{
			HealthComp->OnHPChanged.RemoveDynamic(this, &UParcelHUDWidget::K2_OnHPChanged);
			HealthComp->OnHPChanged.AddDynamic(this, &UParcelHUDWidget::K2_OnHPChanged);
			K2_OnHPChanged(HealthComp->GetHP(), HealthComp->GetMaxHP());
			bHealthBound = true;
		}

		// 운반
		if (UCharacterCarryComponent* CarryComp = OwningPawn->FindComponentByClass<UCharacterCarryComponent>())
		{
			CarryComp->OnCarriedBoxChanged.RemoveDynamic(this, &UParcelHUDWidget::HandleOnCarriedBoxChanged);
			CarryComp->OnCarriedBoxChanged.AddDynamic(this, &UParcelHUDWidget::HandleOnCarriedBoxChanged);

			HandleOnCarriedBoxChanged(CarryComp->GetCarriedBox());
			bCarryBound = true;
		}
		
		// 던지기 차징 게이지
		if (UParcelHeroComponent* HeroComp = OwningPawn->FindComponentByClass<UParcelHeroComponent>())
		{
			HeroComp->OnThrowChargeChanged.RemoveDynamic(this, &UParcelHUDWidget::HandleOnThrowChargeChanged);
			HeroComp->OnThrowChargeChanged.AddDynamic(this, &UParcelHUDWidget::HandleOnThrowChargeChanged);
			HandleOnThrowChargeChanged(HeroComp->IsChargingThrow(), HeroComp->GetThrowChargeRatio());
			bHeroBound = true;
		}
	}
	
	// 4. 멀티플레이 안전장치
	if (CachedGameState.IsValid() && bInteractionBound && bHealthBound && bCarryBound && bComboBound && bCharacterStateBound && bHeroBound)
	{
		GetWorld()->GetTimerManager().ClearTimer(RetryBindTimerHandle);
		INGAMEHUD_LOG(Log, TEXT("[UI] 모든 인게임 HUD 요소가 안전하게 완전 결합되었습니다."));
	}
	else
	{
		if (!RetryBindTimerHandle.IsValid() && GetWorld())
		{
			GetWorld()->GetTimerManager().SetTimer(RetryBindTimerHandle, this, &UParcelHUDWidget::TryBindUIEvents, 0.1f, true);
			INGAMEHUD_LOG(Warning, TEXT("[UI] 일부 액터 복제 대기 중. 0.1초 후 결합을 재시도합니다."));
		}
	}
}

// 핸들러 함수

void UParcelHUDWidget::HandleOnTeamScoreChanged(int32 NewTeamScore)
{
	FText FormattedText = FText::Format(FText::FromString(TEXT("팀 점수 : {0}")), FText::AsNumber(NewTeamScore));
	K2_OnTeamScoreChanged(FormattedText);
}

void UParcelHUDWidget::HandleOnComboChanged(int32 NewComboCount)
{
	if (NewComboCount > 0 && CachedGameState.IsValid())
	{
		UTeamScoreComponent* TeamScoreComp = CachedGameState->FindComponentByClass<UTeamScoreComponent>();
		if (TeamScoreComp)
		{
			float CurrentMultiplier = TeamScoreComp->GetComboMultiplier();
			
			int32 BonusPercent = FMath::RoundToInt((CurrentMultiplier - 1.0f) * 100.f);

			FText FormattedText;
			if (BonusPercent > 0)
			{
				FormattedText = FText::Format(
					FText::FromString(TEXT("연속 배송 성공! {0} Combo! (+{1}% 점수 보너스!)")), 
					FText::AsNumber(NewComboCount),
					FText::AsNumber(BonusPercent)
				);
			}
			else
			{
				FormattedText = FText::Format(
					FText::FromString(TEXT("연속 배송 성공! {0} Combo!")), 
					FText::AsNumber(NewComboCount)
				);
			}
			K2_OnComboChanged(NewComboCount, FormattedText);
			return;
		}
	}
	
	K2_OnComboChanged(0, FText::GetEmpty());
}

void UParcelHUDWidget::HandleOnExpirationTimeChanged(float NewExpirationTime)
{
	if (GetWorld() && NewExpirationTime > 0.0f)
	{
		CachedExpirationTime = GetWorld()->GetTimeSeconds() + NewExpirationTime;

		GetWorld()->GetTimerManager().ClearTimer(UILocalTimerHandle);
		GetWorld()->GetTimerManager().SetTimer(UILocalTimerHandle, this, &UParcelHUDWidget::UpdateLocalTimer, 0.2f, true);
	}
}

void UParcelHUDWidget::HandleOnInteractionFocusChanged(AActor* NewFocusedActor)
{
	if (NewFocusedActor)
	{
		FText PromptText = FText::FromString(TEXT("E 키를 눌러 상호작용"));
		K2_OnCrosshairStateChanged(true, PromptText);
	}
	else
	{
		K2_OnCrosshairStateChanged(false, FText::GetEmpty());
	}
}

void UParcelHUDWidget::HandleOnCarriedBoxChanged(ADeliveryBox* NewCarriedBox)
{
	if (!NewCarriedBox)
	{
		K2_OnCarriedBoxInfoChanged(false, FText::GetEmpty(), FText::GetEmpty(), FGameplayTag());
		return;
	}
	
	FBoxData CarriedBoxData = NewCarriedBox->GetBoxData();
	
	FText BoxNameText = FText::FromString(CarriedBoxData.DisplayName);
	FText FormattedName = FText::Format(
		FText::FromString(TEXT("{0} ({1}kg)")), 
		BoxNameText, 
		FText::AsNumber(CarriedBoxData.Weight)
	);
	
	FText DestinationText = FText::FromString(TEXT("목적지 : 미지정 구역"));
	if (CarriedBoxData.TargetZoneTag.IsValid())
	{
		FString ZoneString = CarriedBoxData.TargetZoneTag.ToString();
		ZoneString.ReplaceInline(TEXT("Delivery."), TEXT(""));
		ZoneString.ReplaceInline(TEXT("Zone."), TEXT(""));
		
		DestinationText = FText::Format(
			FText::FromString(TEXT("목적지 : {0} 구역")), 
			FText::FromString(ZoneString)
		);
	}
	
	K2_OnCarriedBoxInfoChanged(true, FormattedName, DestinationText, CarriedBoxData.BoxTypeTag);
}

void UParcelHUDWidget::UpdateLocalTimer()
{
	float CurrentWorldTime = GetWorld()->GetTimeSeconds();
	float RawRemainingTime = CachedExpirationTime - CurrentWorldTime;

	if (RawRemainingTime <= 0.0f)
	{
		GetWorld()->GetTimerManager().ClearTimer(UILocalTimerHandle);
		RawRemainingTime = 0.0f;
	}
	
	// Min Sec 쪼개기
	int32 TotalSeconds = FMath::FloorToInt(RawRemainingTime);
	int32 Minutes = TotalSeconds / 60;
	int32 Seconds = TotalSeconds % 60;
	
	// 언리얼 내부 함수를 사용해서 2자리 수 맞추기
	FNumberFormattingOptions TwoDigitOptions;
	TwoDigitOptions.MinimumIntegralDigits = 2;
	
	FText MinText = FText::AsNumber(Minutes, &TwoDigitOptions);
	FText SecText = FText::AsNumber(Seconds, &TwoDigitOptions);
	
	// Format Text 문자열 조립
	FText FormattedTime = FText::Format(
		FText::FromString(TEXT("남은시간 {0} : {1}")), 
		MinText, 
		SecText
	);
	
	K2_OnRemainingTimeChanged(FormattedTime, RawRemainingTime);
}

void UParcelHUDWidget::HandleOnCharacterStateChanged(const FGameplayTagContainer& ActiveTags)
{
	K2_OnCharacterStateChanged(ActiveTags);
}

void UParcelHUDWidget::HandleOnThrowChargeChanged(bool bIsCharging, float ChargeRatio)
{
	K2_OnThrowChargeChanged(bIsCharging, ChargeRatio);
}

3. WBP_InGameHUD 연동

(추가 작업)게이지가 100%가 되면 반짝이게 해서 플레이어가 직관적으로 알 수 있도록 연출했다.

 

참고로 Charge Ratio 다른 함수나 다른 이벤트 그래프 영역에서 이 값이 두고두고 필요해서 어쩔 수 없이 변수로 저장(Set)해야 한다면, 반드시 최대한 그래프 앞선 타이밍에 하얀색 실행 선을 Set 노드에 먼저 통과시켜서 값을 저장한 뒤, 그 이후의 실행 흐름에서 Get으로 꺼내 써야 안전하다!

 

[2. 디버깅]

1. 점프 시 대량의 에러 로그 발생과 함께 3~4초간 렉이 발생하고 게임이 프리징되는 현상 해결 -> 우선 GameplayTag를 점검하여 누락된 태그를 추가했으며,RequestGameplayTag 함수의 두 번째 인자인 bErrorIfNotFound 값을 false로 지정하면, 에디터에 태그 등록을 깜빡했더라도 에러를 터뜨리지 않고 그냥 깔끔하게 빈 태그를 반환하므로 렉을 유발하지 않도록 할 수 있음.

FGameplayTag ThrowingAction = FGameplayTag::RequestGameplayTag(TEXT("Character.Action.Throwing"), false);
            FGameplayTag ThrowingState = FGameplayTag::RequestGameplayTag(TEXT("Character.State.Throwing"), false);

 

 

[3. 캐릭터 위에 플레이어 아이디 출력]

 

1. NameplateWidget 구현

멀티플레이어 게임에서 나를 포함한 다른 플레이어들의 머리 위에 실시간으로 닉네임이나 ID가 떠 있게 하려면 구조가 완전히 달라야 한다. 화면에 고정된 2D UI가 아니라, 각 캐릭터 액터의 머리 위에 3D 공간상으로 따라다니는 3D 오브헤드 네임플레이트(Overhead Nameplate) 위젯과 이를 부착할 WidgetComponent가 필요하다.

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "ParcelNameplateWidget.generated.h"

class UTextBlock;

/**
 *
 */
UCLASS()
class PARCEL_KNIGHT_API UParcelNameplateWidget : public UUserWidget
{
	GENERATED_BODY()

public:
	void SetPlayerName(const FString& InName);

protected:
	UPROPERTY(meta = (BindWidget))
	TObjectPtr<UTextBlock> Txt_PlayerName;
};

#include "UI/ParcelNameplateWidget.h"
#include "Components/TextBlock.h"

void UParcelNameplateWidget::SetPlayerName(const FString& InName)
{
	if (Txt_PlayerName)
	{
		Txt_PlayerName->SetText(FText::FromString(InName));
	}
}

 

2. ParcelCharacter. 보완

이제 메인 캐릭터 클래스에 위에서 만든 위젯을 담아둘 UWidgetComponent를 부착하고, 네트워크 복제 타이밍에 맞춰 닉네임을 밀어 넣어주는 안전장치를 설계한다. 기존 헤더 파일의 public: 영역과 protected: 영역에 각각 아래 코드를 보완했다.

 

// [UI] 3D 아이디 위젯
	virtual void OnRep_PlayerState() override;
    
    // Nameplate Component
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Component")
	TObjectPtr<UWidgetComponent> NameplateWidgetComp;
	
	void UpdateOverheadNameplate();
	
	FTimerHandle NameplateRetryTimerHandle;

 

더보기
#include "Character/ParcelCharacter.h"
#include "ParcelLog.h"
#include "Character/RagdollComponent.h"
#include "Character/ParcelHeroComponent.h"
#include "Character/ParcelInteractionComponent.h"
#include "Character/ParcelMovementStatComponent.h"
#include "Character/CharacterCarryComponent.h"
#include "Character/ParcelPlayerStateComponent.h"
#include "Components/DFKnockbackComponent.h"
#include "Components/DFStatusEffectComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Net/UnrealNetwork.h"
#include "TimerManager.h"
#include "Components/WidgetComponent.h"
#include "Core/HealthComponent.h"
#include "Core/ParcelPlayerState.h"
#include "UI/ParcelNameplateWidget.h"


DEFINE_LOG_CATEGORY(LogCharacter);

AParcelCharacter::AParcelCharacter()
{
    PrimaryActorTick.bCanEverTick = false;

    SetReplicateMovement(true);
    
    // 캐릭터 액터 자체를 네트워크에 복제
    bReplicates = true;

    // 움직임이 잦은 플레이어 캐릭터라서 기본보다 높은 빈도로 네트워크 갱신을 요청
    SetNetUpdateFrequency(100.f);

    // 네트워크 상태가 안정적일 때도 너무 낮은 빈도로 떨어지지 않게 최소 갱신 빈도를 지정합니다.
    SetMinNetUpdateFrequency(33.f);

    bUseControllerRotationPitch = false;
    bUseControllerRotationYaw = false;
    bUseControllerRotationRoll = false;

    // 이동 입력 방향을 바라보도록 캐릭터를 자동 회전
    GetCharacterMovement()->bOrientRotationToMovement = true;

    // 공중에서 이동 입력이 얼마나 반영되는지 정합니다.
    GetCharacterMovement()->AirControl = 0.35f;

	// 컴포넌트 조립
  PlayerStateComp = CreateDefaultSubobject<UParcelPlayerStateComponent>(TEXT("PlayerStateComp"));
	RagdollComp = CreateDefaultSubobject<URagdollComponent>(TEXT("RagdollComp"));
	StatusEffectComponent = CreateDefaultSubobject<UDFStatusEffectComponent>(TEXT("StatusEffectComponent"));
	KnockbackComponent = CreateDefaultSubobject<UDFKnockbackComponent>(TEXT("KnockbackComponent"));
	HeroComp = CreateDefaultSubobject<UParcelHeroComponent>(TEXT("HeroComp"));
	InteractionComp = CreateDefaultSubobject<UParcelInteractionComponent>(TEXT("InteractionComp"));
	MovementStatComp = CreateDefaultSubobject<UParcelMovementStatComponent>(TEXT("MovementStatComp"));
	CarryComp = CreateDefaultSubobject<UCharacterCarryComponent>(TEXT("CarryComp"));
	NameplateWidgetComp = CreateDefaultSubobject<UWidgetComponent>(TEXT("NameplateWidgetComp"));
	NameplateWidgetComp->SetupAttachment(GetMesh());

	bIsRagdoll = false;
	bIsGettingUp = false;
}

void AParcelCharacter::BeginPlay()
{
    Super::BeginPlay();
}

void AParcelCharacter::OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode)
{
	Super::OnMovementModeChanged(PrevMovementMode, PreviousCustomMode);

	if (HasAuthority() && PlayerStateComp)
	{
		FGameplayTag InAirTag = FGameplayTag::RequestGameplayTag(TEXT("Character.State.InAir"));

		if (GetCharacterMovement()->MovementMode == MOVE_Falling)
		{
			PlayerStateComp->AddStateTag(InAirTag);
			PLAYER_LOG(All, TEXT("[Server] %s 캐릭터가 공중 상태(MOVE_Falling)로 진입했습니다. (InAir 태그 추가)"), *GetName());
		}
	}
}

void AParcelCharacter::OnJumped_Implementation()
{
	Super::OnJumped_Implementation();
	if (HasAuthority() && PlayerStateComp)
	{
		PlayerStateComp->AddStateTag(FGameplayTag::RequestGameplayTag(TEXT("Character.State.InAir")));
	}
}

void AParcelCharacter::Landed(const FHitResult& Hit)
{
	Super::Landed(Hit);
    
	if (HasAuthority() && PlayerStateComp)
	{
		// 1. 공중 체공 상태 태그 해제
		FGameplayTag InAirTag = FGameplayTag::RequestGameplayTag(TEXT("Character.State.InAir"));
		if (PlayerStateComp->HasStateTag(InAirTag))
		{
			PlayerStateComp->RemoveStateTag(InAirTag);
			PLAYER_LOG(All, TEXT("[Server] %s 캐릭터가 지면에 착지했습니다. (InAir 태그 제거)"), *GetName());
		}

		// [안전장치] 착지했으므로 혹시라도 지워지지 않고 남아있을 점프 액션 태그를 확실하게 청소
		PlayerStateComp->RemoveStateTag(FGameplayTag::RequestGameplayTag(TEXT("Character.Action.Jump")));
	}

	if (MovementStatComp)
	{
		MovementStatComp->RefreshMoveSpeed();
	}
}

void AParcelCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    // 부모 클래스의 입력 설정을 먼저 실행
    Super::SetupPlayerInputComponent(PlayerInputComponent);
    
    // 입력 컴포넌트 통로를 히어로 컴포넌트 내부의 바인딩 연산으로 넘김
    if (HeroComp)
    {
       HeroComp->InitializePlayerInput(PlayerInputComponent);
    }
}

void AParcelCharacter::SetRagdollState(bool bNewIsRagdoll, bool bNewIsGettingUp)
{
	bIsRagdoll = bNewIsRagdoll;
	bIsGettingUp = bNewIsGettingUp;
	
	if (HasAuthority() && PlayerStateComp)
	{
		FGameplayTag RagdollTag = FGameplayTag::RequestGameplayTag(TEXT("Character.State.Ragdoll"));
		FGameplayTag GettingUpTag = FGameplayTag::RequestGameplayTag(TEXT("Character.State.GettingUp"));

		if (bIsRagdoll) PlayerStateComp->AddStateTag(RagdollTag);
		else            PlayerStateComp->RemoveStateTag(RagdollTag);

		if (bIsGettingUp) PlayerStateComp->AddStateTag(GettingUpTag);
		else              PlayerStateComp->RemoveStateTag(GettingUpTag);
	}

	if (bIsGettingUp)
	{
		GetWorldTimerManager().ClearTimer(GetUpTimerHandle);

		GetWorldTimerManager().SetTimer(
			GetUpTimerHandle,
			this,
			&AParcelCharacter::FinishGetUp,
			1.3f,
			false
		);
	}
	else
	{
		GetWorldTimerManager().ClearTimer(GetUpTimerHandle);
	}
}

void AParcelCharacter::FinishGetUp()
{
	bIsGettingUp = false;

	// [보완] 일어서기 타이머(기상 몽타주)가 끝나면 서버에서 GettingUp 태그 확실히 회수
	if (HasAuthority() && PlayerStateComp)
	{
		PlayerStateComp->RemoveStateTag(FGameplayTag::RequestGameplayTag(TEXT("Character.State.GettingUp")));
	}
}

void AParcelCharacter::PossessedBy(AController* NewController)
{
    // 서버의 possession 처리 흐름을 유지
    Super::PossessedBy(NewController);

    // 서버 Possessed 시점에 HeroComponent에 이벤트를 넘김
    if (HeroComp)
    {
       HeroComp->AddInputMappingContext();
    }

	//HealthComponent를 찾아서 사망을 바인드하는 코드, TODO: HealthCompoent확인 필요
    if (UHealthComponent* HealthComp = FindComponentByClass<UHealthComponent>())
    {
        if (AParcelPlayerState* PS = GetPlayerState<AParcelPlayerState>())
        {
            HealthComp->OnDeathDelegate.AddUniqueDynamic(PS, &AParcelPlayerState::HandleDeath);
        }
    }
	
	UpdateOverheadNameplate();
}

void AParcelCharacter::OnRep_PlayerState()
{
	Super::OnRep_PlayerState();

	UpdateOverheadNameplate();
}

void AParcelCharacter::UpdateOverheadNameplate()
{
	APlayerState* PS = GetPlayerState();
	if (!PS) return;

	if (NameplateWidgetComp)
	{
		if (UParcelNameplateWidget* NameWidget = Cast<UParcelNameplateWidget>(NameplateWidgetComp->GetUserWidgetObject()))
		{
			FString Nickname = PS->GetPlayerName();
			NameWidget->SetPlayerName(Nickname);

			GetWorldTimerManager().ClearTimer(NameplateRetryTimerHandle);
            
			PLAYER_LOG(All, TEXT("[%s] 머리 위 네임플레이트 동기화 완료: %s"), *GetName(), *Nickname);
		}
		else
		{
			if (!NameplateRetryTimerHandle.IsValid() && GetWorld())
			{
				GetWorldTimerManager().SetTimer(
					NameplateRetryTimerHandle, 
					this, 
					&AParcelCharacter::UpdateOverheadNameplate, 
					0.1f, 
					false
				);
			}
		}
	}
}

void AParcelCharacter::OnRep_Controller()
{
	// 클라이언트에서 복제된 Controller 변경 처리를 부모 클래스에 맡깁니다.
	Super::OnRep_Controller();

	// 클라이언트의 Controller 복제 시점에 HeroComponent에 이벤트를 넘김
	if (HeroComp)
	{
		HeroComp->AddInputMappingContext();
	}
}

void AParcelCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
}

3. 상태이상을 표현하기 위한 기능 추가

더보기
#pragma once

#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "GameFramework/Character.h"
#include "TimerManager.h"
#include "ParcelCharacter.generated.h"

class UParcelPlayerStateComponent;
class UParcelMovementStatComponent;
class URagdollComponent;
class UParcelHeroComponent;
class UParcelInteractionComponent;
class UCharacterCarryComponent;
class UDFStatusEffectComponent;
class UDFKnockbackComponent;
class UAnimMontage;
class UWidgetComponent;

// 플레이어가 조종하는 기본 캐릭터 클래스
// 이동, 시점 회전, 점프, 래그돌 테스트 입력을 처리하고 멀티플레이 복제를 지원
//
// 담당자: 김로운
UCLASS()
class PARCEL_KNIGHT_API AParcelCharacter : public ACharacter
{
    GENERATED_BODY()

public:
	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
	
	AParcelCharacter();
    virtual void BeginPlay() override;
    
    // 플레이어 입력 컴포넌트에 Enhanced Input 액션들을 바인딩
    virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;

    // 서버에서 이 캐릭터가 컨트롤러에 빙의될 때 호출, Listen Server의 로컬 플레이어 입력 매핑 등록을 보강
	virtual void PossessedBy(AController* NewController) override;
	
	// [UI] 3D 아이디 위젯
	virtual void OnRep_PlayerState() override;

	void SetRagdollState(bool bNewIsRagdoll, bool bNewIsGettingUp);

	UFUNCTION(BlueprintCallable, Category = "Ragdoll")
	void FinishGetUp();

	UFUNCTION(BlueprintPure, Category = "Ragdoll")
	bool GetIsRagdoll() const { return bIsRagdoll; }

	UFUNCTION(BlueprintPure, Category = "Ragdoll")
	bool GetIsGettingUp() const { return bIsGettingUp; }

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Ragdoll")
	TObjectPtr<UAnimMontage> GetUpMontage;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
	TObjectPtr<UAnimMontage> GetUpBackMontage;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
	TObjectPtr<UAnimMontage> GetUpFrontMontage;

protected:
	virtual void OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode) override;
	
	// Player State Component
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Component")
    TObjectPtr<UParcelPlayerStateComponent> PlayerStateComp;
    
	// Ragdoll Component
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Component")
	TObjectPtr<URagdollComponent> RagdollComp;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
	TObjectPtr<UDFStatusEffectComponent> StatusEffectComponent;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
	TObjectPtr<UDFKnockbackComponent> KnockbackComponent;

	UPROPERTY(BlueprintReadOnly, Category = "Ragdoll")
	bool bIsRagdoll = false;

	UPROPERTY(BlueprintReadOnly, Category = "Ragdoll")
	bool bIsGettingUp = false;

	FTimerHandle GetUpTimerHandle;
	
	// Hero Component
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Component")
	TObjectPtr<UParcelHeroComponent> HeroComp;
	
	// Interaction Component
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Component")
	TObjectPtr<UParcelInteractionComponent> InteractionComp;
	
	// MovementStat Component
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Component")
	TObjectPtr<UParcelMovementStatComponent> MovementStatComp;
	
	// CharacterCarry Component
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
	TObjectPtr<UCharacterCarryComponent> CarryComp;
	
	// Nameplate Component
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Component", meta = (AllowPrivateAccess = "true"))
	TObjectPtr<UWidgetComponent> NameplateWidgetComp;
	
	void UpdateOverheadNameplate();
	
	FTimerHandle NameplateRetryTimerHandle;
	
	UFUNCTION()
	void OnCharacterStateTagsChanged(const FGameplayTagContainer& ActiveTags);

public: 
    // 클라이언트에서 Controller 값이 복제되어 바뀔 때 호출, 원격 접속 클라이언트가 possession 이후 입력 매핑을 놓치지 않게 합니다.
    virtual void OnRep_Controller() override;
    
    // 점프 시작 시점과 지면 착지 타이밍 이식
    virtual void OnJumped_Implementation() override;
    virtual void Landed(const FHitResult& Hit) override;
    
    // 양손, 무브먼트, UI 등이 상태 창고에 접근할 수 있도록 열어주는 게터
    UFUNCTION(BlueprintPure, Category = "Character|Components")
    FORCEINLINE UParcelPlayerStateComponent* GetParcelPlayerStateComponent() const { return PlayerStateComp; }
    UFUNCTION(BlueprintPure, Category = "Character|Components")
    FORCEINLINE UParcelInteractionComponent* GetParcelInteractionComponent() const { return InteractionComp; }
    UFUNCTION(BlueprintPure, Category = "Character|Components")
    FORCEINLINE UParcelMovementStatComponent* GetParcelMovementStatComponent() const { return MovementStatComp; }
    UFUNCTION(BlueprintPure, Category = "Character|Components")
    FORCEINLINE UCharacterCarryComponent* GetCharacterCarryComponent() const { return CarryComp; }
    UFUNCTION(BlueprintPure, Category = "Components")
    FORCEINLINE URagdollComponent* GetRagdollComponent() const { return RagdollComp; }
    UFUNCTION(BlueprintPure, Category = "Components")
    FORCEINLINE UParcelHeroComponent* GetParcelHeroComponent() const { return HeroComp; }
	UFUNCTION(BlueprintPure, Category = "Components")
	FORCEINLINE UDFStatusEffectComponent* GetStatusEffectComponent() const { return StatusEffectComponent; }
	UFUNCTION(BlueprintPure, Category = "Components")
	FORCEINLINE UDFKnockbackComponent* GetKnockbackComponent() const { return KnockbackComponent; }
};
#include "Character/ParcelCharacter.h"
#include "ParcelLog.h"
#include "Character/RagdollComponent.h"
#include "Character/ParcelHeroComponent.h"
#include "Character/ParcelInteractionComponent.h"
#include "Character/ParcelMovementStatComponent.h"
#include "Character/CharacterCarryComponent.h"
#include "Character/ParcelPlayerStateComponent.h"
#include "Components/DFKnockbackComponent.h"
#include "Components/DFStatusEffectComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Net/UnrealNetwork.h"
#include "TimerManager.h"
#include "Components/WidgetComponent.h"
#include "Core/HealthComponent.h"
#include "Core/ParcelPlayerState.h"
#include "UI/ParcelNameplateWidget.h"


DEFINE_LOG_CATEGORY(LogCharacter);

AParcelCharacter::AParcelCharacter()
{
    PrimaryActorTick.bCanEverTick = false;

    SetReplicateMovement(true);
    
    // 캐릭터 액터 자체를 네트워크에 복제
    bReplicates = true;

    // 움직임이 잦은 플레이어 캐릭터라서 기본보다 높은 빈도로 네트워크 갱신을 요청
    SetNetUpdateFrequency(100.f);

    // 네트워크 상태가 안정적일 때도 너무 낮은 빈도로 떨어지지 않게 최소 갱신 빈도를 지정합니다.
    SetMinNetUpdateFrequency(33.f);

    bUseControllerRotationPitch = false;
    bUseControllerRotationYaw = false;
    bUseControllerRotationRoll = false;

    // 이동 입력 방향을 바라보도록 캐릭터를 자동 회전
    GetCharacterMovement()->bOrientRotationToMovement = true;

    // 공중에서 이동 입력이 얼마나 반영되는지 정합니다.
    GetCharacterMovement()->AirControl = 0.35f;

	// 컴포넌트 조립
  PlayerStateComp = CreateDefaultSubobject<UParcelPlayerStateComponent>(TEXT("PlayerStateComp"));
	RagdollComp = CreateDefaultSubobject<URagdollComponent>(TEXT("RagdollComp"));
	StatusEffectComponent = CreateDefaultSubobject<UDFStatusEffectComponent>(TEXT("StatusEffectComponent"));
	KnockbackComponent = CreateDefaultSubobject<UDFKnockbackComponent>(TEXT("KnockbackComponent"));
	HeroComp = CreateDefaultSubobject<UParcelHeroComponent>(TEXT("HeroComp"));
	InteractionComp = CreateDefaultSubobject<UParcelInteractionComponent>(TEXT("InteractionComp"));
	MovementStatComp = CreateDefaultSubobject<UParcelMovementStatComponent>(TEXT("MovementStatComp"));
	CarryComp = CreateDefaultSubobject<UCharacterCarryComponent>(TEXT("CarryComp"));
	NameplateWidgetComp = CreateDefaultSubobject<UWidgetComponent>(TEXT("NameplateWidgetComp"));
	if (NameplateWidgetComp)
	{
		NameplateWidgetComp->SetupAttachment(GetMesh());
		NameplateWidgetComp->SetWidgetSpace(EWidgetSpace::Screen);
		NameplateWidgetComp->SetDrawSize(FVector2D(250.f, 80.f));
		NameplateWidgetComp->SetRelativeLocation(FVector(0.f, 0.f, 210.f));
	}
	if (PlayerStateComp)
	{
		PlayerStateComp->OnCharacterStateTagsChanged.AddUniqueDynamic(this, &AParcelCharacter::OnCharacterStateTagsChanged);
	}

	bIsRagdoll = false;
	bIsGettingUp = false;
}

void AParcelCharacter::BeginPlay()
{
    Super::BeginPlay();
	
	if (PlayerStateComp)
	{
		PlayerStateComp->OnCharacterStateTagsChanged.AddUniqueDynamic(this, &AParcelCharacter::OnCharacterStateTagsChanged);
	}
	
	if (GetWorld())
	{
		FTimerHandle StandaloneNameplateTimer;
		GetWorldTimerManager().SetTimer(
			StandaloneNameplateTimer, 
			this, 
			&AParcelCharacter::UpdateOverheadNameplate, 
			0.2f, // 0.2초 뒤 안정적으로 데이터가 로드되었을 때 실행
			false
		);
	}
}

void AParcelCharacter::OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode)
{
	Super::OnMovementModeChanged(PrevMovementMode, PreviousCustomMode);

	if (HasAuthority() && PlayerStateComp)
	{
		FGameplayTag InAirTag = FGameplayTag::RequestGameplayTag(TEXT("Character.State.InAir"));

		if (GetCharacterMovement()->MovementMode == MOVE_Falling)
		{
			PlayerStateComp->AddStateTag(InAirTag);
			PLAYER_LOG(All, TEXT("[Server] %s 캐릭터가 공중 상태(MOVE_Falling)로 진입했습니다. (InAir 태그 추가)"), *GetName());
		}
	}
}

void AParcelCharacter::OnJumped_Implementation()
{
	Super::OnJumped_Implementation();
	if (HasAuthority() && PlayerStateComp)
	{
		PlayerStateComp->AddStateTag(FGameplayTag::RequestGameplayTag(TEXT("Character.State.InAir")));
	}
}

void AParcelCharacter::Landed(const FHitResult& Hit)
{
	Super::Landed(Hit);
    
	if (HasAuthority() && PlayerStateComp)
	{
		// 1. 공중 체공 상태 태그 해제
		FGameplayTag InAirTag = FGameplayTag::RequestGameplayTag(TEXT("Character.State.InAir"));
		if (PlayerStateComp->HasStateTag(InAirTag))
		{
			PlayerStateComp->RemoveStateTag(InAirTag);
			PLAYER_LOG(All, TEXT("[Server] %s 캐릭터가 지면에 착지했습니다. (InAir 태그 제거)"), *GetName());
		}

		// [안전장치] 착지했으므로 혹시라도 지워지지 않고 남아있을 점프 액션 태그를 확실하게 청소
		PlayerStateComp->RemoveStateTag(FGameplayTag::RequestGameplayTag(TEXT("Character.Action.Jump")));
	}

	if (MovementStatComp)
	{
		MovementStatComp->RefreshMoveSpeed();
	}
}

void AParcelCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    // 부모 클래스의 입력 설정을 먼저 실행
    Super::SetupPlayerInputComponent(PlayerInputComponent);
    
    // 입력 컴포넌트 통로를 히어로 컴포넌트 내부의 바인딩 연산으로 넘김
    if (HeroComp)
    {
       HeroComp->InitializePlayerInput(PlayerInputComponent);
    }
}

void AParcelCharacter::SetRagdollState(bool bNewIsRagdoll, bool bNewIsGettingUp)
{
	bIsRagdoll = bNewIsRagdoll;
	bIsGettingUp = bNewIsGettingUp;
	
	if (HasAuthority() && PlayerStateComp)
	{
		FGameplayTag RagdollTag = FGameplayTag::RequestGameplayTag(TEXT("Character.State.Ragdoll"));
		FGameplayTag GettingUpTag = FGameplayTag::RequestGameplayTag(TEXT("Character.State.GettingUp"));

		if (bIsRagdoll) PlayerStateComp->AddStateTag(RagdollTag);
		else            PlayerStateComp->RemoveStateTag(RagdollTag);

		if (bIsGettingUp) PlayerStateComp->AddStateTag(GettingUpTag);
		else              PlayerStateComp->RemoveStateTag(GettingUpTag);
	}

	if (bIsGettingUp)
	{
		GetWorldTimerManager().ClearTimer(GetUpTimerHandle);

		GetWorldTimerManager().SetTimer(
			GetUpTimerHandle,
			this,
			&AParcelCharacter::FinishGetUp,
			1.3f,
			false
		);
	}
	else
	{
		GetWorldTimerManager().ClearTimer(GetUpTimerHandle);
	}
}

void AParcelCharacter::FinishGetUp()
{
	bIsGettingUp = false;

	// [보완] 일어서기 타이머(기상 몽타주)가 끝나면 서버에서 GettingUp 태그 확실히 회수
	if (HasAuthority() && PlayerStateComp)
	{
		PlayerStateComp->RemoveStateTag(FGameplayTag::RequestGameplayTag(TEXT("Character.State.GettingUp")));
	}
}

void AParcelCharacter::PossessedBy(AController* NewController)
{
    // 서버의 possession 처리 흐름을 유지
    Super::PossessedBy(NewController);

    // 서버 Possessed 시점에 HeroComponent에 이벤트를 넘김
    if (HeroComp)
    {
       HeroComp->AddInputMappingContext();
    }

	//HealthComponent를 찾아서 사망을 바인드하는 코드, TODO: HealthCompoent확인 필요
    if (UHealthComponent* HealthComp = FindComponentByClass<UHealthComponent>())
    {
        if (AParcelPlayerState* PS = GetPlayerState<AParcelPlayerState>())
        {
            HealthComp->OnDeathDelegate.AddUniqueDynamic(PS, &AParcelPlayerState::HandleDeath);
        }
    }
	
	UpdateOverheadNameplate();
}

void AParcelCharacter::OnRep_PlayerState()
{
	Super::OnRep_PlayerState();

	UpdateOverheadNameplate();
}

void AParcelCharacter::UpdateOverheadNameplate()
{
	APlayerState* PS = GetPlayerState();
	if (!PS) return;

	if (NameplateWidgetComp)
	{
		if (UParcelNameplateWidget* NameWidget = Cast<UParcelNameplateWidget>(NameplateWidgetComp->GetUserWidgetObject()))
		{
			FString Nickname = PS->GetPlayerName();
			NameWidget->SetPlayerName(Nickname);
			
			if (PlayerStateComp)
			{
				NameWidget->UpdateStatusEffects(PlayerStateComp->GetCharacterStateTags());
			}

			GetWorldTimerManager().ClearTimer(NameplateRetryTimerHandle);
            
			PLAYER_LOG(All, TEXT("[%s] 머리 위 네임플레이트 및 상태이상 연동 완료."), *GetName(), *Nickname);
		}
		else
		{
			if (!NameplateRetryTimerHandle.IsValid() && GetWorld())
			{
				GetWorldTimerManager().SetTimer(
					NameplateRetryTimerHandle, 
					this, 
					&AParcelCharacter::UpdateOverheadNameplate, 
					0.1f, 
					false
				);
			}
		}
	}
}

void AParcelCharacter::OnCharacterStateTagsChanged(const FGameplayTagContainer& ActiveTags)
{
	if (NameplateWidgetComp)
	{
		if (UParcelNameplateWidget* NameWidget = Cast<UParcelNameplateWidget>(NameplateWidgetComp->GetUserWidgetObject()))
		{
			NameWidget->UpdateStatusEffects(ActiveTags);
		}
	}
}

void AParcelCharacter::OnRep_Controller()
{
	// 클라이언트에서 복제된 Controller 변경 처리를 부모 클래스에 맡깁니다.
	Super::OnRep_Controller();

	// 클라이언트의 Controller 복제 시점에 HeroComponent에 이벤트를 넘김
	if (HeroComp)
	{
		HeroComp->AddInputMappingContext();
	}
}

void AParcelCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
}
#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "GameplayTagContainer.h"
#include "ParcelNameplateWidget.generated.h"

class UTextBlock;

/**
 *
 */
UCLASS()
class PARCEL_KNIGHT_API UParcelNameplateWidget : public UUserWidget
{
	GENERATED_BODY()

public:
	void SetPlayerName(const FString& InName);

	void UpdateStatusEffects(const FGameplayTagContainer& ActiveTags);

protected:
	UPROPERTY(meta = (BindWidget))
	TObjectPtr<UTextBlock> Txt_PlayerName;
	
	UPROPERTY(meta = (BindWidget))
	TObjectPtr<UTextBlock> Txt_StatusEffect;
	
	UFUNCTION(BlueprintImplementableEvent, Category = "ParcelUI")
	void K2_OnStatusEffectsChanged(const FGameplayTagContainer& ActiveTags);
};
#include "UI/ParcelNameplateWidget.h"
#include "Components/TextBlock.h"

void UParcelNameplateWidget::SetPlayerName(const FString& InName)
{
	if (Txt_PlayerName)
	{
		Txt_PlayerName->SetText(FText::FromString(InName));
	}
}

void UParcelNameplateWidget::UpdateStatusEffects(const FGameplayTagContainer& ActiveTags)
{
	K2_OnStatusEffectsChanged(ActiveTags);
	
	if (Txt_StatusEffect)
	{
		TArray<FString> StatusTexts;
		
		if (ActiveTags.HasTagExact(FGameplayTag::RequestGameplayTag(TEXT("Character.State.Ragdoll"))))
		{
			StatusTexts.Add(TEXT("기절"));
		}
		if (ActiveTags.HasTagExact(FGameplayTag::RequestGameplayTag(TEXT("Character.State.Dead"))))
		{
			StatusTexts.Add(TEXT("죽음"));
		}
		if (ActiveTags.HasTagExact(FGameplayTag::RequestGameplayTag(TEXT("Character.State.Exhausted"))))
		{
			StatusTexts.Add(TEXT("지침"));
		}
		if (ActiveTags.HasTagExact(FGameplayTag::RequestGameplayTag(TEXT("Character.State.Carrying"))))
		{
			StatusTexts.Add(TEXT("운반중"));
		}
		
		// Todo : 함정 State 랑 연동지어서 Reverse 상태도 Tag로 연동할 필요.

		if (StatusTexts.Num() > 0)
		{
			FString FinalStr = TEXT("[") + FString::Join(StatusTexts, TEXT(", ")) + TEXT("]");
			Txt_StatusEffect->SetText(FText::FromString(FinalStr));
			Txt_StatusEffect->SetVisibility(ESlateVisibility::Visible);
		}
		else
		{
			Txt_StatusEffect->SetVisibility(ESlateVisibility::Collapsed);
		}
	}
}

*개인 프로젝트 및 개인 공부*

1. Framework/UI 작업을 위한 리팩토링 진행(기존 작성 코드).. 이 작업이 꽤나 오래걸렸음.

 

2. 언리얼 엔진 5.8로 버전 업그레이드 작업을 진행하였음. (MCP 기능 지원)

이게 엄청 오래걸렸음....ㅜㅜ



*오늘의 총평*

팀프로젝트는 무사히 1차 테스트를 완료했고, 나머지 UI 담당 파트를 열심히 제작할 계획이다.

문제는 개인프로젝트를 기존에는 5.7.4 버전으로 제작하고 있었는데, 5.8.0 버전으로 업그레이드를 하면서 어려운 점이 많았다. 하루종일 이것만 붙잡고 있었던 것 같다.

'TIL' 카테고리의 다른 글

07.18 TIL  (0) 2026.07.18
07.16 TIL  (0) 2026.07.16
07.14 TIL  (0) 2026.07.14
07.13 TIL  (0) 2026.07.13
07.10 TIL  (0) 2026.07.10