Unreal - 상호작용(Edge Traversal)

2025. 11. 9. 22:51·Unreal 프로젝트 다이어리/두번째 프로젝트

플레이어가 원래는 갈수없는 길을 벽을기대고 아슬아슬하게 넘어가는

Edge Traversal을 구현하였습니다

 

구현방식은 이와같습니다

1. 캐릭터한테 커스텀 컴포넌트인 InteractionComponent를 달아주기

2.. CollisionActor로 플레이어의 오버랩을 감지하여 타입별 액션 분기점 나누고 방향의 기준점 정하기

3. 인터렉션 전용 파쿠르 데이터를 담당하는 Struct인 InteractionData 만들기

4.인터렉션 컴포넌트에서 LeftIn LeftOut, Left In Right Out, Right In Right Out, Right In Left Out 사이클 설계하기

5. 길을 따라 설치될 스플라인 액터를 만들고 캐릭터를 해당 스플라인 액터 위치로 이동하게 만들기

6. 인터렉션 데이터를 기반으로 작동하며 모션워핑으로 위치 맞추기

 

먼저 인터렉션 감지를 담당하는 액터를 만들었습니다

 

어떤 타입의 상호작용이 일어나면 어떤 함수를 실행할지 저장해두는 맵을 선언

TMap<EInteractionType, FInteractionDelegate> interactionStartMap;
TMap<EInteractionType, FInteractionDelegate> interactionEndMap;

 

오버랩 설정을 해줍니다

만약 델리게이트에 바인딩 되어있다면 해당 맞는 함수를 실행한다 의미입니다

void AAInteractionCollision::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor == mainChar && OtherComp == capsuleComp)
	{
		if (FInteractionDelegate* interactionDelegate = interactionStartMap.Find(interactionType))
			interactionDelegate->ExecuteIfBound();
		
	}

}

void AAInteractionCollision::OnOverlapEnd(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	if (OtherActor == mainChar && OtherComp == capsuleComp)
	{
		if (FInteractionDelegate* interactionDelegate = interactionEndMap.Find(interactionType))
			interactionDelegate->ExecuteIfBound();

	}
}

 

각 맵들을 함수에 바인딩해주었습니다

void AAInteractionCollision::InitializeStartInteractionMap()
{
	interactionStartMap.Add(EInteractionType::LedgeWalkRight, FInteractionDelegate::CreateUObject(this, &AAInteractionCollision::DoLedgeWalkRight));
	interactionStartMap.Add(EInteractionType::LedgeWalkLeft, FInteractionDelegate::CreateUObject(this, &AAInteractionCollision::DoLedgeWalkLeft));
	interactionStartMap.Add(EInteractionType::WallCling, FInteractionDelegate::CreateUObject(this, &AAInteractionCollision::DoWallCling));
	interactionStartMap.Add(EInteractionType::Ladder, FInteractionDelegate::CreateUObject(this, &AAInteractionCollision::DoLadder));
}

void AAInteractionCollision::InitializeEndInteractionMap()
{
	interactionEndMap.Add(EInteractionType::LedgeWalkRight, FInteractionDelegate::CreateUObject(this, &AAInteractionCollision::EndLedgeWalkRight));
	interactionEndMap.Add(EInteractionType::LedgeWalkLeft, FInteractionDelegate::CreateUObject(this, &AAInteractionCollision::EndLedgeWalkLeft));
	interactionEndMap.Add(EInteractionType::WallCling, FInteractionDelegate::CreateUObject(this, &AAInteractionCollision::EndWallCling));
	interactionEndMap.Add(EInteractionType::Ladder, FInteractionDelegate::CreateUObject(this, &AAInteractionCollision::EndLadder));
}

 

스플라인액터를 만들어주었습니다

public:	
	AGripSplineActor();

private:
	UPROPERTY(EditAnywhere)
	USceneComponent* rootComp;

	UPROPERTY(EditAnywhere)
	USplineComponent* handSpline;


public:
	FORCEINLINE USplineComponent* GetHandSpline() const 
	{ return handSpline; }
////////////////////////////////////////////////
#include "GripSplineActor.h"
#include "Components/SplineComponent.h"

AGripSplineActor::AGripSplineActor()
{
	PrimaryActorTick.bCanEverTick = true;

	handSpline = CreateDefaultSubobject<USplineComponent>(TEXT("HandSpline"));
	RootComponent = handSpline;
	handSpline->SetMobility(EComponentMobility::Movable);

	handSpline->bDrawDebug = true;
}

 

스플라인을 찾습니다 찾아서 해당 담당스플라인을 변수에 저장해줍니다

AGripSplineActor* UUInteractionComponent::findInteractionSpline(float& outDistance) const
{
	outDistance = FLT_MAX;
	TArray<AGripSplineActor*> foundSplines;

	for (TActorIterator<AGripSplineActor> it(GetWorld()); it; ++it)
		foundSplines.Add(*it);

	if (foundSplines.Num() == 0)
		return nullptr;

	foundSplines.Sort([this](const AGripSplineActor& a, const AGripSplineActor& b)
		{
			const float distA = FVector::Dist(a.GetActorLocation(), ownerCharacter->GetActorLocation());
			const float distB = FVector::Dist(b.GetActorLocation(), ownerCharacter->GetActorLocation());
			return distA < distB;
		});

	AGripSplineActor* workSpline = foundSplines[0];
	outDistance = FVector::Dist(workSpline->GetActorLocation(), ownerCharacter->GetActorLocation());
	return workSpline;
}

 

스플라인의 가장 가까운 포인트를 저장해줍니다

void UUInteractionComponent::AttachHandToSpline(AGripSplineActor* targetSpline)
{
	if (!targetSpline || !ownerCharacter || !currentCollision)
		return;

	currentTargetSpline = targetSpline;
	// InteractionVector 기준으로 캐릭터 회전
	FVector targetForward = currentCollision->GetInteractionVector();
	targetForward.Z = 0.f;
	targetForward.Normalize();
	ownerCharacter->SetActorRotation(targetForward.Rotation());

	////////////////////////////////////////////////////////
	USplineComponent* spline = targetSpline->GetHandSpline();
	if (!spline)
		return;
	FVector closestPoint = spline->FindLocationClosestToWorldLocation(mainChar->GetActorLocation(), ESplineCoordinateSpace::World);
	closestPoint.Z = ownerCharacter->GetActorLocation().Z;
	gripImpactPoint = closestPoint;



	
}

 

콜리전 프리셋 InteractionWall을 만들고 해당 콜리전 프리셋이 InteractionWall인 액터에 대해 배열로 추가하여

InteractionWall 콜리전 프리셋을 가진 벽 컴포넌트를 찾고 제어하였습니다

void UUInteractionComponent::FindInteractionWall()
{
	interactionWalls.Empty();

	for (TActorIterator<AActor> it(GetWorld()); it; ++it)
	{
		AActor* actor = *it;
		if (!actor) continue;

		TArray<UPrimitiveComponent*> primComps;
		actor->GetComponents<UPrimitiveComponent>(primComps);

		for (UPrimitiveComponent* prim : primComps)
		{
			if (!prim) 
				continue;

			if (prim->GetCollisionProfileName() == FName("InteractionWall"))
				interactionWalls.Add(actor);
			
		}
	}
}

 

벽에서 움직일때 캐릭터가 앞만 보면서 움직임을 하도록 기본 무브먼트를 제어해주었습니다

void UUInteractionComponent::InteractionRotSet(bool set)
{
	mainChar->GetCharacterMovement()->bOrientRotationToMovement = set;
	mainChar->GetCharacterMovement()->bUseControllerDesiredRotation = set;
}

 

캐릭터를 해당 스플라인의 가장 가까운점으로 값을 구했으니 그위치로 캐릭터를 이동시켜주었습니다

void UUInteractionComponent::UpdateSplineMovement(float deltaTime)
{
	
	if (!mainChar || !currentTargetSpline)
		return;

	USplineComponent* spline = currentTargetSpline->GetHandSpline();
	if (!spline) 
		return;

	FVector closestPoint = spline->FindLocationClosestToWorldLocation(mainChar->GetActorLocation(), ESplineCoordinateSpace::World);
	closestPoint.Z = mainChar->GetActorLocation().Z;

	mainChar->SetActorLocation(closestPoint);


	if (currentInteractionState == EInteractionState::LedgeWalkLeft ||
		currentInteractionState == EInteractionState::LedgeWalkRight)
	{
		if (mainController->bMoveForward)   
			mainChar->direction = 180.f;
		else if (mainController->bMoveBack)  
			mainChar->direction = -180.f;
		else if (mainController->bMoveRight)
			mainChar->direction = 180.f;
		else if (mainController->bMoveLeft)
			mainChar->direction = -180.f;
	}
}

 

left in left out, left in right out, right in right out, right in left out 같은 사이클을 만들어주었습니다

EInteractionCycleState UUInteractionComponent::EValuateLedgeWalkResult(EInteractionState currentLedgeDirection)
{

	int currentType = 0;
	if (currentLedgeDirection == EInteractionState::LedgeWalkLeft)
		currentType = -1;
	else if (currentLedgeDirection == EInteractionState::LedgeWalkRight)
		currentType = 1;

	EInteractionCycleState result = EInteractionCycleState::Start;

	if (lastType == 0)
	{
		//신사이클
		lastType = currentType;
		result = EInteractionCycleState::Start;
	}
	else
	{
		if (lastType == currentType)
			result = EInteractionCycleState::Cancle; // Cancel
		else
			result = EInteractionCycleState::End;   // Finish

		if (result != EInteractionCycleState::Start)
			lastType = 0;
	}

	return result;
}

 

노티파이에서 호출할 상태를 리셋할 함수를 만들어주었습니다

void UUInteractionComponent::SetInteractionState(bool bActive, const EInteractionState& setState)
{
	if (!mainChar)
		return;
	SetCollisionIgnoreWall(bActive);
	
	const float ledgeWalkSpeed = 150.f;
	const float wallClingSpeed = 100.f;
	const float ladderSpeed = 160.f;

	const float changeWalkSpeed =
		(setState == EInteractionState::LedgeWalkLeft || setState == EInteractionState::LedgeWalkRight) ? ledgeWalkSpeed :
		(setState == EInteractionState::WallCling) ? wallClingSpeed :
		(setState == EInteractionState::Ladder) ? ladderSpeed :
		defaultWalkkSpeed;
	const float setWalkSpeed = bActive ? changeWalkSpeed : defaultWalkkSpeed;
	
	mainChar->walkSpeed = setWalkSpeed;
	mainChar->GetCharacterMovement()->MaxWalkSpeed = setWalkSpeed;

	mainChar->LockAction("Run", false);
	mainChar->LockAction("Jump", false);

	if (!bActive)
		currentInteractionState = EInteractionState::None;

}

 

결과물

 

 

 

 

 

'Unreal 프로젝트 다이어리 > 두번째 프로젝트' 카테고리의 다른 글

Unreal - 상호작용(사다리 타기)  (0) 2025.11.15
Unreal - 상호작용(승강기)  (0) 2025.11.12
Unreal - 파쿠르 벽차기  (0) 2025.11.03
Unreal - 파쿠르 볼트(Vault)  (0) 2025.11.02
Unreal - 파쿠르 잡고 올라가기  (0) 2025.10.31
'Unreal 프로젝트 다이어리/두번째 프로젝트' 카테고리의 다른 글
  • Unreal - 상호작용(사다리 타기)
  • Unreal - 상호작용(승강기)
  • Unreal - 파쿠르 벽차기
  • Unreal - 파쿠르 볼트(Vault)
lucodev
lucodev
커피와 노트북 그리고 개발
  • lucodev
    루코 개발테이블
    lucodev
  • 전체
    오늘
    어제
    • 분류 전체보기 (211) N
      • Unreal 프로젝트 다이어리 (108) N
        • 첫번째 프로젝트 (73)
        • 두번째 프로젝트 (35) N
      • Unreal 팁 (8)
      • Unreal 디버깅 (8)
      • C++ 프로그래머스 (52)
        • Stack,Queue (7)
        • Hash (4)
        • Heap (2)
        • Sort (5)
        • Exhaustive search (5)
        • Greedy (2)
        • BFS , DFS (7)
        • Graph (2)
        • Dynamic Programming (1)
        • C++ Math (2)
        • 기타 문제 (14)
      • C++ 백준 (4)
      • C++ 팁 (1)
      • 개인 코테 & 스타디 <비공개> (29)
        • 코드 개인보관함 (9)
        • 코딩테스트+@ (11)
        • 알고리즘 스타디 (6)
        • 알고리즘 스타디 과제 (3)
        • 비공개 (0)
  • 인기 글

  • 최근 글

  • 최근 댓글

  • 링크

  • 공지사항

  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 태그

    언리얼 시퀀스
    언리얼 behavior tree
    unreal 파쿠르
    언리얼 상호작용
    언리얼 프로그래스바
    언리얼 파쿠르
    언리얼
    unreal inventory
    언리얼 behaviortree
    언리얼 parkour
    언리얼 ui
    언리얼 motionmatching
    unreal 인벤토리
    언리얼 모션매칭
    언리얼 인벤토리
    Unreal Parkour
    언리얼 비헤이비어트리
    언리얼 컷씬
    unreal 시퀀스
    unreal 모션매칭
  • hELLO· Designed By정상우.v4.10.3
lucodev
Unreal - 상호작용(Edge Traversal)
상단으로

티스토리툴바