23.06.19 ~ 2023.06.25 개발일지
애님 인스턴스에서 게임 플레이 태그의 인터페이스 함수 HasMatchingGameplayTag
를 사용하면
Thread Safe하지 않다는 경고가 나오게 된다
이 경우 해결 방법이 몇가지 있었는데,
첫번째는 부르게 되는 함수에 meta = (BlueprintThreadSafe)
를 추가하는 것이다
다만 지금 부르는 함수 HasMatchingGameplayTag
는 인터페이스에서 구현된 클래스이고,
굳이 meta 속성을 붙이자고 override하는 것이 불필요하다고 느껴졌다
두번째는 프로젝트 설정에서 Allow Multi Threaded Animation Update
를 해제하는 것이다
이 경우도 엔진의 모든 애님 블루프린트의 속성을 바꾸게 되는 것이고,
특정 함수를 위한 조치가 아니라고 느껴져 다음 방식을 사용하게 되었다
세번째는 바로 HasTag
를 사용하는 것이다
장황하게 써놨지만, 사실 블루프린트 함수 HasTag
가 작동하는 방식이 인터페이스 HasMatchingGameplayTag
와 동일하기 때문이다
GameplayTagInterface.cpp
bool IGameplayTagAssetInterface::HasMatchingGameplayTag(FGameplayTag TagToCheck) const
{
FGameplayTagContainer OwnedTags;
GetOwnedGameplayTags(OwnedTags);
return OwnedTags.HasTag(TagToCheck);
}
BlueprintGameplayTagLibrary.cpp
bool UBlueprintGameplayTagLibrary::HasTag(const FGameplayTagContainer& TagContainer, FGameplayTag Tag, bool bExactMatch)
{
if (bExactMatch)
{
return TagContainer.HasTagExact(Tag);
}
return TagContainer.HasTag(Tag);
}
두 함수 모두 컨테이너의 HasTag
함수를 사용한다
모션은 지금까지 구현한 것과 비슷하게 ConstructorHelper를 통해 불러오고,
타격받을때 사용한 코드를 활용하여 사망모션이 나오도록 구현하였다
IFEnemyAnimInstance.cpp
void UIFEnemyAnimInstance::DeathAnim(AActor* Target, AActor* Causer)
{
if (!(::IsValid(Target) && ::IsValid(Causer))) return;
// check if character and enemy are facing(DotProduct on both character's forward vector)
float DotProduct = FVector::DotProduct(Causer->GetActorForwardVector(), Target->GetActorForwardVector());
FRotator CurrentRotation = Target->GetActorRotation();
if (DotProduct < 0)
{
Target->SetActorRotation(FRotator(CurrentRotation.Pitch, Causer->GetActorRotation().Yaw + 180, CurrentRotation.Roll));
Montage_Play(DeadFrontMontage);
}
else
{
Target->SetActorRotation(FRotator(CurrentRotation.Pitch, Causer->GetActorRotation().Yaw, CurrentRotation.Roll));
Montage_Play(DeadBackMontage);
}
}
구한 메테리얼의 인스턴스를 만들고, 이를 메쉬의 메티리얼로 지정해주었다
그 후 사망하게 될 때 Timeline을 활용해 사라지는 듯한 느낌을 주었다
Appearance 값에 따라 material 값이 조정된다
IFEnemy.cpp
{
...
// setting DissolveTimeline
OnDissolveTimelineFunction.BindDynamic(this, &AIFEnemy::UpdateDissolve);
DissolveTimeline->AddInterpFloat(DissolveCurveFloat, OnDissolveTimelineFunction);
OnDissolveTimelineFinished.BindDynamic(this, &AIFEnemy::SetDestroy);
DissolveTimeline->SetTimelineFinishedFunc(OnDissolveTimelineFinished);
DissolveTimeline->SetPlayRate(0.4f);
// setting mesh material
UMaterialInterface* NewMaterial = LoadObject<UMaterialInterface>
(nullptr, TEXT("/Game/InFiniteFighter/Characters/Mannequin_UE4/Materials/M_Mannequin_Inst.M_Mannequin_Inst"));
MIDCharacter = UMaterialInstanceDynamic::Create(NewMaterial, nullptr);
GetMesh()->SetMaterial(0, MIDCharacter);
GetMesh()->SetMaterial(1, MIDCharacter);
// setting weapon material
UMaterialInterface* NewWeapon = LoadObject<UMaterialInterface>
(nullptr, TEXT("/Game/InFiniteFighter/AI/Materials/weapon_2_Inst.weapon_2_Inst"));
MIDWeapon = UMaterialInstanceDynamic::Create(NewWeapon, nullptr);
Weapon->SetMaterial(0, MIDWeapon);
}
...
void AIFEnemy::UpdateDissolve(float InTimeline)
{
MIDCharacter->SetScalarParameterValue(TEXT("Appearance"), InTimeline);
MIDWeapon ->SetScalarParameterValue(TEXT("Appearance"), InTimeline);
}
...
void AIFEnemy::SetDestroy()
{
Destroy();
}
다만 구현을 하며 아쉬웠던 점이, TimelineFinished에는 람다식을 바인딩 하지 못한다는 것과,
기본으로 주어지는 Destroy
함수는 UFUNCTION이 지정되지 않아 바로 델리게이트에 바인딩 할 수 없다는 점이었다
그래서 상당히 못난 SetDestroy
함수를 사용하게 되었는데, 개선할 수 있는 방식을 찾아봐야겠다
완성된 결과는 다음과 같다
(처형모션으로 사망 시)
(데미지를 받고 사망 시)
적 AI가 언제나 캐릭터를 바라보도록 구현해주었다
다만 이 경우, 몽타주를 실행한 후라면 갑작스럽게 방향이 바뀌기 때문에,
현재 Enemy의 Rotation과 가야할 Rotation이 10도 이상 틀어져있다면 보간해주었다
IFEnemy.cpp
void AIFEnemy::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
FVector LookVector = PlayerCharacter->GetActorLocation() - GetActorLocation();
LookVector.Z = 0.0f;
FRotator TargetRot = FRotationMatrix::MakeFromX(LookVector).Rotator();
if (!AnimInstance->IsAnyMontagePlaying() && !HasMatchingGameplayTag(StunTag))
{
float RotationDifference = (GetActorRotation() - TargetRot).GetNormalized().Yaw;
if (FMath::Abs(RotationDifference) < 10)
SetActorRotation(TargetRot);
else
SetActorRotation(FMath::RInterpTo(GetActorRotation(), TargetRot, DeltaTime, 5));
}
}
적이 언제나 근접 공격, 원거리 공격만 반복하는 것은 재미적인 요소가 떨어지기 때문에,
조금이라도 더 생동감을 주기위해 캐릭터 주위를 배회하도록 만들어주었다
BTTask_SetPatrolPos.cpp
EBTNodeResult::Type UBTTask_SetPatrolPos::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);
...
FVector Origin = TargetPawn->GetActorLocation();
float PatrolRadious = 800.0f;
FNavLocation NextPatrolPos;
if (NavSystem->GetRandomPointInNavigableRadius(Origin, PatrolRadious, NextPatrolPos))
{
OwnerComp.GetBlackboardComponent()->SetValueAsVector(TEXT("PatrolPos"), NextPatrolPos.Location);
return EBTNodeResult::Succeeded;
}
return EBTNodeResult::Failed;
}
새롭게 만든 SetPatrolPos
클래스를 통해 적이 나갈 방향을 설정해주었고,
지정한 위치로 이동하는 방식으로 구현하였다
이 시퀀스만 반복했을때의 모습은 다음과 같다
여러가지 동작(패턴)을 만든다고해서 그것이 반드시 재미있는 게임과 AI가 되지는 않는다
특히 AI에게는 랜덤성이 필요하고, 이를 위해 지금껏 만든 액션들 (근접 공격, 원거리 공격, 이동)을 무작위로 고르는 데코레이터를 만들었다
BTDecorator_RandomChance.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "AI/BTDecorator_RandomChance.h"
UBTDecorator_RandomChance::UBTDecorator_RandomChance()
{
NodeName = TEXT("Random");
}
bool UBTDecorator_RandomChance::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
float Rand = FMath::FRand();
return SuccessRate >= Rand;
}
간단한 방식인 랜덤한 값을 뽑아 수치가 높으면 성공하는 방식으로 구현했다
이때 SuccessRate는 UPROPERTY(EditAnywhere, meta = (ClampMin = 0.0f, ClampMax = 1.0f))
를 추가해
에디터에서 0 ~ 1 의 값 중 하나만 고를 수 있도록 구현하였다
최종적으로 지금까지 구현한 AI의 BT는 다음과 같다