AI
BehaviorTree 생성
- BehaviorTree 변수 선언은 Enemy 파생에서 선언하고, 트리의 사용은 Controller에서 Enemy에 있는 BehaviorTree 가 담긴 변수를 가져와서 사용하게 된다.
- 즉, BehaviorTree는 AIController 클래스에서 실행된다.
- Controller와 Enemy는 1대1 관계이다.
- CEnemy_AI
- 헤더 파일 - BehaviorTree 클래스 포인터 변수인 ‘비헤이비어 트리’ 선언 - BehaviorTree 포인터 자료형의 ‘비헤이비어 트리 가져오기’ 함수를 선언하고 ‘비헤이비어 트리’ 변수를 반환하는것으로 정의한다.
//header UPROPERTY(EditDefaultsOnly, Category = "Team") class UBehaviorTree* BehaviorTree; //비헤이비어 트리 저장- CPP 파일 - 생성자 함수 확인 - ‘에셋 가져오기’ 함수를 ‘BehaviorTree’ 로 지정하고 생성한 ‘BT_Melee’ 에셋의 레퍼런스 주소를 복사해서 불러온다. (기본값 지정)
//CPP CHelpers::CreateActorComponent<UCAIBehaviorComponent>(this, &Behavior, "Behavior"); - CAIController
- CPP 파일 - ‘빙의 활성’ 함수 확인 - AIController에 있는 ‘비헤이비어 트리 실행’ 함수에 ‘적의 비헤이비어 트리 가져오기 함수 호출’을 넣고 호출한다.
//CPP RunBehaviorTree(Enemy->GetBehaviorTree()); //트리 실행 - BT_Melee
- 생성 : 콘텐트 - 인공지능 - BehaviorTree
- 루트 - Sequence 검색 후 연결 - Wait 검색 후 Sequence 노드와 연결
블랙보드 연결 설정
- BB_Melee
- 키는 필요하다면 ‘오브젝트’를 상속받아서 생성해야한다.
- 주의 사항 : 블랙보드 디테일 - 키타입 - 베이스 클래스 : Object로 지정되어있다면 키에 값을 넣지 못하게 된다.
- 해결 방법 : 오브젝트에 넣을 값의 자료형으로 이곳에서 변경해줘야한다.
- 지금은 Character 자료형 사용
- 해결 방법 : 오브젝트에 넣을 값의 자료형으로 이곳에서 변경해줘야한다.
- 주의 사항 : 블랙보드 디테일 - 키타입 - 베이스 클래스 : Object로 지정되어있다면 키에 값을 넣지 못하게 된다.
- 생성 : 콘텐츠 - 인공지능 - 블랙보드
- 새 키 - 오브젝트 형 선택 후 이름 변경 : Target - 키 타입 - 베이스 클래스 : Character
- 키는 필요하다면 ‘오브젝트’를 상속받아서 생성해야한다.
블랙보드 매 프레임 감지 호출 함수
- CAIController
- 감지 업데이트 활성 함수의 매개변수인 ‘동적 배열 포인터 주소’는 값이 이전에도 감지된 객체들이 ‘누적’되어서 들어온다.
- 그래서, 매개변수를 쓰지는 않고, 현재 순간에 감지된 엑터들 리스트를 사용한다.
- 헤더 파일 - BeginPlay 재정의 선언 - 델리게이트 함수인 ‘감지 업데이트 활성’ 함수에 ‘엑터 포인터 동적배열 주소 자료형을 넣고 선언한다.
//header private: //매 프레임 감지 델리게이트 UFUNCTION() void OnPerceptionUpdated(const TArray<AActor*>& UpdatedActors); protected: virtual void BeginPlay() override;- CPP 파일 - BeginPlay 정의 - ‘감지’ 변수의 ‘감지 업데이트 활성’ 함수의 ‘함수 연결’에 ‘연결할 함수가 있는 클래스, 연결할 함수’를 넣고 호출한다. - ‘감지 업데이트 활성’ 함수 정의 - ‘엑터들’ 동적 배열 변수 선언 - ‘감지’의 ‘현재 감지된 엑터들 가져오기’ 함수에 ‘감지 구분 : 모든 감지 가져오기 , 감지된 엑터들 ; 엑터들’을 넣고 호한다. - 감지된 엑터 확인 테스트
//CPP //비헤비어 컴포넌트 가져오기 Behavior = CHelpers::GetComponent<UCAIBehaviorComponent>(Enemy); void ACAIController::OnPerceptionUpdated(const TArray<AActor*>& UpdatedActors) { //GetCurrentlyPerceivedActors : 현재 감지되어있는 엑터들 배열 반환 함수 //nullptr : 어느걸로 감지되었든 전부 받는다. TArray<AActor*> actors; //감지된 엑터들 저장 Perception->GetCurrentlyPerceivedActors(nullptr, actors); for (AActor* actor : actors) { CLog::Print(actor->GetName()); } } - 감지 업데이트 활성 함수의 매개변수인 ‘동적 배열 포인터 주소’는 값이 이전에도 감지된 객체들이 ‘누적’되어서 들어온다.
블랙보드 키에 값 할당
- AIController 클래스에는 ‘블랙 보드 컴포넌트’ 오브젝트 변수가 이미 선언되어있다.
- 다만, 초기화가 되어있지 않다.
- ObjectPtr : 오브젝트 자료형 포인터를 의미한다.
- TObjectPtr<T>* : 스마트 포인터
- 나중에 언리얼 취업을 진지하게 생각한다면 반드시 ‘스마트 포인터’ 찾아서 정리해라
- 사용법은 포인터와 똑같다.
- 포인터를 세부적으로 나눠놓은것이다.
- 자동으로 제거되는 포인터들, 메모리들을 관리하기 위해 사용한다.
- CHelpers
- ‘엑터 컴포넌트 생성 함수’ 복사 - 매개변수의 T**를 TObjectPtr<T>*로 변경
- CAIController
- CPP 파일 - 생성자 함수 확인 - 부모 함수에 있는 블랙보드 변수에 ‘엑터 컴포넌트 생성’ 함수 호출하고 저장하기 - ‘빙의 활성’ 함수 확인 - ‘적’의 ‘비헤이비어 트리 가져오기’ 함수 호출 후 비어있는지 확인한다. - ‘AI컨트롤의 블랙보드’의 ‘가져오기’ 함수를 호출하여 가져온 ‘블랙보드의 주소’를 블랙보드 컴포넌트의 ‘블랙보드’ 변수를 선언하고 저장한다. - ‘블랙보드 사용’ 함수에 ‘적의 비헤이비어 트기 가져오기 함수 호출의 블랙보드 에셋 , 블랙보드’를 넣고 호출한다. - ‘감지 업데이트 활성’ 함수 확인 - ‘엑터들’의 ‘요소 반환하기’ 함수의 결과가 0보다 크다면 : ‘블랙보드’의 ‘오브젝트 값 설정’ 함수에 (값을 설정할 키 이름, 세팅할 값 : 감지된 엑터들 중 첫번쨰 값)을 넣고 호출한다. , 반환한다. - ‘블랙보드’의 ‘오브젝트 값 설정’ 함수에 (값을 설정할 키 이름, 세팅할 값 : Null)을 넣고 호출한다.
//CPP //Blackboard는 AIController 클래스 내부에 이미 선언되어있는 UBlackboardComponent 변수다. CHelpers::CreateActorComponent<UBlackboardComponent>(this, &Blackboard, "Blackboard"); void ACAIController::OnPossess(APawn* InPawn) { Super::OnPossess(InPawn); //폰에 빙의될거다. Enemy = Cast<ACEnemy_AI>(InPawn); SetGenericTeamId(Enemy->GetTeamID()); //트리 없으면 실행 안한다. CheckNull(Enemy->GetBehaviorTree()); //블랙보드 사용한다고 알린다. UBlackboardComponent* blackboard = Blackboard.Get(); //블랙보드 '주소' 가져오기 UseBlackboard(Enemy->GetBehaviorTree()->BlackboardAsset, blackboard); RunBehaviorTree(Enemy->GetBehaviorTree()); //트리 실행 } void ACAIController::OnPerceptionUpdated(const TArray<AActor*>& UpdatedActors) { //GetCurrentlyPerceivedActors : 현재 감지되어있는 엑터들 배열 반환 함수 //nullptr : 어느걸로 감지되었든 전부 받는다. TArray<AActor*> actors; //감지된 엑터들 저장 Perception->GetCurrentlyPerceivedActors(nullptr, actors); if (actors.Num() > 0) { //감지된 엑터 배열의 0번을 '타겟'으로 지정한다. Blackboard->SetValueAsObject("Target", actors[0]); return; } //타겟을 비운다. Blackboard->SetValueAsObject("Target", nullptr); } - BT_Melee
- 루트 - 디테일 - 블랙보드 에셋 : 생성한 블랙보드 할당
- 루트 - Selector 검색 후 연결 - Wait 검색 후 연결 - Wait에 우클릭 - 데코레이션 - 블랙보드 클릭 - 디테일 - 블랙보드 - 블랙보드 키 : Target - 키 쿼리 : Is Not Set
- 결과 : 키 쿼리를 Is Not Set으로 설정했기 때문에, 감지 범위 밖에 있다면 순서대로 실행되고 관잧라 중단 : None이기 때문에 유지되지만, 감지범위로 들어가면 조건이 맞았기에 성공이 반환되고, Selector이기 때문에 Root 노드로 올라가서 실행이 중단된다.
서비스 생성
- CBTService_Melee
- 생성 : UBTService 상속
- BTService 내부에 ‘간격 : Interval’과 ‘랜덤 편차 : RandomDeviation’ 변수가 선언되어있다.
- TickNode : 랜덤 편차에 의해 생성된 범위에 값이 들어가면 실행되는 노드다.
- NodeName, Interval, RandomDeviation 등이 선언되어있다.
- CBTService_Melee의 Owner는 Service를 실행하는 BehaviorTree를 실행하는 컴포넌트가 들어온다.
- 즉, OwnerComp.GetOwner : BehaviorTree를 실행하는 오너이니 AIController가 들어온다.
- 헤더 파일 - 생성자 선언 - TickNode 재정의 선언 - ‘ActionRange’ 변수를 선언하고 150으로 초기화한다.
//header private: UPROPERTY(EditAnywhere, Category = "Action") float ActionRange = 150; public: UCBTService_Melee(); protected: void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;- CPP 파일 - 생성자 정의 - ‘노드 이름’을 ‘Melee’로 설정한다. - ‘간격’을 0.1f로 설정한다. - ‘랜덤 편차’를 0.0f로 설정한다. - TickNode 정의 - 매개 변수로 들어온 서비스의 오너 컴포넌트인 ‘비헤이비어 트리’의 ‘오저 가져오기’함수를 호출하고 해당 트리를 실행하는 엑터가 AIController이기 때문에 해당 클래스로 ‘캐스트’한 결과를 AIController 포인터 변수 ‘컨트롤러’를 선언하고 저장한다. - ‘컨트롤러’의 ‘폰 가져오기’ 함수를 호출하고 해당 폰이 Enemy_AI이기 때문에 해당 클래스로 ‘캐스트’하고 결과를 Enemy_AI 포인터 변수 ‘ai’ 를 선언하고 저장한다.
- Service 클래스 컴파일 문제
- LNK2001 : 확인할 수 없는 외부 기호
- 모듈이 없어서 생기는 문제다.
- GameplayCameras에서 한번 고쳤던 문제였다.
- 해결 방법 : 해당 문제가 발생한(__cdecl 다움에 적힌 클래스 명) 클래스 복사 후 구글 검색
- 언리얼 매뉴얼 사이트 들어가기
- 해당 클래스 문서로 들어가기
- References 확인
- Module 확인 후 복사
- Build.cs 파일 선택 - AddRange 목록에 붙여넣은 후 컴파일 실행
- LNK2001 : 확인할 수 없는 외부 기호
- BT_Melee
- 루트 - Sequence 검색 후 연결 - 우클릭 - 서비스 추가 : 작성한 Service 클래스에서 설정한 NodeName 확인 - Wait 검색 후 연결
- 결과 : Wait이 실행되는 동안 ‘Melee 서비스’의 TickNode에 정의한 코드대로 계속 실행된다.
- 이것을 Sequence에 할당하면 성공이 뜨는 모든 순간에 서비스의 TickNode 함수가 호출된다.
- 이것을 이용해서 값을 설정해서 이용할것이다.
AI 움직임 제어 컴포넌트
- CAIBehaviorComponent
- 생성 : 액터 컴포넌트 상속
- AI의 움직임을 관할할 컴포넌트다.
- 헤더 파일 - ‘AI 상태 타입’ 열거형 자료형 선언 : ‘대가, 접근, 공격, 순찰, 타격, 회피, 사망’ - 상태가 변하는것을 외부에 알릴 ‘AI 상태 타입 변함’ 델리게이트를 선언하고 ‘이전 타입, 새 타입’을 받을 수 있게 한다. - AI의 상태노드 키 이름을 저장할 ‘AI 상태’ 변수 선언 - 블랙 보드의 키를 맞출 이름 자료형의 ‘타겟 키’ 변수를 선언하고 ‘타겟’으로 초기화한다. - 외부에서 해당 클래스의 상태 타입을 사용하기 위해 ‘타입 가져오기’ 함수 선언 - 상태 확인 함수 : 대기 ~ 사망 확안 함수 선언 - ‘블랙보드 설정’ 함수에 ‘블랙보드 컴포넌트 포인터 변수’를 받을 수 있도록 선언하고 ‘클래스 내부에 선언된 블랙보드에 매개변수로 들어온 블랙보드 저장’으로 정의한다. - 상태 설정 함수 : 대기 ~ 사망 설정 모드 함수 선언 - ‘타입 변경’ 함수를 선언하고 ‘상태 타입 열거형’ 매개 변수를 받을 수 있도록 선언한다. - 델리게이트 변수인 ‘AI 상태 타입 변경됨 활성’ 변수 선언 - 블랙보드 컴포넌트 포인터 변수 ‘블랙보드’ 선언
//header 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); private: UPROPERTY(EditAnywhere, Category = "Key") FName AIStateTypeKey = "AIState"; UPROPERTY(EditAnywhere, Category = "Key") FName TargetKey = "Target"; 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: void SetWaitMode(); void SetApproachMode(); void SetActionMode(); void SetPatrolMode(); void SetHittedMode(); void SetAvoidMode(); void SetDeadMode(); private: void ChangeType(EAIStateType InType); public: FAIStateTypeChanged OnAIStateTypeChanged; //AI 상태 변경 델리게이트 private: class UBlackboardComponent* Blackboard; //블렉보드 저장- CPP 파일 - ‘상태 확인’ 함수 와 ‘상태 설정’ 함수 정의 - 상태 확인 : ‘타입 가져오기’ 함수 호출과 해당 상태가 같은지 확인 결과를 반환한다. - ‘타입 변경’ 함수에 해당 상태를 넣고 호출한다. - ‘타입 변경’ 함수 정의 - ‘타입 가져오기’ 함수 호출 결과를 ‘이전 타입’ 변수를 선언하고 저장한다. - ‘블랙보드’의 ‘열거형 값 설정’ 함수에 ‘AI 상태 타입 키 , 매개변수로 들어온 타입을 uint8로 캐스팅’을 넣고 호출한다. - ‘AI 상태 타입 변함 활성’의 ‘연결 함수 확인’ 함수를 호출한 결과가 true라면 : ‘AI 상태 타입 변함 활성’의 ‘연결 함수 실행’에 ‘이전 타입, 매개 변수 타입’을 넣고 호출한다.
//CPP EAIStateType UCAIBehaviorComponent::GetType() { //GetValueAsEnum : 블랙보드에 설정된 열거형 키 타입에 할당된 열거형을 가져온다. 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; } //상태 설정 함수 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); } - CEnemy_AI
- 헤더 파일 - 생성한 AIBehaviorComponent 포인터 변수 ‘비헤이비어’ 선언
//header UPROPERTY(VisibleAnywhere) class UCAIBehaviorComponent* Behavior;- CPP 파일 - AIBehaviorComponent 자료형의 ‘액터 컴포넌트 생성’ 함수 선언
//CPP CHelpers::CreateActorComponent<UCAIBehaviorComponent>(this, &Behavior, "Behavior"); - CAIController
- 원래 Get 함수를 선언해서 Enemy_AI 클래스의 ‘비헤이비어’를 가져와서 저장해야하지만 하지 않은 이유 : 해당 클래스도 ‘컴포넌트’로 생성했기 때문에 CPP 파일에서 GetComponent 함수로 간단하게 가져올 수 있기 때문이다.
- SetBlackboard를 통해서 값을 세팅하고 그것을 이용하는 것이다.
- 헤더 파일 - AIBehaviorComponent 포인터 변수 ‘비헤이비어’ 선언
//header class UCAIBehaviorComponent* Behavior;- CPP 파일 - ‘빙의 활성’ 함수 확인 - ‘컴포넌트 가져오기’ 함수를 AIBehaviorComponent로 지정하고 ‘적’을 넣어서 호출한 결과를 ‘비헤이비어’에 저장한다. - ‘비헤이비어’의 ‘블랙보드 설정’에 ‘블랙보드’를 넣어 호출한다.
//CPP //비헤비어 컴포넌트 가져오기 Behavior = CHelpers::GetComponent<UCAIBehaviorComponent>(Enemy); Behavior->SetBlackboard(Blackboard); //AI 비헤비어 컴포넌트의 'Blackboard'에 컨트롤러의 Blackboard 설정 - 원래 Get 함수를 선언해서 Enemy_AI 클래스의 ‘비헤이비어’를 가져와서 저장해야하지만 하지 않은 이유 : 해당 클래스도 ‘컴포넌트’로 생성했기 때문에 CPP 파일에서 GetComponent 함수로 간단하게 가져올 수 있기 때문이다.
- BB_Melee
- 열거형 키 타입 할당 방법 : 열거형 타입으로 바로 들어가면 선언한 역러형 자료형이 나오지 않는다.
- 열거형 이름 : 코드에서 선언한 ‘열거형 자료형 이름’을 치면 해당 타입으로 자동으로 할당된다.
- 주의 사항 : 이름이 틀리거나 잘못 선언한다면 나오지 않는다.
- 나오지 않았다면 뭔가가 잘못된것이니 확인해야한다.
- 새키 - Enum형 선택 - 이름 ; AIState - 블랙보드 디테일 - 키 타입 - 열거형 이름 - 코드에서 선언한 열거형 자료형 이름 ; EAIStateType
- 열거형 키 타입 할당 방법 : 열거형 타입으로 바로 들어가면 선언한 역러형 자료형이 나오지 않는다.
- 정리 : ChangeType을 하면 ‘블랙보드’의 ‘AIState’ 키에 바꾼 타입을 값으로 준다는 것이다.
- 이 모든것을 AIController에서 제어한다.
BlackBoard 설명
- BehaviorTree의 판단에 사용하는 데이터(변수) 집합을 의미한다.
- NPC의 의사 결정은 블랙보드에 있는 데이터를 기반으로 진행된다.
- 키 - SelfActor : AI를 실행하고있는 자기 자신
- 블랙보드 디테일
- 인스턴스 동기화 됨 : 볼랙보드의 해당 옵션을 체크한 ‘키 값’을 사용하는 모든 객체는 해당 키값을 ‘공유’하게 된다.
- 나중에 TeamAI 만들때 이것을 체크해서 공유하여 사용하게 된다.
- 인스턴스 동기화 됨 : 볼랙보드의 해당 옵션을 체크한 ‘키 값’을 사용하는 모든 객체는 해당 키값을 ‘공유’하게 된다.
Behavior 설명
- 배치된 노드 우측 상단 숫자 : 노드 실행 순서
- Root : 트리의 시작
- Blanch : 트리의 중간
- Composite : Task를 어떻게 수행하는지를 결정하는 노드들이다.
- Selector : 성공하면 부모 노드로 올라가서 성공으로 반환한다.
- 실패하면 다음 노드로 넘어간다.
- 즉, 마지막 실행 상태를 부모 노드에게 반환한다.
- Sequence : 이전 작업이 성공하면 다음 노드로 이동한다.
- 패러렐 : 메인과 서브가있다.
- 메인이 끝나면 서브도 끝난다.
- 메인과 서브의 작업들이 동시에 돌아기면서 동작하지만, 메인이 끝나면 서브들의 작업도 끝난다.
- Selector : 성공하면 부모 노드로 올라가서 성공으로 반환한다.
- Composite : Task를 어떻게 수행하는지를 결정하는 노드들이다.
- Leaf : 트리의 끝
- Task(작업)로 배치된다.
- 성공, 실패, 진행중이 있다.
- Wait : 대기시간이 모드 끝나면 성공을 띄운다.
- 리프는 Task 하나만 있다.
- Task(작업)로 배치된다.
- Decorater : 조건(블랙보드 값)을 판단하고 조건을 충족하면 노드를 실행한다.
- 조건이 맞지 않는다면 해당 노드를 ‘실패’로 띄운다.
- 조건이 참 : 성공 / 조건이 거짓 : 실패 로 해당 노드를 띄운다.
- Leaf와 Blanch 어디에나 붙을 수 있다.
- CoolTime이라는 데코레이션은 쿨타임 시간이 지나면 ‘성공’으로 띄운다.
- 조건이 맞지 않아 앞으로 실행이 되지 않더라도, 실행 도중 조건이 맞이 않게된다면 이미 실행되는 순서들은 계속 실행된다.
- 조건이 될 노드는 기획이나 AI 디자이너가 담당하게 된다.
- 기본적으로는 블랙보드를 사용한다.
- 키 쿼리
- Is Set : 키에 값이 설정되었다면 노드를 실행한다.
- Target is Not Set : 키에 값이 설정되지 않았다면 노드를 실행한다.
- 관찰자 중단
- None : 데코의 조건이 맞아 노드가 실행된 후 끝나도 ‘중단 시키지 않고’ 다시 실행 시킨다.
- Self : 조건을 만족하지 못하면 해당 노드를 중단시키고 ‘실패’로 띄운다.
- 조건이 맞지 않는다면 해당 노드를 ‘실패’로 띄운다.
- Service : 값을 제공하기 위한 옵션이다.
- 디테일 - 서비스 - 간격 : 서비스가 실행되는 간격
- 디테일 - 서비스 - 랜덤 편차 : 간격에 + - 하는 값을 넣고 해당 범위를 간격으로 하는 옵션이다.
- Run EQS
기본 순서
- Root - Selector - Sequence - MoveTo : 이동
- Root - Selector - Sequence - Wait : 대기