Unreal GAS (23) - Click to Move

wnsduf0000·2025년 12월 1일

Unreal_GAS

목록 보기
24/34
  • Click to Move
    • Aura는 TopDown(하향식) 형태의 게임 (디아블로, 패스 오브 엑자일 등) 이므로, 마우스 클릭을 통해 캐릭터를 이동하는 일반적인 하향식 게임의 기능이 필요함.
    • 이동은 마우스 좌클릭을 통해 실시하게 할 예정임.
    • Plan
      • Unreal의 TopDown 템플릿 프로젝트는 기본적인 탑다운 기능을 잘 구현해 둔 템플릿이지만, 멀티플레이 시 마우스를 누른 상태로 두지 않으면 클라이언트는 제대로 이동하지 못한다는 단점이 존재함.
        (서버 캐릭터는 정상적으로 작동하나, 클라이언트 측에서만 이러한 문제가 발생)
        • 이동 자체는 MovementComponent를 사용하는데, MovementComponent는 이미 자체적인 Replication 지원이 내장되어 있어 마우스를 누른 상태로 유지하면 정상적으로 이동함.
        • 이동 키를 짧게 누르면 AIBlueprintHelperLibrary의 SimpleMoveToLocation()을 호출해서 클릭된 지점까지 네비게이션 시스템을 통한 길찾기를 실시하는데, 해당 함수가 기본적으로 Replication을 지원하지 않기 때문에 발생하는 문제라고 볼 수 있음.
      • TopDown 템플릿에서의 이러한 한계를 극복하려면, AddMovementInput을 사용해서 이동해야 한다는 점을 알 수 있음.
        • SimpleMoveToLocation()은 네비 메시 바운드 볼륨을 이용하여 자동 길찾기 기능을 제공한다는 장점이 있으나, Replication이 되지 않기 때문에, 멀티플레이를 기준에서는 사용하는 의미가 없기 때문임.
      • 마우스 클릭 시, 캐릭터와 목적지(LineTrace Hit 지점)의 위치를 캐시하여, 캐릭터가 목적지의 위치에 닿을 때까지 AddMovementInput()을 호출하는 것이 기본 설계임.
        • 문제점: 직선 이동의 경우 구현이 간단하나, 캐릭터와 목적지 사이에 장애물이 있는 경우, 별도의 길찾기 기능 구현이 필요함.
          • 장애물 주변 지점의 위치를 제공해서 우회할 수 있게 해주는 함수는 존재하나, 지점마다 캐릭터가 쳐다보는 방향이 갑작스럽게 바뀌게 되면 부자연스러워 보일 수 있음.
          • 해당 문제를 해결하기 위해, 얻은 위치들을 이용하여 Spline을 생성하고, 이를 통해 부드럽게 방향 전환을 하여 이동할 수 있도록 구현하려 함.
    • Implement Click to Move and AutoRunning (Pathfinding)
      • AuraPlayerController의 헤더에 다음과 같은 변수들을 생성한다.
        // Pathfinding
        FVector CachedDestination = FVector::ZeroVector;
        float FollowTime = 0.f;
        float ShortPressThreshold = 0.5f;
        bool bAutoRunning = false;
        bool bTargeting = false;
        
        UPROPERTY(EditDefaultsOnly)
        float AutoRunAcceptanceRadius = 50.f;
        
        UPROPERTY(VisibleAnywhere)
        TObjectPtr<USplineComponent> Spline;
        • CachedDestination은 말 그대로 목표 지점의 벡터를 캐싱해두기 위한 변수이다.
        • FollowTime은 마우스 입력이 지속되는 동안 Aura가 커서를 따라간 시간을 저장하기 위한 변수이다.
        • ShortPressThreshold는 짧은 클릭과 긴 클릭을 구분하기 위한 변수이다.
        • bAutoRunning은 자동 길찾기 기능을 사용하기 위한 변수이다.
          UNavigationSystem의 함수를 통해 UNavigationPath를 반환받으면, 여기서 PathPoint를 획득하여 Spline에 추가해준 후, 이 Spline을 따라서 이동해야 하는데, 이동은 Tick에서 처리하기 때문에 항상 이동 함수가 호출되지 않도록 구분하는 역할이다.
        • bTargeting은 커서 위에 적 몬스터와 같이 하이라이트 되는 액터가 존재하는 경우를 구분하기 위한 변수이다.
          적을 클릭한 경우, 이동이 아니라 GameplayAbility를 사용하도록 할 것이기 때문에, 이를 구분하기 위한 용도이다.
        • AutoRunAcceptanceRadius는 Spline을 통해 목적지까지 이동 후, 마지막 지점에서 허용되는 오차 범위를 말한다. 너무 크면 클릭한 지점에서 동떨어진 지점에서 멈출 수 있지만, 너무 작으면 계속해서 이동하려고 하는 문제가 생길 수 있다.
        • Spline은 부드러운 이동을 위해 사용하려는 Spline 컴포넌트이다.
      • AuraPlayerController의 AbilityInputPressed(), AbilityInputHeld(), AbilityInputReleased() 함수는 아래와 같이 작성한다.
        // AuraPlayerController.cpp
        
        AAuraPlayerController::AAuraPlayerController()
        {
        	bReplicates = true;
        	
        	Spline = CreateDefaultSubobject<USplineComponent>(TEXT("Spline"));
        }
        
        void AAuraPlayerController::PlayerTick(float DeltaTime)
        {
        	Super::PlayerTick(DeltaTime);
        
        	CursorTrace();
        
        	AutoRun();
        }
        
        void AAuraPlayerController::AbilityInputTagPressed(FGameplayTag InputTag)
        {
        	if (InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
        	{
        		bTargeting = CurrentTarget ? true : false;
        		bAutoRunning = false;
        	}
        }
        
        void AAuraPlayerController::AbilityInputTagReleased(FGameplayTag InputTag)
        {
        	if (!InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
        	{
        		if (GetASC())
        		{
        			GetASC()->AbilityInputTagReleased(InputTag);
        		}
        		return;
        	}
        
        	if (bTargeting)
        	{
        		if (GetASC())
        		{
        			GetASC()->AbilityInputTagReleased(InputTag);
        		}
        	}
        	else
        	{
        		APawn* ControlledPawn = GetPawn();
        		if (FollowTime <= ShortPressThreshold && ControlledPawn)
        		{
        			if (UNavigationPath* NavPath = UNavigationSystemV1::FindPathToLocationSynchronously(this, ControlledPawn->GetActorLocation(), CachedDestination))
        			{
        				Spline->ClearSplinePoints();
        				for (const FVector& PointLoc : NavPath->PathPoints)
        				{
        					Spline->AddSplinePoint(PointLoc, ESplineCoordinateSpace::World);
        					DrawDebugSphere(GetWorld(), PointLoc, 8.f, 8, FColor::Green, false, 5.f);
        				}
        				CachedDestination = NavPath->PathPoints[NavPath->PathPoints.Num() - 1];
        				bAutoRunning = true;
        			}
        		}
        		FollowTime = 0.f;
        		bTargeting = false;
        	}
        }
        
        void AAuraPlayerController::AbilityInputTagHeld(FGameplayTag InputTag)
        {
        	if (!InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
        	{
        		if (GetASC())
        		{
        			GetASC()->AbilityInputTagHeld(InputTag);
        		}
        		return;
        	}
        
        	if (bTargeting)
        	{
        		if (GetASC())
        		{
        			GetASC()->AbilityInputTagHeld(InputTag);
        		}
        	}
        	else
        	{
        		FollowTime += GetWorld()->GetDeltaSeconds();
        
        		FHitResult Hit;
        		if (GetHitResultUnderCursor(ECC_Visibility, false, Hit))
        		{
        			CachedDestination = Hit.ImpactPoint;
        		}
        
        		if (APawn* ControlledPawn = GetPawn<APawn>())
        		{
        			const FVector WorldDirection = (CachedDestination - ControlledPawn->GetActorLocation()).GetSafeNormal();
        			ControlledPawn->AddMovementInput(WorldDirection);	
        		}
        	}
        }
        
        void AAuraPlayerController::AutoRun()
        {
        	if (!bAutoRunning) return;
        
        	if (APawn* ControlledPawn = GetPawn<APawn>())
        	{
        		const FVector LocationOnSpline = Spline->FindLocationClosestToWorldLocation(ControlledPawn->GetActorLocation(), ESplineCoordinateSpace::World);
        		const FVector Direction = Spline->FindDirectionClosestToWorldLocation(LocationOnSpline, ESplineCoordinateSpace::World);
        		ControlledPawn->AddMovementInput(Direction);
        
        		const float DistanceToDestination = (LocationOnSpline - CachedDestination).Length();
        		if (DistanceToDestination <= AutoRunAcceptanceRadius)
        		{
        			bAutoRunning = false;
        		}
        	}
        }
        • AbilityInputTagPressed()
          • 새로 클릭 이벤트가 들어왔을 때, 마우스 좌클릭인지 확인하여, 좌클릭인 경우에는 이동과 GameplayAbility의 사용을 구분해야 하기 때문에, 현재 마우스 커서가 적을 가르키고 있는지를 검사하여 bTargeting의 값을 변경한다.
          • 새로 클릭 이벤트가 들어왔으므로, GameplayAbility를 사용하던, 새로운 지점으로 이동하던 어느 쪽이든 관계 없이 기존의 자동 이동은 취소해야 하므로, bAutoRunning을 false로 한다.
        • AbilityInputTagHeld()
          • 마우스 좌클릭이 아닌 입력이거나, 마우스 좌클릭이라 해도 커서 아래 적이 있는 경우에는 AuraAbilitySystemComponent의 AbilityInputHeld()를 호출한다.
          • 둘 다 아닌 경우는 땅을 클릭한 것이라고 생각할 수 있으므로,
            • 커서가 눌리는 동안 캐릭터가 커서를 따라다니는 시간을 저장하는 변수 FollowTime에 DeltaSecond를 더해준다.
            • 커서 트레이스(APlayerController의 GetHitResultAtScreenPosition()을 통해 실시되는 LineTrace)를 실시하여 목표 지점(CachedDestination)을 캐싱해둔다.
            • 캐싱된 목표 지점과 캐릭터의 현재 위치를 통해 방향을 구해서 AddMovementInput()을 호출한다.
        • AbilityInputTagReleased()
          • 마우스 좌클릭이 아니거나 적이 커서 아래 있는 경우, AuraAbilitySystemComponent의 AbilityInputReleased()를 호출한다.
          • 둘 다 아닌 경우 땅을 클릭한 것이라고 볼 수 있다.
            • 마우스 클릭을 짧게 한 경우, 클릭 지점까지의 경로를 찾아서 이동하도록 할 것이다.
            • UNavigationSystemV1은 FindPathToLocationSynchronously()를 통해 찾아낸 경로인 UNavigationPath를 반환한다.
              • UNavigationSystemV1의 함수를 사용하는데, 이는 Build.cs에 Dependency에 NavigationSystem을 명시해주어야 빌드가 가능하다.
              • NavmeshBoundVolume을 레벨에 배치해야만이 UNavigationSystemV1의 길찾기 함수가 제대로 작동한다.
                (AI 길찾기 기능 또한 NavmeshBoundVolume이 필요)
              • 에디터 - 프로젝트 설정의 ‘Navigation System’에서 반드시 Allow Client Side Navigation를 체크해주어야 클라이언트 측에서도 길찾기 기능을 사용할 수 있다.
                (기본적으로는 서버 측만 사용 가능)
            • AuraPlayerController의 Spline의 현재 모든 포인트를 제거하기 위해 ClearSplinePoints()를 호출하고, 다시 UNavigationPath가 제공하는 PathPoints (찾아낸 경로 상의 포인트들) 들을 AddSplinePoint()로 더해준다.
            • 단, 목표 지점이 Spline의 끝부분과 불일치 할 수 있다.
              (다른 액터를 클릭하거나 한 경우)
              이를 방지하기 위해, CachedDestination을 PathPoints의 마지막 지점으로 변경해주고, AutoRun()을 호출할 수 있도록 bAutoRunning을 true로 한다.
            • 클릭이 종료되었으므로, FollowTime과 bTargeting은 초기화한다.
        • AutoRun()
          • Spline은 Spline상에서의 위치를 월드 상의 위치(벡터)로 변환해주는 함수나, Spline상에서의 방향을 월드 상의 방향(벡터) 값으로 변환해주는 함수를 제공한다.
          • FindLocationClosestToWorldLocation()을 통해 현재 캐릭터의 Spline상에서의 위치를 얻는다.
          • FindDirectionClosestToWorldLocation()을 통해 해당 위치에서의 방향 벡터를 얻어, 해당 방향으로 AddMovementInput()을 실시하여 캐릭터를 이동시킨다.
          • 목표 지점에 도달했는지 검사를 위해, CachedDestination과 현재 Spline상에서의 위치의 차이를 길이로 변환하여, 이 값이 AutoRunAcceptanceRadius보다 작은지 확인하고, 작은 경우 bAutoRunning을 false로 하여 자동 이동을 중단하도록 한다.
profile
저는 게임 개발자로 일하고 싶어요

0개의 댓글