🛠 PlacementHelperActor 기능 가이드
개요
Editor 전용 유틸리티 액터로, StaticMesh 배치 시 정렬·초기화·무작위화·머티리얼·콜리전 등을 손쉽게 조작할 수 있도록 지원합니다.
🔧 주요 기능 목록
기능명 설명 디버그 표시 단축
| AlignToGround | 지면에 Z 위치 맞춤 | 라인 + 위치 메시지 | 버튼 |
| AlignSlope | 메시 하단 노멀에 Pitch·Roll 맞춤 | 노멀 라인 + 각도 표시 | 버튼 |
| RandomYaw | 지정 각도 간격 기반 랜덤 Yaw | 화살표 + 회전 각도 | 버튼 |
| RandomLocationInRadius | XY 방향 무작위 이동 | 이동 라인 + 반경 스피어 | 버튼 |
| ResetLocation / ResetRotation | 인스턴스별 초기 위치·회전 복구 | 텍스트 알림 | 버튼 |
| CycleMaterial | 슬롯 0 머티리얼 순환 | 이름 표시 | 버튼 |
| ToggleCollision | Collision 상태 순환(No→Overlap→Block) | 상태명 표시 | 버튼 |
🖱 사용법
- 액터 배치 후 Details 패널에서 원하는 옵션 지정
- 아래 기능 버튼 클릭 (또는 편의 키 매핑)
- 시각 피드백 확인 (HUD 상단 메시지 + Viewport 디버그 표시)
🎨 시각 피드백 예시
피드백 요소 시각화 설명
| On-screen 메시지 | HUD 상단 | 액터 이름 + 변경 정보 표시 |
| DrawDebugLine / Arrow | Viewport | 회전/노멀/위치 이동 선 시각화 |
| DrawDebugSphere | 반경 표시 | RandomLocation 이동 범위 표시 |
| DrawDebugString | 머리 위 텍스트 | 충돌 상태, 머티리얼 이름 등 부가 정보 |
⚠️ 주의 사항
- Play 후 메시지가 안 보일 경우 → Simulate 버튼을 눌렀다 끄면 복구됨
- 충돌 토글에서 Overlap 단계는 트리거 디버그용 (일반 장식물은 Block/None만으로 충분)
- RandomYawStep = 0 으로 설정하면 완전 랜덤 회전
- Reset* 기능은 액터 별 기본값(ResetLocationOrigin, ResetRotationValue) 기준
📁 경로 및 에셋 구성
- 클래스 경로: Content/Editor/
- 에디터에선 BP_PlacementHelperActor 블루프린트로 사용 가능
- 기능 구현은 모두 C++ (PlacementHelperActor.cpp) 기반
DEFINE_LOG_CATEGORY(LogASPlacement);
namespace PlacementConst { constexpr float TraceDepth = 10000.f; }
APlacementHelperActor::APlacementHelperActor()
{
MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp"));
RootComponent = MeshComp;
MeshComp->SetMobility(EComponentMobility::Movable);
MeshComp->bEditableWhenInherited = true;
}
void APlacementHelperActor::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
#if WITH_EDITOR
if (bAutoAlign)
{
AlignToGround();
}
#endif
}
/* ───────────────────────────────── AlignToGround ───────────────────────────── */
void APlacementHelperActor::AlignToGround()
{
#if WITH_EDITOR
if (!MeshComp) return;
const FVector Start = MeshComp->GetComponentLocation();
const FVector End = Start - FVector(0.f, 0.f, PlacementConst::TraceDepth);
FHitResult Hit;
FCollisionQueryParams Params(SCENE_QUERY_STAT(AlignToGround), true);
Params.AddIgnoredActor(this);
if (GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_WorldStatic, Params))
{
float DeltaZ = 0.f;
if (bUseMeshBottom)
{
const FBoxSphereBounds Bounds = MeshComp->Bounds;
const float MeshBottomZ = Bounds.Origin.Z - Bounds.BoxExtent.Z;
DeltaZ = Hit.Location.Z - MeshBottomZ;
}
else
{
DeltaZ = Hit.Location.Z - Start.Z;
}
FVector NewLoc = Start; NewLoc.Z += DeltaZ;
MeshComp->SetWorldLocation(NewLoc);
// ── 시각 피드백 ──
const FString Msg = FString::Printf(TEXT("[%s] AlignToGround ΔZ = %.1f cm"), *GetActorLabel(), DeltaZ);
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(static_cast<uint64>(GetUniqueID()), 1.5f, FColor::Blue, Msg);
}
DrawDebugLine(GetWorld(), Start, Hit.Location, FColor::Green, false, 1.5f);
DrawDebugPoint(GetWorld(), Hit.Location, 10.f, FColor::Emerald, false, 1.5f);
UE_LOG(LogASPlacement, Log, TEXT("Aligned (%s). ΔZ=%.1f cm"), bUseMeshBottom ? TEXT("Bottom") : TEXT("Pivot"), DeltaZ);
}
#endif
}
/* ───────────────────────────────── AlignSlope ─────────────────────────────── */
void APlacementHelperActor::AlignSlope()
{
#if WITH_EDITOR
if (!MeshComp) return;
const FVector Start = MeshComp->GetComponentLocation();
const FVector End = Start - FVector(0,0,PlacementConst::TraceDepth);
FHitResult Hit;
FCollisionQueryParams P(SCENE_QUERY_STAT(AlignSlope), true);
P.AddIgnoredActor(this);
if (GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_WorldStatic, P))
{
FRotator SlopeRot = Hit.Normal.Rotation();
if (!bAlignYawToSlope)
{
SlopeRot.Yaw = MeshComp->GetComponentRotation().Yaw;
}
MeshComp->SetWorldRotation(SlopeRot);
// ── 시각 피드백 ──
const FString Msg = FString::Printf(TEXT("[%s] AlignSlope P=%.1f R=%.1f"), *GetActorLabel(), SlopeRot.Pitch, SlopeRot.Roll);
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(static_cast<uint64>(GetUniqueID()), 1.5f, FColor::Purple, Msg);
}
DrawDebugLine(GetWorld(), Hit.Location, Hit.Location + Hit.Normal * 120.f, FColor::Magenta, false, 1.5f, 0, 2.f);
UE_LOG(LogASPlacement, Log, TEXT("AlignSlope to %.1f, %.1f"), SlopeRot.Pitch, SlopeRot.Roll);
}
#endif
}
/* ───────────────────────────────── RandomYaw ───────────────────────────────── */
void APlacementHelperActor::RandomYaw()
{
#if WITH_EDITOR
if (!MeshComp) return;
float Angle = FMath::FRandRange(0.f, 360.f);
if (RandomYawStep > KINDA_SMALL_NUMBER)
{
Angle = FMath::GridSnap(Angle, RandomYawStep);
}
FRotator R = MeshComp->GetComponentRotation();
R.Yaw = Angle;
MeshComp->SetWorldRotation(R);
const FString Msg = FString::Printf(TEXT("[%s] Yaw %.0f°"), *GetActorLabel(), Angle);
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(static_cast<uint64>(GetUniqueID()), 1.5f, FColor::Orange, Msg);
}
const FVector Loc = MeshComp->GetComponentLocation();
const FVector Dir = MeshComp->GetForwardVector() * 120.f;
DrawDebugDirectionalArrow(GetWorld(), Loc, Loc + Dir, 80.f, FColor::Orange, false, 1.5f);
UE_LOG(LogASPlacement, Log, TEXT("RandomYaw → %.0f°"), Angle);
#endif
}
/* ───────────────────────────── RandomLocationInRadius ─────────────────────── */
void APlacementHelperActor::RandomLocationInRadius()
{
#if WITH_EDITOR
if (!MeshComp) return;
const FVector PrevLoc = MeshComp->GetComponentLocation();
FVector2D Rand = FMath::RandPointInCircle(RandomRadius);
FVector NewLoc = PrevLoc + FVector(Rand.X, Rand.Y, 0.f);
MeshComp->SetWorldLocation(NewLoc);
// 시각 피드백
if (GEngine)
{
const FString Msg = FString::Printf(TEXT("[%s] RandomLoc Δ(%.0f, %.0f)"), *GetActorLabel(), Rand.X, Rand.Y);
GEngine->AddOnScreenDebugMessage(static_cast<uint64>(GetUniqueID()), 1.5f, FColor::Green, Msg);
}
DrawDebugSphere(GetWorld(), PrevLoc, RandomRadius, 16, FColor::Green, false, 1.5f);
DrawDebugLine (GetWorld(), PrevLoc, NewLoc, FColor::Green, false, 1.5f, 0, 2.f);
UE_LOG(LogASPlacement, Log, TEXT("RandomLocation Δ(%.0f, %.0f)"), Rand.X, Rand.Y);
#endif
}
/* ───────────────────────────── ResetLocation / Rotation ───────────────────── */
void APlacementHelperActor::ResetLocation()
{
#if WITH_EDITOR
if (MeshComp)
{
MeshComp->SetWorldLocation(ResetLocationOrigin);
const FString Msg = FString::Printf(TEXT("[%s] ResetLocation"), *GetActorLabel());
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(static_cast<uint64>(GetUniqueID()), 1.5f, FColor::White, Msg);
}
UE_LOG(LogASPlacement, Log, TEXT("ResetLocation → %s"), *ResetLocationOrigin.ToString());
}
#endif
}
void APlacementHelperActor::ResetRotation()
{
#if WITH_EDITOR
if (MeshComp)
{
MeshComp->SetWorldRotation(ResetRotationValue);
const FString Msg = FString::Printf(TEXT("[%s] ResetRotation"), *GetActorLabel());
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(static_cast<uint64>(GetUniqueID()), 1.5f, FColor::White, Msg);
}
UE_LOG(LogASPlacement, Log, TEXT("ResetRotation → %s"), *ResetRotationValue.ToString());
}
#endif
}
/* ───────────────────────────── Material Cycler ────────────────────────────── */
void APlacementHelperActor::CycleMaterial()
{
#if WITH_EDITOR
if (!MeshComp || MaterialList.Num() == 0) return;
CurrentMatIdx = (CurrentMatIdx + 1) % MaterialList.Num();
UMaterialInterface* NewMat = MaterialList[CurrentMatIdx];
MeshComp->SetMaterial(0, NewMat);
const FString Msg = FString::Printf(TEXT("[%s] Material → %s"), *GetActorLabel(), *NewMat->GetName());
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(static_cast<uint64>(GetUniqueID()), 1.5f, FColor::Turquoise, Msg);
}
DrawDebugString(GetWorld(), MeshComp->GetComponentLocation() + FVector(0,0,60.f), NewMat->GetName(), nullptr, FColor::Turquoise, 1.5f, false);
UE_LOG(LogASPlacement, Log, TEXT("MaterialCycler → %s"), *NewMat->GetName());
#endif
}
/* ───────────────────────────── Collision Toggle ───────────────────────────── */
void APlacementHelperActor::ToggleCollision()
{
if (!MeshComp) return;
CurrentCollIdx = (CurrentCollIdx + 1) % CollisionCycle.Num();
const ECollisionEnabled::Type NewMode = CollisionCycle[CurrentCollIdx];
MeshComp->SetCollisionEnabled(NewMode);
#if WITH_EDITOR
const FString ModeStr = UEnum::GetValueAsString(NewMode);
UE_LOG(LogASPlacement, Log, TEXT("%s → %s"), *GetActorLabel(), *ModeStr);
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(static_cast<uint64>(GetUniqueID()), 1.5f, FColor::Cyan,
FString::Printf(TEXT("[%s] Collision: %s"), *GetActorLabel(), *ModeStr));
}
DrawDebugString(GetWorld(), GetActorLocation() + FVector(0,0,50.f), ModeStr, nullptr, FColor::Yellow, 1.0f, false);
#endif
}'언리얼 엔진' 카테고리의 다른 글
| UE5 커스텀 유틸리티: PlacementHelperActor로 정렬·랜덤·토글을 한 번에 (1) | 2025.07.18 |
|---|---|
| Unreal Character - 캐릭터 공격 판정 트러블슈팅 정리 (0) | 2025.05.29 |
| Unreal Character - 캐릭터 공격 판정 시스템 구현 정리 (0) | 2025.05.29 |
| Unreal Character - 락온 시스템 구현 (0) | 2025.05.29 |
| Unreal Engine - 오픈월드 개발 기술스택: World Partition과 PCG (1) | 2025.05.21 |