이 글은 전 글과 이어집니다
2025.11.09 - [Unreal5 프로젝트 다이어리2] - Unreal - 상호작용(Edge Traversal)
Unreal - 상호작용(Edge Traversal)
플레이어가 원래는 갈수없는 길을 벽을기대고 아슬아슬하게 넘어가는Edge Traversal을 구현하였습니다 구현방식은 이와같습니다1. 캐릭터한테 커스텀 컴포넌트인 InteractionComponent를 달아주기2.. Coll
lucodev.tistory.com
승강기를 만들어보았습니다
승강기의 작동원리는 이와 같습니다
1. 승강기는 스플라인을 따라 이동하며 스플라인의 0번 (제일위 시작지점) 부터 .last (제일밑 끝지점) 까지 이동합니다
2. 작동은 레버를 당겨서 작동합니다
3. 엘레베이터의 움직임은 처음에 점점 빨라졌다가 도착할떄 점점 느려지는 InterpEaseInOut 움직임을 가집니다
제작과정은 이렇습니다
1. Interaction Collision으로 현재 인터렉션 타입이 엘레베이터인것을 구분합니다
2. 레버 를 만들고 레버의 애니메이션을 재생하는 함수를 만듭니다
3.엘레베이터 액터를 만들고 spline을 따라 위 아래 로 이동하는 로직을 구현합니다
4.노티파이로 엘레베이터 작동시점을 제어합니다
5.모션워핑을 사용하여 레버 작동 위치를 계산하고 해당 위치로 이동합니다
6. 디테일 (투명벽 + 양쪽기어) 제작
1. Interaction Collision으로 인터렉션 타입 구분
엘레베이터가 여러개가 있을수도있으니 현재 엘레베이터가 내가 지금 닿은 콜리전의 엘레베이터인것을
참조연결하여 해당 작동 엘레베이터를 알아옵니다
public:
UPROPERTY()
class AMovingElevator* movingElevator;
InteractionCollision내에 맵에 바인드된 함수 즉 오버랩되었을때 실행되는함수와 엔드오버랩되었을때 실행되는 함수에서
현재 Collision이 먼지 구분하고 상호작용 객체로 등록합니다
void AAInteractionCollision::DoActiveElevator()
{
if (mainChar && mainChar->interactionComp)
mainChar->interactionComp->currentCollision = this;
}
void AAInteractionCollision::DeActiveElevator()
{
if (mainChar && mainChar->interactionComp)
mainChar->interactionComp->currentCollision = nullptr;
}
2. 레버 제작
UPROPERTY(EditAnywhere)
TArray<UAnimMontage*> leverAnims;
void PlayAnimationIdx(int32 idx);
void AInteractionLever::PlayAnimationIdx(int32 idx)
{
CustomTimeDilation = 1.f;
if (!leverMesh || leverAnims.Num() == 0)
return;
UAnimMontage* selectMontage = leverAnims[idx];
UAnimInstance* animInst = leverMesh->GetAnimInstance();
if (animInst)
animInst->Montage_Play(selectMontage, 1.f);
}
동적배열로 몽타쥬를 실행하며 0번은 Push 1번은 Pull로 한다고 약속해주었습니다
레버는 CustomTimeDelation을 사용하여 작동후 작동했을때의 위치를 고정해주었습니다
void ULeverAnimInstance::AnimNotify_EndInteraction()
{
if (owner)
owner->CustomTimeDilation = 0.f;
}
void ULeverAnimInstance::AnimNotify_StartInteraction()
{
if (owner)
owner->CustomTimeDilation = 1.f;
}
3. 엘레베이터 움직이는 로직을 만들고 위->아래 / 아래 -> 위로 가는 엘레베이터 활성화 함수 생성
bElevateActive bool 변수가 활성화되면 Spline을 따라 PlatformMesh 가 위->아래 or 아래->위 로 가는 함수입니다
움직임은 InterpEaseInOut을 사용하여 Spline의 위치를 따라 처음에는 느렸다가 점점 빨라지며
도착할떄 점점 느려지게 설계하였습니다
void AMovingElevator::MovingPlatform(float deltaTime)
{
if (!pathSpline || !platFormMesh || !bElevateActive)
return;
splineLength = pathSpline->GetSplineLength();
accelElapsed += deltaTime;
float alpha = FMath::Clamp(accelElapsed / (accelTime * maxSpeedRatio), 0.f, 1.f);
float easedAlpha = FMath::InterpEaseInOut(0.f, 1.f, alpha, 3.f);
// directionMap: None=0, Down=-1, Up=1
TArray<int32> directionMap = { 0, -1, 1 };
int32 moveDir = directionMap[static_cast<int32>(elevatorState)];
if (moveDir == -1)
splineDis = easedAlpha * splineLength;
else if (moveDir == 1)
splineDis = (1.f - easedAlpha) * splineLength;
//////////////////////////////////////////////
splineDis = FMath::Clamp(splineDis, 0.f, splineLength);
if ((moveDir == -1 && splineDis >= splineLength) || (moveDir == 1 && splineDis <= 0.f))
{
if (moveDir == -1) splineDis = splineLength;
else splineDis = 0.f;
elevatorState = EElavatorState::None;
accelElapsed = 0.f;
bElevateActive = false;
}
platFormMesh->SetWorldLocation(pathSpline->GetLocationAtDistanceAlongSpline(splineDis, ESplineCoordinateSpace::World));
UpdateInvisibleCollision();
}
함수를 호출할때 아래->위 / 위->아래 인것을 알려주고 state을 정의해주는 함수입니다
void AMovingElevator::ActivateElavator()
{
if (elevatorState != EElavatorState::None)
return;
AInteractionLever* lever = GetLeverActor();
if (!lever)
return;
//아래인경우 위로
if (splineDis <= 0.f)
{
lever->PlayAnimationIdx(0);
elevatorState = EElavatorState::Down;
}
//위에있을때 아래
else if (splineDis >= pathSpline->GetSplineLength())
{
lever->PlayAnimationIdx(1);
elevatorState = EElavatorState::Up;
}
accelElapsed = 0.f;
}
3-2 TrypInteraction함수(E 상호작용키) 에서 조건체크하는 함수와 실행함수를 생성
조건
bool UUInteractionComponent::CanActiveElavator()
{
//충돌x 타입이 맞지않으면 작동x
if (!currentCollision || currentCollision->interactionType != EInteractionType::Elevator)
return false;
movingElevator = currentCollision->linkElevator;
if (!movingElevator)
return false;
if (movingElevator->bElevateActive)
return false;
//움직이지않으면 true
return movingElevator->elevatorState == EElavatorState::None;
}
실행
void UUInteractionComponent::DoActiveElavator()
{
currentInteractionState = EInteractionState::Elavator;
const FInteractionDataRow* row = GetInteractionData("Elevator");
if (!row)
return;
if (movingElevator)
movingElevator->ActivateElavator();
UAnimMontage* playMontage = nullptr;
bool bPushLever;
switch (movingElevator->elevatorState)
{
case EElavatorState::Down: // 위 -> 아래
bPushLever = true;
playMontage = row->startPlayMontage;
break;
case EElavatorState::Up: // 아래 -> 위
bPushLever = false;
playMontage = row->endPlayMontage;
break;
default:
break;
}
if (playMontage)
{
MotionWarpElevator(bPushLever);
ownerCharacter->PlayAnimMontage(playMontage, row->playRatio);
}
}
4. 노티파이에서 변수를 호출하여 실행
void UUInteractionComponent::NotifyForElevator()
{
if (!movingElevator)
return;
movingElevator->bElevateActive = true;
mainChar->LockAction("Jump", false);
mainChar->LockAction("Draw", false);
mainChar->LockAction("Attack", false);
}
void UMainCharacterAnimInstance::AnimNotify_ElevatorMovingStart()
{
if (!interactionComp || !mainCharacter)
return;
mainCharacter->interactionComp->NotifyForElevator();
}
5. 모션워핑을 사용하여 위치를 계산하고 자연스럽게 회전, 이동
위치는 레버를 작동하여 레버와 캐릭터의 위치를 맞추기위해 엘레베이터에 pushMarkerPoint / pullMarkerPoint로
SceneComponent를 달아주었습니다
void UUInteractionComponent::MotionWarpElevator(bool bPush)
{
if (!ownerCharacter || !currentCollision)
return;
FVector targetForward = currentCollision->GetInteractionVector();
targetForward.Z = 0.f;
targetForward.Normalize();
FRotator targetRot = targetForward.Rotation();
FVector targetLoc;
if (bPush)
targetLoc = movingElevator->pushMarkerPointComp->GetComponentLocation();
else
targetLoc = movingElevator->pullMarkerPointComp->GetComponentLocation();
motionWarpComp->AddOrUpdateWarpTargetFromLocationAndRotation(
FName("MarkerPoint"),
targetLoc, targetRot
);
}

6. 도착후 투명벽 + 양쪽기어 처리
양쪽에 기어 바퀴를 달아서 Roll로 회전시켰으며 투명벽을 만들어 도착했을때만 NoCollision 아닐시 전부 Block시켜주었습니다
void AMovingElevator::UpdateInvisibleCollision()
{
if (!invisibleWall)
return;
const float floorTHR = 1.0f; //부동소수점 오차 임계값 설정
if (splineDis >= splineLength - floorTHR)
invisibleWall->SetCollisionEnabled(ECollisionEnabled::NoCollision);
else
invisibleWall->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
}
캐릭터가 z축으로 너무 빨리 이동하면 카메라붐이 같이 못따라와서 시점이 어색해줄수있으니
카메라붐의 maxdistance값을 제한해줍니다

결과물

모션워핑을 사용하여 플레이어가 정확하게 레버를 당기게 연출하였습니다
위 -> 아래

아래 -> 위

'Unreal 프로젝트 다이어리 > 두번째 프로젝트' 카테고리의 다른 글
| Unreal - StatComponent (0) | 2025.11.21 |
|---|---|
| Unreal - 상호작용(사다리 타기) (0) | 2025.11.15 |
| Unreal - 상호작용(Edge Traversal) (0) | 2025.11.09 |
| Unreal - 파쿠르 벽차기 (0) | 2025.11.03 |
| Unreal - 파쿠르 볼트(Vault) (0) | 2025.11.02 |