1. 스플라인을 따라 메시가 자동 세팅되는 사다리 만들기
하드코딩이 아닌 사다리의 크기를 유연하게 쓰고싶어서 사다리를 스플라인컴포넌트 길이에 비례하여
메시가 일정한 비율로 세팅되도록 제작하였습니다
public:
ALadderActor();
protected:
virtual void BeginPlay() override;
public:
virtual void OnConstruction(const FTransform& transform) override;
public:
UPROPERTY(EditAnywhere)
USceneComponent* sceneComp;
UPROPERTY(EditAnywhere, Category="Ladder")
UStaticMesh* ladderMesh;
UPROPERTY(EditAnywhere, Category = "Ladder")
class USplineComponent* ladderSpline;
UPROPERTY(EditAnywhere, Category = "Ladder")
TArray<class USplineMeshComponent*> ladderMeshes;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ladder")
float stepLength = 600.f;
};
void ALadderActor::OnConstruction(const FTransform& transform)
{
Super::OnConstruction(transform);
for (USplineMeshComponent* Comp : ladderMeshes)
{
if (Comp)
{
Comp->UnregisterComponent();
Comp->DestroyComponent();
}
}
ladderMeshes.Empty();
if (!ladderMesh || !ladderSpline) return;
const float splineLength = ladderSpline->GetSplineLength();
const int32 stepCount = FMath::CeilToInt(splineLength / stepLength);
for (int32 i = 0; i < stepCount; i++)
{
const float startDist = i * stepLength;
const float endDist = FMath::Min((i + 1) * stepLength, splineLength);
FVector StartPos = ladderSpline->GetLocationAtDistanceAlongSpline(startDist, ESplineCoordinateSpace::Local);
FVector StartTan = ladderSpline->GetTangentAtDistanceAlongSpline(startDist, ESplineCoordinateSpace::Local);
FVector EndPos = ladderSpline->GetLocationAtDistanceAlongSpline(endDist, ESplineCoordinateSpace::Local);
FVector EndTan = ladderSpline->GetTangentAtDistanceAlongSpline(endDist, ESplineCoordinateSpace::Local);
USplineMeshComponent* NewMesh = NewObject<USplineMeshComponent>(this);
NewMesh->SetStaticMesh(ladderMesh);
NewMesh->SetForwardAxis(ESplineMeshAxis::Z);
NewMesh->SetStartAndEnd(StartPos, StartTan, EndPos, EndTan, true);
NewMesh->SetMobility(EComponentMobility::Movable);
NewMesh->AttachToComponent(ladderSpline, FAttachmentTransformRules::KeepRelativeTransform);
NewMesh->RegisterComponent();
ladderMeshes.Add(NewMesh);
}
}
이렇게 하면 만약 스플라인을 늘리면 비율에 맞게 알아서 사다리메시가 세팅이됩니다

2. 사다리에 모션워핑에 넘겨줄 위치벡터 (사다리손잡이) 위치 구하기
사다리는 얼마나 길어질지도 모르고 스플라인위치에 따라서 변경될수도 있기에
사다리손잡이 위치는 언제든 유동적으로 변할수있다
내가 구하는방법은 스플라인 전체의 길이를 구하고
손잡이 개수 - 1로 나눠서 각 손잡이 사이 간격을 계산합니다
그다음 스플라인에서 해당 거리에 해당하는 월드 위치 좌표를 얻은뒤
기본 스플라인 위치에서 z축만 개별적으로 조절할수있도록 handle offset 배열을 사용하여
uscenecomponent위치에 적용하였습니다
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Handle")
TArray<USceneComponent*> handlePoints;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Handle")
int32 ladderHandleCount;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Handle")
TArray<float> handleZOffset;
//offset 저장값 if(idx 0 ~ 9 )
//35, 28, 20, 11, 5, -5, -11, -20, -28, -35
생성자
ladderHandleCount = 10;
for (int32 i = 0; i < ladderHandleCount; i++)
{
FString HandleName = FString::Printf(TEXT("HandlePoint_%02d"), i);
USceneComponent* NewHandle = CreateDefaultSubobject<USceneComponent>(*HandleName);
NewHandle->SetupAttachment(ladderSpline);
handlePoints.Add(NewHandle);
}
OnConstruction
if (!ladderSpline || handlePoints.Num() == 0)
return;
float handleInterval = 0.f;
if (handlePoints.Num() > 1)
handleInterval = splineLength / (handlePoints.Num() - 1);
for (int32 i = 0; i < handlePoints.Num(); i++)
{
if (handlePoints[i])
{
const float dist = i * handleInterval;
const FVector pos = ladderSpline->GetLocationAtDistanceAlongSpline(dist, ESplineCoordinateSpace::Local);
handlePoints[i]->SetRelativeLocation(pos);
}
}
for (int32 i = 0; i < handlePoints.Num(); i++)
{
if (handlePoints[i])
{
FVector pos = ladderSpline->GetLocationAtDistanceAlongSpline(i * handleInterval, ESplineCoordinateSpace::World);
if (handleZOffset.IsValidIndex(i))
pos.Z += handleZOffset[i];
handlePoints[i]->SetWorldLocation(pos);
}
}
+ 스플라인 길이구하기
const float splineLength = ladderSpline->GetSplineLength();

오프셋값을 조절하여 손잡이에 scenecomponent위치를 맞춰가며 오프셋값을 지정해주었습니다
0번 -> 즉 사다리의 제일 밑부분 오프셋을 적용한 scenecomponent의 위치설정

3. 현재 사용할 사다리액터 찾기
public:
ALadderActor* findInteractionLadder(float& outDistance) const;
FVector ladderLoc;
ALadderActor* UUInteractionComponent::findInteractionLadder(float& outDistance) const
{
outDistance = FLT_MAX;
TArray<ALadderActor*> foundLadders;
for (TActorIterator<ALadderActor> it(GetWorld()); it; ++it)
foundLadders.Add(*it);
if (foundLadders.Num() == 0)
return nullptr;
foundLadders.Sort([this](const ALadderActor& A, const ALadderActor& B)
{
const float distA = FVector::Dist(A.GetActorLocation(), ownerCharacter->GetActorLocation());
const float distB = FVector::Dist(B.GetActorLocation(), ownerCharacter->GetActorLocation());
return distA < distB;
});
ALadderActor* nearestLadder = foundLadders[0];
outDistance = FVector::Dist(nearestLadder->GetActorLocation(), ownerCharacter->GetActorLocation());
return nearestLadder;
}
ALadderActor* ladder = findInteractionLadder(distance);
if (!ladder)
return;
ladderLoc = ladder->GetActorLocation();
4.사다리 상호작용 상태에 맞춰, 모션 데이터의 애니메이션을 모션 워핑과 함께 실행
bool UUInteractionComponent::DoLadderInternal(EInteractionState newState, FName rowKey, bool bUp, bool bEndOverride)
{
currentInteractionState = newState;
const FInteractionDataRow* row = GetInteractionData(rowKey);
if (!row) return false;
float distance;
ALadderActor* ladder = findInteractionLadder(distance);
if (!ladder) return false;
currentLadder = ladder;
SetInteractionState(true, newState);
EInteractionCycleState cycleResult = EvaluateLadderResult(newState);
switch (cycleResult)
{
case EInteractionCycleState::Start:
MotionWarpLadder(bUp, false);
ownerCharacter->PlayAnimMontage(row->startPlayMontage, row->playRatio);
break;
case EInteractionCycleState::End:
MotionWarpLadder(bUp, bEndOverride);
ownerCharacter->PlayAnimMontage(row->endPlayMontage, row->playRatio);
break;
case EInteractionCycleState::Cancle:
MotionWarpLadder(bUp, bEndOverride);
ownerCharacter->PlayAnimMontage(row->canclePlayMontage, row->playRatio);
break;
}
return true;
}
void UUInteractionComponent::DoLadderUp()
{
DoLadderInternal(EInteractionState::LadderUp, "LadderUp", true);
}
void UUInteractionComponent::DoLadderDown()
{
DoLadderInternal(EInteractionState::LadderDown, "LadderDown", false, true);
}
모션워핑에서 계산된 위치는 이와같습니다
1. LadderUp<올라갈때> Ladder의 2번 인덱스에 붙습니다
2. LadderDown<내려갈때> Ladder의 마지막 인덱스에 붙습니다
3. 도착했을때 Ladder의 EndMarker의 위치로 이동합니다
<모션워핑은 루트를 이동시키기 때문에 setactorlocation으로 스플라인에 붙을때의 오차를 계산해주기위해
캡슐 오프셋값을 빼주었습니다
void UUInteractionComponent::MotionWarpLadder(bool bUp, bool bEnd)
{
// bUp - bEnd
// true - false -> Position[2]위치 <시작매달리기>
// false - false-> Num - 1 위치 < 마지막위치 >
// false - true -> end의 마커포인트
mainChar->GetCharacterMovement()->SetMovementMode(MOVE_Flying);
FVector targetForward = currentCollision->GetInteractionVector();
FRotator targetRot = targetForward.Rotation();
USplineComponent* ladderSpline = currentLadder->ladderSpline;
if (!ladderSpline)
return;
FVector targetPos;
FVector rootTargetPos;
FVector capsuleOffset = mainChar->GetActorLocation() - mainChar->GetMesh()->GetComponentLocation();
if (bEnd && !bUp)
{
rootTargetPos = currentLadder->endPointMarker->GetComponentLocation();
}
else
{
if (bUp)
{
targetPos = currentLadder->handleWorldPositions[2];
}
else
{
int32 lastIndex = currentLadder->handleWorldPositions.Num() - 1;
targetPos = currentLadder->handleWorldPositions[lastIndex];
//루트모션 모션워핑 오차
targetPos.Z += 30.f;
}
rootTargetPos = targetPos - capsuleOffset;
}
motionWarpComp->AddOrUpdateWarpTargetFromLocationAndRotation(
FName("LadderPoint"),
rootTargetPos,
targetRot
);
}
5.캐릭터를 스플라인에 붙혔다가 떼는 기능 구현
if (currentLadder &&
(currentInteractionState == EInteractionState::LadderUp ||
currentInteractionState == EInteractionState::LadderDown))
{
mainChar->GetCharacterMovement()->Velocity = FVector::ZeroVector;
USplineComponent* ladderSpline = currentLadder->ladderSpline;
if (!ladderSpline)
return;
FVector closestPoint = ladderSpline->FindLocationClosestToWorldLocation(mainChar->GetActorLocation(), ESplineCoordinateSpace::World);
//work
mainChar->SetActorLocation(closestPoint);
if (bIsCliming)
{
if (climbTargetPos.Z > climbStartPos.Z)
mainChar->direction = 180.f;
else if (climbTargetPos.Z < climbStartPos.Z)
mainChar->direction = -180.f;
}
else
{
int32 nextUpIdx = currentHandleidx + 1;
int32 nextDownIdx = currentHandleidx - 1;
bool bCanMoveUp = currentLadder->handleWorldPositions.IsValidIndex(nextUpIdx);
bool bCanMoveDown = currentLadder->handleWorldPositions.IsValidIndex(nextDownIdx) && nextDownIdx >= 2;
if (mainController->bMoveForward && bCanMoveUp)
mainChar->direction = 180.f;
else if (mainController->bMoveBack && bCanMoveDown)
mainChar->direction = -180.f;
else
mainChar->direction = 0.f;
}
}
캐릭터를 붙히거나 떼는 함수 구현
void UUInteractionComponent::NotifyForLadder(bool bAttach)
{
bAttachSpline = bAttach;
float distance;
ALadderActor* ladder = findInteractionLadder(distance);
if (!ladder)
return;
currentLadder = ladder;
InteractionRotSet(false);
AttachHandToLadderSpline(ladder);
}
void UUInteractionComponent::DetachLadder()
{
bAttachSpline = false;
InteractionRotSet(true);
mainChar->LockAction("Jump", false);
mainChar->LockAction("Draw", false);
mainChar->LockAction("Attack", false);
}
5. 사다리액션이 시작하면 사다리에붙어있는 카메라로 시점 이동시키기
먼저 카메라컴포넌트를 붙혀주었습니다
UPROPERTY(EditAnywhere, Category="Camera")
class UCameraComponent* ladderCamera;
ladderCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("LadderCamera"));
ladderCamera->SetupAttachment(RootComponent);
ladderCamera->SetRelativeLocation(FVector(-320.f, 0.f, 415.f));
ladderCamera->SetRelativeRotation(FRotator(-30.f, 0.f, 0.f));
캐릭터의 컴포넌트 혹은 캐릭터클래스에서 카메라 시점을 옮겨주는 행위는 요청만 하고
실행하는위치는 캐릭터의 컨트롤러에서 실행합니다
void AMainCharacterController::SwitchActorCamera(AActor* targetActor, float blendTime)
{
if (!targetActor)
return;
UCameraComponent* camera = targetActor->FindComponentByClass<UCameraComponent>();
SetViewTargetWithBlend(targetActor, blendTime, EViewTargetBlendFunction::VTBlend_Cubic);
}
void AMainCharacterController::ReturnPlayerCamera(float bldneTime)
{
SetViewTargetWithBlend(mainCharacter, bldneTime, EViewTargetBlendFunction::VTBlend_Cubic);
}
void UUInteractionComponent::DoLadderUp()
{
DoLadderInternal(EInteractionState::LadderUp, "LadderUp", true);
mainController->SwitchActorCamera(currentLadder, 0.9f);
}
결과
올라가기

내려가기

'Unreal 프로젝트 다이어리 > 두번째 프로젝트' 카테고리의 다른 글
| Unreal - UI (HpWidget, PostureWidget) (0) | 2025.11.21 |
|---|---|
| Unreal - StatComponent (0) | 2025.11.21 |
| Unreal - 상호작용(승강기) (0) | 2025.11.12 |
| Unreal - 상호작용(Edge Traversal) (0) | 2025.11.09 |
| Unreal - 파쿠르 벽차기 (0) | 2025.11.03 |