디버그 구체로 검증 끝냈으니 이제 실제 비주얼로 교체

Niagara도 고려했지만 조준점 하나 띄우는 데는 과함 -> 데칼로

 

크로스헤어만 쓰면 어디 떨어질지 모름 (포물선이라 조준선 방향이 착지점이 아님)
데칼만 쓰면 정확히 에임하는 느낌이 아님

-> 크로스헤어랑 데칼 둘 다 씀

  위치 역할 구현
크로스헤어 화면 중앙 (2D) 이 방향을 본다 UI 위젯
데칼 바닥 (3D) 여기 떨어진다 DecalComponent

 

데칼 = 표면에 투영되는 스티커

  • 바닥이 경사져 있으면 그 경사를 따라 휘어짐
  • 계단이나 울퉁불퉁한 지형에도 자연스럽게 붙음

 

FDItemInventoryComponent.h

protected:
    // 조준 마커용 데칼 머티리얼 (Material Domain = Deferred Decal 이어야 함)
    UPROPERTY(EditDefaultsOnly, Category = "Item|Throw")
    class UMaterialInterface* AimDecalMaterial;

    // X=투영 깊이, Y/Z=평면 크기
    UPROPERTY(EditDefaultsOnly, Category = "Item|Throw")
    FVector AimDecalSize = FVector(200.f, 60.f, 60.f);

    // 조준선 위젯 클래스 (WBP_AimCrosshair)
    UPROPERTY(EditDefaultsOnly, Category = "Item|Throw")
    TSubclassOf<class UUserWidget> AimCrosshairWidgetClass;

private:
    // 현재 떠있는 데칼 - 매 프레임 새로 만들면 비용 큼, 위치만 갱신하려고 붙잡아둠
    UPROPERTY()
    class UDecalComponent* ActiveAimDecal;

    UPROPERTY()
    class UUserWidget* ActiveCrosshair;

 

FDItemInventoryComponent.cpp

#include "Components/DecalComponent.h"
#include "Blueprint/UserWidget.h"

void UFDItemInventoryComponent::StartAiming()
{
    bIsAiming = true;
    SetComponentTickEnabled(true);

    // 데칼 생성 - 위치는 매 프레임 갱신할 거라 일단 원점
    if (!ActiveAimDecal && AimDecalMaterial)
    {
        ActiveAimDecal = UGameplayStatics::SpawnDecalAtLocation(
            GetWorld(),
            AimDecalMaterial,
            AimDecalSize,
            FVector::ZeroVector,
            FRotator(-90.f, 0.f, 0.f),
            0.f                          // LifeSpan 0 = 무한 (직접 제거)
        );
    }

    // 조준선 위젯 - 조준하는 본인 화면에만
    AFDHiderCharacter* Hider = Cast<AFDHiderCharacter>(GetOwner());
    if (Hider && Hider->IsLocallyControlled() && !ActiveCrosshair && AimCrosshairWidgetClass)
    {
        if (APlayerController* PC = Cast<APlayerController>(Hider->GetController()))
        {
            ActiveCrosshair = CreateWidget<UUserWidget>(PC, AimCrosshairWidgetClass);
            if (ActiveCrosshair)
            {
                ActiveCrosshair->AddToViewport();
            }
        }
    }
}

void UFDItemInventoryComponent::StopAiming()
{
    bIsAiming = false;
    SetComponentTickEnabled(false);

    if (ActiveAimDecal)
    {
        ActiveAimDecal->DestroyComponent();
        ActiveAimDecal = nullptr;
    }

    if (ActiveCrosshair)
    {
        ActiveCrosshair->RemoveFromParent();
        ActiveCrosshair = nullptr;
    }
}

 

UpdateTrajectory - DrawDebugSphere 제거하고 데칼 위치 갱신으로 교체

    // 착지 지점에 조준 데칼 표시
    if (bHit && ActiveAimDecal)
    {
        // 표면에서 살짝 띄워서 배치 - 투영 볼륨 안에 표면이 확실히 들어오게
        const FVector DecalLoc = PathResult.HitResult.Location
            + PathResult.HitResult.ImpactNormal * 50.f;

        ActiveAimDecal->SetWorldLocation(DecalLoc);

        // 표면 법선을 따라 눕히기 - 경사면에도 자연스럽게 붙음
        const FRotator DecalRot = (-PathResult.HitResult.ImpactNormal).Rotation();
        ActiveAimDecal->SetWorldRotation(DecalRot);

        ActiveAimDecal->SetVisibility(true);
    }
    else if (ActiveAimDecal)
    {
        // 허공을 향하면 데칼 숨김
        ActiveAimDecal->SetVisibility(false);
    }

 

 

에디터 작업

1) 데칼 머티리얼 (M_AimDecal)

  • Material Domain = Deferred Decal (이거 안 하면 데칼로 안 써짐)
  • Blend Mode = Translucent
  • TextureSample 노드 -> 원형 텍스처 지정
  • RGB -> Base Color / A -> Opacity

 

2) 조준선 위젯 (WBP_AimCrosshair)

  • Canvas Panel 추가
  • 그 안에 Image 배치
  • Anchor = 정중앙, Position (0,0), Alignment (0.5, 0.5)
  • Brush -> 십자선 텍스처

 

3) BP_FDHiderCharacter -> ItemInventoryComp

  • Aim Decal Material = M_AimDecal

  • Aim Decal Size = (200, 40, 40)
  • Aim Crosshair Widget Class = WBP_AimCrosshair

 

 

트러블슈팅

1) 데칼이 엄청 가까이 가야만 보임

 

AimDecalSize의 X가 20이었음

X = 투영 깊이

데칼은 박스 모양 투영 볼륨인데, X가 작으면 박스가 얇아서 표면이 볼륨 안에 안 들어옴

 

2) 위젯에 앵커 드롭다운이 안 나옴

Hierarchy 보니 Image가 루트에 바로 붙어있었음

앵커는 Canvas Panel의 자식일 때만 설정 가능

 

-> Canvas Panel 먼저 넣고 그 안에 Image 배치

 

 

알게 된 것

핸들을 붙잡아두는 패턴

대상 핸들 타입 왜 붙잡나
소리 UAudioComponent* 나중에 Stop() 부르려고
데칼 UDecalComponent* 매 프레임 위치 갱신하려고
위젯 UUserWidget* 조준 끝날 때 제거하려고

생성 -> 핸들 저장 -> 조작 -> 정리 구조

매 프레임 새로 만들었다 지우면 비용이 큼

하나 만들어두고 재사용하는 게 표준

 

ImpactNormal로 데칼 회전 계산

HitResult.ImpactNormal = 맞은 표면이 어느 방향을 향하는가

  • 평평한 바닥: (0, 0, 1) (위)
  • 경사면: 기울어진 값

이걸 뒤집어서(-Normal) 회전값으로 만들면 데칼이 그 표면에 딱 붙음

Material Domain = Deferred Decal

일반 머티리얼(Surface)과 데칼 머티리얼은 도메인이 다름

이거 안 바꾸면 데칼 슬롯에 할당 자체가 안 됨

 


크로스헤어 (십자선)

StartAiming()
    ↓
CreateWidget(WBP_AimCrosshair)  ← 위젯 만들고
AddToViewport()                  ← 화면에 붙임
    ↓
(조준하는 동안 그냥 가만히 있음)
    ↓
StopAiming()
    ↓
RemoveFromParent()               ← 화면에서 뗌

 

데칼 (원)

3D 월드에 투영되는 스티커

StartAiming()
    ↓
SpawnDecalAtLocation()  ← 데칼 하나 만들어둠 (위치는 아무데나)
    ↓
매 프레임 (UpdateTrajectory)
    ↓
PredictProjectilePath()로 착지 지점 계산
    ↓
데칼의 위치를 그 지점으로 이동  ← SetWorldLocation
데칼의 회전을 바닥 경사에 맞춤  ← SetWorldRotation
    ↓
StopAiming()
    ↓
DestroyComponent()      ← 데칼 제거

 

핵심: 매 프레임 데칼을 새로 만드는 게 아니라 하나 만들어두고 위치만 옮김

 

데칼 회전

바닥이 평평하지 않을 수 있음

경사면이나 계단이면 데칼도 그 각도로 누워야 자연스러움

const FRotator DecalRot = (-PathResult.HitResult.ImpactNormal).Rotation();

 

ImpactNormal = 맞은 표면이 어느 방향을 보고 있나

  • 평평한 바닥 -> (0, 0, 1) (위를 봄)
  • 경사면 -> 기울어진 값

이걸 뒤집으면(-) 데칼이 표면을 향해 쏘는 방향

그 방향으로 데칼을 회전시키면 표면에 딱 붙음

+ Recent posts