일정표
| 시간 | 할 일 | 비고 |
| 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 버전으로 업그레이드를 하면서 어려운 점이 많았다. 하루종일 이것만 붙잡고 있었던 것 같다.