이번에는 궁수 AI를 만들것이다. 원거리에서 화살을 날리는 궁수 AI는 근거리 AI와 함께 게임에 배치하면 적 공격 패턴이 다양해져 게임에 재미를 더할 수 있다. Behavior Tree는 일전에 구현한 Melee와 크게 다르지 않다. 적을 인식하면 무기를 장착하고 활 시위를 당긴 후 화살을 날리게 만들 것이다.

 

목차

     

     


     

     

     

     

     
    Plugins
     
      Weapon
     
        Resource
     
          Icon128.png
    weapon_thumbnail_icon.png
     
        Source
     
          Weapon  
          SWeaponCheckBoxes.h .cpp
    SWeaponDetailsView.h .cpp

    SWeaponDoActionData.h .cpp
    SWeaponEquipmentData.h .cpp
    SWeaponHitData.h .cpp

    SWeaponLeftArea.h .cpp
    Weapon.Build.cs
    WeaponAssetEditor.h .cpp
    WeaponAssetFactory.h .cpp
    WeaponCommand.h .cpp 
    WeaponContextMenu.h .cpp
    WeaponModule.h .cpp
    WeaponStyle.h .cpp 
     
       

     

     
    Source
        BehaviorTree
        CBTService_Melee.h .cpp
    CBTService_Range.h .cpp
    CBTTaskNode_Action.h .cpp

    CBTTaskNode_Equip.h .cpp
    CBTTaskNode_Hitted.h .cpp

    CBTTaskNode_Patrol.h .cpp
    CBTTaskNode_Speed.h .cpp
    CPatrol.h .cpp
        Characters
        CAIController.h .cpp
    CAnimInstance.h .cpp
    CEnemy.h .cpp 
    CEnemy_AI.h.cpp
    CPlayer.h.cpp
    ICharacter.h .cpp
        Components
        CAIBehaviorComponent.h .cpp
    CFeetComponent.h .cpp

    CMontagesComponent.h .cpp 
    CMovementComponent.h .cpp 
    CStateComponent.h .cpp
    CStatusComponent.h .cpp  
    CWeaponComponent.h .cpp 
    CZoomComponent.h .cpp
        Notifies
        CAnimNotifyState_BeginAction.h .cpp
    CAnimNotifyState_BowString.h .cpp
    CAnimNotify_CameraShake.h .cpp 
    CAnimNotify_End_Parkour.h .cpp
    CAnimNotifyState_EndAction.h .cpp
    CAnimNotify_EndState.h .cpp
    CAnimNotifyState.h .cpp
    CAnimNotifyState_CameraAnim.h .cpp
    CAnimNotifyState_Collision.h .cpp 
    CAnimNotifyState_Combo.h .cpp
    CAnimNotifyState_Equip.h .cpp
    CAnimNotifyState_SubAction.h .cpp
        Parkour
        CParkourComponent.h .cpp
        Utilities
        CHelper.h
    CLog.h .cpp
        Weapons
        CArrow.h .cpp
    CAura.h .cpp
    CCamerModifier.h .cpp
    CGhostTrail.h .cpp
    CRotate_Object.h .cpp
    CThornObject.h .cpp
    CAnimInstance_Bow.h .cpp
    CAttachment_Bow.h .cpp
    CDoAction_Around.h .cpp
    CDoAction_Bow.h .cpp
    CDoAction_Combo.h .cpp
    CDoAction_Warp.h .cpp
    CSubAction_Around.h .cpp
    CSubAction_Bow.h .cpp
    CSubAction_Fist.h .cpp
    CSubAction_Hammer.h .cpp
    CSubAction_Sword.h .cpp
    CDoAction_Warp.h .cpp

    CAttachment.h .cpp
    CDoAction.h .cpp
    CEquipment.h .cpp
    CSubAction.h .cpp
    CWeaponAsset.h .cpp
    CWeaponData.h .cpp
    CWeaponStructures.h .cpp
        Global.h
    CGameMode.h .cpp
    U2212_06.Build.cs
        U2212_06.uproject
     

     

     

     

     

    CBTTaskNode_Action에 Abort 추가하기

     

     


     

    CDoAction

     

    CDoAction.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "UObject/NoExportTypes.h"
    #include "Weapons/CWeaponStructures.h"
    #include "CDoAction.generated.h"
    
    UCLASS(Abstract)//DoAction 그 자체로는 객체화되면 안 되기 때문에 Abstract을 붙여준다.
    class U2212_06_API UCDoAction : public UObject
    {
    	GENERATED_BODY()
    
    public:
    	FORCEINLINE bool GetBeginAction() { return bBeginAction; }
    	FORCEINLINE bool GetInAction() { return bInAction; }
    
    public:
    	UCDoAction();
    
    	virtual void BeginPlay //재정의 할 수 있도록 virtual로 만든다.
    	(
    		class ACAttachment* InAttachment,
    		class UCEquipment* InEquipment,
    		class ACharacter* InOwner,
    		const TArray<FDoActionData>& InDoActionDatas,//CWeaponStucture내의 FDoActionData 
    		const TArray<FHitData>& InHitDatas //CWeaponStucture내의 FHitData
    	);
    	virtual void Tick(float InDeltaTime) { }
    
    public:
    	//재정의 할 수 있도록 virtual로 만든다.
    	virtual void DoAction();
    	virtual void Begin_DoAction();
    	virtual void End_DoAction();
    
    public:
    	//CDoAction_Bow에서 재정의 시키기위해 virtual로 만든다.
    	//BeginEquip될 때 충돌체를 꺼주고 Unequip될 때 충돌체를 켜준다.
    	UFUNCTION()
    		virtual void OnBeginEquip() { }
    	
    	UFUNCTION()
    		virtual void OnUnequip() { }
    
    public:
    	UFUNCTION()
    		virtual void OnAttachmentBeginCollision() {}
    
    	UFUNCTION()
    		virtual void OnAttachmentEndCollision() {}
    
    	UFUNCTION()
    		virtual void OnAttachmentBeginOverlap(class ACharacter* InAttacker, AActor* InAttackCauser, class ACharacter* InOther) { }
    
    	UFUNCTION()
    		virtual void OnAttachmentEndOverlap(class ACharacter* InAttacker, class ACharacter* InOther) { }
    
    protected:
    	bool bInAction;
    	bool bBeginAction;
    
    	class ACharacter* OwnerCharacter;
    	class UWorld* World;
    
    	class UCMovementComponent* Movement;
    	class UCStateComponent* State;
    
    	TArray<FDoActionData> DoActionDatas;
    	TArray<FHitData> HitDatas;
    };

    인라인 Get 함수 추가

    • FORCEINLINE bool GetBeginAction() { return bBeginAction; }

     

     

     

    CDoAction.cpp

    더보기
    #include "Weapons/CDoAction.h"
    #include "Global.h"
    #include "CAttachment.h"
    #include "CEquipment.h"
    #include "GameFramework/Character.h"
    #include "Components/CStateComponent.h"
    #include "Components/CMovementComponent.h"
    
    UCDoAction::UCDoAction()
    {
    }
    
    void UCDoAction::BeginPlay(ACAttachment* InAttachment, UCEquipment* InEquipment, ACharacter* InOwner, const TArray<FDoActionData>& InDoActionDatas, const TArray<FHitData>& InHitDatas)
    {
    	OwnerCharacter = InOwner;
    	World = OwnerCharacter->GetWorld();
    
    	State = CHelpers::GetComponent<UCStateComponent>(OwnerCharacter);
    	Movement = CHelpers::GetComponent<UCMovementComponent>(OwnerCharacter);
    
    	DoActionDatas = InDoActionDatas;
    	HitDatas = InHitDatas;
    }
    
    void UCDoAction::DoAction()
    {
    	bInAction = true;
    
    	State->SetActionMode();
    }
    
    void UCDoAction::Begin_DoAction()
    {
    	bBeginAction = true;
    }
    
    void UCDoAction::End_DoAction()
    {
    	bInAction = false;
    	bBeginAction = false;
    
    	State->SetIdleMode();
    
    	Movement->Move();
    	Movement->DisableFixedCamera();
    }

    변경사항 없음.

     


     

     

    CBTTaskNode_Action

     

    CBTTaskNode_Action.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "BehaviorTree/BTTaskNode.h"
    #include "CBTTaskNode_Action.generated.h"
    
    UCLASS()
    class U2212_06_API UCBTTaskNode_Action : public UBTTaskNode
    {
    	GENERATED_BODY()
    
    public:
    	UCBTTaskNode_Action();
    
    protected:
    	EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
    	void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
    	EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& Owner0Comp, uint8* NodeMemory) override;
    };

    Abort 함수 추가

    • EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& Owner0Comp, uint8* NodeMemory) override;

     

     

     

    CBTTaskNode_Action.cpp

    더보기
    #include "BehaviorTree/CBTTaskNode_Action.h"
    #include "Global.h"
    #include "Characters/CEnemy_AI.h"
    #include "Characters/CAIController.h"
    #include "Components/CStateComponent.h"
    #include "Components/CWeaponComponent.h"
    #include "Weapons/CDoAction.h"
    
    UCBTTaskNode_Action::UCBTTaskNode_Action()
    {
    	NodeName = "Action";
    
    	bNotifyTick = true;
    }
    
    EBTNodeResult::Type UCBTTaskNode_Action::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
    {
    	Super::ExecuteTask(OwnerComp, NodeMemory);
    
    	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
    	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
    
    	UCWeaponComponent* weapon = CHelpers::GetComponent<UCWeaponComponent>(ai);
    	CheckNullResult(weapon, EBTNodeResult::Failed);
    
    	//Weapon 안에 공격할 때 정지여부(CanMove)가 있다.
    	//Player기준: Weapon에서 DoAction을 한다. DoAction은 WeaponStructure를 가지고 있다. WeaponSturcture 내의 movement를 통해서 정지 여부를 결정한다.
    	//하지만 Enemy는 알 수 없다. 그래서 controller->StopMovement()로 정지시킨다.
    	controller->StopMovement();
    	weapon->DoAction();
    
    	return EBTNodeResult::InProgress;
    }
    
    void UCBTTaskNode_Action::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
    {
    	Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);
    
    	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
    	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
    
    
    	UCWeaponComponent* weapon = CHelpers::GetComponent<UCWeaponComponent>(ai);
    	UCStateComponent* state = CHelpers::GetComponent<UCStateComponent>(ai);
    
    	bool bCheck = true;
    	bCheck &= (state->IsIdleMode());
    	bCheck &= (weapon->GetDoAction()->GetInAction() == false);
    
    	if (bCheck)
    	{
    		FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);//Succeeded로 끝낸다. 리턴이 없는 Tick 같은 곳에서 사용한다.
    
    		return;
    	}
    }
    
    EBTNodeResult::Type UCBTTaskNode_Action::AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
    {
    	Super::AbortTask(OwnerComp, NodeMemory);
    
    	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
    	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
    
    	UCWeaponComponent* weapon = CHelpers::GetComponent<UCWeaponComponent>(ai);
    	if (weapon == nullptr)//무기가 없다면
    		return EBTNodeResult::Failed;//실패 종료한다.
    
    	bool bBeginAction = weapon->GetDoAction()->GetBeginAction();//CDoAction.h에서 만든 FORCEINLINE Get함수. bBeginAction 변수의 true, false 여부를 알려준다.
    	//bBeginAction=true면 공격이 시작되었다는 의미, false면 공격이 시작하지 않았다는 의미다.
    	if (bBeginAction == false)
    		weapon->GetDoAction()->Begin_DoAction();//공격을 시작한다.
    
    	weapon->GetDoAction()->End_DoAction();//공격을 끝낸다.
    
    	return EBTNodeResult::Succeeded;//성공 종료한다.
    }

    Abort 함수 정의

    • EBTNodeResult::Type UCBTTaskNode_Action::AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
      • if (weapon == nullptr)
        return EBTNodeResult::Failed;
        • WeaponComponent가 없다면 EBTNodeResult::Failed 리턴하고 종료.
      • bool bBeginAction = weapon->GetDoAction()->GetBeginAction();
        • 위의 CDoAction.h에서 만든 FORCEINLINE Get함수
        • bBeginAction 변수가 true, false 인지 알려준다.
      • if (bBeginAction == false)
        weapon->GetDoAction()->Begin_DoAction();
        • 공격을 시작하지 않았다면 공격을 시작한다.
      • return EBTNodeResult::Succeeded;
        • 위에서  EBTNodeResult::Failed 리턴하지 않았다면 EBTNodeResult::Succeeded를 리턴하고 종료.

     

     


     

    실행화면

     

     

     


     

     

     

     

     

     

    Range 구현하기

     

    Melee를 구현한 것과 같이

    • BP_CAIController_Range
    • BP_CEnemy_Range
    • BB_Range
    • BT_Range

    를 생성한다.

     


     

     

    CBTService_Range 생성

     

    새 C++ 클래스 - BTService - CBTService_Range 생성

     

     

    CBTService_Range.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "BehaviorTree/BTService.h"
    #include "CBTService_Range.generated.h"
    
    UCLASS()
    class U2212_06_API UCBTService_Range : public UBTService
    {
    	GENERATED_BODY()
    
    private:
    	UPROPERTY(EditAnywhere, Category = "Action")
    		float AvoidRange = 500;//탐지되었을때 회피범위
    
    	UPROPERTY(EditAnywhere, Category = "Action")
    		bool bDrawDebug;//디버깅 그리기 회피범위
    
    public:
    	UCBTService_Range();
    
    protected:
    	void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
    };

     

     

     

     

    CBTService_Range.cpp

    더보기
    #include "BehaviorTree/CBTService_Range.h"
    #include "Global.h"
    #include "GameFramework/Character.h"
    #include "Characters/CAIController.h"
    #include "Characters/CEnemy_AI.h"
    #include "Components/CStateComponent.h"
    #include "Components/CAIBehaviorComponent.h"
    
    UCBTService_Range::UCBTService_Range()
    {
    	NodeName = "Range";
    
    	Interval = 0.1f;//호출 간격. 0.1초 마다 호출
    	RandomDeviation = 0.0f;
    }
    
    void UCBTService_Range::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
    {
    	Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
    
    	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
    	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
    	UCStateComponent* state = CHelpers::GetComponent<UCStateComponent>(ai);
    	UCAIBehaviorComponent* aiState = CHelpers::GetComponent<UCAIBehaviorComponent>(ai);
    
    	if (bDrawDebug)
    	{
    		FVector start = ai->GetActorLocation();//ACEnemy_AI의 위치
    		start.Z -= 25;
    
    		FVector end = start;
    		end.Z += 50;
    
    		DrawDebugCylinder(ai->GetWorld(), start, end, AvoidRange, 10, FColor::Red, true, Interval, 0, 0);//디버깅 원기둥 그리기
    	}
    
    	if (state->IsHittedMode())
    	{
    		aiState->SetHittedMode();
    
    		return;
    	}
    
    	ACharacter* target = aiState->GetTarget();
    	if (target == nullptr)//target이 없다면(=시야 범위 내에 적이 없다면)
    	{
    		//EAIFocusPriorty: Gameplay(Gameplay모드), LastFocusPriority(마지막 Focus 받은애를 우선순위에서 제거), Move(이동 Focus)
    		controller->ClearFocus(EAIFocusPriority::Gameplay);//바라보는 Focus를 Gameplay 모드에서 제거
    		aiState->SetWaitMode();//타겟이 없다면 Wait모드로 만들어준다.
    
    		return;
    	}
    
    	//감지가 되었을 때
    	controller->SetFocus(target);//감지가 된 target쪽으로 SetFocus하여 바라보게 만든다.
    
    	float distance = ai->GetDistanceTo(target);//현재 나의 위치에서 target까지의 거리를 구한다.
    	if (distance < AvoidRange)//거리가 설정한 공격범위 보다 작다면
    	{
    		aiState->SetAvoidMode();//Avoid 모드로 만들어준다.
    
    		return;//회피를 하고 리턴
    	}
    
    	aiState->SetActionMode();//Action 모드로 만들어준다. BT_Range에 Action 블랙보드 밑에 Equip, Action를 연결하여 무기를 장착하고 공격하게 만든다.
    }

     

     

     


     

    CBTTaskNode_Equip

     

     

    CBTTaskNode_Equip.h

    더보기
    #pragma once
    
    #include "CoreMinimal.h"
    #include "BehaviorTree/BTTaskNode.h"
    #include "Components/CWeaponComponent.h"
    #include "CBTTaskNode_Equip.generated.h"
    
    UCLASS()
    class U2212_06_API UCBTTaskNode_Equip : public UBTTaskNode
    {
    	GENERATED_BODY()
    
    private:
    	UPROPERTY(EditAnywhere, Category = "Type")
    		EWeaponType Type;
    
    public:
    	UCBTTaskNode_Equip();
    
    protected:
    	EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
    	void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
    	EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
    };

    변경사항 없음

     

     

     

    CBTTaskNode_Equip.cpp

    더보기
    #include "BehaviorTree/CBTTaskNode_Equip.h"
    #include "Global.h"
    #include "Characters/CEnemy_AI.h"
    #include "Characters/CAIController.h"
    #include "Components/CStateComponent.h"
    #include "Weapons/CEquipment.h"
    
    UCBTTaskNode_Equip::UCBTTaskNode_Equip()
    {
    	NodeName = "Equip";
    
    	bNotifyTick = true;
    }
    
    EBTNodeResult::Type UCBTTaskNode_Equip::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
    {
    	Super::ExecuteTask(OwnerComp, NodeMemory);
    
    	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
    	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
    
    	UCWeaponComponent* weapon = CHelpers::GetComponent<UCWeaponComponent>(ai);
    	CheckNullResult(weapon, EBTNodeResult::Failed);//weapon이 없다면 EBTNodeResult::Failed 리턴
    
    	if (Type == weapon->GetWeaponType())//장착될 무기 == 현재 장착된 무기
    		return EBTNodeResult::Succeeded;//새로 장착시킬 필요 없으니 바로 Succeeded를 리턴
    	
    	switch (Type)
    	{
    		case EWeaponType::Sword: weapon->SetSwordMode(); break;//Sword 장착
    		case EWeaponType::Bow: weapon->SetBowMode(); break;//Bow 장착
    	}
    
    	return EBTNodeResult::InProgress;//장착 동작이 나올 수 있도록 InProgress 리턴
    }
    
    void UCBTTaskNode_Equip::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
    {
    	Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);
    
    	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
    	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
    
    	UCWeaponComponent* weapon = CHelpers::GetComponent<UCWeaponComponent>(ai);//weapon을 가져온다.
    
    	const bool* bEquipped = weapon->GetEquipment()->GetEquipped();
    
    	UCStateComponent* state = CHelpers::GetComponent<UCStateComponent>(ai);
    	if(state->IsIdleMode() && *bEquipped)
    	{
    		FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);//Succeeded로 끝낸다. 리턴이 없는 Tick 같은 곳에서 사용한다.
    
    		return;
    	}
    }
    
    EBTNodeResult::Type UCBTTaskNode_Equip::AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
    {
    	Super::AbortTask(OwnerComp, NodeMemory);
    
    	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
    	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
    
    	UCWeaponComponent* weapon = CHelpers::GetComponent<UCWeaponComponent>(ai);
    	if (weapon == nullptr)//무기가 없다면
    		return EBTNodeResult::Failed;//실패 리턴
    	
    
    	bool bBeginEquip = weapon->GetEquipment()->GetBeginEquip();//GetBeginEquip()으로 CEquipment의 bBeginEquip를 넣어준다. bBeginEquip=true면 Equip이 시작되었다는 의미다.
    	if (bBeginEquip == false)
    		weapon->GetEquipment()->Begin_Equip();
    
    	weapon->GetEquipment()->End_Equip();
    
    	return EBTNodeResult::Aborted;//Aborted는 취소 마감, 위(Root)로 올라감. Succeeded는 성공종료.
    }

    Bow 장착 추가하기

    • EBTNodeResult::Type UCBTTaskNode_Equip::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
      • switch (Type)
        {
        case EWeaponType::Sword: weapon->SetSwordMode(); break;//Sword 장착
        case EWeaponType::Bow: weapon->SetBowMode(); break;//Bow 장착
        }

     


     

     

    BP_CAIController_Range 생성

     

    Perception

    • AI Perception
      • Sense Config
        • Implementation: AISense_Sight
        • Sight Radius: 1000.0
        •  Lose Sight Radius: 1200.0
        • PeripheralVisionHalfAngleDegrees: 180.0
      • 최대수명: 5.0

     

     

    • AISense_Sight
    • AI Hearing config: Make Noise 감지
    • AI Touch config: 닿았을 때 감지. Capsule의 Overlap 시 감지.
    • AI Damaged config: Damage가 0보다 클 때 감지. TakeDamage 함수에 감지가 내장되어있다.

     

     

     

     


     

    BP_CEnemy_Range 생성

     


     

     

    BB_Range 생성

     


     

     

     

    BT_Range 생성

     

     

     


     

     

    실행화면

     

     

    '를 눌러 디버깅 정보가 보이도록 하고 오른쪽 숫자패드 4번을 눌러 AISense_Sight이 보이도록 한다.