https://youtu.be/_G0_9tUjaRs?si=eu1vj4mJ_4h7ipQr
캐릭터의 앞에 일정 높이 이하의 장애물이 있으면, 장애물을 뛰어넘는 기능을 구현했습니다.
기능 설명
- 캐릭터의 앞에 뛰어 넘을 수 있는 장애물이 있는지 Trace를 사용하여 탐색합니다.
- 뛰어 넘을 수 있는 장애물이 있을 경우 장애물의 깊이를 Trace를 사용하여 계산합니다.
- 장애물의 끝에서 일정 거리 떨어진 부분으로 애니메이션을 재생하며 캐릭터를 이동시킵니다.
구현
장애물 탐색
캐릭터의 위치를 TraceStart로 설정하고 TraceStart에서 캐릭터의 전방으로 VaultableObjectTraceDistance만큼 떨어진 거리를 TraceEnd로 설정하여 Trace를 실행합니다. 이 때 Trace의 Hit가 발생할 경우, Trace에 Hit된 오브젝트의 깊이를 계산하는 CalculateVaultableObjectDepth 함수를 실행합니다.
Trace의 Hit가 발생하지 않았을 경우, 이전 TraceStart에서 VaultableObjectInterval만큼 새로운 TraceStart를 설정하여 Trace를 실행합니다. 이 과정은 최대 VaultableObjectTraceCount만큼 반복됩니다. VaultableObjectTraceCount만큼 Trace를 실행하여 Hit가 발생하지 않았을 경우, 장애물을 뛰어넘지 않습니다.
코드
.h
public:
/** Vault를 실행하는 함수입니다. */
UFUNCTION(BlueprintCallable, Category = "Vaulting")
void ExecuteVault();
protected:
/** 장애물을 뛰어넘을 때 MotionWarp를 실행할 수 있는지 나타내는 변수입니다. */
UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "Vaulting")
bool bCanVaultWarp;
/** 뛰어넘을 수 있는 오브젝트를 탐색하는 Trace의 횟수입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Vaulting")
int32 VaultableObjectTraceCount;
/** 뛰어넘을 수 있는 오브젝트를 탐색하는 Trace간의 사이의 거리입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Vaulting")
float VaultableObjectTraceInterval;
/** 뛰어넘을 수 있는 오브젝트와의 거리입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Vaulting")
float VaultableObjectDistance;
/** 뛰어넘을 수 있는 오브젝트의 탐색에 실행하는 SphereTrace의 반지름입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Vaulting")
float VaultableObjectTraceRadius;
.cpp
void APRPlayerCharacter::ExecuteVault()
{
// 뛰어넘을 오브젝트를 탐색합니다.
for(int Index = 0; Index < VaultableObjectTraceCount; Index++)
{
FHitResult HitResult;
const FVector TraceStart = GetActorLocation() + FVector(0.0f, 0.0f, Index * VaultableObjectTraceInterval);
const FVector TraceEnd = TraceStart + (GetActorForwardVector() * VaultableObjectDistance);
TArray<AActor*> ActorsToIgnore;
ActorsToIgnore.Add(this);
// 디버그 옵션을 설정합니다.
EDrawDebugTrace::Type DebugType = EDrawDebugTrace::None;
if(bVaultDebug)
{
DebugType = EDrawDebugTrace::ForDuration;
}
// 캐릭터가 뛰어넘을 수 있는 거리 안에 오브젝트가 존재하는 Trace를 실행합니다.
bool bIsHit = UKismetSystemLibrary::SphereTraceSingle(GetWorld(), TraceStart, TraceEnd, VaultableObjectTraceRadius, UEngineTypes::ConvertToTraceType(ECC_Visibility),
false, ActorsToIgnore, DebugType, HitResult, true);
if(bIsHit)
{
if(bVaultDebug)
{
DrawDebugSphere(GetWorld(), HitResult.ImpactPoint, 10.0f, 12, FColor::Blue, false, 5.0f);
}
CalculateVaultableObjectDepth(HitResult.ImpactPoint);
// 뛰어넘을 오브젝트를 탐색했을 경우 탐색을 마칩니다.
break;
}
}
}
장애물 깊이 탐색
Trace로 장애물을 뛰어넘기 시작하는 위치(VaultingStartLocation)와 장애물을 뛰어넘은 위치(VaultingLocation)을 구합니다. 반복문으로 DepthTraceCount만큼 Trace를 실행합니다. Trace를 실행할 때마다 DepthTraceInterval만큼 캐릭터의 전방으로 TraceStart를 갱신하며 오브젝트의 깊이를 탐색합니다.
DepthTraceCount만큼 Trace를 실행하여 마지막 Trace까지 충돌이 발생했을 경우, 장애물이 뛰어넘을 거리보다 넓으므로 Vault를 실행하지 않습니다. 반대로 충돌이 발생하지 않았을 경우, 장애물을 넘어서 탐색한 것이므로 착지할 위치를 계산합니다.
코드
.h
public:
/** Vault를 비활성화하는 함수입니다. */
UFUNCTION(BlueprintCallable, Category = "Vaulting")
void DisableVaultWarp();
protected:
/** Trace의 충돌한 지점을 기준으로 뛰어넘을 수 있는 오브젝트의 치수를 계산하는 함수입니다. */
void CalculateVaultableObjectDepth(FVector TraceImpactPoint);
protected:
/** 장애물을 뛰어넘을 때 MotionWarp를 실행할 수 있는지 나타내는 변수입니다. */
UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "Vaulting")
bool bCanVaultWarp;
/** 장애물을 뛰어넘기 시작하는 위치입니다. */
UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "Vaulting")
FVector VaultStartLocation;
/** 장애물을 뛰어넘는 위치입니다. */
UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "Vaulting")
FVector VaultingLocation;
/** 뛰어넘을 수 있는 오브젝트의 치수를 계산하는 Trace의 횟수입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Vaulting|DepthTrace")
int32 DepthTraceCount;
/** 뛰어넘을 수 있는 오브젝트의 치수를 계산하는 Trace간의 사이의 거리입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Vaulting|DepthTrace")
float DepthTraceInterval;
/** 뛰어넘을 수 있는 오브젝트의 치수를 계산하는 위를 향한 Trace의 Offset입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Vaulting|DepthTrace")
float DepthTraceUpOffset;
/** 뛰어넘을 수 있는 오브젝트의 치수를 계산하는 아래를 향한 Trace의 Offset입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Vaulting|DepthTrace")
float DepthTraceDownOffset;
.cpp
void APRPlayerCharacter::DisableVaultWarp()
{
bCanVaultWarp = false;
// ExecuteVaultMotionWarp 함수의 bInZOffset 변수에 영향을 줘서
// 장애물을 뛰어넘을 수 없을 경우에 생기는 버그를 방지하기 위해서 초기화합니다.
VaultLandLocation = FVector(0.0f, 0.0f, 20000.0f);
}
void APRPlayerCharacter::CalculateVaultableObjectDepth(FVector TraceImpactPoint)
{
// 뛰어넘을 오브젝트의 치수를 계산합니다.
for(int Index = 0; Index < DepthTraceCount; Index++)
{
FHitResult HitResult;
const FVector TraceStart = TraceImpactPoint
+ FVector(0.0f, 0.0f, DepthTraceUpOffset)
+ (GetActorForwardVector() * Index * DepthTraceInterval);
const FVector TraceEnd = TraceStart - FVector(0.0f, 0.0f, DepthTraceDownOffset);
TArray<AActor*> ActorsToIgnore;
ActorsToIgnore.Add(this);
// 디버그 옵션을 설정합니다.
EDrawDebugTrace::Type DebugType = EDrawDebugTrace::None;
if(bVaultDebug)
{
DebugType = EDrawDebugTrace::ForDuration;
}
// 뛰어넘을 장애물과 캐릭터 사이의 거리를 측정합니다.
bool bIsHit = UKismetSystemLibrary::SphereTraceSingle(GetWorld(), TraceStart, TraceEnd, VaultableObjectTraceRadius, UEngineTypes::ConvertToTraceType(ECC_Visibility),
false, ActorsToIgnore, DebugType, HitResult, true);
if(bIsHit)
{
// HitResult.bStartPenetrating: Break Hit Result 블루프린트 노드에서 Initial Overlap 변수로 나타냅니다.
// Trace 시작 시 충돌이 발생했는지 여부를 나타내는 변수입니다.
if(HitResult.bStartPenetrating)
{
// Trace를 시작할 때 Trace 원점에서 충돌이 발생했을(Trace가 충돌로 시작됐을) 경우 Vault를 비활성화합니다.
DisableVaultWarp();
break;
}
else
{
// 점차 거리를 늘려가면서 탐색합니다.
// 첫 번째 탐색일 경우
if(Index == 0)
{
VaultStartLocation = HitResult.ImpactPoint;
if(bVaultDebug)
{
DrawDebugSphere(GetWorld(), VaultStartLocation, 15.0f, 12, FColor::White, false, 5.0f);
}
}
VaultingLocation = HitResult.ImpactPoint;
if(bVaultDebug)
{
DrawDebugSphere(GetWorld(), VaultingLocation, 10.0f, 12, FColor::Yellow, false, 5.0f);
}
bCanVaultWarp = true;
}
}
else
{
// 탐색을 마칠 경우 장애물을 넘어서 탐색한 것이므로 착지할 위치를 계산합니다.
CalculateVaultLandLocation(HitResult.TraceStart);
break;
}
}
if(bVaultDebug && VaultingLocation != FVector::ZeroVector)
{
DrawDebugSphere(GetWorld(), VaultingLocation, 15.0f, 12, FColor::Purple, false, 5.0f);
}
ExecuteVaultMotionWarp();
}
착지 위치 계산
오브젝트의 깊이를 탐색하는 Trace의 마지막 위치에서 캐릭터의 전방으로 VaultLandDistance만큼 떨어진 위치를 Trace 시작점으로 하여 VaultLanddownOffset만큼 아래를 향해 Trace를 실행하여 Hit가 발생한 위치를 착지 위치(VaultLandLocation)으로 설정합니다.
코드
.h
protected:
/** Trace의 마지막 위치를 기준으로 캐릭터가 착지할 위치를 계산하는 함수입니다. */
void CalculateVaultLandLocation(FVector TraceEndLocation);
protected:
/** 장애물을 뛰어넘고 착지하는 위치입니다. */
UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "Vaulting")
FVector VaultLandLocation;
/** 오브젝트를 뛰어넘고 착지할 거리입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Vaulting|Landing")
float VaultLandDistance;
/** 오브젝트를 뛰어넘고 착지할 위치를 계산하는 아래를 향한 Trace의 Offset입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Vaulting|Landing")
float VaultLandDownOffset;
.cpp
void APRPlayerCharacter::CalculateVaultLandLocation(FVector TraceEndLocation)
{
FHitResult HitResult;
const FVector TraceStart = TraceEndLocation + (GetActorForwardVector() * VaultLandDistance);
const FVector TraceEnd = TraceStart - FVector(0.0f, 0.0f, VaultLandDownOffset);
TArray<AActor*> ActorsToIgnore;
ActorsToIgnore.Add(this);
// 디버그 옵션을 설정합니다.
EDrawDebugTrace::Type DebugType = EDrawDebugTrace::None;
if(bVaultDebug)
{
DebugType = EDrawDebugTrace::ForDuration;
}
bool bIsLandHit = UKismetSystemLibrary::LineTraceSingle(GetWorld(), TraceStart, TraceEnd, UEngineTypes::ConvertToTraceType(ECC_Visibility),
true, ActorsToIgnore, DebugType, HitResult, true);
if(bIsLandHit)
{
VaultLandLocation = HitResult.ImpactPoint;
if(bVaultDebug)
{
DrawDebugSphere(GetWorld(), VaultLandLocation, 10.0f, 12, FColor::Cyan, false, 5.0f);
}
}
}
Motionwarping 설정
Vault를 실행할 수 있고, 캐릭터의 Mesh의 Z축을 기준으로 VaultLandLocationZOffset의 범위 안에 착지 지점(VaultLandLocation)의 Z축이 존재할 경우, Motionwarping을 설정한 후 장애물을 뛰어넘는 AnimMontage를 재생합니다. AnimMontage의 MotionWarping 노티파이 스테이트는 다음과 같이 3개가 필요합니다.
각 MotionWarping 노티파이 스테이트의 Warp Target Name을 각각 VaultStart, Vaulting, VaultLand로 설정한 후 Ignore ZAxis를 false로 설정합니다.
이후 코드에서 FMotionWarpingTarget의 구조체의 Name이 MotionWarping 노티파이 스테이트의 Warp Target Name와 일치해야합니다.
AnimMontage를 재생할 때 속도를 유지하고 장애물을 뛰어넘기 위해서 노티파이 스테이트 클래스를 생성하여 NotifyBegin 함수에서 캐릭터 무브먼트의 MovementMode를 Flying으로 설정하고 콜리전을 비활성화하고 착지할 때 NotifyEnd 함수에서 MovementMode를 Falling으로 설정하고 콜리전을 활성화하도록 구현합니다.
코드
.h
public:
/** Vault를 비활성화하는 함수입니다. */
UFUNCTION(BlueprintCallable, Category = "Vaulting")
void DisableVaultWarp();
/** 장애물을 뛰어넘는 상태를 설정하는 함수입니다. */
void SetVaultState();
/** 장애물을 뛰어넘은 후 상태를 초기화하는 함수입니다. */
void ResetVaultState();
protected:
/** 장애물을 뛰어넘는 AnimMontage의 MotionWarp를 실행하는 함수입니다. */
void ExecuteVaultMotionWarp();
/** 장애물을 뛰어넘는 AnimMontage가 종료될 때 호출하는 함수합니다. */
UFUNCTION()
void OnVaultAnimMontageEnded(UAnimMontage* NewVaultAnimMontage, bool bInterrupted);
protected:
/** 장애물을 뛰어넘는 AnimMontage입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Vaulting")
TObjectPtr<UAnimMontage> VaultAnimMontage;
/** 장애물을 뛰어넘기 시작할 때 사용하는 MotionWarpingTarget의 이름입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Vaulting")
FName VaultStartName;
/** 장애물을 뛰어넘을 때 사용하는 MotionWarpingTarget의 이름입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Vaulting")
FName VaultingName;
/** 장애물을 뛰어넘고 착지할 때 사용하는 MotionWarpingTarget의 이름입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Vaulting")
FName VaultLandName;
/** 오브젝트를 뒤어넘고 착지하는 위치의 높이를 제한하는 Offset입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Vaulting|Landing")
float VaultLandLocationZOffset;
.cpp
void APRPlayerCharacter::DisableVaultWarp()
{
bCanVaultWarp = false;
// ExecuteVaultMotionWarp 함수의 bInZOffset 변수에 영향을 줘서
// 장애물을 뛰어넘을 수 없을 경우에 생기는 버그를 방지하기 위해서 초기화합니다.
VaultLandLocation = FVector(0.0f, 0.0f, 20000.0f);
}
void APRPlayerCharacter::SetVaultState()
{
GetCharacterMovement()->SetMovementMode(MOVE_Flying);
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
void APRPlayerCharacter::ResetVaultState()
{
GetCharacterMovement()->SetMovementMode(MOVE_Falling);
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
DisableVaultWarp();
}
void APRPlayerCharacter::ExecuteVaultMotionWarp()
{
bool bInZOffset = UKismetMathLibrary::InRange_FloatFloat(VaultLandLocation.Z,
GetMesh()->GetComponentLocation().Z - VaultLandLocationZOffset,
GetMesh()->GetComponentLocation().Z + VaultLandLocationZOffset);
if(bCanVaultWarp && bInZOffset)
{
if(GetMotionWarping())
{
// VaultStart
FMotionWarpingTarget VaultStartMotionWarpingTarget = FMotionWarpingTarget();
VaultStartMotionWarpingTarget.Name = VaultStartName;
VaultStartMotionWarpingTarget.Location = VaultStartLocation;
VaultStartMotionWarpingTarget.Rotation = GetActorRotation();
GetMotionWarping()->AddOrUpdateWarpTarget(VaultStartMotionWarpingTarget);
// Vaulting
FMotionWarpingTarget VaultingMotionWarpingTarget = FMotionWarpingTarget();
VaultingMotionWarpingTarget.Name = VaultingName;
VaultingMotionWarpingTarget.Location = VaultingLocation;
VaultingMotionWarpingTarget.Rotation = GetActorRotation();
GetMotionWarping()->AddOrUpdateWarpTarget(VaultingMotionWarpingTarget);
// VaultLand
FMotionWarpingTarget VaultLandMotionWarpingTarget = FMotionWarpingTarget();
VaultLandMotionWarpingTarget.Name = VaultLandName;
VaultLandMotionWarpingTarget.Location = VaultLandLocation;
VaultLandMotionWarpingTarget.Rotation = GetActorRotation();
GetMotionWarping()->AddOrUpdateWarpTarget(VaultLandMotionWarpingTarget);
PlayAnimMontage(VaultAnimMontage, 1.2f);
GetMesh()->GetAnimInstance()->OnMontageBlendingOut.AddDynamic(this, &APRPlayerCharacter::OnVaultAnimMontageEnded);
}
}
}
void APRPlayerCharacter::OnVaultAnimMontageEnded(UAnimMontage* NewVaultAnimMontage, bool bInterrupted)
{
if(VaultAnimMontage == NewVaultAnimMontage)
{
DisableVaultWarp();
}
GetMesh()->GetAnimInstance()->OnMontageBlendingOut.RemoveDynamic(this, &APRPlayerCharacter::OnVaultAnimMontageEnded);
}
ANS_PRVault.cpp
void UANS_PRVault::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration, const FAnimNotifyEventReference& EventReference)
{
Super::NotifyBegin(MeshComp, Animation, TotalDuration, EventReference);
if(MeshComp)
{
APRPlayerCharacter* PRPlayer = Cast<APRPlayerCharacter>(MeshComp->GetOwner());
if(IsValid(PRPlayer))
{
PRPlayer->SetVaultState();
}
}
}
void UANS_PRVault::NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
Super::NotifyEnd(MeshComp, Animation, EventReference);
if(MeshComp)
{
APRPlayerCharacter* PRPlayer = Cast<APRPlayerCharacter>(MeshComp->GetOwner());
if(IsValid(PRPlayer))
{
PRPlayer->ResetVaultState();
}
}
}
캐릭터가 이동할 때 장애물이 있을 경우 바로 장애물을 넘어가기
캐릭터에 CapsuleCollision을 추가하여 OnComponentBeginOverlap 델리게이트에 Vault를 실행하는 함수를 바인딩하여 사용하면 됩니다. 이 때 뛰어넘을 장애물의 오버랩 이벤트 생성이 true로 설정되어야 Overlap이 실행됩니다.
코드
.h
private:
/** VaultCollision의 OnComponentBeginOverlap 델리게이트에 바인딩하는 함수입니다. */
UFUNCTION()
void OnVaultCollisionBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
private:
/** 장애물을 뛰어넘을 수 있는지 탐색하는 CapsuleComponent 클래스입니다. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Vaulting", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UCapsuleComponent> VaultCollision;
.cpp
APRPlayerCharacter::APRPlayerCharacter()
{
...
// VaultCollision
VaultCollision = CreateDefaultSubobject<UCapsuleComponent>(TEXT("VaultCollision"));
VaultCollision->InitCapsuleSize(10.0f, 38.0f);
VaultCollision->SetRelativeLocation(FVector(30.0f, 0.0f, -30.0f));
VaultCollision->SetCollisionProfileName(TEXT("VaultCollision"));
VaultCollision->SetCollisionEnabled(ECollisionEnabled::NoCollision);
VaultCollision->SetupAttachment(RootComponent);
}
void APRPlayerCharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
// VaultCollision
VaultCollision->OnComponentBeginOverlap.AddDynamic(this, &APRPlayerCharacter::OnVaultCollisionBeginOverlap);
}
void APRPlayerCharacter::OnVaultCollisionBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
ExecuteVault();
}
'Project > Replica' 카테고리의 다른 글
[Project Replica] PoolableInterface / BaseObjectPoolSystem (0) | 2024.06.25 |
---|---|
[Project Replica] EffectSystem (0) | 2024.04.30 |
[Project Replica] DamageSystem 대미지 시스템 (0) | 2024.01.11 |
[Project Replica] AI 생성 시스템 AISpawnSystem (1) | 2023.12.04 |
[Project Replica] 극한회피 Extreme Dodge/TimeStopSystem (2) | 2023.10.26 |