#include "FallingObstacle.h"
#include "Components/StaticMeshComponent.h"
#include "TimerManager.h"
#include "UObject/ConstructorHelpers.h"
AFallingObstacle::AFallingObstacle()
{
PrimaryActorTick.bCanEverTick = false;
// Base 이동/회전 사용 안 함
MoveSettings.MoveSpeed = 0.f;
MoveSettings.MaxRange = 0.f;
RotationSettings.bEnableRotation = false;
static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(
TEXT("/Game/Resources/Megascans/3D/Old_Wooden_Log_wcjheff/Medium/wcjheff_tier_2.wcjheff_tier_2")
);
if (MeshAsset.Succeeded() && StaticMeshComp)
{
StaticMeshComp->SetStaticMesh(MeshAsset.Object);
StaticMeshComp->SetSimulatePhysics(false);
StaticMeshComp->SetEnableGravity(false);
StaticMeshComp->SetNotifyRigidBodyCollision(true);
StaticMeshComp->BodyInstance.bUseCCD = true;
StaticMeshComp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
StaticMeshComp->SetCollisionResponseToAllChannels(ECR_Block);
}
}
BeginPlay – 낙하 준비
void AFallingObstacle::BeginPlay()
{
Super::BeginPlay();
InitialLocation = GetActorLocation();
// 낙하 시작 위치 (위로 올림)
SetActorLocation(InitialLocation + FVector(0, 0, SpawnHeightOffset));
if (StaticMeshComp)
{
StaticMeshComp->OnComponentHit.AddDynamic(this, &AFallingObstacle::HandleMeshHit);
}
if (bAutoDrop)
{
GetWorldTimerManager().SetTimer(
DropTimerHandle,
this,
&AFallingObstacle::StartDrop,
DropDelay,
false
);
}
}
Drop 시작
void AFallingObstacle::StartDrop()
{
if (bDropping) return;
bDropping = true;
if (!StaticMeshComp) return;
StaticMeshComp->SetSimulatePhysics(true);
StaticMeshComp->SetEnableGravity(true);
StaticMeshComp->SetLinearDamping(0.05f);
StaticMeshComp->SetAngularDamping(0.1f);
}
착지 후 굴림 처리
void AFallingObstacle::HandleMeshHit(
UPrimitiveComponent* HitComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
FVector NormalImpulse,
const FHitResult& Hit)
{
if (!bDropping || bLanded) return;
bLanded = true;
if (!StaticMeshComp) return;
FVector Dir = GetActorForwardVector();
Dir.Z = 0.f;
Dir = Dir.GetSafeNormal();
const float YawJitter = FMath::RandRange(-60.f, 60.f);
Dir = Dir.RotateAngleAxis(YawJitter, FVector::UpVector).GetSafeNormal();
const FVector NewVel = Dir * RollSpeed;
StaticMeshComp->SetPhysicsLinearVelocity(
FVector(NewVel.X, NewVel.Y, 0.f),
true
);
if (RollTorqueInRadians > 0.f)
{
const FVector TorqueAxis =
FVector::CrossProduct(FVector::UpVector, Dir).GetSafeNormal();
StaticMeshComp->AddTorqueInRadians(
TorqueAxis * RollTorqueInRadians,
NAME_None,
true
);
}
GetWorldTimerManager().SetTimer(
DestroyTimerHandle,
this,
&AFallingObstacle::DestroySelf,
DestroyDelayAfterLanding,
false
);
}
핵심 아이디어
회전 방향(+, -)에 따라 접선 방향이 달라짐
CrossProduct(Up, Radial)로 접선 벡터 계산
StepTrigger(Box)로 “밟기” 판별
Timer로 회전 속도 주기 랜덤 변경
회전 랜덤화
void ARotatingLaunchObstacle::RandomizeRotation()
{
const float NewSpeed = FMath::RandRange(MinYawSpeed, MaxYawSpeed);
const float Dir = bRandomizeDirection
? (FMath::RandBool() ? 1.f : -1.f)
: 1.f;
RotationSettings.RotationSpeedPerSecond =
FRotator(0.f, NewSpeed * Dir, 0.f);
}
밟았을 때 발사 로직
void ARotatingLaunchObstacle::OnObstacleBeginOverlap(AActor* OtherActor)
{
ACharacter* Character = Cast<ACharacter>(OtherActor);
if (!Character) return;
const float ObZ = GetActorLocation().Z;
const float ChZ = Character->GetActorLocation().Z;
if (ChZ < ObZ + MinStepZOffset) return;
const double Now = GetWorld()->GetTimeSeconds();
if (Now - LastLaunchTimeSec < LaunchCooldown) return;
LastLaunchTimeSec = Now;
FVector Radial = Character->GetActorLocation() - GetActorLocation();
Radial.Z = 0.f;
Radial = Radial.GetSafeNormal();
const float YawSign =
(RotationSettings.RotationSpeedPerSecond.Yaw >= 0.f) ? 1.f : -1.f;
const FVector Tangent =
FVector::CrossProduct(GetActorUpVector(), Radial)
.GetSafeNormal() * YawSign;
const FVector LaunchVel =
Tangent * LaunchStrength + FVector(0.f, 0.f, LaunchUp);
Character->LaunchCharacter(LaunchVel, true, true);
}
추가한 기능
BeginPlay 시 N개 즉시 스폰
Timer 기반 반복 스폰
DataTable 확률 기반 장애물 선택
Patrol 계열만 이동 / 회전 랜덤
Falling / Launch는 컨셉 고정
거리 기반 중복 방지
완전 정지 상태 방지 (최소 1개 동작 보장)
스폰 위치 중복 방지
#include "EngineUtils.h"
static bool IsFarFromExisting(
UWorld* World,
const FVector& Candidate,
float MinDist)
{
for (TActorIterator<ABaseObstacle> It(World); It; ++It)
{
const ABaseObstacle* Existing = *It;
if (!IsValid(Existing)) continue;
if (FVector::DistSquared(
Existing->GetActorLocation(),
Candidate) < MinDist * MinDist)
{
return false;
}
}
return true;
}
증상
FallingObstacle이 높은 곳에서 떨어지는 것처럼 보이지 않음
마치 바닥 근처에서 바로 낙하하는 느낌
원인
SpawnVolume에서 스폰 직후 아래 코드 실행:
Ob->ClampToBounds();
FallingObstacle은 BeginPlay에서
SpawnHeightOffset만큼 위로 올리는데,
Clamp가 Z까지 포함하면서 다시 박스 안으로 눌림.
해결 방법
if (!Ob->IsA<AFallingObstacle>())
{
Ob->ClampToBounds();
}
설계가 깔끔해도
스폰
경계
타이머
가 섞이면 실행 순서 이슈가 반드시 발생함
FallingObstacle은 컨셉상 경계 Clamp 예외 대상
Physics Impulse는 결과가 튀기 쉬움
→ 속도 직접 세팅이 훨씬 안정적
SpawnVolume의 “랜덤 속성 부여”는 강력하지만
👉 최종 검증(유효 동작 보장) 없으면
멈춘 장애물이 생길 수 있음