[UE] Behavior Tree: 순찰(Patrol)

적의 상태에 따라 이동속도를 다르게 설정해보자. 적 Behavior Tree에 순찰 기능을 추가하고 Player가 접근하면 달려오도록 수정해보자.
목차
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 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 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 CWeaponStructures.h .cpp |
||
Global.h CGameMode.h .cpp U2212_06.Build.cs |
||
U2212_06.uproject | ||
Speed 변경하기
CBTTaskNode_Speed 생성
새 C++ 클래스 - BTTaskNode - CBTTaskNode_Speed 생성


CBTTaskNode_Speed.h
#pragma once #include "CoreMinimal.h" #include "BehaviorTree/BTTaskNode.h" #include "Components/CMovementComponent.h" #include "CBTTaskNode_Speed.generated.h" UCLASS() class U2212_06_API UCBTTaskNode_Speed : public UBTTaskNode { GENERATED_BODY() private: UPROPERTY(EditAnywhere, Category = "Type") ESpeedType Type; public: UCBTTaskNode_Speed(); private: EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; };
CBTTaskNode_Speed.cpp
#include "BehaviorTree/CBTTaskNode_Speed.h" #include "Global.h" #include "Characters/CEnemy_AI.h" #include "Characters/CAIController.h" UCBTTaskNode_Speed::UCBTTaskNode_Speed() { NodeName = "Speed"; } EBTNodeResult::Type UCBTTaskNode_Speed::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) { //EBTNodeResult::Type은 Composite과 Task 공유해서 사용한다. Super::ExecuteTask(OwnerComp, NodeMemory); ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());//OwnerComp로부터 controller를 가져온다. ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());//가져온 controller의 Pawn을 사용하여 Enemy_AI를 캐스팅한다. UCMovementComponent* movement = CHelpers::GetComponent<UCMovementComponent>(ai); movement->SetSpeed(Type);//CMovementComponent의 SetSpeed함수로 Type으로 지정한 속도를 넣고 변경한다. return EBTNodeResult::Succeeded; }
※ 참고) EBTNodeResult의 Type들 - Succeeded, Failed, Aborted, InProgress

EBTNodeResult는 BehaviorTreeTypes.h에 선언되어 있고,
Behavior Tree 내의 Composite, Task 모두 사용한다.
BT_Melee

실행화면

Patrol
Build.cs
Build.cs
using UnrealBuildTool; public class U2212_06 : ModuleRules { public U2212_06(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; PublicIncludePaths.Add(ModuleDirectory); PublicDependencyModuleNames.Add("Core"); PrivateDependencyModuleNames.Add("CoreUObject"); PrivateDependencyModuleNames.Add("Engine"); PrivateDependencyModuleNames.Add("InputCore"); PublicDependencyModuleNames.Add("Niagara"); PublicDependencyModuleNames.Add("AIModule"); PublicDependencyModuleNames.Add("GameplayTasks"); PublicDependencyModuleNames.Add("NavigationSystem"); } }
모듈 추가
- PublicDependencyModuleNames.Add("NavigationSystem");
CPatrolPath 생성
새 C++ 클래스 - Actor - CPatrolPath 생성


CPatrolPath.h
#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "CPatrolPath.generated.h" UCLASS() class U2212_06_API ACPatrolPath : public AActor { GENERATED_BODY() private: UPROPERTY(EditAnywhere, Category = "Loop") bool bLoop; UPROPERTY(EditAnywhere, Category = "Path") int32 Index; UPROPERTY(EditAnywhere, Category = "Path") bool bReverse; private: UPROPERTY(VisibleDefaultsOnly) class USceneComponent* Root; UPROPERTY(VisibleDefaultsOnly) class USplineComponent* Spline; UPROPERTY(VisibleDefaultsOnly) class UTextRenderComponent* Text; public: FORCEINLINE class USplineComponent* GetSpline() { return Spline; } public: ACPatrolPath(); void OnConstruction(const FTransform& Transform) override; protected: virtual void BeginPlay() override; public: FVector GetMoveTo(); void UpdateIndex(); };
CPatrolPath.cpp
#include "BehaviorTree/CPatrolPath.h" #include "Global.h" #include "Components/SplineComponent.h" #include "Components/TextRenderComponent.h" ACPatrolPath::ACPatrolPath() { bRunConstructionScriptOnDrag = false; CHelpers::CreateComponent<USceneComponent>(this, &Root, "Root"); CHelpers::CreateComponent<USplineComponent>(this, &Spline, "Spline", Root); CHelpers::CreateComponent<UTextRenderComponent>(this, &Text, "Text", Root); Spline->SetRelativeLocation(FVector(0, 0, 30)); Spline->bHiddenInGame = false; Text->SetRelativeLocation(FVector(0, 0, 120)); Text->SetRelativeRotation(FRotator(0, 180, 0)); Text->HorizontalAlignment = EHorizTextAligment::EHTA_Center; Text->TextRenderColor = FColor::Red; Text->bHiddenInGame = true; } void ACPatrolPath::OnConstruction(const FTransform& Transform) { Super::OnConstruction(Transform); #if WITH_EDITOR Text->Text = FText::FromString(GetActorLabel());//GetActorLabel()은 Editor에 있는 함수라서 Project Package 시에는 지워줘야 한다. #endif Spline->SetClosedLoop(bLoop); } void ACPatrolPath::BeginPlay() { Super::BeginPlay(); } FVector ACPatrolPath::GetMoveTo() { //다음 Spline Point 이동을 위해 Spline의 다음 Point의 World 위치를 리턴. return Spline->GetLocationAtSplinePoint(Index, ESplineCoordinateSpace::World); } void ACPatrolPath::UpdateIndex() { int32 count = Spline->GetNumberOfSplinePoints();//cout변수에 Spline Points 숫자를 넣어준다. if (bReverse)//역방향 { if (Index > 0) { Index--; return; } if (Spline->IsClosedLoop()) { Index = count - 1; return; } Index = 1; bReverse = false; return; } if (Index < count - 1)//정방향 { Index++; return; } if(Spline->IsClosedLoop()) { Index = 0; return; } Index = count - 2; bReverse = true;//역방향으로 만든다. }
BP_CPatrolPath 생성 + Viewport에 Spline 그리기


BP_CPatrolPath 생성 후 Viewport에 Spline을 배치한다.
BP_Enemy_Melee의 Patrol - Project Path에 Viewport에 배치한 BP_CPatrolPath 중 하나를 할당한다.
CBTTaskNode_Patrol 생성
새 C++ 클래스 - BTTaskNode - CBTTaskNode_Patrol 생성


CBTTaskNode_Patrol.h
#pragma once #include "CoreMinimal.h" #include "BehaviorTree/BTTaskNode.h" #include "CBTTaskNode_Patrol.generated.h" UCLASS() class U2212_06_API UCBTTaskNode_Patrol : public UBTTaskNode { GENERATED_BODY() private: UPROPERTY(EditAnywhere, Category = "Patrol") bool bDebugMode;//디버그 모드 on/off UPROPERTY(EditAnywhere, Category = "Patrol") float AcceptanceDistance = 20;//순찰 경로 간격 UPROPERTY(EditAnywhere, Category = "Random") float RandomRadius = 1500;//순찰 경로가 없을 때 랜덤으로 움직이는 범위 public: UCBTTaskNode_Patrol(); protected: virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override; };
CBTTaskNode_Patrol.cpp
#include "BehaviorTree/CBTTaskNode_Patrol.h" #include "Global.h" #include "CPatrolPath.h" #include "Components/SplineComponent.h" #include "Components/CAIBehaviorComponent.h" #include "Characters/CEnemy_AI.h" #include "Characters/CAIController.h" #include "NavigationSystem.h" UCBTTaskNode_Patrol::UCBTTaskNode_Patrol() { NodeName = "Patrol"; bNotifyTick = true;//작성해야 Tick이 실행된다. Tick을 실행하려면 반드시 작성해야 한다. } EBTNodeResult::Type UCBTTaskNode_Patrol::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) { Super::ExecuteTask(OwnerComp, NodeMemory); ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner()); ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn()); UCAIBehaviorComponent* behavior = CHelpers::GetComponent<UCAIBehaviorComponent>(ai); //PatrolPath가 있을때 if (!!ai->GetPatrolPath()) { //움직일 위치를 Blackboard에 넣는다. FVector moveToPoint = ai->GetPatrolPath()->GetMoveTo(); behavior->SetPatrolLocation(moveToPoint); if (bDebugMode)//순찰 경로 DrawDebug DrawDebugSphere(ai->GetWorld(), moveToPoint, 25, 25, FColor::Green, true, 5); return EBTNodeResult::InProgress; } //PatrolPath가 없을때 FVector location = ai->GetActorLocation(); UNavigationSystemV1* navSystem = FNavigationSystem::GetCurrent<UNavigationSystemV1>(ai->GetWorld()); CheckNullResult(navSystem, EBTNodeResult::Failed);//navSystem이 없다면 EBTNodeResult::Failed 리턴 //결과값을 리턴 받는다. FNavLocation point(location); while (true) { //갈 수 있는 위치가 나올때까지 계속 돌린다. if (navSystem->GetRandomPointInNavigableRadius(location, RandomRadius, point)) break; } behavior->SetPatrolLocation(point.Location); if (bDebugMode)//순찰 경로 DrawDebug DrawDebugSphere(ai->GetWorld(), point.Location, 25, 25, FColor::Green, true, 5); return EBTNodeResult::InProgress; } void UCBTTaskNode_Patrol::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()); UCAIBehaviorComponent* behavior = CHelpers::GetComponent<UCAIBehaviorComponent>(ai); FVector location = behavior->GetPatrolLocation();//이동할 위치를 location 변수에 담아준다. EPathFollowingRequestResult::Type result = controller->MoveToLocation(location, AcceptanceDistance, false); switch(result) { case EPathFollowingRequestResult::Failed: { FinishLatentTask(OwnerComp, EBTNodeResult::Failed); } break; case EPathFollowingRequestResult::AlreadyAtGoal: { if (ai->GetPatrolPath()) ai->GetPatrolPath()->UpdateIndex();//다음 위치를 갱신해준다. FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded); } break; } }
※ 참고) PathFollwingComponent.h의 EPathFollowingRequestResult

CEnemy_AI
CEnemy_AI.h
#pragma once #include "CoreMinimal.h" #include "Characters/CEnemy.h" #include "CEnemy_AI.generated.h" UCLASS() class U2212_06_API ACEnemy_AI : public ACEnemy { GENERATED_BODY() private: UPROPERTY(EditDefaultsOnly, Category = "AI") class UBehaviorTree* BehaviorTree; UPROPERTY(EditDefaultsOnly, Category = "AI") uint8 TeamID = 2;//TeamID를 0~255번까지 지정가능하다. 255번은 중립이다. ID 같으면 아군이고 ID가 다르면 적이다. private: UPROPERTY(EditDefaultsOnly, Category = "Label") float LabelViewAmount = 3000.0f; private: UPROPERTY(EditAnywhere, Category = "Patrol") class ACPatrolPath* PatrolPath;//클래스 밖에서도 지정할 수 있어야 한다. 서로간 만들어진 객체를 참조할것이라서 softObjectPtr 사용 #if WITH_EDITOR private: UPROPERTY(VisibleDefaultsOnly) class UWidgetComponent* LabelWidget; #endif private: UPROPERTY(VisibleDefaultsOnly) class UCWeaponComponent* Weapon; UPROPERTY(VisibleDefaultsOnly) class UCAIBehaviorComponent* Behavior; public: FORCEINLINE uint8 GetTeamID() { return TeamID; } FORCEINLINE class UBehaviorTree* GetBehaviorTree() { return BehaviorTree; } FORCEINLINE class ACPatrolPath* GetPatrolPath() { return PatrolPath; } public: ACEnemy_AI(); protected: virtual void BeginPlay() override; public: virtual void Tick(float DeltaTime) override; private: void UpdateLabelRenderScale(); };
변수 추가
- UPROPERTY(EditAnywhere, Category = "Patrol")
class ACPatrolPath* PatrolPath;- 클래스 밖에서도 지정할 수 있어야 한다. 서로간 만들어진 객체를 참조할것이라서 softObjectPtr 사용
인라인 함수 추가
- FORCEINLINE class ACPatrolPath* GetPatrolPath() { return PatrolPath; }
CEnemy_AI.cpp
#include "Characters/CEnemy_AI.h" #include "Global.h" #include "Components/CWeaponComponent.h" #include "Components/CAIBehaviorComponent.h" #include "Components/WidgetComponent.h" #include "Components/CStatusComponent.h" #include "Widgets/CUserWidget_Label.h" ACEnemy_AI::ACEnemy_AI() { PrimaryActorTick.bCanEverTick = true; CHelpers::CreateComponent<UWidgetComponent>(this, &LabelWidget, "Label", GetMesh()); CHelpers::CreateActorComponent<UCWeaponComponent>(this, &Weapon, "Weapon"); CHelpers::CreateActorComponent<UCAIBehaviorComponent>(this, &Behavior, "Behavior"); TSubclassOf<UCUserWidget_Label> labelClass; CHelpers::GetClass<UCUserWidget_Label>(&labelClass, "WidgetBlueprint'/Game/Widgets/WB_Label.WB_Label_C'"); LabelWidget->SetWidgetClass(labelClass); LabelWidget->SetRelativeLocation(FVector(0, 0, 220)); LabelWidget->SetDrawSize(FVector2D(120, 0)); LabelWidget->SetWidgetSpace(EWidgetSpace::Screen); } void ACEnemy_AI::BeginPlay() { Super::BeginPlay(); LabelWidget->InitWidget(); UCUserWidget_Label* label = Cast<UCUserWidget_Label>(LabelWidget->GetUserWidgetObject()); label->UpdateHealth(Status->GetHealth(), Status->GetMaxHealth()); label->UpdateName(GetName()); label->UpdateControllerName(GetController()->GetName()); } void ACEnemy_AI::Tick(float DeltaTime) { Super::Tick(DeltaTime); UCUserWidget_Label* label = Cast<UCUserWidget_Label>(LabelWidget->GetUserWidgetObject()); if (!!label) { label->UpdateHealth(Status->GetHealth(), Status->GetMaxHealth()); UpdateLabelRenderScale(); } } void ACEnemy_AI::UpdateLabelRenderScale() { UCUserWidget_Label* label = Cast<UCUserWidget_Label>(LabelWidget->GetUserWidgetObject()); CheckNull(label); APlayerCameraManager* cameraManager = UGameplayStatics::GetPlayerCameraManager(GetWorld(), 0); FVector cameraLocation = cameraManager->GetCameraLocation(); FVector targetLocation = GetController()->GetTargetLocation(); float distance = FVector::Distance(cameraLocation, targetLocation); float sizeRate = 1.0f - (distance / LabelViewAmount); if (distance > LabelViewAmount) { label->SetVisibility(ESlateVisibility::Collapsed); return; } label->SetVisibility(ESlateVisibility::Visible); label->SetRenderScale(FVector2D(sizeRate, sizeRate)); }
변경사항 없음.
CAIBehaviorComponent
CAIBehaviorComponent.h
#pragma once #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "CAIBehaviorComponent.generated.h" UENUM(BlueprintType) enum class EAIStateType : uint8 { Wait = 0, Approach, Action, Patrol, Hitted, Avoid, Dead, Max, }; DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAIStateTypeChanged, EAIStateType, InPrevType, EAIStateType, InNewType); UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) class U2212_06_API UCAIBehaviorComponent : public UActorComponent { GENERATED_BODY() private: UPROPERTY(EditAnywhere, Category = "Key") FName AIStateTypeKey = "AIState"; UPROPERTY(EditAnywhere, Category = "Key") FName TargetKey = "Target"; UPROPERTY(EditAnywhere, Category = "Key") FName PatrolLocationKey = "Patrol_Location"; private: EAIStateType GetType(); public: bool IsWaitMode(); bool IsApproachMode(); bool IsActionMode(); bool IsPatrolMode(); bool IsHittedMode(); bool IsAvoidMode(); bool IsDeadMode(); public: UCAIBehaviorComponent(); protected: virtual void BeginPlay() override; public: FORCEINLINE void SetBlackboard(class UBlackboardComponent* InBlackboard) { Blackboard = InBlackboard; } public: class ACharacter* GetTarget(); public: FVector GetPatrolLocation(); void SetPatrolLocation(const FVector& InLocation); public: void SetWaitMode(); void SetApproachMode(); void SetActionMode(); void SetPatrolMode(); void SetHittedMode(); void SetAvoidMode(); void SetDeadMode(); private: void ChangeType(EAIStateType InType); public: FAIStateTypeChanged OnAIStateTypeChanged; private: class UBlackboardComponent* Blackboard; };
변수 추가
- UPROPERTY(EditAnywhere, Category = "Key")
FName PatrolLocationKey = "Patrol_Location";
함수 추가
- FVector GetPatrolLocation();
- void SetPatrolLocation(const FVector& InLocation);
CAIBehaviorComponent.cpp
#include "Components/CAIBehaviorComponent.h" #include "Global.h" #include "GameFramework/Character.h" #include "BehaviorTree/BlackboardComponent.h" UCAIBehaviorComponent::UCAIBehaviorComponent() { } void UCAIBehaviorComponent::BeginPlay() { Super::BeginPlay(); } EAIStateType UCAIBehaviorComponent::GetType() { return (EAIStateType)Blackboard->GetValueAsEnum(AIStateTypeKey); } bool UCAIBehaviorComponent::IsWaitMode() { return GetType() == EAIStateType::Wait; } bool UCAIBehaviorComponent::IsApproachMode() { return GetType() == EAIStateType::Approach; } bool UCAIBehaviorComponent::IsActionMode() { return GetType() == EAIStateType::Action; } bool UCAIBehaviorComponent::IsPatrolMode() { return GetType() == EAIStateType::Patrol; } bool UCAIBehaviorComponent::IsHittedMode() { return GetType() == EAIStateType::Hitted; } bool UCAIBehaviorComponent::IsAvoidMode() { return GetType() == EAIStateType::Avoid; } bool UCAIBehaviorComponent::IsDeadMode() { return GetType() == EAIStateType::Dead; } ACharacter* UCAIBehaviorComponent::GetTarget() { //Blackboard 내의 TargetKey를 리턴해준다. return Cast<ACharacter>(Blackboard->GetValueAsObject(TargetKey)); } FVector UCAIBehaviorComponent::GetPatrolLocation() { return Blackboard->GetValueAsVector(PatrolLocationKey); } void UCAIBehaviorComponent::SetPatrolLocation(const FVector& InLocation) { Blackboard->SetValueAsVector(PatrolLocationKey, InLocation); } void UCAIBehaviorComponent::SetWaitMode() { ChangeType(EAIStateType::Wait); } void UCAIBehaviorComponent::SetApproachMode() { ChangeType(EAIStateType::Approach); } void UCAIBehaviorComponent::SetActionMode() { ChangeType(EAIStateType::Action); } void UCAIBehaviorComponent::SetPatrolMode() { ChangeType(EAIStateType::Patrol); } void UCAIBehaviorComponent::SetHittedMode() { ChangeType(EAIStateType::Hitted); } void UCAIBehaviorComponent::SetAvoidMode() { ChangeType(EAIStateType::Avoid); } void UCAIBehaviorComponent::SetDeadMode() { ChangeType(EAIStateType::Dead); } void UCAIBehaviorComponent::ChangeType(EAIStateType InType) { EAIStateType prevType = GetType(); Blackboard->SetValueAsEnum(AIStateTypeKey, (uint8)InType); if (OnAIStateTypeChanged.IsBound()) OnAIStateTypeChanged.Broadcast(prevType, InType); }
FVector UCAIBehaviorComponent::GetPatrolLocation()
- return Blackboard->GetValueAsVector(PatrolLocationKey);
void UCAIBehaviorComponent::SetPatrolLocation(const FVector& InLocation)
- Blackboard->SetValueAsVector(PatrolLocationKey, InLocation);
BB_Melee

새 키 - Patrol_Location 추가
※ 참고) Instance Synced
- 같은 Blackboard를 가진 애들 끼리 해당 값을 공유해준다.
- Instance Synced를 체크하면 군집 이동 등이 용이하다. ex. Enemy 한 명이 Player를 발견하면 발견한 Enemy뿐만 아니라 나머지 Enemy들이 다 같이 접근한다.
MT_Melee

실행화면

'⭐ Unreal Engine > UE RPG AI' 카테고리의 다른 글
[UE] Behavior Tree: 무기 장착 Abort (0) | 2023.08.01 |
---|---|
[UE] Behavior Tree: 피격(Hitted) (0) | 2023.07.31 |
[UE] Behavior Tree: 무기 장착 및 공격 (0) | 2023.07.27 |
[UE] Behavior Tree: 무기 장착(Equip) (0) | 2023.07.26 |
[UE] Behavior Tree 시작 (1) | 2023.07.24 |
댓글
이 글 공유하기
다른 글
-
[UE] Behavior Tree: 피격(Hitted)
[UE] Behavior Tree: 피격(Hitted)
2023.07.31 -
[UE] Behavior Tree: 무기 장착 및 공격
[UE] Behavior Tree: 무기 장착 및 공격
2023.07.27 -
[UE] Behavior Tree: 무기 장착(Equip)
[UE] Behavior Tree: 무기 장착(Equip)
2023.07.26 -
[UE] Behavior Tree 시작
[UE] Behavior Tree 시작
2023.07.24
댓글을 사용할 수 없습니다.