디버그 구체로 검증 끝냈으니 이제 실제 비주얼로 교체
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) (위를 봄)
- 경사면 -> 기울어진 값
이걸 뒤집으면(-) 데칼이 표면을 향해 쏘는 방향
그 방향으로 데칼을 회전시키면 표면에 딱 붙음
'프로젝트 > Funny Or Die (멀티플레이 술래잡기 게임)' 카테고리의 다른 글
| [트러블슈팅] EOS 세션 연동 (0) | 2026.07.15 |
|---|---|
| 인벤토리 UI 및 아이템 획득 알림 (0) | 2026.07.14 |
| 소리 아이템 구현 (0) | 2026.07.13 |
| 투사체 명중 시 술래 스턴 (0) | 2026.07.13 |
| 투사체 조준 및 발사 (X키 조준 -> 좌클릭 발사) (0) | 2026.07.13 |
