[개요]

  • 플레이어 이동
  • 애니메이션
  • 사격
  • 체력과 죽음 처리
  • 비헤이비어 트리를 통한 AI구현
  • 승패 조건

 

[프로젝트 설정]

  • 빈 프로젝트를 생성한다. 프로젝트 이름 SimpleShooter
  • 다운받은 프로젝트를 버전에 맞춰서 포팅하고 안의 애셋들을 모두 이주시킨다

 

[비고]

  • 해당 섹션은 UE5가 아닌 여전히 UE4이지만 같은 강의에 있으므로 같은 카테고리에 둠
  • 상관없이 UE5로 진행하려 했으나 빠른 진도를 위해 4.26으로 진행

 

[158. Skeletal Animation 101]

 

언리얼에서의 스켈레톤은 스켈레탈 메시와 애니메이션이 결합된 형태이다.

 

[163. Inverse Transforming Vectors]

 

기본적으로 모든 오브젝트는 월드 공간에 상대적이다.

어떤 오브젝트가 회전했을 때 로컬 공간 입장에서 속도의 방향은 그대로겠지만 월드 공간에서 바라봤을 때는 방향이 바뀐다.

 

이때 속도의 방향을 해당 오브젝트의 트랜스폼으로 역변환을 하면 속도의 방향을 바라보는 관점이 월드 공간에서 로컬 공간으로 바뀐다.

 

역변환이 아닌 Transform Direction을 통해 반환받은 Yaw값과 속도를 회전값으로 분리해서 반환받은 Yaw값은 동일하기 때문에 기본적으로 월드좌표에 상대적임을 확인할 수 있다.

 

그래서 캐릭터(카메라) 회전과 관련없이 속도의 방향만을 통해서 회전값을 구하고 각을 구해서 애니메이션을 블렌딩 할 수 있다.

 

덧붙여서 위의 노드들을 C++ 코드로 작성하면 아래와 같다

 

/* KismetLibrary로 제공되는 함수들 */
KISMET_MATH_INLINE
FVector UKismetMathLibrary::InverseTransformDirection(const FTransform& T, FVector Direction)
{
    return T.InverseTransformVectorNoScale(Direction);
}

KISMET_MATH_FORCEINLINE
FRotator UKismetMathLibrary::MakeRotFromX(const FVector& X)
{
    return FRotationMatrix::MakeFromX(X).Rotator();
}

/* */

float AShooterCharacter::GetDirection()
{
    FVector InverseTransformDirection = GetActorTransform().InverseTransformVectorNoScale(GetVelocity());
    return FRotationMatrix::MakeFromX(InversTransformDirection).Rotator().Yaw;
}
// 위와 같이 구현하면 KismetMathLibrary에 접근할 필요가 없다

 

참고 링크 : https://community.gamedev.tv/t/inverse-transform-direction-in-c/147893

정리하자면 바라보는 역변환 시 바라보는 관점이 달라진다는 것이다. 본질이 바뀌는것이 아니다.

 

[164. Calculating Animation Speeds]

 

걷기/뛰기 애니메이션에는 각각 적절한 속력를 적용시켜야 움직임이 어색하지 않을것이다.

예를들어 100의 속력에 맞춰서 제작된 걷기모션에 300의 속력으로 움직인다면 마치 허공답보를 하는듯한 어색함이 느껴질 것이다.

이 속력을 구하는 방법을 우선 알아봐야한다.

 

공식은 다음과 같다

foot_speed = (y_finish - y_start) / (t_finish - t_start)

 

발 뒤꿈치가 땅에 닿았을 때 해당 본의 y좌표와 시간, 그리고 발 뒤꿈치가 떨어질 때 해당 본의 y좌표와 시간이 필요하다. 아주 정확한 값을 사용하는게 아니라 반올림을 한 근사값을 사용할것이기 때문에 약간의 오차는 괜찮을것이다.

y축을 사용하는 이유는 해당 애니메이션의 기즈모가 y축이 forward로 되어있기 때문이다.

 

 

 

 

위의 사진은 달리기 모션이고 계산결과는 아래와 같다.

foot_speed = (-28.533447 - 16.474468) / (0.3 - 0.16)
           = -321.485

 

반올림시 대략 350이 나온다.

같은 방법으로 걷기 모션을 계산하면 반올림시 대략 150이 나온다.

현재 예제에서만 해당하는 내용이고 다른 애니메이션을 사용한다면 따로 계산해봐야 할 것이다.

 

50단위가 되기때문에 최대속력인 350에서 50을 나누면 7개의 그리드로 설정이 가능하다는 것을 알 수 있다.

 

 

 

블렌드 스페이스를 조금 더 세부적으로 만들게 되었다.

 

[169. Spawning Particle Effects]

 

SpawnEmitterAtLocation 말고도 SpawnEmitterAttached로 파티클을 재생시킬 수 있다.

 

[170. Player View Point]

 

void GetPlayerViewPoint(FVector out_Location, FRotator out_Rotation);

 

플레이어의 시점 정보를 레퍼런스에 담아서 준다.

AI라면 눈에 해당하는 시점, 사람이라면 카메라의 시점을 의미한다.

 

[171. Line Tracing By Channel]

 

플레이어 시점의 시작점(카메라 위치), 시작점+(방향벡터 * 길이)로 라인 트레이싱을 한다면 카메라의 정 중앙에 라인 트레이스롤 쏜다.

 

[181. Checking AI Line Of Sight]

 

bool LineOfSightTo(const AActor* Other, FVector ViewPoint, bool bAlternateChecks);

 

AIController를 소유한 액터의 ViewPoint에서 Other의 위치에 라인 트레이싱을 해서 충돌 검출시 true를 반환해준다.

 

 

[182. BehaviorTrees And Blackboards]

 

블랙보드는 AI의 기억과 비슷한 개념이다.

행동 트리는 AIController가 사용하거나 실행할 수 있는 것이다.

 

void RunBehaviorTree(UBehaviorTree* BTAsset);
// 행동 트리를 실행한다.

 

행동 트리는 런타임 도중에 실행 흐름을 확인할 수 있다.

작명 규칙은 보통 행동 트리는 BT_[이름], 블랙보드는 BB_[이름]으로 짓는다.

 

[183. C++에서 칠판 키 설정하기]

 

블랙보드는 <key, value> 형식으로 관리된다. key는 Entry Name, value는 실제 값이다.

C++에서 블랙보드에 접근하려면 GetBlackboardComponent() 함수를 사용해주면 된다.

 

GetBlackboardComponent()->SetValueAsVector(
    TEXT("PlayerLocation"), // BB에서 만든 key 이름
    PlayerPawn->GetActorLocation() // value
);

 

[185. BT Decorators And Selectors]

 

노드에 데코레이터를 붙여서 조건에 따라 실행 여부를 결정할 수 있다. 일종의 if이다.

 

위와 같은 경우는 Blackboard의 key인 PlayerLocation에 value가 설정되어있는지를 체크하는 부분이다.

설정 여부는 value의 존재 유무로 판단한다 (SetValue, ClearValue)

 

기본적으로 관찰자 중단이 None으로 설정되어 있다면 노드의 실행 도중에 조건이 바뀌어도 노드를 재시작하지 않는다.

하지만 그 외의 값으로 설정되어 있다면 노드의 실행을 중단하고 재시작한다.

 

Notify Observer

  • On Result Change: 조건이 변경될 때마다 재시작한다.
  • On Value Change: 관찰중인 블랙보드의 값이 변경될 때마다 재시작한다.

 

Observer Aborts

  • None: 아무것도 중단하지 않는다.
  • Self: 자신과 이 노드 아래 실행중인 서브트리도 중단한다.
  • Lower Priority: 오른쪽에 있는 모든 노드를 중단한다.
  • Both: Self + Lower Priority

 

[187. Executing BTTasks]

 

FName UBTTask_BlackboardBase::GetSelectedBlackboardKey();
// UBTTask_BlackboardBase에서 선택된 블랙보드 키를 가져온다.

 

 

ExecuteTask 함수를 오버라이딩해서 구현하면 된다.

 

키의 값을 추적해야 할 필요가 있는 경우에는 BTTask_BlackboardBase를 상속받아서 만들고 추적할 필요가 없는 경우에는 BTTaskNode를 상속받아서 만들면 된다.

 

[188. BTTasks That Use The Pawn]

 

Move To 노드의 디테일 패널에서 Observe Blackboard Value가 false라면 이동 목표값(Blackboard Key)이 변하더라도 해당 작업을 끝까지 수행한다. true라면 이동 목표값이 변하면 노드를 재실행한다.

데코레이터 노드의 Notify Observer와 비슷한 개념으로 보인다.

 

 

AAIController* UBehaviorTreeComponent::GetAIOwner();

 

ExecuteTask의 매개변수인 OwnerComp를 통해 블랙보드 뿐만 아니라 AIController에 접근할 수 있다.

이 컨트롤러로 빙의중인 Pawn을 받아오는것도 당연히 가능하다.

 

[189. BTServices In C++]

 

행동 트리의 노드가 실행되는동안 매 틱마다 실행된다.

 

[190. Ignoring Actors In Line Traces]

 

FCollisionParams으로 라인 트레이싱 등의 충돌 검사시 충돌 규칙을 정할 수 있다.

특정 액터나 컴포넌트와의 충돌을 무시할 수 있다.

 

FCollisionQueryParams Params;
Params.AddignoredActor(this);

 

 

DetachFromControllerPendingDestory();
// 컨트롤러에서 폰을 안전하게 분리한다.

 

AI는 컨트롤러가 없어지면 행동 트리도 실행이 중지된다.

 

[191. Ending The Game]

 

게임모드는 보통 서버에만 존재하기 때문에 UI나 재시작 등의 로직은 플레이어 컨트롤러에서 실행되어야 한다.

 

현재 프로젝트의 로직은 ShooterPawn이 게임모드의 함수를 호출하는 방식을 택하고 있다.

이게 맞는 방법인지는 아직 잘 모르겠다.

 

현재 호출은 이런식으로 이루어진다.

 

추가로 만약 UE5로 진행중이었다면 Standalone으로 실행해야 에러가 발생하지 않는다.

 

[193. Displaying A Lose Screen]

 

일반적으로 UI는 블루프린트로 작성하는게 훨씬 쉽다.

UI가 매우 복잡해지지 않는 이상 C++로 작성하는것이 크게 이점이 있지는 않다.

 

[194. Iterating Other Actors]

 

TActorRange<AController>(GetWorld());

 

월드 내에 존재하는 AController를 모두 가져온다.

GetAllActorsOfClass와 유사한 것으로 보인다.

 

[195. Calculating The Win Condition]

 

죽은 캐릭터가 플레이어 컨트롤러를 소유중인지, 맵에 AI 컨트롤러가 존재하지 않는지 등의 여부를 검사해서 게임 종료 조건을 설정할 수 있다.

 

[197. Weapon Sound Effects]

 

총구 화염 파티클을 총구에 붙였던것처럼 사운드도 특정 컴포넌트에 붙여서 재생시킬 수 있다.

 

[198. Randomized Sound Cues]

 

사운드 큐는 일종의 머티리얼과 비슷한 개념으로 보인다.

랜덤한 소리를 재생하거나 음향효과를 만드는 등 편집된 애셋이다.

 

 

[199. Sound Spatialization]

 

소리에 입체감을 주기 위해서는 감쇠(Attenuation)와 공간화(Spatialization)를 지속시키는 것이 매우 중요하다.

사운드 큐에 사운드 감쇠 애셋을 적용시켜서 입체감을 준다.

 

 

[202. Aim Offsets]

 

에임 오프셋은 블렌드 스페이스와 비슷하게 생겼다.

Yaw, Pitch 두 값을 이용해서 두 개의 애니메이션을 블렌드한다.

 

단순히 컨트롤러의 회전값으로만 pitch값을 설정하면 위를 바라보는것은 되지만 아래를 보는것은 문제가 생긴다.

 

언리얼에서 회전자는 기본적으로 음수로 내려가지 않고 0˚~360˚에서만 계속 반복된다. (최단경로 고려X)

그래서 0˚ 아래로 내리는 순간 360˚로 값이 바뀌기 때문에 총구가 갑자기 하늘을 향하게 된다.

수학적으로도 -90˚ = 270˚ 이기 때문에 틀린것은 아니다. 다만 최단경로를 고려할때는 -90˚를 꼭 사용해야 한다.

회전자는 정규화를 시키면 최단경로를 고려하여 값을 반환해주기 때문에 음수도 정상적으로 나오게 된다.

 

 

GetControlRotation().GetNormalize().Pitch;

 

원하는 값을 구하려면 컨트롤러의 회전자를 정규화 시켜서 pitch값을 사용하면 되지만 기본적으로 제공되는 블루프린트 함수는 단일 회전자를 정규화 시켜주는게 없다.

 

그래서 Delta를 사용하는데, 컨트롤러의 회전자(A)에 항상 pitch가 0인 액터의 회전자(B)를 빼서 정규화를 시키고 pitch값을 가져온다면 언제나 컨트롤러의 pitch값이 나오게된다.

 

 

AI의 행동 트리는 플레이어의 위치 벡터만 가지고 있기 때문에 고저차가 존재하는 곳에서는 회전값이 없어서 직선으로만 발사하므로 정상적인 전투를 하지 못한다.

Vector 대신 Object(Actor)로 변경하면 MoveTo에 의해 고저차에 의한 회전자값까지 적용되어서 플레이어를 정확히 조준하게 된다.

'언리얼 엔진 > UE5 C++' 카테고리의 다른 글

[UE5 C++] Toon Tanks (v2)  (0) 2022.10.04
[UE5 C++] Crypt Raider  (0) 2022.09.29
[UE5 C++] Obstacle Assalult  (0) 2022.09.28
[UE5 C++] Warehouse Wreckage  (0) 2022.09.28

+ Recent posts