Infinite Fighter 개발일지 (15)

유영준·2023년 6월 25일
1

UE5 UNSEEN

목록 보기
15/18
post-thumbnail

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개선

언제나 캐릭터 바라보도록

적 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는 다음과 같다

profile
토비폭스가 되고픈 게임 개발자

0개의 댓글