AI

CJB_ny·2022년 12월 20일
0

UE4

목록 보기
9/20
post-thumbnail

1. AIController 예약시스템 기반

일단 Behavior Tree를 구현하기 전에 언리얼4에서 간단하게 AI를 만드는 방법을 살펴 볼 것이다.

언제나 그렇듯 유니티와 다르게 어느정도의 틀이 잡혀있다.

그래서 AI도 제공? 틀을 제공을 함.

아래처럼 부모클래스를 AIController로 만들어 준다.

그리고 아래의 코드와 같이 처주면됨.

(지금 당장 뭔지는 알 필요 없음)

OnPossess는 빙의할 때,

OnUnPossess는 빙의 하제할 때 사용하는 거임.

아레 코드는 GetTimerManager를 통해 일정시간마다 함수를 호출하는 부분임.

GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &AMyAIController::RandomMove, 3.f, true);

이후 MyCharacter 생성자 부분에 이거 추가 ㄱㄱ.

그리고 Tool로 돌아가서 볼륨-> 네비 메시 바운드 볼륨 하고

크기와 위치를 알맞게 설정을 해준다. (맵을 감싸게)

그리고 P누르면 위와 같이 됨.

결과물

2. BT 1

BlackBoard는

우리가 MyAnimInstance를 만들고 이것을 상속을 받는 애니매이션 블루프린트 클래스를 만들어서

모든 애니매이션 관련 데이터들을 MyAnimInstance에서 관리를 했었는데

BlackBoard도 AI관련 데이터들을 관리를 해주는 역할을 맡을 것이다.

BT는 BB에 있는 데이터를 기반으로 어떤 식으로 동작을 할지 트리 구조로 만드는 것이다.

아래 처럼 간단하게 BT일단 만들어 주도록 하자.

그리고 아래와 같은 헤더와 생성자에 저딴 코드 넣자

#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackBoardData.h"
#include "BehaviorTree/BlackBoardComponent.h"

AMyAIController::AMyAIController()
{
	static ConstructorHelpers::FObjectFinder<UBehaviorTree> BT(TEXT("BehaviorTree'/Game/AI/BT_MyCharacter.BT_MyCharacter'"));
	if (BT.Succeeded())
	{
		BeHaviorTree = BT.Object;
	}

	static ConstructorHelpers::FObjectFinder<UBlackBoardData> BD (TEXT("BlackboardData'/Game/AI/BB_MyCharacer.BB_MyCharacer'"));
	if (BD.Succeeded())
	{
		BlackBoardData = BD.Object;
	}
}
  1. 에서 예약 시스템 기반 AI 가 아니라 우리가 만들어놓은 behavior tree대로 움직일 것이다.

실제로 화면상에서는 아무것도 안하고있지만 BT대로 동작을 하고있는것을 확인이 가능하다.

BT 만들기

이렇게 위에처럼 만들면 5초동안 가만히 있다가 특정좌표로 움직이는 것이다.

먼저 아래와 같이 c++클래스 만들어준다.

그리고 코드들은 위와 같이 해준다. AI에서 RandomMove 코드 들고오면됨.

그리고 아래와 같이 FindPatrolPos가 뜰 것이다.

위와같이 NodeName을 생성자에서 설정해준 대로 뜸.

결과물

3. BT 2

오늘 할 것은 적 캐릭터가 플레이어를 발견하면 쫒아와서 때릴 수 있도록 만들 것이다.

그러면 발견했을 때의 해야할 행동과 발견하지 못했을 때의 해야할 행동으로 나눌 필요가 있는데, 이때 사용할 수 있는 부분이 "Selector"이다.

근데 여기서도 매 프레임 마다 플레이어를 찾는다면 부담이 되니까 1~2초 정도마다 탐색을 할 수 있도록 할려면 Selector에 "서비스"라는 것을 사용을 해주면 된다.

BD 설정

Key Type의 Base Class를 아래와 같이 설정 해준다.

BP_MyCharacter는 어차피 MyCharacter c++클래스를 상속받아서 Base Class를 BP_MyCharacter로 해도 상관없음.

만들기

Service를 부모로 갖는 cpp클래스를 추가를 해준다.

그리고 1~2초 정도 특정시간마다 탐색을 하기를 원하기 때문에 TickNode리는 함수를 아래처럼 추가를 해줄 것이다.

이후 아래부분을 구현을 해주어야한다.

생성자에서 InterVal이라는 변수를 통해서 TickNode의호출 주기를 설정 할 수 있다.

// TickNode를 호출할 주기를 정해주기 위한 생성자
UBTService_ServiceTarget::UBTService_ServiceTarget()
{
	NodeName = TEXT("SearchTarget");
	Interval = 1.0f;
}
// 주기가 정해 져야함
void UBTService_ServiceTarget::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);

	// CurrentPawn :: 현재 이 AI를 실행하는 주체
	auto CurrentPawn = OwnerComp.GetAIOwner()->GetPawn();
	if (CurrentPawn == nullptr) return;

	UWorld* World = CurrentPawn->GetWorld();
	FVector Center = CurrentPawn->GetActorLocation();
	float SearchRadius = 500.f;

	if (World == nullptr) return;

	TArray<FOverlapResult> OverlapResults;
	FCollisionQueryParams QueryParams(NAME_None, false, CurrentPawn);

	bool bResult = World->OverlapMultiByChannel
	(
		OverlapResults,
		Center,
		FQuat::Identity,
		ECollisionChannel::ECC_EngineTraceChannel2,
		FCollisionShape::MakeSphere(SearchRadius),
		QueryParams
	);

	if (bResult)
	{
		// Target찾은 경우
		for (auto& OverlapResult : OverlapResults)
		{
			AMyCharacter* MyCharacter = Cast<AMyCharacter>(OverlapResult.GetActor());

			// 진짜 Player가 맞는지 이중 체크
			if (MyCharacter && MyCharacter->GetController()->IsPlayerController())
			{
				OwnerComp.GetBlackboardComponent()->SetValueAsObject(FName(TEXT("Target")), MyCharacter);

				// 16은 정밀도임
				DrawDebugSphere(World, Center, SearchRadius, 16, FColor::Green, false, 0.3f);

				return;
			}
		}
	}
	else
	{
		OwnerComp.GetBlackboardComponent()->SetValueAsObject(FName(TEXT("Target")), nullptr);
	}

	DrawDebugSphere(World, Center, SearchRadius, 16, FColor::Red, false, 0.3f);

}

위의 코드는 주기마다 해야할 로직이다. 로직이라기 보다는 그냥 탐색 범위 설정하는 부분이다.

중간 결과물

범위 안에 들어가면 초록색으로 뜸.


Sequence는 하나라도 실패하면 빠져나오는 것이고 Selector는 foreach문 같은 것임 실패하면 다른 자식노드 실행하러감.

위와같이 해주면 일단 범위 안에 들어오면 막 따라옴.

근데 왼쪽에 sequence를 추가를 해서 사정범위 안에 들어오면 공격할 수 있는지 여부를 분기를 해주기위해서 만들어줌. 이것은 c++코드로 작업을 하는게 더 효율적임.


여기에서 부터 이제 공격할 수 있는지 없는지를 판별할 수 있도록 데코레이션을 c++로 만들고 툴에서 연결연결하는 식으로 해야한다.

물론 Task도 만들어야함. 그래서 아래와 같이 데코, BTTask cpp클래스로 만들어 주도록 하자.

먼저 데코부터 작업을 하는데 이녀석은 그냥 범위 안에 들어와서 공격범위 안에 있는지 없는지를 판별 해주는 역할을 할 데코레이션이다. 아래와 같이 작업을 함.

그리고나서 아까 sequence에다가 CanAttack이라는 데코를 달아주자.

오른쪽 CanAttack 데코에는 Inverse condition을 켜주어야 반대로 동작을 함.

즉, CalculateRawConditionValue 함수가 false일 때 동작을 하는 sequence임.

그리고 BTTask_Attack을 작업을 할 것이다. 전체코드는 아래가 전부임.

주의할점은 아래와 같음.

그리고 중간에 람다를 사용했는데 MyCharacter에 공격이 끝났을 시점을 Delegate로 만들어서 BroadCast를 통해서 알림을 받는 부분이다.

AddLambda가 알림받은 부분을 람다로 처리하는 부분임.

MyCharacter로 가보면

이렇게 Delegate선언해주고 몽타주 애니매이션 끝났을 때

위처럼 다 전파을 해주는 식이다.

Lambda ❗

현재 bIsAttacking이라는 변수가 UBTTask_Attack의 멤버 변수이기 때문에

위와 같이 this를 캡쳐를 한 것이다.

https://blockdmask.tistory.com/491
2-2) 람다 캡처 사용법
이제 람다 모양 {} () 중 맨 앞에 있던 [] 대괄호에 대해서 설명해보겠습니다.
일단 이 대괄호 부분을 캡쳐라고 부릅니다.
캡처는 람다 외부에 정의되어있는 변수나 상수를 람다 내부에서 사용하기 위해서 사용합니다.

결과물

profile
https://cjbworld.tistory.com/ <- 이사중

0개의 댓글