[Day 4] AI Monster with C++

๋ฒ ๋ฆฌํˆฌ์Šคยท2025๋…„ 8์›” 7์ผ

TIL: Today I Learned

๋ชฉ๋ก ๋ณด๊ธฐ
12/93

๐Ÿ“Œ ๋ชฉํ‘œ

  • ํ”Œ๋ ˆ์ด์–ด๋ฅผ ์ธ์‹ํ•˜๊ณ  ์ถ”์ ํ•˜๋Š” ๋ชฌ์Šคํ„ฐ AI๋ฅผ ๋งŒ๋“ ๋‹ค
  • ๊ทผ์ ‘ํ•˜๋ฉด ๊ณต๊ฒฉํ•œ๋‹ค
  • ์ผ์ • ์‹œ๊ฐ„ ์ด์ƒ ๊ฐ€๊นŒ์ด ์žˆ์œผ๋ฉด '๋ถ„๋…ธ ์ƒํƒœ'๋กœ ์ „ํ™˜ํ•˜์—ฌ ๋” ๋นจ๋ผ์ง„๋‹ค

โœ… ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

  • Unreal Engine 5 ์‹คํ–‰
  • ์ƒˆ ํ”„๋กœ์ ํŠธ > Games > First Person ํ…œํ”Œ๋ฆฟ
  • With Starter Content ์„ ํƒ ๊ฐ€๋Šฅ
  • ํ”„๋กœ์ ํŠธ ์ด๋ฆ„: MonShooting
  • With C++ ํ™œ์„ฑํ™”!

๐Ÿงฑ Step 1: MonsterCharacter ํด๋ž˜์Šค

์ด ํด๋ž˜์Šค๋Š” ์‹ค์ œ ๋ชฌ์Šคํ„ฐ ์บ๋ฆญํ„ฐ์˜ ์›€์ง์ž„, ๊ณต๊ฒฉ, ๋ถ„๋…ธ ์ƒํƒœ ๋“ฑ์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.

MonsterCharacter.h

UCLASS()
class MONSHOOTING_API AMonsterCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	AMonsterCharacter();

protected:
	virtual void BeginPlay() override;

public:	
	virtual void Tick(float DeltaTime) override;

	UPROPERTY(EditAnywhere, Category = "AI")
	float DetectionRange = 1000.0f;  // ํ”Œ๋ ˆ์ด์–ด ๊ฐ์ง€ ๊ฑฐ๋ฆฌ

	UPROPERTY(EditAnywhere, Category = "AI")
	float AttackRange = 150.0f;      // ๊ณต๊ฒฉ ๊ฑฐ๋ฆฌ

	UPROPERTY(EditAnywhere, Category = "AI")
	float EnrageTime = 5.0f;         // ๋ช‡ ์ดˆ ์ด์ƒ ๊ฐ€๊นŒ์ด ์žˆ์œผ๋ฉด ๋ถ„๋…ธ

	UPROPERTY(EditAnywhere, Category = "AI")
	float NormalSpeed = 300.0f;

	UPROPERTY(EditAnywhere, Category = "AI")
	float EnragedSpeed = 600.0f;

private:
	APawn* TargetPlayer;   // ์ถ”์  ๋Œ€์ƒ
	float TimeInRange;     // ๊ทผ์ ‘ ์‹œ๊ฐ„ ๋ˆ„์ 
	bool bIsEnraged;       // ๋ถ„๋…ธ ์ƒํƒœ ์—ฌ๋ถ€

	void CheckPlayerProximity(float DeltaTime); // ๊ฑฐ๋ฆฌ ์ฒดํฌ
	void AttackPlayer();                        // ๊ณต๊ฒฉ ์‹คํ–‰
	void BecomeEnraged();                       // ๋ถ„๋…ธ ์ „ํ™˜
};

MonsterCharacter.cpp

AMonsterCharacter::AMonsterCharacter()
{
	PrimaryActorTick.bCanEverTick = true;

	// ๊ธฐ๋ณธ ์ด๋™ ์†๋„ ์„ค์ •
	GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;
}
void AMonsterCharacter::BeginPlay()
{
	Super::BeginPlay();

	// ํƒ€๊ฒŸ์„ ํ˜„์žฌ ํ”Œ๋ ˆ์ด์–ด๋กœ ์„ค์ •
	TargetPlayer = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);

	// ์ดˆ๊ธฐํ™”
	TimeInRange = 0.0f;
	bIsEnraged = false;
}
// ๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค ๊ฑฐ๋ฆฌ ์ฒดํฌ ์ˆ˜ํ–‰
void AMonsterCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (TargetPlayer)
	{
		CheckPlayerProximity(DeltaTime);
	}
}
void AMonsterCharacter::CheckPlayerProximity(float DeltaTime)
{
	// ํ”Œ๋ ˆ์ด์–ด์™€์˜ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ
	float Distance = FVector::Dist(GetActorLocation(), TargetPlayer->GetActorLocation());

	if (Distance <= DetectionRange)
	{
		// AIController๋ฅผ ํ†ตํ•ด ์ถ”์  ์‹œ์ž‘
		AAIController* AIController = Cast<AAIController>(GetController());
		if (AIController)
		{
			AIController->MoveToActor(TargetPlayer);
		}

		// ์ผ์ • ์‹œ๊ฐ„ ๊ฐ€๊นŒ์ด ์žˆ์œผ๋ฉด ๋ถ„๋…ธ ์ƒํƒœ๋กœ
		TimeInRange += DeltaTime;
		if (!bIsEnraged && TimeInRange >= EnrageTime)
		{
			BecomeEnraged();
		}

		// ๊ณต๊ฒฉ ๊ฑฐ๋ฆฌ ์•ˆ์ด๋ฉด ๊ณต๊ฒฉ
		if (Distance <= AttackRange)
		{
			AttackPlayer();
		}
	}
	else
	{
		// ๋ฉ€์–ด์ง€๋ฉด ํƒ€์ด๋จธ ์ดˆ๊ธฐํ™”
		TimeInRange = 0.0f;
	}
}
void AMonsterCharacter::AttackPlayer()
{
	// ์ง€๊ธˆ์€ ๋กœ๊ทธ๋งŒ ์ถœ๋ ฅ (๋‚˜์ค‘์— ๋ฐ๋ฏธ์ง€ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ)
	UE_LOG(LogTemp, Warning, TEXT("๋ชฌ์Šคํ„ฐ๊ฐ€ ํ”Œ๋ ˆ์ด์–ด๋ฅผ ๊ณต๊ฒฉํ•ฉ๋‹ˆ๋‹ค!"));
}

void AMonsterCharacter::BecomeEnraged()
{
	bIsEnraged = true;
	GetCharacterMovement()->MaxWalkSpeed = EnragedSpeed;
	UE_LOG(LogTemp, Warning, TEXT("๋ชฌ์Šคํ„ฐ๊ฐ€ ๋ถ„๋…ธ ์ƒํƒœ๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!"));
}

๐Ÿง  Step 2: MonsterAIController ํด๋ž˜์Šค

์ด ํด๋ž˜์Šค๋Š” AI์˜ '๋‘๋‡Œ'์ž…๋‹ˆ๋‹ค. Behavior Tree๋ฅผ ์‹คํ–‰ํ•˜๊ณ , ์ถ”์  ๋Œ€์ƒ์„ ์„ค์ •ํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

MonsterAIController.h

UCLASS()
class MONSHOOTING_API AMonsterAIController : public AAIController
{
	GENERATED_BODY()

public:
	AMonsterAIController();
	virtual void BeginPlay() override;

	UPROPERTY(EditDefaultsOnly, Category = "AI")
	UBehaviorTree* BehaviorTreeAsset;

	void SetTargetActor(AActor* NewTarget);
};

MonsterAIController.cpp

AMonsterAIController::AMonsterAIController()
{
	// ์—์…‹ ๋กœ๋“œ: Behavior Tree๋Š” ๋ฆฌ์†Œ์Šค ๊ฒฝ๋กœ๊ฐ€ ์ •ํ™•ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค!
	static ConstructorHelpers::FObjectFinder<UBehaviorTree> BTAsset(TEXT("/Game/AI/BT_MonsterBehavior"));
	if (BTAsset.Succeeded())
	{
		BehaviorTreeAsset = BTAsset.Object;
	}
}
void AMonsterAIController::BeginPlay()
{
	Super::BeginPlay();

	if (BehaviorTreeAsset)
	{
		RunBehaviorTree(BehaviorTreeAsset);

		// ํƒ€๊ฒŸ์„ ํ”Œ๋ ˆ์ด์–ด๋กœ ์„ค์ •
		APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
		if (PlayerPawn)
		{
			SetTargetActor(PlayerPawn);
		}
	}
}
void AMonsterAIController::SetTargetActor(AActor* NewTarget)
{
	if (GetBlackboardComponent())
	{
		GetBlackboardComponent()->SetValueAsObject(TEXT("TargetActor"), NewTarget);
	}
}

๐Ÿงฉ Step 3: Behavior Tree & Blackboard

AI์˜ ํ–‰๋™์„ ์‹œ๊ฐ์ ์œผ๋กœ ์กฐ์ ˆํ•  ์ˆ˜ ์žˆ๋Š” ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค.
Blackboard๋Š” ๊ธฐ์–ต ์ €์žฅ์†Œ, Behavior Tree๋Š” ํ–‰๋™ ์ ˆ์ฐจ์ž…๋‹ˆ๋‹ค.

Blackboard Key ๋ชฉ๋ก

Key ์ด๋ฆ„ํƒ€์ž…์šฉ๋„
TargetActorObject์ถ”์  ๋Œ€์ƒ
IsEnragedBool๋ถ„๋…ธ ์ƒํƒœ

Behavior Tree ๊ตฌ์กฐ

[Root]
 โ””โ”€โ”€ Selector
      โ”œโ”€โ”€ Sequence
      โ”‚    โ”œโ”€โ”€ Check: TargetActor != null
      โ”‚    โ””โ”€โ”€ MoveTo(TargetActor)
      โ””โ”€โ”€ Task: AttackPlayer

โš™๏ธ Step 4: ์ปค์Šคํ…€ Task ๋…ธ๋“œ

AttackPlayer๋ฅผ Behavior Tree ์•ˆ์—์„œ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” C++ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.

BTTask_AttackPlayer.h

UCLASS()
class MONSHOOTING_API UBTTask_AttackPlayer : public UBTTaskNode
{
	GENERATED_BODY()

public:
	UBTTask_AttackPlayer();
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};

BTTask_AttackPlayer.cpp

EBTNodeResult::Type UBTTask_AttackPlayer::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	AAIController* AIController = OwnerComp.GetAIOwner();
	if (!AIController) return EBTNodeResult::Failed;

	AMonsterCharacter* Monster = Cast<AMonsterCharacter>(AIController->GetPawn());
	if (!Monster) return EBTNodeResult::Failed;

	Monster->AttackPlayer(); // ์‹ค์ œ ๊ณต๊ฒฉ ์‹คํ–‰

	return EBTNodeResult::Succeeded;
}

โœ… ํ•ต์‹ฌ ์š”์•ฝ

๊ตฌ์„ฑ ์š”์†Œ์—ญํ• 
MonsterCharacterํ”Œ๋ ˆ์ด์–ด์™€์˜ ๊ฑฐ๋ฆฌ ์ฒดํฌ โ†’ ์ถ”์  โ†’ ๊ณต๊ฒฉ โ†’ ๋ถ„๋…ธ ์ƒํƒœ ์ „ํ™˜
MonsterAIControllerBehavior Tree ์‹คํ–‰ ๋ฐ ํƒ€๊ฒŸ ์ง€์ •
BlackboardTargetActor, IsEnraged ์ƒํƒœ๊ฐ’ ์ €์žฅ ๋ฐ ๊ด€๋ฆฌ
Behavior Tree์ถ”์ ยท๊ณต๊ฒฉ ํ”Œ๋กœ์šฐ ์ œ์–ด
์ปค์Šคํ…€ TaskBT์—์„œ C++ ๊ณต๊ฒฉ ํ•จ์ˆ˜(AttackPlayer()) ์‹คํ–‰

profile
Shin Ji Yong // Unreal Engine 5 ๊ณต๋ถ€์ค‘์ž…๋‹ˆ๋‹ค~

0๊ฐœ์˜ ๋Œ“๊ธ€