● 서론
이번에는 태스크 노드를 이용하여 비헤이비어 트리에서 몬스터가 공격 기능을 수행할 수 있도록
구현해 보았습니다. 유독 버그가 많았고 수정할점을 찾는데 오래걸렸던것 같습니다.
구현에 대한 기본 설계
// 공격 관련
public:
virtual void SetAIAttackFinishedDelegate(const FAIAttackFinished& AIAttackFinished) = 0;
virtual void StartAIAttack() = 0;
AI Interface 클래스에 델리게이트 등록을 위한 함수와 공격 기능을 시작하기 위한 함수를 선언하여
몬스터 클래스에 구현을 하는것을 목표로 구현합니다.
델리게이트의 경우 태스크 노드의 마무리를 InProgress 상태로 끝낼것이기 때문에
몽타주의 재생이 다 끝났을 경우 델리게이트를 통해 Success로 반환시킬 예정입니다.
Start AI Attack의 경우 몬스터의 AnimInstance에 접근하여 Play Attack Montage 함수를 만들어
공격 몽타주를 실행시킬것 입니다.
AnimInstance->OnMontageEnded.AddDynamic(this, &ARPGEnemy::MontageEnded);
이후 다음과 같이 델리게이트를 구현하여 몽타주가 끝났을 때 MontageEnded를 호출하도록 하고
AttackFinished.ExecuteIfBound();
MontgeEnded 함수에 다음 코드 구현하여 몽타주가 종료되어서 Success를 반환시키려고 합니다.
다만 구현 과정에서 여러 문제가 발생하였습니다.
구현중 생긴 문제와 해결
1. 델리게이트의 바인딩 문제
FAIAttackFinished AIAttackFinished;
로 델리게이트의 객체를 선언하고
BindUFunction을 통해 공격이 끝났을 때 호출할 함수를 바인딩 하려고 했는데,
ExecuteTask함수의 OwnerComp 매개변수를 전달하는대에 어려움이 있었습니다.
그래서 방법을 찾던 중 람다 함수를 통하여 함수 내부에 함수를 호출하는 방법을 사용하기로 하였습니다.
FAIAttackFinished AIAttackFinished;
AIAttackFinished.BindLambda(
[&]()
{
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
UE_LOG(LogTemp, Log, TEXT("Lambda"));
}
);
람다 함수를 통해 내부에 FinishLatentTask 함수를 이용해서 OwnerComp에 Succeeded를 전달하도록 하였습니다.
이로 인해서 몬스터가 공격후에도 InProgress가 지속되어 공격상태가 계속 지속되는것을 Succeeded로 전환하여
해제될 수 있을 것 입니다.
성공적으로 람다 함수가 호출되었고
비헤이비어 트리에서 Attack 태스크 노드가 성공적으로 종료되는것을 확인 할 수 있습니다.
2. 몽타주 애니메이션의 재생문제
성공적으로 공격 성공 판정이 되었고 체력이 감소했음에도 불과하고 정작 애니메이션이
나타나지 않는 문제가 발견되었습니다.
각각의 함수에 로그를 찍어 확인했음에도 PlayMontage와 MontageEnded 모두 정상적으로
로그가 찍힌것을 보아 함수는 제대로 실행된다는 것을 확인할 수 있었습니다.
반나절 정도 고민했는데 애니메이션 블루프린트의 애님그래프에서
몽타주의 슬롯을 연결하지 않은 문제점을 발견하였습니다.
문제점을 알고 난 후 이걸로 고민한게 참 어이없기도 하고.. 그랬습니다..
그룹들을 Output Pose와 연결해주고 난 후 문제가 해결되었습니다.
3. 공격시 몬스터의 회전 불가능 문제
지금 몬스터의 공격 판정은 Attack In Range 데코레이터를 통해
캐릭터와의 거리가 공격 거리보다 짧은지를 판단하여 공격 여부를 결정하기 때문에
몬스터의 공격을 피해 몬스터의 뒷쪽으로 이동하면 거리상으로는 공격범위 안이라
몬스터는 공격하던 방향으로 계속 공격하는 문제가 발생하였습니다.
방향을 회전하는 기능을 Attack 노드 내부에 구현할까, 따로 노드를 생성할까 고민하다가
NPC가 캐릭터를 바라보는 상황같은 여러 상황에서 쓸 수 있을 것 같기에 태스크 노드로 만들기로 하였습니다.
EBTNodeResult::Type UBTTask_TurnToTarget::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
// Super 클래스의 함수 성공 여부를 저장
EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);
// 현재 컨트롤중인 Pawn을 가져오는데 실패했으면 Failed 반환
APawn* CurrentPawn = OwnerComp.GetAIOwner()->GetPawn();
if (!CurrentPawn)
{
return EBTNodeResult::Failed;
}
// Target을 가져오는데 실패했으면 Failed 반환
APawn* Target = Cast<APawn>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(FName(TEXT("Target"))));
if (!CurrentPawn)
{
return EBTNodeResult::Failed;
}
// AI 인터페이스
IRPGEnemyAIInterface* AICurrentPawn = Cast<IRPGEnemyAIInterface>(CurrentPawn);
float PatrolRadius = AICurrentPawn->GetAIPatrolRadius();
if (!AICurrentPawn)
{
return EBTNodeResult::Failed;
}
FVector TargetLocation = Target->GetActorLocation();
FVector DirectionToTarget = TargetLocation - CurrentPawn->GetActorLocation();
FRotator RotatorToTarget = DirectionToTarget.Rotation();
CurrentPawn->SetActorRotation(RotatorToTarget);
return EBTNodeResult::Succeeded;
}
몬스터의 회전을 위해 TurnToTarget의 코드를 다음과 같이 구성했습니다.
TargetLocation에 타겟의 위치를 저장하고
DirectionToTarget에 타겟의 위치와 몬스터의 위치를 뺀 값을 저장하여 몬스터에서 타겟까지의 방향벡터를 계산합니다.
DirectionToTarget.Rotation()를 통해 방향벡터에서 방향정보를 추출한 결과를 RotatorToTarget에 넣어 전환할 방향을
구하고
SetActorRotation을 통해 몬스터의 방향을 돌려줬습니다.
새로운 공격을 시작할 때 몬스터가 성공적으로 Target의 방향으로 회전하는것을 확인할 수 있습니다!
3. 공격시 몬스터의 회전 불가능 문제
캐릭터가 사망한 후에도 몬스터가 계속 캐릭터의 최종 위치를 공격하는 문제가 생겼는데
이는 캐릭터가 사망한 후 Target을 nullptr로 바꾸는 기능이 없어서 그렇습니다.
구현한 Detect 서비스를 추가해 공격 관련 노드들을 실행할 때 Detect 서비스를 수행하도록 해서
타겟에 대한 nullptr을 설정할 수 있도록 하였습니다.
몬스터가 공격 후 캐릭터가 사망하였고 Target이 nullptr값으로 설정되어 다시 정찰 노드를 수행하는 모습입니다.
최종 구현 코드
BTTask_Attack.cpp
// ExecuteTask
...
// AI 인터페이스
IRPGEnemyAIInterface* AICurrentPawn = Cast<IRPGEnemyAIInterface>(CurrentPawn);
float PatrolRadius = AICurrentPawn->GetAIPatrolRadius();
if (!AICurrentPawn)
{
return EBTNodeResult::Failed;
}
FAIAttackFinished AIAttackFinished;
AIAttackFinished.BindLambda(
[&]()
{
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
}
);
AICurrentPawn->SetAIAttackFinishedDelegate(AIAttackFinished);
AICurrentPawn->StartAIAttack();
...
우선 Attack 노드가 실행되면 다음과 같이 인터페이스를 이용하여 몬스터에 델리게이트를 연결하는 함수와
공격을 실행하는 함수를 작동시킵니다.
// Enemy.cpp
...
void ARPGEnemy::MontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
// 공격 몽타주 종료
if (Montage == AnimInstance->GetAttackMontage())
{
// 공격 종료시 상태정보
AnimInstance->SetIsAttacking(false);
// 델리게이트
AttackFinished.ExecuteIfBound();
}
}
void ARPGEnemy::SetAIAttackFinishedDelegate(const FAIAttackFinished& AIAttackFinished)
{
AttackFinished = AIAttackFinished;
}
void ARPGEnemy::StartAIAttack()
{
AnimInstance->PlayAttackMontage();
}
...
MontageEnded 함수에서 몽타주 종료시 델리게이트를 브로드캐스팅 하여 Attack 태스크 노드에서 바인딩한
람다함수를 실행하여 태스크 노드를 성공으로 처리합니다.
StartAIAttack 함수는 몬스터의 AnimInstance의 공격 몽타주를 플레이하는 함수를 실행시켜
공격 애니메이션을 수행하게 합니다.
void URPGEnemyAnimInstance::PlayAttackMontage()
{
Montage_Play(AttackMontage, 1.0f);
}
몽타주 재생 함수는 다음과 같이 구성되어있습니다.
● 공격 구현 최종 결과
몬스터가 캐릭터의 위치를 추적하여 캐릭터를 공격하는 기능을 구현완료 하였습니다.
서로 전투하는 모습입니다. 막상 만들어보니 몬스터와 캐릭터의 공격 거리가 너무 짧은 느낌이네요
이 부분은 수정을 해야할 것 같습니다.
다음 포스팅에서는 캐릭터의 방어 기능과 방어를 이용한 몬스터의 공격을 패링하는 기능을
추가해 보도록 하겠습니다!
감사합니다.
'[게임 개발] 개발 일지 > RPG' 카테고리의 다른 글
14. 파티클과 사운드를 이용한 패링 연출 효과 (0) | 2023.09.29 |
---|---|
13. 방어 액션과 패링 기능의 구현 (0) | 2023.09.27 |
11. AI를 이용한 캐릭터 탐지 (0) | 2023.09.24 |
10. 적 구현과 정찰 AI (0) | 2023.09.22 |
9. 캐릭터 스탯과 체력 UI 구현 (0) | 2023.09.19 |