Unreal - AI (Behavior Tree 설계하기)

2026. 2. 2. 19:20·Unreal 프로젝트 다이어리/두번째 프로젝트

미리보기

구현내용

플레이어와 전투를 주고받는 AI의 기초설계를 구현하였습니다

구현한 AI의 내용은 이와 같습니다

  • AI - Perception + 커스텀범위 시야기반 인식
  • BehaviorTree 를 사용한 AI 설계
  • Blackboard / Task를 사용한 AI설계
  • NavMesh를 사용한 Move To
  • 플레이어를 인식시 Draw On / 플레이어 타게팅이 풀리면 Draw Off
  • Posture 기반 전투시스템

사용클래스

사용 클래스 사용 목적
BB_EasternSamurai(블랙보드) AI의 모든 판단 데이터 관리자
BT_EasternSamurai(비헤이비어트리) 적 캐릭터의 의사결정 흐름 담당
BaseAIController(AI-Controller) 모든 AI가 오버라이드할 베이스 AI-Controller
(Perception 으로 인지담당)
AIController_Normal(AI-Controller) 캐릭터의 중심 제어 AI 컨트롤러 클래스
BehaviorTree실행 & Blackboard 초기화
Task / Service (Task) 단일 책임 원칙 행동 / (Service) 환경 감지 및 분기판단
EnemyActionComponent(액터 컴포넌트) 실제 캐릭터의 행동 상태를 관리하는 독립 컴포넌트

 

구현C++

플레이어 인식을 위해 AI - Perception 시스템을 사용

특정 고정 시야각 방식이 아닌 360도를 기본 감지 범위를 설정하고

실제 시야 개념은 별도의 로직으로 직접 구현

AI의 State에 따라 시야 범위와 반응 방식을 동적으로 변경하여

경계 모드 및 전투 진입과 같은 단계적인 인식 시스템 구성

이를 통해 단순 거리 기반 감지가 아닌 상황 기반 인식 구조를 구현

 

먼저 BT를 작동시켜줍니다

작동되는곳은 AI - Controller에서 작동됩니다

모든 AI가 오버라이드 할 대상인 모체 Base를 만들고 그 controller를 상속받아 설계하였습니다

 

모체가되는 Controller는 Perception을 사용하여 BlackBoard에 타겟을 인식합니다

 

BaseAIController 코드 더보기

더보기
ABaseAIController::ABaseAIController()
{
	perceptionComp = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("PerceptionComp"));
	sight = CreateDefaultSubobject<UAISenseConfig_Sight>("SightConfig");

	sight->SightRadius = 600; 
	sight->LoseSightRadius = 800; 
	sight->PeripheralVisionAngleDegrees = 360; 

	perceptionComp->ConfigureSense(*sight);
	perceptionComp->SetDominantSense(sight->GetSenseImplementation());

	sight->DetectionByAffiliation.bDetectEnemies = true;
	sight->DetectionByAffiliation.bDetectFriendlies = true;
	sight->DetectionByAffiliation.bDetectNeutrals = true;

	perceptionComp->OnTargetPerceptionUpdated.AddDynamic(this, &ABaseAIController::OnTargetPerceptionUpdated);
}

void ABaseAIController::OnPossess(APawn* InPawn)
{
	Super::OnPossess(InPawn);

	if (behaviorTreeFile)
	{
		UseBlackboard(behaviorTreeFile->BlackboardAsset, blackBoardComp);
		RunBehaviorTree(behaviorTreeFile);
	}
}

void ABaseAIController::OnTargetPerceptionUpdated(AActor* actor, FAIStimulus stimulus)
{
	if (!actor || !actor->ActorHasTag(FName("Player")))
		return;

	APawn* perceivedPawn = Cast<APawn>(actor);
	if (!perceivedPawn)
		return;

	blackBoardComp->SetValueAsObject(FName("Target"),
		stimulus.WasSuccessfullySensed() ? perceivedPawn : nullptr);

	if (stimulus.WasSuccessfullySensed())
		OnTargetDetected(perceivedPawn);
	else
		OnTargetLost(actor);
}

 

BehaviorTree의 초기설계는 이와 같습니다

Service로 EnumState를 확인하며 분기를 나누고

State에 따라 데코레이터로 나누어 설계하였습니다

 

모든 태스크는 인스턴스화로 동일 BehaviorTree를 공유하되 별도의 인스턴스화를 완료해주었습니다

행동을 할때는 Task에 신호를 붙잡아두고 원할때 신호를 이전시켜주었습니다.

 

Posture , 가드 시스템은 이전글에서 설명했으니 스킵하도록 하겠습니다

2026.01.12 - [Unreal 프로젝트 다이어리/두번째 프로젝트] - Unreal - 가드 (Guard) / 체간 (Posture) 시스템

 

Unreal - 가드 (Guard) / 체간 (Posture) 시스템

미리보기구현내용플레이어와 적이 상호작용하며 서로 영향을 주고받는이른바 제 2의 체력인 세키로의 체간 및 가드 시스템을 구현했습니다 플레이어의 체간 시스템적의 체간 시스템플레이어

lucodev.tistory.com

 

Task

플레이어 에게 접근하는 MoveToTarget 태스크입니다

T_MoveTotTarget 구현 C++

 

NavMesh 기반 MoveTo를 사용하였으며

AIMoveRequest를 사용하여 SetAcceptanceRadius로 접근범위를 설정하고

EBTNodeResult로 신호를 제어합니다

더보기
UT_MoveToTarget::UT_MoveToTarget()
{
	NodeName = TEXT("Move To Target");
	bNotifyTick = true;
	bCreateNodeInstance = true;
}

EBTNodeResult::Type UT_MoveToTarget::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	if (!Initialize(OwnerComp))
		return EBTNodeResult::Failed;

	if (!enemyAICon || !enemy)
		return EBTNodeResult::Failed;

	AActor* Target = Cast<AActor>(
		enemyAICon->GetBlackboardComponent()->GetValueAsObject("Target")
	);

	if (!Target)
		return EBTNodeResult::Failed;

	FAIMoveRequest moveReq;
	moveReq.SetGoalActor(Target);
	moveReq.SetAcceptanceRadius(acceptanceRadius);
	moveReq.SetUsePathfinding(true);

	FNavPathSharedPtr path;
	auto pathResult = enemyAICon->MoveTo(moveReq, &path);

	if (pathResult == EPathFollowingRequestResult::Failed)
		return EBTNodeResult::Failed;

	return EBTNodeResult::InProgress;
}

void UT_MoveToTarget::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);

	if (!enemyAICon)
		return;

	UBlackboardComponent* blackB = enemyAICon->GetBlackboardComponent();
	if (!blackB)
		return;
	bool bCombat = blackB->GetValueAsBool(TEXT("CombatMode"));
	AActor* target = Cast<AActor>(blackB->GetValueAsObject(TEXT("Target")));

	if (!bCombat)
	{
		enemyAICon->StopMovement(); 
		FinishLatentTask(OwnerComp, EBTNodeResult::Aborted);
		return;
	}

	UPathFollowingComponent* pathComp = enemyAICon->GetPathFollowingComponent();
	if (!pathComp)
		return;

	if (pathComp->GetStatus() == EPathFollowingStatus::Idle)
		FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);

}

 

TS_Combat Check C++

 

bool형 CombatMode라는 이름으로 시야안에 있을때 봤다 보지 못했다를 판단합니다

더보기
bool AAIController_Normal::IsTargetInFront(AActor* target) const
{
	if (!GetPawn() || !target) return false;

	FVector forwardVec = GetPawn()->GetActorForwardVector();
	FVector dir = (target->GetActorLocation() - GetPawn()->GetActorLocation()).GetSafeNormal();

	float dotP = FVector::DotProduct(forwardVec, dir);

	return dotP > 0.5f;
}
UTS_CombatCheck::UTS_CombatCheck()
{
	NodeName = "Combat Check";
	Interval = 0.2f;  
	RandomDeviation = 0.f;
}


void UTS_CombatCheck::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	AAIController_Normal* aiCon =Cast<AAIController_Normal>(OwnerComp.GetAIOwner());

	if (!aiCon)
		return;

	UBlackboardComponent* BlackboardComp =OwnerComp.GetBlackboardComponent();
	if (!BlackboardComp)
		return;


	AActor* target = Cast<AActor>(BlackboardComp->GetValueAsObject(TEXT("Target")));

	if (!target)   
	{
		BlackboardComp->SetValueAsBool(TEXT("CombatMode"), false);
		BlackboardComp->SetValueAsBool(TEXT("bAlertMode"), false);
		return;
	}

	AMainCharacter* player = Cast<AMainCharacter>(target);  
	if (!player)
		return;

	const bool bAlert = BlackboardComp->GetValueAsBool(TEXT("bAlertMode"));

	EPlayerLifeState lifeState = player->lifeState;
	if (lifeState != EPlayerLifeState::Alive)
	{
		BlackboardComp->SetValueAsBool(TEXT("CombatMode"), false);
		BlackboardComp->SetValueAsBool(TEXT("AlertMode"), false);
		player->lockOnSystem->DisableLockOn();
		return;
	}

	if (bAlert)
	{
		BlackboardComp->SetValueAsBool(TEXT("CombatMode"), true);
		return;
	}

	const bool bIsCombat =aiCon->IsTargetInFront(target);
	BlackboardComp->SetValueAsBool(TEXT("CombatMode"), bIsCombat);

	
}

무기 Draw On / Off

플레이어가 AI의 시야각 범위 내에 노출되면 적은 무기를 Draw On 한뒤 행동합니다

플레이어가 사망하거나 시야각 범위로 나가게되면 Draw Off를 실행하여 무장해제를 실행합니다

 

SkeletalMesh의 소켓기능을 적극적으로 사용하였습니다

소켓을 생성하여 Draw On / Off 할때 칼의 위치를 프레임단위로 맞춰주어 무장 / 무장헤제를 구현하였습니다

기본적으로 이벤트 발생시 노티파이로 제어합니다

 

플레이어가 범위 내에 접근하면 무장하고 범위밖으로 가면 무장을 해제합니다

 

 

AI의 BlendSpace 애니메이션을 제외한

모든 애니메이션은 DataTable기반 탐색기반 알고리즘으로 작동합니다

 

여러 프로젝트를 걸치며 AI를 만들어봤는데 항상 있던 문제가 있었습니다

그것은 바로 플레이어가 적을 때린다 -> 적이 HIT 액션을 취한다 라는 간단한로직밖에없다면

플레이어가 적을일방적으로 때릴수 있게 됩니다

그래서 플레이어가 적을 때렸을때 히트카운트 / 확률 에 기반하여

일정 히트시 확률적으로 적이 나의 공격으로부터 벗어나는 닷지를 실행하는 로직을 추가하여주었습니다.

 

Evade 탈출 로직 C++

Evade는 다음 모든 전제 조건 + 확률 판정을 통과해야 실행됩니다

 

랜덤 임계값 4- 5회 

짧은 시간 내 연속 피격이 랜덤 임계값 초과 시

그리고 0.6초 이상 맞지 않으면

콤보가 아니더라도 총 4번 맞으면 Evade 후보 등록

 

즉 연속피격 >= 4~5회 이거나

누적 피격 >=4회 이거나

70% 확률 임계값 통과할경우 최종 Evade 를 실행합니다

10

구현부 C++

더보기
int32 consecutiveHitCount = 0;
float lastHitTime = 0.f;

UPROPERTY(EditAnywhere, Category="Evade")
float comboResetTime = 0.6;

UPROPERTY(EditAnywhere, Category = "Evade")
int32 minEvadeHit = 4;

UPROPERTY(EditAnywhere, Category = "Evade")
int32 maxEvadeHit = 5;

UPROPERTY(EditAnywhere, Category = "Evade")
float evadeChance = 0.7f;

UPROPERTY(EditAnywhere, Category = "Evade")
int32 totalHitCount = 0;
int32 currentEvadeHit = 0;

UPROPERTY(EditAnywhere, Category = "Evade")
int32 totalEvadeHit = 4;
void AEnemyBaseCharacter::ReceiveHit(const FGameHitSystemData& damageData, AActor* attacker)
{
	//테스트 모드--
	if (bNoHitTest)
		return;
        
    //Evade 탈출 로직    
	if (currentEvadeHit == 0)
		currentEvadeHit = FMath::RandRange(minEvadeHit, maxEvadeHit);

	float now = GetWorld()->GetTimeSeconds();
	if (now - lastHitTime > comboResetTime)
	{
		consecutiveHitCount = 0;
	}
	lastHitTime = now;
	consecutiveHitCount++;
	totalHitCount++;

	//방향 계산
	hitDirection = damageData.hitDirection;

	//데미지 처리
	ApplyDamage(damageData);

	//포스쳐 처리
	ApplyPosture();

	//evade처리
	if (currentPosture < maxPosture)
	{
		if (currentEvadeHit == 0)
		{
			currentEvadeHit = FMath::RandRange(minEvadeHit, maxEvadeHit);
		}

		bool bComboEvade = consecutiveHitCount >= currentEvadeHit;
		bool bTotalEvade = totalHitCount >= totalEvadeHit;

		if ((bComboEvade || bTotalEvade) && FMath::FRand() <= evadeChance)
		{
			consecutiveHitCount = 0;
			totalHitCount = 0;
			currentEvadeHit = 0;

			SetSuperArmorType(ESuperArmorType::Invincible);
			actionComp->SetActionState(EEnemyActionState::Evade);
			Evade();
			return;
		}
	}

	if (currentPosture >= maxPosture)
	{
		actionComp->SetActionState(EEnemyActionState::Execution);
		StaggerKnockDown();
	}

	else
		actionComp->SetActionState(EEnemyActionState::Stagger);


	soundSubsystem->PlaySound("EnemyHit", this, GetActorLocation());
	//4. 히트 리액션
	if (CanPlayHitReaction())
		DamagedMontageSelector();

	//피처리
	SpawnBloodDecal(damageData.hitLocation);

	if (bTestMode_GuardMotioning)
		return;

	UpdateFloatWidget();
}

영상

https://youtu.be/y0igD_Wl93E

 

저작자표시 비영리 변경금지 (새창열림)

'Unreal 프로젝트 다이어리 > 두번째 프로젝트' 카테고리의 다른 글

Unreal - AI (EQS Strafe 이동)  (0) 2026.02.06
Unreal - AI (순찰, 전투)  (0) 2026.02.05
Unreal - 달리 줌 (Dolly - Zoom)  (0) 2026.01.25
Unreal - 상호작용 추가  (2) 2026.01.24
Unreal - 트리거 매니저 ( 낙사 & 오토세이브 )  (1) 2026.01.21
'Unreal 프로젝트 다이어리/두번째 프로젝트' 카테고리의 다른 글
  • Unreal - AI (EQS Strafe 이동)
  • Unreal - AI (순찰, 전투)
  • Unreal - 달리 줌 (Dolly - Zoom)
  • Unreal - 상호작용 추가
lucodev
lucodev
언리얼 포폴개발 일기
  • lucodev
    루코 개발테이블
    lucodev
  • 전체
    오늘
    어제
    • 분류 전체보기 (236)
      • Unreal 프로젝트 다이어리 (132)
        • 첫번째 프로젝트 (73)
        • 두번째 프로젝트 (59)
      • Unreal 팁 (8)
      • Unreal 디버깅 (8)
      • C++ 프로그래머스 (52)
        • Stack,Queue (7)
        • Hash (4)
        • Heap (2)
        • Sort (5)
        • Exhaustive search (5)
        • Greedy (2)
        • BFS , DFS (7)
        • Graph (2)
        • Dynamic Programming (1)
        • C++ Math (2)
        • 기타 문제 (14)
      • C++ 백준 (5)
      • C++ 팁 (1)
      • 개인 코테 & 스타디 <비공개> (29)
        • 코드 개인보관함 (9)
        • 코딩테스트+@ (11)
        • 알고리즘 스타디 (6)
        • 알고리즘 스타디 과제 (3)
        • 비공개 (0)
  • 인기 글

  • 최근 글

  • 최근 댓글

  • 링크

  • 공지사항

  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 태그

    언리얼 parkour
    unreal inventory
    언리얼 비헤이비어트리
    언리얼 behavior tree
    unreal
    unreal npc
    unreal 상호작용
    언리얼 컷씬
    언리얼 세키로
    unreal 파쿠르
    언리얼 시퀀스
    언리얼 ui
    unreal 인벤토리
    언리얼 behaviortree
    언리얼 상호작용
    언리얼 파쿠르
    언리얼 인벤토리
    언리얼 인터렉션
    언리얼
    unreal 세키로
  • hELLO· Designed By정상우.v4.10.3
lucodev
Unreal - AI (Behavior Tree 설계하기)
상단으로

티스토리툴바