NavigationSytstem은 단어 그대로 내비게이션 시스템을 사용하기 위해 필요하고 AIModule은 AIController를 사용하기 위해서 필요하다. GameplayTasks는 이번시간은 아니지만 차후 Behavior Tree를 사용할 때 필요하므로 미리 넣어준다.
// MyAIController.h
UCLASS()
class TESTUNREALENGINE_API AMyAIController : public AAIController
{
GENERATED_BODY()
public:
AMyAIController();
virtual void OnPossess(APawn* InPawn) override;
virtual void OnUnPossess() override;
private:
void RandomMove();
private:
// 타이머 매니저에 콜백을 등록하기 위해 필요한 핸들
FTimerHandle TimerHandle;
};
// MyAIController.cpp
#include "NavigationSystem.h"
#include "Blueprint/AIBlueprintHelperLibrary.h"
AMyAIController::AMyAIController()
{
}
void AMyAIController::OnPossess(APawn* InPawn)
{
// 빙의 시
Super::OnPossess(InPawn);
// 월드의 타이머 매니저에 콜백을 등록한다
GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &AMyAIController::RandomMove, 3.f, true);
}
void AMyAIController::OnUnPossess()
{
// 빙의 해제 시
Super::OnUnPossess();
// 등록된 콜백을 해제한다
GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
}
void AMyAIController::RandomMove()
{
auto CurrentPawn = GetPawn();
// 월드의 내비게이션 시스템을 가져온다
UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());
if (NavSystem == nullptr)
return;
FNavLocation RandomLocation;
// 월드의 내비게이션 범위중 원점(첫번째 매개변수)을 기준으로 반지름 만큼의 범위를 한정지어
// 랜덤한 좌표를 가져온다(RandomLocation에 저장됨)
if (NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, 500.f, RandomLocation))
{
UAIBlueprintHelperLibrary::SimpleMoveToLocation(this, RandomLocation);
}
}
OnPossess와 OnUnPossess는 빙의 시, 빙의 해제 시 실행되는 함수이다. OnOverlapBegin/End같은 트리거 이벤트라고 보면 될 듯 하다.
빙의시 타이머 매니저에 의해 콜백이 등록되고 해제되는데 이와 같은 패턴이 서버를 만들때도 비슷하게 만들어진다.
노티파이가 호출할 함수이다. bAttacking을 false로 만들어주면 다시 이동이 가능해질 것이다.
노티파이를 설정했다면 이벤트그래프에서 표시가 될 것이다.
AnimNotify_ 라는 접두어가 붙은 이벤트가 새로 생성된다. 애니메이션 재생 도중에 노티파이가 설정된 구간을 지나면 이벤트를 호출하는 개념이다.
이제 공격 도중 움직일 수 없고 공격 모션이 끝나면 다시 이동할 수 있게 잘 수정되었다.
하지만 문제가 한가지 또 있다. 좌클릭을 연속으로 입력하거나 입력을 유지하고 있으면 공격 모션이 계속 초기화 되어 재생되는 것이다.
// Main.cpp
void AMain::Attack()
{
// 1회성에 한해 공격하도록 함
if (!bAttacking)
{
bAttacking = true;
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance && CombatMontage)
{
// 몽타주 재생. 1.35배만큼 빠르게 재생
AnimInstance->Montage_Play(CombatMontage, 1.35f);
// 섹션으로 건너 뜀
AnimInstance->Montage_JumpToSection(FName("Attack_1"), CombatMontage);
}
}
}
void AMain::AttackEnd()
{
bAttacking = false;
// 공격이 끝났음에도 마우스를 계속 누르고 있다면 다시 공격 실행
if (bLMBDown)
{
Attack();
}
}
이것 또한 간단하게 수정이 가능하다. Attack의 구현부를 bAttacking으로 모두 감싸면 한번 실행될 때 true가 되므로 이후로는 실행이 되지 않는다. 추가로 마우스를 계속 누르고 있을 때 공격모션이 계속 이루어지는것이 편리하므로 해당 부분도 겸사겸사 간단하게 처리한다.
[65. Anim Montages (Attack!) #3]
공격 모션을 추가하고 공격시 매번 같은 모션이 나오는것이 아닌, 무작위로 재생하게끔 하면 게임이 단조롭지 않을 것이다.
void AMain::Attack()
{
if (!bAttacking)
{
bAttacking = true;
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance && CombatMontage)
{
// 0-1 사이의 난수 발생
int32 Section = FMath::RandRange(0, 1);
switch (Section)
{
case 0:
AnimInstance->Montage_Play(CombatMontage, 2.2f);
AnimInstance->Montage_JumpToSection(FName("Attack_1"), CombatMontage);
break;
case 1:
AnimInstance->Montage_Play(CombatMontage, 1.8f);
AnimInstance->Montage_JumpToSection(FName("Attack_2"), CombatMontage);
break;
default:
;
}
}
}
}
몽타주 섹션에서 이어진 프리뷰까지 끊어주면 공격 모션이 랜덤하게 재생된다.
[정리]
애니메이션 몽타주는 여러개의 애니메이션을 엮어서 만들 수 있다. 섹션으로 나누어서 특정 섹션만 반복하거나 섹션끼리 연달아서 재생할 수도 있다.