0. 개요
언리얼에서 기본 템플릿으로 주어지는 탑-다운 템플릿에서 클릭하여 캐릭터를 이동하는 것은 문제가 있다.
향상된 입력을 사용하게 되면서 Released 상태의 Move 함수는 복제를 지원하여 서버에서 올바르게 이동하지만, Pressed 상태의 함수는 AI 컨트롤러의 Move 함수를 사용하기 때문에 복제가 되지 않아 멀티플레이 환경에서 움직이지 않는다.
해당 문제점을 해결하고 장애물의 주위를 돌아서 가는 길 찾기 로직을 도입할 것이다.
1. 드래그해서 이동하기 - AuraPlayerController
(1) 멤버 변수 설정
// 이동 관련
FVector CachedDestination = FVector::ZeroVector;
float FollowTime = 0.f;
// 짧게 누름 경계값
float ShortPressThresold = 0.5f;
// 자동 이동
bool bAutoRunning = false;
// 자동 이동 허용 반경
UPROPERTY(EditDefaultsOnly)
float AutoRunAcceptanceRadius = 50.f;
// 길찾기 스플라인
UPROPERTY(VisibleAnywhere)
TObjectPtr<USplineComponent> Spline;
};
AAuraPlayerController::AAuraPlayerController()
{
// 서버에서 발생한 변경 사항을 복제하여 모든 클라이언트로 전송(브로드 캐스팅)
bReplicates = true;
// 길 찾기 스플라인
Spline = CreateDefaultSubobject<USplineComponent>("Spline");
}
클릭으로 이동할 때 필요한 값을 멤버 변수로 설정한다.
(2) 클릭 입력에 대한 판단 내리기
왼쪽 버튼을 클릭하였을 때 이동할 것인지, 적을 클릭했다면 적을 공격할 것인지 판단하여야 한다.
이전에 적 위에 커서를 올렸을 때 외곽선을 나타내도록 하는 CursorTrace 함수를 참고한다.
void AAuraPlayerController::CursorTrace()
{
FHitResult CursorHit;
// 트레이스 채널, 단순 충돌 확인, 반환되는 FHitResult 구조체의 주소
GetHitResultUnderCursor(ECollisionChannel::ECC_Visibility, false, CursorHit);
if (!CursorHit.bBlockingHit)
return;
LastActor = ThisActor;
// 마우스 커서와 충돌한 액터 꺼내오기
ThisActor = CursorHit.GetActor();
/* 커서에서 라인트레이스
(1) LastActor가 nullptr, ThisActor가 nullptr
- 적이 아닌 액터
- 아무일도 일어나지 않음
(2) LastActor가 nullptr, ThisActor가 유효
- 적에게 처음으로 마우스를 가져다 댐
- ThisActor->HighlightActor();
(3) LastActor가 유효, ThisActor가 nullptr
- 적을 가리키다가 더 이상 마우스가 위치하지 않음
- LastActor->UnHighlightActor();
(4) LastActor와 ThisActor 모두 유효, LastActor != ThisActor
- 적을 가리키다가 다른 적을 가리키게 됨
- LastActor->UnHighlightActor();
- ThisActor->HighlightActor();
(5) LastActor와 ThisActor 모두 유효, LastActor == ThisActor
- 이미 Highlight 함수가 발동
- 아무 일도 일어나지 않음*/
즉, 이 함수에 따라 ThisActor 또는 LastActor가 nullptr이 아닐 경우 적을 타겟팅 하는것이므로, 능력을 발동하는 함수인 AbilityInputTag 함수를 해당 액터를 타겟으로 발동시키게 하면 된다.
// 타겟
bool bTargeting = false;
멤버 변수로 타겟팅 중인지 판단하는 불리언을 선언한다.
void AAuraPlayerController::AbilityInputTagPressed(FGameplayTag InputTag)
{
if (InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
{
// ThisActor가 nullptr이 아니면 true
bTargeting = ThisActor ? true : false;
bAutoRunning = 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(ECollisionChannel::ECC_Visibility, false, Hit))
{
// Hit.Location도 사용 가능
CachedDestination = Hit.ImpactPoint;
}
if (APawn* ControlledPawn = GetPawn())
{
// 방향 벡터 구하기
const FVector WorldDirection = (CachedDestination - ControlledPawn->GetActorLocation()).GetSafeNormal();
ControlledPawn->AddMovementInput(WorldDirection);
}
}
}
- 분석
(1) !InputTag if 문
마우스를 떼는 시점에, 마지막으로 실행되는 Held 함수에서 적 위에서 클릭이 떼어졌다면 능력을 실행한다.
이후 함수를 종료한다.
(2) bTargeting if 문
타겟이 있다면 어빌리티를 발동한다
(3) else 문
타겟이 없다면 HitResult 구조체를 이용하여 커서 아래의 HitResult를 가져오고, 해당 HitResult의 Location 또는 ImpactPoint를 가져온다.
이후에 벡터를 정규화(Normalize) 하고 방향 벡터를 폰에게 추가하는 AddMovementInput 함수를 사용한다.
2. 길 찾기
(1) 헤더 파일 포함
#include "Player/AuraPlayerController.h"
#include "Interaction/EnemyInterface.h"
#include "EnhancedInputSubsystems.h"
#include "Input/AuraInputComponent.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "Components/SplineComponent.h"
#include "AbilitySystem/AuraAbilitySystemComponent.h"
#include "AuraGameplayTags.h"
#include "NavigationSystem.h"
#include "NavigationPath.h"
(2) 클릭 해제 후 길 찾기
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);
}
}
여기까지는 AbilityInputTagHeld와 동일하다.
else
{
// 경계값보다 짧게 눌렀으면 목적지로 길 찾기 시작
APawn* ControlledPawn = GetPawn();
if (FollowTime <= ShortPressThresold && 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);
}
bAutoRunning = true;
}
}
FollowTime = 0.f;
bTargeting = false;
}
}
- FollowTime이 짧게 누름 경계값보다 적을 때 길 찾기를 실행할 것이다.
- NavigationSystemV1의 FindPathToLocationSynchronously 함수를 사용하여 폰의 지금 위치에서 캐싱된 목적지 까지의 길 찾기를 수행한다.
- 생성한 스플라인을 초기화하고 찾은 길 Vector를 순회하여 각 점을 스플라인에 추가한다.
- 길 찾기 수행 후 FollowTime과 타겟팅을 취소한다.
(3) 링커 오류 해결 - Aura.Build.cs
NavigationSystem 모듈을 프로젝트에 추가해야 한다.
using UnrealBuildTool;
public class Aura : ModuleRules
{
public Aura(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "GameplayAbilities", "UMG" });
PrivateDependencyModuleNames.AddRange(new string[] { "GameplayTags", "GameplayTasks", "NavigationSystem" });
// Uncomment if you are using Slate UI
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
// Uncomment if you are using online features
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
}
}
(4) 내비 메시 바운드 볼륨 추가
길 찾기를 수행하려면 맵 상에 내비 메시 바운드 볼륨을 추가해야 한다.
볼륨을 추가하고 크기를 맵을 덮을 정도로 지정하고, 뷰 포트에서 P를 눌러 표시되는지 확인한다.
3. 클릭해서 이동하기 - 움직이기
(1) 이동 - PlayerTick 함수
이제 찾은 경로를 이용하여 PlayerTick에서 움직이는 것을 구현한다.
void AAuraPlayerController::PlayerTick(float DeltaTime)
{
Super::PlayerTick(DeltaTime);
//
CursorTrace();
if (!bAutoRunning)
return;
// 길 찾기 이용하여 움직임
if (APawn* ControlledPawn = GetPawn())
{
// 폰에 가장 가까운 스플라인의 벡터
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;
}
}
}
폰에 가장 가까운 스플라인을 찾아서 방향 벡터를 가져온다.
방향 벡터를 이용해 폰을 움직이고, 폰이 진행하고 있는 스플라인에서 목적지까지의 거리를 계산해서 자동 이동 허용 범위에 도달하면 자동 이동을 중단한다.
(2) 리팩토링 - AutoRun 함수
void AAuraPlayerController::AutoRun()
{
if (!bAutoRunning)
return;
// 길 찾기 이용하여 움직임
if (APawn* ControlledPawn = GetPawn())
{
// 폰에 가장 가까운 스플라인의 벡터
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;
}
}
}
void AAuraPlayerController::PlayerTick(float DeltaTime)
{
Super::PlayerTick(DeltaTime);
// 커서 추적
CursorTrace();
// 클릭으로 이동
AutoRun();
}
(3) 멀티플레이 환경에서 내비게이션 활성화
편집 - 프로젝트 세팅 - 내비게이션 시스템 - 클라이언트 측면 내비게이션 허용 체크
해당 옵션을 사용하지 않으면 멀티플레이 환경에서 클릭해도 이동하지 않는다.
4. 디버그
(1) 물체 주위를 클릭하면 계속 이동하는 현상
커서 트레이스가 물체의 콜리전 표면를 목적지로 하기 때문에 발생한다.
해당 물체의 콜리전 프리셋을 Custom으로 변경하고 Visiblity 채널을 무시로 지정한다.
카메라 콜리전 채널 또한 무시로 설정하면 해당 물체에 가까워져도 카메라가 충돌하지 않는다.
(2) 내비 메시 볼륨이 없는 곳을 클릭했을 때 계속 이동하는 현상
콜리전이 존재하는 물체의 주변은 내비 메시로 지정되지 않는다.
AbilityInputTagReleased의 else 문을 수정하여 해당 경우에 내비게이션 경로의 마지막 포인트를 목적지로 재설정한다.
따라서 항상 도달 가능한 위치가 목적지가 된다.
else
{
// 경계값보다 짧게 눌렀으면 목적지로 길 찾기 시작
APawn* ControlledPawn = GetPawn();
if (FollowTime <= ShortPressThresold && 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;
}
'UE 5 스터디 > Gameplay Ability System(GAS)' 카테고리의 다른 글
8-10. 투사체 - (1) 투사체 생성, 투사체 움직임 컴포넌트(Projectile Movement Component) (0) | 2024.12.05 |
---|---|
8-9. 코드 리펙토링 - 메세지 팝업 디버그, RPC (0) | 2024.12.05 |
8-7. 입력 - (4) 입력에 따른 어빌리티 활성화 (0) | 2024.12.03 |
8-6. 입력 - (3) 입력 버튼과 버튼 상태에 따른 함수 및 태그 바인딩 (0) | 2024.12.03 |
8-5. 입력 - (2) 액션에 어빌리티 연결 / 커스텀 입력 컴포넌트(Aura Input Component) (0) | 2024.12.03 |