참고: https://www.vitorcantao.com/post/climbing-system/ - 젤다 벽타기 제작
Recreating the Climbing System from Zelda BOTW in Unreal (C++)
Introduction The Legend of Zelda: Breath of the Wild needs no introduction. It’s a game that redefined the modern open-world formula. It’s all about exploration, discovery, and freedom. As such, movement plays a fundamental role in making the game loop
www.vitorcantao.com
요약도식:
구현 코드 위치: UMainCharacterMovementComponent, UMainCharacterAnimInstance, AMainCharacter, AMainCharacterPlayerController 네 개의 클래스에 Climbing관련 구현 코드가 모두 존재
1. CharacterMovementComponent Setup
Default CharacterMovementComponent를 상속하여 Climbing 시스템을 넣을 CharacterMovmentComponent를 생성한다.
* 완성된 클래스를 기준으로 스크린샷을 찍어 이미 함수들이 존재하는 상태이나 UCharacterMovementComponent를 상속받아서 생성했다는 점을 확인하기 위한 스크린샷
* Character Class생성자에 CustomCharacterMovementComponent인 UMainCharacterMovementComponent를 Default CharacterMovementComponent를 대체하여 생성함
- CharacterMovementComponent에는 7가지 기본 제공되는 모드가 있다. 걷기, 수영, 낙하 등 및 Custom을 포함한다. 걸을 때는 걷기 모드, 떨어질때는 낙하 모드 등을 이용하고 Climbing은 기본제공되는 모드가 없으므로 Custom을 이용해 제작하여 Climbing때 사용한다. Custom은 개발자가 원하는 모드를 제작하고자 할때 사용가능하도록 제공하는 모드.
- 주요 함수의 실행 다이어그램
- PerformMovement : 물리적인 충격, 힘 및 중력같은 외부 물리를 처리하고 애니메이션 루트모션 움직임을 계산하는 클래스. 네트워크에 연결되지 않은 게임에서는 매틱마다 호출되는 함수.
- StartNewPhysics : 현재 이동모드에 따라서 Phys함수를 선택. 이동모드는 SetMovementMode를 통해 enum값으로 제어됨. 예를 들어 현재 걷기 모드인 경우 StartNewPhysics는 걷기에 알맞은 물리학을 처리하는 PhysWalking을 호출.
- PhysCustom : 가상함수로써 개발자가 구현을 작성할 수 있다. Phys함수 내부에서 SetMovementMode를 통해 모드를 변경하고 StartNewPhysics를 호출하여 즉시 다른 물리학으로 변경하는 것이 가능하다.
즉시 물리학 변동이 필요한 경우, 예를 들어 Climbing 중 갑작스레 떨어지는 경우 Falling으로 변경
SetMovementMode(EMovementMode::MOVE_Custom, ECustomMovementMode::ECMOVE_Climbing);
위의 코드와 같은 형태로 기본적으로 제공되는 EMovementMode::MOVE_Custom의 enum값을 통해 PhysCutom으로 변경하고 여러 종류의 모드를 구현할 경우를 위하여 SetMovementMode의 두번째 파라미터 값에 개발자가 정의한 CustomEnum값을 받을 수 있도록 되어있다. 따라서 개발자가 임의로 정의한 CustomEnum값을 통해 여러종류의 PhysCustom을 제작하여 제어할 수 있다.
- 케릭터 이동 계산이 끝나면 OnMovementUpdated 함수가 호출된다. 플레이어가 등반을 원할 경우 여기서 SetMovementMode를 호출하여 모드를 변경할 수 있다. SetMovementMode 함수가 호출되면 OnMovementModeChanged함수가 트리거된다.
2. The Climbing
- 표면 탐색
라인트레이스를 이용할 경우 탐색에 정확도가 떨어지고 사각지대가 발생하므로 모든 충돌을 균일하게 처리가능한 Shape Sweep을 이용한다. Character Capsule Collision보다 조금 더 큰 모양을 사용하여 조금 더 가장자리에 대한 정확한 검사를 진행한다. 충돌검사를 진행할 함수도 SweepAndStoreWallHits를 선언하고 정의한다.
충돌 검사는 상시로 진행되어야 하므로 TickComponent에서 호출하도록한다.
* 자기자신은 충돌검사에서 제외하기 위해서 멤버변수로 선언된 FCollisionQueryParams ClimbQueryParams에 자기자신을 삽입한다.
* TickComponent에서 SweepAndSotrWallHits를 호출하여 상시로 충돌검사를 진행할 수 있도록 한다.
- const FCollisionShape CollisionShape = FCollisionShape::MakeCapsule(CollisionCapsuleRadius, CollisionCapsuleHalfHeight);
CollisionCapsuleRadius와 CollisionCapsuleHalfHeight 멤버변수로 선언된 값을 이용해 MakeCapsule를 통하여 캡슐을 생성한다. 두 값은 임의로 정해줄 수 있다. 지금은 케릭터의 CapsuleComponent보다 큰 값으로 임의로 설정하였다.
- const FVector StartOffset = UpdatedComponent->GetForwardVector() * 20;
const FVector Start = UpdatedComponent->GetComponentLocation() + StartOffset;
const FVector End = Start + UpdatedComponent->GetForwardVector();
케릭터 위치에서의 단일 Capsule에 의한 검사면 충분하므로 Start와 End가 다를필요가 없어 같은 위치에서의 검사를 진행한다. 이때, 지형에대한 검사가 무시되는 버그가 발생할 수 있으므로 StartOffset을 취해준다.
- TArray<FHitResult> Hits;
const bool HitWall = GetWorld()->SweepMultiByChannel(Hits, Start, End, FQuat::Identity, ECC_WorldStatic, CollisionShape, ClimbQueryParams);
SweepMultiByChannel함수를 통해 여러 충돌을 감지할 수 있도록하고 검사를 위해 생성한 Capsule에 케릭터의 회전을 적용할 필요는 없으므로 회전값에 FQuat::Identity를 사용한다.
- Entering Climbing Mode
케릭터의 Forward Vector의 좌우 25도 안쪽으로 벽의 Normal Vector가 들어올 때 Climbing 가능하도록 체크한다. 이때 벽의 Normal과 Forward Vector의 단순 내적을 통한 판별을 진행할 경우 벽의 기울기를 제대로 알기 어려워진다. 따라서 Forward Vector의 좌우 25도 안쪽으로 들어오는 값인지에 대한 검사와 벽이 얼마만큼의 기울기를 가지고 있는지에 대한 검사를 분리한다.
이를 위해서 먼저 벽의 Normal값을, ‘UpVector(0, 0, 1)의 값을 Normal값으로 가지는 평면’에 투영한다. 그렇게 하면 벽의 Normal값 중 Z값이 0이 되어 Z값을 무시할 수 있게된다. 이 값과 케릭터의 Forward Vector와의 내적을 진행하여 둘 사이의 이루는 각을 구한다. 그리고 ‘UpVector(0, 0, 1)의 값을 Normal값으로 가지는 평면’에 투영된 벡터와 벽의 Normal 사이에 내적을 진행하면 벽의 기울기를 구할 수 있다. BOTW에서는 수직인 경우인 내적값이 0인 경우에만 등반이 불가능하지만 필요에 따라서는 구한 값을 통해 제약을 줄 수 있다.
* 케릭터의 Forward Vector와 좌우 25도의 범위
* 간단하게 확인가능한 벡터의 투영의 형태(한 축의 값이 0이 된다, 그림에서는 Y축)
- const FVector HorizontalNormal = Hit.Normal.GetSafeNormal2D();
벽의 Normal값을 UpVector(0, 0, 1)의 값을 Normal로 가지는 평면에 투영한 벡터.
- const float HorizontalDot = FVector::DotProduct(UpdatedComponent->GetForwardVector(), -HorizontalNormal);
케릭터의 Normal과 벽의 Normal이 이루는 각을 내적을 통해 구한다. -HorizontalNormal인 이유는 앞에서 구한 벽의 Normal값은 케릭터 Forward Vector와 마주보기 때문에 -1을 곱해주어 서로 같은 방향으로 바라보게 하는 것. -1을 곱해주지 않으면 BackSpace Culling과 같은 원리로 25도 안쪽에 벽면이 있는지는 전혀 판별할 수 없다.
- const float VerticalDot = FVector::DotProduct(Hit.Normal, HorizontalNormal);
사영된 벽의 Normal과 원래 Normal을 내적하여 둘 사이의 각을 구하는 것. 즉 기울기를 구하는 것
- const float HorizontalDegree = FMath::RadiansToDegrees(FMath::Acos(HorizontalDot));
케릭터 Forward Vector와 사영된 벽의 Normal을 통해 구한 값으로 이루는 각을 구한다.
- const bool bIsCeiling = FMath::IsNearlyZero(VerticalDot);
사영된 벽의 Normal과 벽의 Normal의 내적값을 통해 올라가려는 곳이 천장인지 판별하는 것. 0이라면 천장이기 때문
- 예외 처리
물체의 크기가 등반하기에 충분히 크지 않은 경우 등반을 할 수 없다. 이를 판별하기 위하여 눈의 위치에서 Line Trace를 수행하여 체크한다.
* 몸통보다 낮은 물체의 경우 등반을 할 수 없다
- 특별한 내용 없이 눈의 위치를 Start로 잡고 거기서 Forward Vecctor만큼 일정 거리만큼 떨어진 위치를 End로 잡아서 Line Trace를 진행한다.
이때 Line Trace의 끝지점인 End의 위치에 따라 검사의 정확도가 달라질 수 있다. Line Trace의 길이가 너무 작으면 경사진 표면을 감지하지 못할 수 있다. 반면에 너무 크면 잘못된 경우로 먼 거리에 있는 다른 표면을 잘못 감지할 수 있다.
따라서 벽의 경사도에 따라서 Line Trace의 End의 길이가 조정될 수 있도록 한다. 여기에 사용될 수 있는게 앞에서 계산했던 CanStartClimbing함수 내의 VerticalDot 값이다.
이 값은 내적값이므로 Cos그래프를 따르는데 벽의 경사도에 따라 값이 달라질 것.
대부분의 경우 벽면이 EyeHeightTrace를 진행할 수 있는 위치에 있다면 Cos그래프의 -90 ~ 90 사이 값을 취할 것이다.
* BaseLength는 말그대로 기본적인 Line Trace의 거리이며 여기에 SteepnessMultiplier의 크기에 따라 거리값이 달라질 것인데 공식을 보면 1 + (1 - SurfaceVerticalDot) * 5이다 곱하는 5는 큰 의미를 부여할 것 없이 적절한 수치를 주었다고 보면 되는 것이고 SurfaceVerticalDot의 값에 앞서 설명한 Cos(-90) ~ Cos(90)의 값이 들어간다고 보면 될 것. 그렇게 구한 길이값을 통해 EyeHeightTrace를 진행한다.
아래의 그림에는 뒤로 눕는 경우만 나타나 있는데 앞으로 눕는 경우에도 경사에 따라 길이가 달라진다. 그러나 이때는 cos(-90) ~ cos(0)의 값을 가지나 cos(0) ~ cos(90)과 대칭을 이루는 값을 가지므로 사잇각이 커짐에따라 길이가 cos(0) ~ cos(90)과는 다르게 길이가 짧아져야 할 것. 하지만 여기에는 적용되어 있지 않다. 따라서 앞으로 눕는 경우에도 사잇각이 커진다면 Line Trace가 길어진다.
* 물체의 기울기에 따라 Line Trace의 길이가 달라진 모습
'Unreal Engine' 카테고리의 다른 글
Outline Shader - 2 (0) | 2023.09.29 |
---|---|
Outline Shader - 1 (1) | 2023.09.29 |
GAS(Gameplay Ability System) Documentation (0) | 2023.09.28 |
BOTW Climbing시스템(젤다의 전설 벽타기) -3 (0) | 2023.09.28 |
BOTW Climbing시스템(젤다의 전설 벽타기) -2 (1) | 2023.09.28 |