오늘의 코드카타
팀 프로젝트 진행 내용
1. 캐릭터 이름표 구현
- 게임 내에서 플레이어 간의 구분이 되지 않아서 캐릭터 위에 플레이어의 이름을 보여주게끔 하고자 했다. 필요하다면 플레이어의 마이크가 활성화되면, 옆에도 같이 보여주면 좋지 않을까 싶었다.
- 특정 거리를 넘어가면 이름표가 보이지 않는 등의 처리가 필요해서 별도의 컴포넌트로 제작을 하고자 했다.
- 먼저 이름을 띄워주는 위젯은 간단하게 캐릭터의 이름을 보여주는 텍스트 블록과 이름을 할당해주는 함수, 해당 위젯의 크기를 조절하는 함수만 구현을 했다.
// AO_NameTagWidget.h
UCLASS()
class AO_API UAO_NameTagWidget : public UAO_UserWidget
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable)
void SetPlayerName(const FText& InName);
UFUNCTION(BlueprintCallable)
void SetUIScale(float InScale);
protected:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> Text_Name;
};
// AO_NameTagWidget.cpp
void UAO_NameTagWidget::SetPlayerName(const FText& InName)
{
if (Text_Name)
{
Text_Name->SetText(InName);
}
}
void UAO_NameTagWidget::SetUIScale(float InScale)
{
SetRenderScale(FVector2D(InScale, InScale));
}
- 이름표의 경우에는 위젯 컴포넌트를 통해 캐릭터한테 부착해서 사용하고자 했다.
- 테스트를 해봤는데, 위젯 스페이스를 월드로 하는 경우에는 글씨가 깨지거나, 플레이어한테 보여지는 각도가 이상한 경우가 있었다. 따라서 스크린으로 지정을 해서 거리에 따라 글씨 크기를 줄이거나 늘리고, 거리가 멀어지면 보이지 않게끔 처리를 하고자 했다.
- 이를 위해 다양한 변수를 받아서 사용하고자 했는데, 아래처럼 컴포넌트에 선언해서 에셋에서 수정할 수 있게끔 구현했다.
// AO_NameplateComponent.h
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class AO_API UAO_NameplateComponent : public UActorComponent
{
...
protected:
UPROPERTY(EditAnywhere, Category = "Nameplate|Widget")
TSubclassOf<UUserWidget> NameplateWidgetClass;
UPROPERTY(EditAnywhere, Category = "Nameplate|Distance")
float MinScaleDistance = 200.f;
UPROPERTY(EditAnywhere, Category = "Nameplate|Distance")
float MaxScaleDistance = 1000.f;
UPROPERTY(EditAnywhere, Category = "Nameplate|Distance")
float MinScale = 0.4f;
UPROPERTY(EditAnywhere, Category = "Nameplate|Distance")
float MaxScale = 1.f;
UPROPERTY(EditAnywhere, Category = "Nameplate|Distance")
float HideDistance = 1500.f;
UPROPERTY(EditAnywhere, Category = "Nameplate|Offset")
float BaseZOffset = 80.f;
UPROPERTY(EditAnywhere, Category = "Nameplate|Offset")
float ExtraZOffset = 10.f;
UPROPERTY(EditAnywhere, Category = "Nameplate|Visibility")
bool bHideForSelf = true;
private:
UPROPERTY(Transient)
TObjectPtr<UWidgetComponent> WidgetComponent;
}
- 매 틱마다 아래의 함수를 실행해서 캐릭터와의 거리에 따라 다양한 처리를 하게끔 구현했다.
- 먼저 자기 자신은 안 보이게끔 막아놓고, 너무 멀면 위젯을 숨기게끔 처리했다.
- 또한 멀어져도 폰트의 크기가 큰 경우, 어색해서 크기가 작아지게끔 보간 방식으로 계산을 해서 UI Scale을 조절했다. 또한 멀어지면 캐릭터가 작아지면서 상대적으로 글씨가 아래로 내려오는 듯한 현상이 보여서 살짝 위로 이동하게끔 보정을 했다.
- 벽이 가리고 있으면 이름표가 안 보이게 하는 것은 고민을 했으나, 팀원들과 상의한 결과 벽을 통과해서 보이는 것이 더 좋을 것으로 보여서 추가하지는 않았다.
void UAO_NameplateComponent::ApplyDistanceVisuals()
{
if (!WidgetComponent)
{
return;
}
// 자기 자신 숨김
if (ShouldHideForSelf())
{
WidgetComponent->SetVisibility(false, true);
return;
}
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
if (!PC || !PC->PlayerCameraManager)
{
WidgetComponent->SetVisibility(true, true);
return;
}
const FVector CameraLocation = PC->PlayerCameraManager->GetCameraLocation();
const FVector OwnerLocation = GetOwner()->GetActorLocation();
const float Distance = FVector::Distance(CameraLocation, OwnerLocation);
// 너무 멀면 숨김
if (Distance > HideDistance)
{
WidgetComponent->SetVisibility(false, true);
return;
}
WidgetComponent->SetVisibility(true, true);
// 멀수록 작아지게
const float Alpha = FMath::Clamp((Distance - MinScaleDistance) / FMath::Max(1.f, (MaxScaleDistance - MinScaleDistance)), 0.f, 1.f);
const float UIScale = FMath::Lerp(MaxScale, MinScale, Alpha);
if (WidgetInstance)
{
WidgetInstance->SetUIScale(UIScale);
}
// 멀수록 살짝 위로
const float ZOffset = BaseZOffset + FMath::Lerp(0.f, ExtraZOffset, Alpha);
WidgetComponent->SetRelativeLocation(FVector(0.f, 0.f, ZOffset));
}
- PlayerState에 델리게이트를 선언해서, 플레이어 이름이 설정되면 델리게이트를 호출하게끔 구현했다.
- 해당 델리게이트가 브로드캐스팅이 되면, 컴포넌트에서
SetDisplayName이 호출되게끔 연결을 해서, 안전성을 보장했다.
// AO_PlayerState.h
DECLARE_MULTICAST_DELEGATE_OneParam(FOnPlayerNameReady, const FText&);
UCLASS()
class AO_API AAO_PlayerState : public APlayerState
{
GENERATED_BODY()
...
public:
virtual void OnRep_PlayerName() override;
// 플레이어 이름 준비 완료되면 델리게이트 호출
FOnPlayerNameReady OnPlayerNameReady;
}
// AO_PlayerState.cpp
void AAO_PlayerState::OnRep_PlayerName()
{
Super::OnRep_PlayerName();
OnPlayerNameReady.Broadcast(FText::FromString(GetPlayerName()));
}
// AO_NameplateComponent.cpp
void UAO_NameplateComponent::SetDisplayName(const FText& InName)
{
EnsureWidgetComponent();
if (WidgetInstance)
{
WidgetInstance->SetPlayerName(InName);
}
}
결과
